@qntx/nostr 0.0.1 → 0.2.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 +38 -0
- package/dist/abort-Ba8tDrOI.mjs.map +1 -0
- package/dist/{builder-BphJa-Y-.d.mts → builder-BWn55Paf.d.mts} +20 -14
- package/dist/{builder-q0VsvnYE.mjs → builder-CyrGKRVP.mjs} +32 -16
- package/dist/builder-CyrGKRVP.mjs.map +1 -0
- package/dist/{client-Bd8B1mYm.mjs → client-BWsX9Cv1.mjs} +354 -195
- package/dist/client-BWsX9Cv1.mjs.map +1 -0
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/core.d.mts +8 -8
- package/dist/core.mjs +8 -9
- package/dist/error-BW7re1UN.d.mts +9 -0
- package/dist/error-B__OMBRY.mjs +14 -0
- package/dist/error-B__OMBRY.mjs.map +1 -0
- package/dist/{error-WEFS-R-4.d.mts → error-CR8Urc84.d.mts} +20 -8
- package/dist/{error-CwBxle6N.mjs → error-DrEIM7FO.mjs} +5 -3
- package/dist/error-DrEIM7FO.mjs.map +1 -0
- package/dist/{event-CzoQEFMF.mjs → event-D8Nfoh4K.mjs} +40 -32
- package/dist/event-D8Nfoh4K.mjs.map +1 -0
- package/dist/{event-B5D2jxt_.d.mts → event-fU3Y01PR.d.mts} +23 -20
- package/dist/{filter-BxwjEkZp.mjs → filter-BQl3GlHt.mjs} +42 -35
- package/dist/filter-BQl3GlHt.mjs.map +1 -0
- package/dist/filter-C0IMByE3.d.mts +37 -0
- package/dist/{gossip-C5RVB1UB.d.mts → gossip-BRQwIM_q.d.mts} +25 -23
- package/dist/{gossip-3xkRRCm_.mjs → gossip-oloDMWEj.mjs} +52 -34
- package/dist/gossip-oloDMWEj.mjs.map +1 -0
- package/dist/gossip.d.mts +1 -1
- package/dist/gossip.mjs +1 -1
- package/dist/{http-6vevbBlu.d.mts → http-C4HwIu4B.d.mts} +7 -7
- package/dist/{http-D2rhokZo.mjs → http-CuazNRIP.mjs} +10 -6
- package/dist/http-CuazNRIP.mjs.map +1 -0
- package/dist/index-B425CAcQ.d.mts +328 -0
- package/dist/index-BVyfUBuG.d.mts +181 -0
- package/dist/{index-BYMnhRIC.d.mts → index-Ba7bJ7DD.d.mts} +68 -50
- package/dist/{index-DaRyjo7O.d.mts → index-Boe8FshM.d.mts} +13 -7
- package/dist/index-CUIvFQdE.d.mts +73 -0
- package/dist/index-JdaS_uce.d.mts +207 -0
- package/dist/index.d.mts +22 -20
- package/dist/index.mjs +19 -18
- package/dist/{key-Dn-aX4x4.mjs → key-Bbtys6o7.mjs} +22 -14
- package/dist/key-Bbtys6o7.mjs.map +1 -0
- package/dist/{key-BiGxGLhg.d.mts → key-BlsYhN1V.d.mts} +9 -6
- package/dist/{kind-BLl4cLOc.mjs → kind-Cxwhhkx8.mjs} +2 -1
- package/dist/kind-Cxwhhkx8.mjs.map +1 -0
- package/dist/{loaders-BNQszDAn.mjs → loaders-o6YXB_Ri.mjs} +329 -305
- package/dist/loaders-o6YXB_Ri.mjs.map +1 -0
- package/dist/loaders.d.mts +3 -3
- package/dist/loaders.mjs +2 -2
- package/dist/memory-C2EVx0_O.mjs +46 -0
- package/dist/memory-C2EVx0_O.mjs.map +1 -0
- package/dist/{memory-BCmhZPek.mjs → memory-index-Do1mz0WF.mjs} +212 -82
- package/dist/memory-index-Do1mz0WF.mjs.map +1 -0
- package/dist/{message-C_Uj-Y-c.mjs → message-DcY6Fi9o.mjs} +38 -24
- package/dist/message-DcY6Fi9o.mjs.map +1 -0
- package/dist/{message-DJC-Jz6T.d.mts → message-iGPIqnn0.d.mts} +15 -8
- package/dist/{nip17-BWq-Kfq5.d.mts → nip17-DNEkz8rM.d.mts} +18 -17
- package/dist/{nip19-D7905oIv.d.mts → nip19-BevCJZsp.d.mts} +23 -3
- package/dist/{nip46-C_DoxiS_.d.mts → nip46-DDV4dC56.d.mts} +13 -12
- package/dist/{nip59-DljW_sYh.d.mts → nip59-zQgdLcEr.d.mts} +28 -25
- package/dist/{nip65-Di5hZRuV.d.mts → nip65-DBfh2smt.d.mts} +8 -3
- package/dist/{nip77-ByxlgoQO.d.mts → nip77-DH4RhS5r.d.mts} +9 -8
- package/dist/nips/blossom.d.mts +37 -37
- package/dist/nips/blossom.mjs +67 -52
- package/dist/nips/blossom.mjs.map +1 -1
- package/dist/nips/nip04.d.mts +2 -3
- package/dist/nips/nip04.mjs +6 -10
- package/dist/nips/nip04.mjs.map +1 -1
- package/dist/nips/nip05.d.mts +37 -43
- package/dist/nips/nip05.mjs +57 -57
- package/dist/nips/nip05.mjs.map +1 -1
- package/dist/nips/nip10.d.mts +18 -19
- package/dist/nips/nip10.mjs +76 -61
- package/dist/nips/nip10.mjs.map +1 -1
- package/dist/nips/nip11.d.mts +9 -8
- package/dist/nips/nip11.mjs +14 -11
- package/dist/nips/nip11.mjs.map +1 -1
- package/dist/nips/nip13.d.mts +9 -9
- package/dist/nips/nip13.mjs +15 -17
- package/dist/nips/nip13.mjs.map +1 -1
- package/dist/nips/nip17.d.mts +1 -1
- package/dist/nips/nip17.mjs +17 -12
- package/dist/nips/nip17.mjs.map +1 -1
- package/dist/nips/nip19.d.mts +1 -1
- package/dist/nips/nip19.mjs +52 -29
- package/dist/nips/nip19.mjs.map +1 -1
- package/dist/nips/nip21.d.mts +5 -6
- package/dist/nips/nip21.mjs +8 -7
- package/dist/nips/nip21.mjs.map +1 -1
- package/dist/nips/nip27.d.mts +7 -8
- package/dist/nips/nip27.mjs +26 -25
- package/dist/nips/nip27.mjs.map +1 -1
- package/dist/nips/nip42.d.mts +5 -6
- package/dist/nips/nip42.mjs +3 -3
- package/dist/nips/nip42.mjs.map +1 -1
- package/dist/nips/nip44.d.mts +24 -7
- package/dist/nips/nip44.mjs +25 -15
- package/dist/nips/nip44.mjs.map +1 -1
- package/dist/nips/nip46.d.mts +1 -1
- package/dist/nips/nip46.mjs +52 -49
- package/dist/nips/nip46.mjs.map +1 -1
- package/dist/nips/nip49.d.mts +8 -7
- package/dist/nips/nip49.mjs +16 -12
- package/dist/nips/nip49.mjs.map +1 -1
- package/dist/nips/nip51.d.mts +25 -27
- package/dist/nips/nip51.mjs +47 -36
- package/dist/nips/nip51.mjs.map +1 -1
- package/dist/nips/nip57.d.mts +12 -13
- package/dist/nips/nip57.mjs +35 -38
- package/dist/nips/nip57.mjs.map +1 -1
- package/dist/nips/nip59.d.mts +1 -1
- package/dist/nips/nip59.mjs +14 -18
- package/dist/nips/nip59.mjs.map +1 -1
- package/dist/nips/nip65.d.mts +1 -1
- package/dist/nips/nip65.mjs +11 -8
- package/dist/nips/nip65.mjs.map +1 -1
- package/dist/nips/nip77.d.mts +1 -1
- package/dist/nips/nip77.mjs +56 -34
- package/dist/nips/nip77.mjs.map +1 -1
- package/dist/nips/nip96.d.mts +21 -12
- package/dist/nips/nip96.mjs +59 -36
- package/dist/nips/nip96.mjs.map +1 -1
- package/dist/nips/nip98.d.mts +12 -12
- package/dist/nips/nip98.mjs +18 -19
- package/dist/nips/nip98.mjs.map +1 -1
- package/dist/{nostr_crypto_wasm_bg-hi5aXi5P.mjs → nostr_crypto_wasm_bg-Czrdty7Q.mjs} +1 -1
- package/dist/nostr_crypto_wasm_bg.wasm +0 -0
- package/dist/{pool-C_j1OmBB.mjs → pool-XmAGaufS.mjs} +611 -487
- package/dist/pool-XmAGaufS.mjs.map +1 -0
- package/dist/pool-tEr7c5bG.d.mts +302 -0
- package/dist/reactive-DDlThDSZ.mjs +324 -0
- package/dist/reactive-DDlThDSZ.mjs.map +1 -0
- package/dist/reactive-DFpqtIf_.d.mts +66 -0
- package/dist/relay.d.mts +5 -4
- package/dist/relay.mjs +2 -2
- package/dist/report-CHld4uWj.mjs +32 -0
- package/dist/report-CHld4uWj.mjs.map +1 -0
- package/dist/{signer-CZ6K6FUc.mjs → signer-CKR9EHce.mjs} +107 -72
- package/dist/signer-CKR9EHce.mjs.map +1 -0
- package/dist/signer.d.mts +3 -3
- package/dist/signer.mjs +3 -2
- package/dist/storage-xXxLGCVb.mjs +1376 -0
- package/dist/storage-xXxLGCVb.mjs.map +1 -0
- package/dist/storage.d.mts +4 -4
- package/dist/storage.mjs +5 -4
- package/dist/store.d.mts +2 -0
- package/dist/store.mjs +2 -0
- package/dist/testing.d.mts +125 -0
- package/dist/testing.mjs +957 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-2pCUprRU.d.mts +47 -0
- package/dist/types-CRQEk0Oi.d.mts +19 -0
- package/dist/util-LfQirgDk.mjs +113 -0
- package/dist/util-LfQirgDk.mjs.map +1 -0
- package/dist/wasm.d.mts +4 -4
- package/dist/wasm.mjs +55 -56
- package/dist/wasm.mjs.map +1 -1
- package/dist/websocket-CmeqvAkp.d.mts +27 -0
- package/package.json +31 -9
- package/dist/builder-q0VsvnYE.mjs.map +0 -1
- package/dist/client-Bd8B1mYm.mjs.map +0 -1
- package/dist/error-B49rbDzT.d.mts +0 -7
- package/dist/error-CwBxle6N.mjs.map +0 -1
- package/dist/error-DZEs5OuZ.mjs +0 -24
- package/dist/error-DZEs5OuZ.mjs.map +0 -1
- package/dist/event-CzoQEFMF.mjs.map +0 -1
- package/dist/filter-BxwjEkZp.mjs.map +0 -1
- package/dist/filter-DUEKP3q1.d.mts +0 -40
- package/dist/gossip-3xkRRCm_.mjs.map +0 -1
- package/dist/http-D2rhokZo.mjs.map +0 -1
- package/dist/index-C0C4e-Gn.d.mts +0 -55
- package/dist/index-DgOugrpF.d.mts +0 -275
- package/dist/index-EwL6fHLo.d.mts +0 -267
- package/dist/index-k2YhCgsh.d.mts +0 -57
- package/dist/key-Dn-aX4x4.mjs.map +0 -1
- package/dist/kind-BLl4cLOc.mjs.map +0 -1
- package/dist/loaders-BNQszDAn.mjs.map +0 -1
- package/dist/memory-BCmhZPek.mjs.map +0 -1
- package/dist/message-C_Uj-Y-c.mjs.map +0 -1
- package/dist/pool-C_j1OmBB.mjs.map +0 -1
- package/dist/pool-DhRi7l-b.d.mts +0 -297
- package/dist/signer-CZ6K6FUc.mjs.map +0 -1
- package/dist/storage-DOy0iLRE.mjs +0 -870
- package/dist/storage-DOy0iLRE.mjs.map +0 -1
- package/dist/types-BzyADqnw.d.mts +0 -19
- package/dist/types-Dn24TPFs.d.mts +0 -43
- package/dist/util-6tfLQ9JF.mjs +0 -73
- package/dist/util-6tfLQ9JF.mjs.map +0 -1
|
@@ -1,26 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { g as NostrError, h as MessageError, l as normalizeURL, v as WasmVerifyPoisonedError } from "./util-LfQirgDk.mjs";
|
|
2
|
+
import { s as verifyEvent } from "./key-Bbtys6o7.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";
|
|
6
7
|
import { isAuthRequired, makeAuthEvent } from "./nips/nip42.mjs";
|
|
8
|
+
import { n as raceSignal, r as throwIfAborted, t as abortReason } from "./abort-Ba8tDrOI.mjs";
|
|
7
9
|
import { Nip77Error, runNegSession } from "./nips/nip77.mjs";
|
|
8
10
|
//#region src/relay/error.ts
|
|
11
|
+
/** Base class for relay errors; carries the relay URL when known. */
|
|
9
12
|
var RelayError = class extends NostrError {
|
|
13
|
+
name = "RelayError";
|
|
10
14
|
url;
|
|
11
|
-
constructor(message, url) {
|
|
12
|
-
super(url ? `${message} (${url})` : message);
|
|
15
|
+
constructor(message, url, options) {
|
|
16
|
+
super(url !== void 0 && url !== "" ? `${message} (${url})` : message, options);
|
|
13
17
|
this.url = url;
|
|
14
18
|
}
|
|
15
19
|
};
|
|
16
|
-
|
|
17
|
-
var
|
|
18
|
-
|
|
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
|
+
};
|
|
19
36
|
//#endregion
|
|
20
37
|
//#region src/relay/websocket.ts
|
|
21
38
|
let impl;
|
|
22
39
|
try {
|
|
23
|
-
if (
|
|
40
|
+
if (globalThis.WebSocket !== void 0) impl = globalThis.WebSocket;
|
|
24
41
|
} catch {}
|
|
25
42
|
/** Inject a WebSocket implementation (required on Node unless undici/global is present). */
|
|
26
43
|
function useWebSocketImplementation(websocketImplementation) {
|
|
@@ -33,8 +50,8 @@ function getWebSocketImplementation() {
|
|
|
33
50
|
//#endregion
|
|
34
51
|
//#region src/relay/url.ts
|
|
35
52
|
/**
|
|
36
|
-
* True for `ws:` / `http:` URLs that are not `.onion`.
|
|
37
|
-
*
|
|
53
|
+
* True for `ws:` / `http:` URLs that are not `.onion`. Local-network `ws://` is still insecure;
|
|
54
|
+
* allow those via a trusted set.
|
|
38
55
|
*/
|
|
39
56
|
function isInsecureRelayUrl(url) {
|
|
40
57
|
try {
|
|
@@ -46,9 +63,211 @@ function isInsecureRelayUrl(url) {
|
|
|
46
63
|
}
|
|
47
64
|
}
|
|
48
65
|
//#endregion
|
|
66
|
+
//#region src/relay/neg-session.ts
|
|
67
|
+
function createNegSession() {
|
|
68
|
+
return {
|
|
69
|
+
queue: [],
|
|
70
|
+
waiter: void 0,
|
|
71
|
+
error: void 0
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function pushNegMsg(session, hex) {
|
|
75
|
+
if (session.waiter) {
|
|
76
|
+
const { waiter } = session;
|
|
77
|
+
session.waiter = void 0;
|
|
78
|
+
waiter.resolve(hex);
|
|
79
|
+
} else session.queue.push(hex);
|
|
80
|
+
}
|
|
81
|
+
function failNegSession(session, err) {
|
|
82
|
+
session.error = err;
|
|
83
|
+
if (session.waiter) {
|
|
84
|
+
const { waiter } = session;
|
|
85
|
+
session.waiter = void 0;
|
|
86
|
+
waiter.reject(err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function failNegErr(session, reason) {
|
|
90
|
+
failNegSession(session, new Nip77Error(reason));
|
|
91
|
+
}
|
|
92
|
+
/** Wire queue/timeout/abort around `runNegSession`. Not a second session class. */
|
|
93
|
+
async function runWiredNegSession(opts) {
|
|
94
|
+
const { session, storage, filter, id, timeoutMs, signal, send, url } = opts;
|
|
95
|
+
const deadline = Date.now() + timeoutMs;
|
|
96
|
+
const timedOut = () => new RelayTimeoutError("negentropy timed out", url);
|
|
97
|
+
const remainingMs = () => deadline - Date.now();
|
|
98
|
+
const next = async () => {
|
|
99
|
+
if (session.error !== void 0) throw session.error;
|
|
100
|
+
if (remainingMs() <= 0) throw timedOut();
|
|
101
|
+
const queued = session.queue.shift();
|
|
102
|
+
if (queued !== void 0) return queued;
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
const timer = setTimeout(() => {
|
|
105
|
+
session.waiter = void 0;
|
|
106
|
+
reject(timedOut());
|
|
107
|
+
}, remainingMs());
|
|
108
|
+
const fail = (err) => {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
session.waiter = void 0;
|
|
111
|
+
reject(err);
|
|
112
|
+
};
|
|
113
|
+
const onAbort = () => {
|
|
114
|
+
if (signal) fail(abortReason(signal));
|
|
115
|
+
};
|
|
116
|
+
session.waiter = {
|
|
117
|
+
resolve: (hex) => {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
signal?.removeEventListener("abort", onAbort);
|
|
120
|
+
resolve(hex);
|
|
121
|
+
},
|
|
122
|
+
reject: (err) => fail(err)
|
|
123
|
+
};
|
|
124
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
return runNegSession({
|
|
128
|
+
storage,
|
|
129
|
+
openingSend: (hex) => {
|
|
130
|
+
send([
|
|
131
|
+
"NEG-OPEN",
|
|
132
|
+
id,
|
|
133
|
+
filter,
|
|
134
|
+
hex
|
|
135
|
+
]);
|
|
136
|
+
},
|
|
137
|
+
msgSend: (hex) => {
|
|
138
|
+
send([
|
|
139
|
+
"NEG-MSG",
|
|
140
|
+
id,
|
|
141
|
+
hex
|
|
142
|
+
]);
|
|
143
|
+
},
|
|
144
|
+
next
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const PING_DUMMY_FILTER = {
|
|
148
|
+
ids: ["a".repeat(64)],
|
|
149
|
+
limit: 0
|
|
150
|
+
};
|
|
151
|
+
/** Browser WebSocket and `ws` both use 1 for OPEN. */
|
|
152
|
+
const WS_OPEN = 1;
|
|
153
|
+
/** Node `ws` delivers `pong` on the EventEmitter, not via addEventListener. */
|
|
154
|
+
function canNativePing(ws) {
|
|
155
|
+
return typeof ws.ping === "function" && (typeof ws.once === "function" || typeof ws.on === "function");
|
|
156
|
+
}
|
|
157
|
+
/** REQ-dummy or native ping/pong keepalive. Does not import class Relay. */
|
|
158
|
+
var PingLoop = class {
|
|
159
|
+
#opts;
|
|
160
|
+
#timer;
|
|
161
|
+
#waiters = /* @__PURE__ */ new Map();
|
|
162
|
+
#gen = 0;
|
|
163
|
+
#nativePing;
|
|
164
|
+
constructor(opts) {
|
|
165
|
+
this.#opts = opts;
|
|
166
|
+
}
|
|
167
|
+
start() {
|
|
168
|
+
this.stop();
|
|
169
|
+
this.#timer = setInterval(() => {
|
|
170
|
+
this.#pingpong();
|
|
171
|
+
}, this.#opts.pingIntervalMs);
|
|
172
|
+
}
|
|
173
|
+
stop() {
|
|
174
|
+
if (this.#timer !== void 0) {
|
|
175
|
+
clearInterval(this.#timer);
|
|
176
|
+
this.#timer = void 0;
|
|
177
|
+
}
|
|
178
|
+
this.#abortCurrentPing();
|
|
179
|
+
}
|
|
180
|
+
hasWaiter(id) {
|
|
181
|
+
return this.#waiters.has(id);
|
|
182
|
+
}
|
|
183
|
+
finishDummyPing(id) {
|
|
184
|
+
const waiter = this.#waiters.get(id);
|
|
185
|
+
if (!waiter) return false;
|
|
186
|
+
this.#waiters.delete(id);
|
|
187
|
+
waiter.resolve(true);
|
|
188
|
+
try {
|
|
189
|
+
this.#opts.send(["CLOSE", id]);
|
|
190
|
+
} catch {}
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
#abortCurrentPing() {
|
|
194
|
+
this.#gen += 1;
|
|
195
|
+
const native = this.#nativePing;
|
|
196
|
+
this.#nativePing = void 0;
|
|
197
|
+
native?.abort();
|
|
198
|
+
for (const waiter of this.#waiters.values()) waiter.resolve(false);
|
|
199
|
+
this.#waiters.clear();
|
|
200
|
+
}
|
|
201
|
+
async #pingpong() {
|
|
202
|
+
const ws = this.#opts.getWs();
|
|
203
|
+
if (!ws || ws.readyState !== WS_OPEN) return;
|
|
204
|
+
if (this.#nativePing !== void 0 || this.#waiters.size > 0) return;
|
|
205
|
+
const gen = this.#gen;
|
|
206
|
+
const ping = canNativePing(ws) ? this.#waitForNativePing(ws) : this.#waitForDummyPing();
|
|
207
|
+
const ok = await new Promise((resolve) => {
|
|
208
|
+
let settled = false;
|
|
209
|
+
const done = (alive) => {
|
|
210
|
+
if (settled) return;
|
|
211
|
+
settled = true;
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
resolve(alive);
|
|
214
|
+
};
|
|
215
|
+
const timer = setTimeout(() => {
|
|
216
|
+
if (this.#nativePing !== void 0 || this.#waiters.size > 0) done(false);
|
|
217
|
+
}, this.#opts.pingTimeoutMs);
|
|
218
|
+
(async () => {
|
|
219
|
+
done(await ping);
|
|
220
|
+
})();
|
|
221
|
+
});
|
|
222
|
+
if (gen !== this.#gen) return;
|
|
223
|
+
if (!ok) {
|
|
224
|
+
this.#abortCurrentPing();
|
|
225
|
+
this.#opts.closeWs();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async #waitForNativePing(ws) {
|
|
229
|
+
return new Promise((resolve) => {
|
|
230
|
+
let settled = false;
|
|
231
|
+
const finish = (alive) => {
|
|
232
|
+
if (settled) return;
|
|
233
|
+
settled = true;
|
|
234
|
+
this.#nativePing = void 0;
|
|
235
|
+
ws.off?.("pong", onPong);
|
|
236
|
+
resolve(alive);
|
|
237
|
+
};
|
|
238
|
+
const onPong = () => finish(true);
|
|
239
|
+
this.#nativePing = { abort: () => finish(false) };
|
|
240
|
+
if (typeof ws.on === "function" && typeof ws.off === "function") ws.on("pong", onPong);
|
|
241
|
+
else if (typeof ws.once === "function") ws.once("pong", onPong);
|
|
242
|
+
else ws.on?.("pong", onPong);
|
|
243
|
+
try {
|
|
244
|
+
if (typeof ws.ping === "function") ws.ping();
|
|
245
|
+
else finish(false);
|
|
246
|
+
} catch {
|
|
247
|
+
finish(false);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async #waitForDummyPing() {
|
|
252
|
+
return new Promise((resolve) => {
|
|
253
|
+
const id = this.#opts.nextSubId("__ping__");
|
|
254
|
+
this.#waiters.set(id, { resolve });
|
|
255
|
+
try {
|
|
256
|
+
this.#opts.send([
|
|
257
|
+
"REQ",
|
|
258
|
+
id,
|
|
259
|
+
PING_DUMMY_FILTER
|
|
260
|
+
]);
|
|
261
|
+
} catch {
|
|
262
|
+
this.#waiters.delete(id);
|
|
263
|
+
resolve(false);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
//#endregion
|
|
49
269
|
//#region src/relay/subscription.ts
|
|
50
270
|
var Subscription = class {
|
|
51
|
-
sendClose;
|
|
52
271
|
id;
|
|
53
272
|
filters;
|
|
54
273
|
handlers;
|
|
@@ -61,9 +280,10 @@ var Subscription = class {
|
|
|
61
280
|
lastCreatedAt;
|
|
62
281
|
/** Event ids at `lastCreatedAt` (same-second reconnect dedup). Not all seen ids. */
|
|
63
282
|
idsAtWatermark = /* @__PURE__ */ new Set();
|
|
283
|
+
#sendClose;
|
|
64
284
|
#abort;
|
|
65
285
|
constructor(filters, opts, sendClose) {
|
|
66
|
-
this
|
|
286
|
+
this.#sendClose = sendClose;
|
|
67
287
|
this.id = createSubscriptionId(opts.id);
|
|
68
288
|
this.filters = filters;
|
|
69
289
|
this.closeOnEose = opts.closeOnEose === true;
|
|
@@ -87,8 +307,8 @@ var Subscription = class {
|
|
|
87
307
|
if (this.closed) return;
|
|
88
308
|
this.closed = true;
|
|
89
309
|
this.#abort?.();
|
|
90
|
-
this
|
|
91
|
-
this.handlers.onclose?.(reason);
|
|
310
|
+
this.#sendClose(this.id);
|
|
311
|
+
invokeSafely(() => this.handlers.onclose?.(reason));
|
|
92
312
|
}
|
|
93
313
|
/** Advance the reconnect watermark after a verified EVENT. */
|
|
94
314
|
noteVerified(event) {
|
|
@@ -101,8 +321,8 @@ var Subscription = class {
|
|
|
101
321
|
if (event.created_at === this.lastCreatedAt) this.idsAtWatermark.add(event.id);
|
|
102
322
|
}
|
|
103
323
|
/**
|
|
104
|
-
* Filters for re-REQ. Original `filters` stay unchanged.
|
|
105
|
-
*
|
|
324
|
+
* Filters for re-REQ. Original `filters` stay unchanged. NIP-01 `since` is inclusive — never
|
|
325
|
+
* `lastCreatedAt + 1`.
|
|
106
326
|
*/
|
|
107
327
|
replayFilters() {
|
|
108
328
|
const since = this.lastCreatedAt;
|
|
@@ -120,65 +340,62 @@ function subscriptionToAsyncIterable(start, opts) {
|
|
|
120
340
|
let error;
|
|
121
341
|
let wake;
|
|
122
342
|
let closer;
|
|
343
|
+
let localClose = false;
|
|
123
344
|
const notify = () => {
|
|
124
345
|
wake?.();
|
|
125
346
|
wake = void 0;
|
|
126
347
|
};
|
|
348
|
+
const waitForWake = async () => new Promise((resolve) => {
|
|
349
|
+
wake = resolve;
|
|
350
|
+
});
|
|
351
|
+
const closeLocal = (reason) => {
|
|
352
|
+
localClose = true;
|
|
353
|
+
closer?.close(reason);
|
|
354
|
+
done = true;
|
|
355
|
+
notify();
|
|
356
|
+
};
|
|
127
357
|
closer = start({
|
|
128
358
|
onevent(event) {
|
|
129
359
|
queue.push(event);
|
|
130
360
|
notify();
|
|
131
361
|
},
|
|
132
362
|
oneose() {
|
|
133
|
-
if (opts?.includeEose === false)
|
|
134
|
-
done = true;
|
|
135
|
-
closer?.close("eose");
|
|
136
|
-
notify();
|
|
137
|
-
}
|
|
363
|
+
if (opts?.includeEose === false) closeLocal("eose");
|
|
138
364
|
},
|
|
139
365
|
onclose(reason) {
|
|
140
|
-
if (
|
|
366
|
+
if (opts?.signal?.aborted === true) localClose = true;
|
|
367
|
+
if (!localClose && reason) error = new RelayClosedError(reason);
|
|
141
368
|
done = true;
|
|
142
369
|
notify();
|
|
143
370
|
}
|
|
144
371
|
});
|
|
145
372
|
if (opts?.signal) {
|
|
146
|
-
if (opts.signal.aborted)
|
|
147
|
-
|
|
148
|
-
done = true;
|
|
149
|
-
} else opts.signal.addEventListener("abort", () => {
|
|
150
|
-
closer?.close("aborted");
|
|
151
|
-
done = true;
|
|
152
|
-
notify();
|
|
153
|
-
}, { once: true });
|
|
373
|
+
if (opts.signal.aborted) closeLocal("aborted");
|
|
374
|
+
else opts.signal.addEventListener("abort", () => closeLocal("aborted"), { once: true });
|
|
154
375
|
}
|
|
155
376
|
return {
|
|
156
377
|
close(reason) {
|
|
157
|
-
|
|
158
|
-
done = true;
|
|
159
|
-
notify();
|
|
378
|
+
closeLocal(reason ?? "closed by client");
|
|
160
379
|
},
|
|
161
380
|
[Symbol.asyncIterator]() {
|
|
162
381
|
return {
|
|
163
382
|
async next() {
|
|
164
383
|
while (true) {
|
|
165
|
-
|
|
166
|
-
|
|
384
|
+
const value = queue.shift();
|
|
385
|
+
if (value !== void 0) return {
|
|
386
|
+
value,
|
|
167
387
|
done: false
|
|
168
388
|
};
|
|
169
|
-
if (error) throw error;
|
|
389
|
+
if (error !== void 0) throw error;
|
|
170
390
|
if (done) return {
|
|
171
391
|
value: void 0,
|
|
172
392
|
done: true
|
|
173
393
|
};
|
|
174
|
-
await
|
|
175
|
-
wake = resolve;
|
|
176
|
-
});
|
|
394
|
+
await waitForWake();
|
|
177
395
|
}
|
|
178
396
|
},
|
|
179
397
|
async return() {
|
|
180
|
-
|
|
181
|
-
done = true;
|
|
398
|
+
closeLocal("iterator returned");
|
|
182
399
|
return {
|
|
183
400
|
value: void 0,
|
|
184
401
|
done: true
|
|
@@ -190,18 +407,6 @@ function subscriptionToAsyncIterable(start, opts) {
|
|
|
190
407
|
}
|
|
191
408
|
//#endregion
|
|
192
409
|
//#region src/relay/subscribe.ts
|
|
193
|
-
function captureListenerError(errors, fn) {
|
|
194
|
-
try {
|
|
195
|
-
fn();
|
|
196
|
-
} catch (err) {
|
|
197
|
-
errors.push(err);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
function flushListenerErrors(errors) {
|
|
201
|
-
if (errors.length === 0) return;
|
|
202
|
-
if (errors.length === 1) throw errors[0];
|
|
203
|
-
throw new AggregateError(errors);
|
|
204
|
-
}
|
|
205
410
|
function openExclusive(ctx, filters, opts) {
|
|
206
411
|
const sub = new Subscription(filters, opts, (id) => {
|
|
207
412
|
ctx.subs.delete(id);
|
|
@@ -267,7 +472,7 @@ function subscribeLive(ctx, filters, opts) {
|
|
|
267
472
|
if (group.sub.eosed) queueMicrotask(() => {
|
|
268
473
|
if (handle.closed || handle.eosed) return;
|
|
269
474
|
handle.eosed = true;
|
|
270
|
-
handle.handlers.oneose?.();
|
|
475
|
+
invokeSafely(() => handle.handlers.oneose?.());
|
|
271
476
|
});
|
|
272
477
|
return handle;
|
|
273
478
|
}
|
|
@@ -290,70 +495,54 @@ function detachLive(ctx, fp, handle) {
|
|
|
290
495
|
function endLiveGroup(ctx, fp, opts) {
|
|
291
496
|
const group = ctx.liveByFp.get(fp);
|
|
292
497
|
if (!group) return;
|
|
293
|
-
const id = group.sub
|
|
498
|
+
const { id } = group.sub;
|
|
294
499
|
const remaining = [...group.attachments];
|
|
295
500
|
group.attachments.clear();
|
|
296
501
|
forgetLiveGroup(ctx, group);
|
|
297
502
|
if (opts.sendClose) try {
|
|
298
503
|
if (ctx.connected()) ctx.send(["CLOSE", id]);
|
|
299
504
|
} catch {}
|
|
300
|
-
const
|
|
301
|
-
for (const att of remaining) captureListenerError(errors, () => {
|
|
505
|
+
for (const att of remaining) invokeSafely(() => {
|
|
302
506
|
att.close(opts.reason);
|
|
303
507
|
});
|
|
304
|
-
flushListenerErrors(errors);
|
|
305
508
|
}
|
|
306
509
|
function deliverLiveEvent(ctx, group, event) {
|
|
307
|
-
const sub = group
|
|
510
|
+
const { sub } = group;
|
|
308
511
|
const attachments = [...group.attachments];
|
|
309
|
-
const
|
|
310
|
-
for (const att of attachments) captureListenerError(errors, () => {
|
|
512
|
+
for (const att of attachments) invokeSafely(() => {
|
|
311
513
|
att.handlers.receivedEvent?.(event.id);
|
|
312
514
|
});
|
|
313
|
-
if (sub.idsAtWatermark.has(event.id))
|
|
314
|
-
flushListenerErrors(errors);
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
515
|
+
if (sub.idsAtWatermark.has(event.id)) return;
|
|
317
516
|
const recipients = [];
|
|
318
517
|
for (const att of attachments) {
|
|
319
518
|
if (att.closed) continue;
|
|
320
519
|
let skip = false;
|
|
321
|
-
|
|
520
|
+
invokeSafely(() => {
|
|
322
521
|
skip = Boolean(att.handlers.alreadyHaveEvent?.(event.id));
|
|
323
522
|
});
|
|
324
523
|
if (!skip) recipients.push(att);
|
|
325
524
|
}
|
|
326
|
-
if (recipients.length === 0)
|
|
327
|
-
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
if (!ctx.acceptEvent(event)) {
|
|
331
|
-
flushListenerErrors(errors);
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
525
|
+
if (recipients.length === 0) return;
|
|
526
|
+
if (!ctx.acceptEvent(event)) return;
|
|
334
527
|
sub.noteVerified(event);
|
|
335
528
|
for (const att of recipients) att.noteVerified(event);
|
|
336
|
-
for (const att of recipients)
|
|
529
|
+
for (const att of recipients) invokeSafely(() => {
|
|
337
530
|
att.handlers.onevent?.(event);
|
|
338
531
|
});
|
|
339
|
-
flushListenerErrors(errors);
|
|
340
532
|
}
|
|
341
533
|
function deliverLiveEose(group) {
|
|
342
|
-
const attachments =
|
|
343
|
-
const errors = [];
|
|
534
|
+
const attachments = [...group.attachments];
|
|
344
535
|
for (const att of attachments) {
|
|
345
536
|
if (att.closed || att.eosed) continue;
|
|
346
537
|
att.eosed = true;
|
|
347
|
-
|
|
538
|
+
invokeSafely(() => {
|
|
348
539
|
att.handlers.oneose?.();
|
|
349
540
|
});
|
|
350
541
|
}
|
|
351
|
-
flushListenerErrors(errors);
|
|
352
542
|
}
|
|
353
543
|
function closeAllSubscriptions(ctx, reason) {
|
|
354
|
-
const
|
|
355
|
-
const fps
|
|
356
|
-
for (const fp of fps) captureListenerError(errors, () => {
|
|
544
|
+
const fps = [...ctx.liveByFp.keys()];
|
|
545
|
+
for (const fp of fps) invokeSafely(() => {
|
|
357
546
|
endLiveGroup(ctx, fp, {
|
|
358
547
|
sendClose: false,
|
|
359
548
|
reason
|
|
@@ -361,14 +550,13 @@ function closeAllSubscriptions(ctx, reason) {
|
|
|
361
550
|
});
|
|
362
551
|
for (const sub of ctx.subs.values()) if (!sub.closed) {
|
|
363
552
|
sub.closed = true;
|
|
364
|
-
|
|
553
|
+
invokeSafely(() => {
|
|
365
554
|
sub.handlers.onclose?.(reason);
|
|
366
555
|
});
|
|
367
556
|
}
|
|
368
557
|
ctx.subs.clear();
|
|
369
558
|
ctx.liveByFp.clear();
|
|
370
559
|
ctx.liveBySubId.clear();
|
|
371
|
-
flushListenerErrors(errors);
|
|
372
560
|
}
|
|
373
561
|
function dropSubscription(ctx, sub, reason) {
|
|
374
562
|
const group = ctx.liveBySubId.get(sub.id);
|
|
@@ -381,7 +569,9 @@ function dropSubscription(ctx, sub, reason) {
|
|
|
381
569
|
}
|
|
382
570
|
ctx.subs.delete(sub.id);
|
|
383
571
|
sub.closed = true;
|
|
384
|
-
|
|
572
|
+
invokeSafely(() => {
|
|
573
|
+
sub.handlers.onclose?.(reason);
|
|
574
|
+
});
|
|
385
575
|
}
|
|
386
576
|
function onSubEvent(ctx, subId, event) {
|
|
387
577
|
const sub = ctx.subs.get(subId);
|
|
@@ -391,12 +581,20 @@ function onSubEvent(ctx, subId, event) {
|
|
|
391
581
|
deliverLiveEvent(ctx, group, event);
|
|
392
582
|
return;
|
|
393
583
|
}
|
|
394
|
-
|
|
584
|
+
invokeSafely(() => {
|
|
585
|
+
sub.handlers.receivedEvent?.(event.id);
|
|
586
|
+
});
|
|
395
587
|
if (sub.idsAtWatermark.has(event.id)) return;
|
|
396
|
-
|
|
588
|
+
let have = false;
|
|
589
|
+
invokeSafely(() => {
|
|
590
|
+
have = Boolean(sub.handlers.alreadyHaveEvent?.(event.id));
|
|
591
|
+
});
|
|
592
|
+
if (have) return;
|
|
397
593
|
if (!ctx.acceptEvent(event)) return;
|
|
398
594
|
sub.noteVerified(event);
|
|
399
|
-
|
|
595
|
+
invokeSafely(() => {
|
|
596
|
+
sub.handlers.onevent?.(event);
|
|
597
|
+
});
|
|
400
598
|
}
|
|
401
599
|
function onSubEose(ctx, subId) {
|
|
402
600
|
const sub = ctx.subs.get(subId);
|
|
@@ -406,7 +604,9 @@ function onSubEose(ctx, subId) {
|
|
|
406
604
|
const group = ctx.liveBySubId.get(subId);
|
|
407
605
|
if (group) deliverLiveEose(group);
|
|
408
606
|
else {
|
|
409
|
-
|
|
607
|
+
invokeSafely(() => {
|
|
608
|
+
sub.handlers.oneose?.();
|
|
609
|
+
});
|
|
410
610
|
if (sub.closeOnEose) sub.close("eose");
|
|
411
611
|
}
|
|
412
612
|
}
|
|
@@ -437,6 +637,7 @@ function streamFilters(subscribe, filters, opts) {
|
|
|
437
637
|
}), { signal: opts?.signal });
|
|
438
638
|
}
|
|
439
639
|
async function fetchFilters(subscribe, filters, opts) {
|
|
640
|
+
throwIfAborted(opts.signal);
|
|
440
641
|
const events = [];
|
|
441
642
|
const seen = /* @__PURE__ */ new Set();
|
|
442
643
|
await new Promise((resolve, reject) => {
|
|
@@ -445,9 +646,12 @@ async function fetchFilters(subscribe, filters, opts) {
|
|
|
445
646
|
if (settled) return;
|
|
446
647
|
settled = true;
|
|
447
648
|
clearTimeout(timer);
|
|
448
|
-
|
|
449
|
-
if (err)
|
|
450
|
-
else
|
|
649
|
+
let reason = "aborted";
|
|
650
|
+
if (err instanceof Error) reason = err.message;
|
|
651
|
+
else if (err === void 0) reason = "fetch complete";
|
|
652
|
+
sub.close(reason);
|
|
653
|
+
if (err === void 0) resolve();
|
|
654
|
+
else reject(err);
|
|
451
655
|
};
|
|
452
656
|
const timer = setTimeout(() => done(), opts.timeoutMs);
|
|
453
657
|
const sub = subscribe(filters, {
|
|
@@ -463,10 +667,10 @@ async function fetchFilters(subscribe, filters, opts) {
|
|
|
463
667
|
done();
|
|
464
668
|
},
|
|
465
669
|
onclose() {
|
|
466
|
-
if (!settled) done();
|
|
670
|
+
if (!settled) done(opts.signal?.aborted === true ? abortReason(opts.signal) : void 0);
|
|
467
671
|
}
|
|
468
672
|
});
|
|
469
|
-
opts.signal?.addEventListener("abort", () => done(
|
|
673
|
+
opts.signal?.addEventListener("abort", () => done(opts.signal ? abortReason(opts.signal) : void 0), { once: true });
|
|
470
674
|
});
|
|
471
675
|
return events;
|
|
472
676
|
}
|
|
@@ -474,7 +678,9 @@ function armEoseTimeout(sub, eoseTimeoutMs) {
|
|
|
474
678
|
const timer = setTimeout(() => {
|
|
475
679
|
if (sub.eosed || sub.closed) return;
|
|
476
680
|
sub.eosed = true;
|
|
477
|
-
|
|
681
|
+
invokeSafely(() => {
|
|
682
|
+
sub.handlers.oneose?.();
|
|
683
|
+
});
|
|
478
684
|
}, eoseTimeoutMs);
|
|
479
685
|
const prevClose = sub.handlers.onclose;
|
|
480
686
|
sub.handlers.onclose = (reason) => {
|
|
@@ -488,205 +694,8 @@ function armEoseTimeout(sub, eoseTimeoutMs) {
|
|
|
488
694
|
};
|
|
489
695
|
}
|
|
490
696
|
//#endregion
|
|
491
|
-
//#region src/relay/neg-session.ts
|
|
492
|
-
function createNegSession() {
|
|
493
|
-
return {
|
|
494
|
-
queue: [],
|
|
495
|
-
waiter: void 0,
|
|
496
|
-
error: void 0
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
function pushNegMsg(session, hex) {
|
|
500
|
-
if (session.waiter) {
|
|
501
|
-
const waiter = session.waiter;
|
|
502
|
-
session.waiter = void 0;
|
|
503
|
-
waiter.resolve(hex);
|
|
504
|
-
} else session.queue.push(hex);
|
|
505
|
-
}
|
|
506
|
-
function failNegSession(session, err) {
|
|
507
|
-
session.error = err;
|
|
508
|
-
if (session.waiter) {
|
|
509
|
-
const waiter = session.waiter;
|
|
510
|
-
session.waiter = void 0;
|
|
511
|
-
waiter.reject(err);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
function failNegErr(session, reason) {
|
|
515
|
-
failNegSession(session, new Nip77Error(reason));
|
|
516
|
-
}
|
|
517
|
-
/** Wire queue/timeout/abort around `runNegSession`. Not a second session class. */
|
|
518
|
-
async function runWiredNegSession(opts) {
|
|
519
|
-
const { session, storage, filter, id, timeoutMs, signal, send, url } = opts;
|
|
520
|
-
const deadline = Date.now() + timeoutMs;
|
|
521
|
-
const timedOut = () => new RelayPublishError("negentropy timed out", url);
|
|
522
|
-
const remainingMs = () => deadline - Date.now();
|
|
523
|
-
const next = () => {
|
|
524
|
-
if (session.error) return Promise.reject(session.error);
|
|
525
|
-
if (remainingMs() <= 0) return Promise.reject(timedOut());
|
|
526
|
-
const queued = session.queue.shift();
|
|
527
|
-
if (queued !== void 0) return Promise.resolve(queued);
|
|
528
|
-
return new Promise((resolve, reject) => {
|
|
529
|
-
const timer = setTimeout(() => {
|
|
530
|
-
session.waiter = void 0;
|
|
531
|
-
reject(timedOut());
|
|
532
|
-
}, remainingMs());
|
|
533
|
-
const fail = (err) => {
|
|
534
|
-
clearTimeout(timer);
|
|
535
|
-
session.waiter = void 0;
|
|
536
|
-
reject(err);
|
|
537
|
-
};
|
|
538
|
-
const onAbort = () => fail(new RelayConnectionError("negentropy aborted", url));
|
|
539
|
-
session.waiter = {
|
|
540
|
-
resolve: (hex) => {
|
|
541
|
-
clearTimeout(timer);
|
|
542
|
-
signal?.removeEventListener("abort", onAbort);
|
|
543
|
-
resolve(hex);
|
|
544
|
-
},
|
|
545
|
-
reject: (err) => fail(err)
|
|
546
|
-
};
|
|
547
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
548
|
-
});
|
|
549
|
-
};
|
|
550
|
-
return await runNegSession({
|
|
551
|
-
storage,
|
|
552
|
-
openingSend: (hex) => {
|
|
553
|
-
send([
|
|
554
|
-
"NEG-OPEN",
|
|
555
|
-
id,
|
|
556
|
-
filter,
|
|
557
|
-
hex
|
|
558
|
-
]);
|
|
559
|
-
},
|
|
560
|
-
msgSend: (hex) => {
|
|
561
|
-
send([
|
|
562
|
-
"NEG-MSG",
|
|
563
|
-
id,
|
|
564
|
-
hex
|
|
565
|
-
]);
|
|
566
|
-
},
|
|
567
|
-
next
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
const PING_DUMMY_FILTER = {
|
|
571
|
-
ids: ["a".repeat(64)],
|
|
572
|
-
limit: 0
|
|
573
|
-
};
|
|
574
|
-
/** Browser WebSocket and `ws` both use 1 for OPEN. */
|
|
575
|
-
const WS_OPEN = 1;
|
|
576
|
-
/** node `ws` delivers `pong` on the EventEmitter, not via addEventListener. */
|
|
577
|
-
function canNativePing(ws) {
|
|
578
|
-
return typeof ws.ping === "function" && (typeof ws.once === "function" || typeof ws.on === "function");
|
|
579
|
-
}
|
|
580
|
-
/** REQ-dummy or native ping/pong keepalive. Does not import class Relay. */
|
|
581
|
-
var PingLoop = class {
|
|
582
|
-
#opts;
|
|
583
|
-
#timer;
|
|
584
|
-
#waiters = /* @__PURE__ */ new Map();
|
|
585
|
-
#gen = 0;
|
|
586
|
-
#nativePing;
|
|
587
|
-
constructor(opts) {
|
|
588
|
-
this.#opts = opts;
|
|
589
|
-
}
|
|
590
|
-
start() {
|
|
591
|
-
this.stop();
|
|
592
|
-
this.#timer = setInterval(() => {
|
|
593
|
-
this.#pingpong();
|
|
594
|
-
}, this.#opts.pingIntervalMs);
|
|
595
|
-
}
|
|
596
|
-
stop() {
|
|
597
|
-
if (this.#timer !== void 0) {
|
|
598
|
-
clearInterval(this.#timer);
|
|
599
|
-
this.#timer = void 0;
|
|
600
|
-
}
|
|
601
|
-
this.#abortCurrentPing();
|
|
602
|
-
}
|
|
603
|
-
hasWaiter(id) {
|
|
604
|
-
return this.#waiters.has(id);
|
|
605
|
-
}
|
|
606
|
-
finishDummyPing(id) {
|
|
607
|
-
const waiter = this.#waiters.get(id);
|
|
608
|
-
if (!waiter) return false;
|
|
609
|
-
this.#waiters.delete(id);
|
|
610
|
-
waiter.resolve(true);
|
|
611
|
-
try {
|
|
612
|
-
this.#opts.send(["CLOSE", id]);
|
|
613
|
-
} catch {}
|
|
614
|
-
return true;
|
|
615
|
-
}
|
|
616
|
-
#abortCurrentPing() {
|
|
617
|
-
this.#gen += 1;
|
|
618
|
-
const native = this.#nativePing;
|
|
619
|
-
this.#nativePing = void 0;
|
|
620
|
-
native?.abort();
|
|
621
|
-
for (const waiter of this.#waiters.values()) waiter.resolve(false);
|
|
622
|
-
this.#waiters.clear();
|
|
623
|
-
}
|
|
624
|
-
async #pingpong() {
|
|
625
|
-
const ws = this.#opts.getWs();
|
|
626
|
-
if (!ws || ws.readyState !== WS_OPEN) return;
|
|
627
|
-
if (this.#nativePing || this.#waiters.size > 0) return;
|
|
628
|
-
const gen = this.#gen;
|
|
629
|
-
const ping = canNativePing(ws) ? this.#waitForNativePing(ws) : this.#waitForDummyPing();
|
|
630
|
-
const ok = await new Promise((resolve) => {
|
|
631
|
-
let settled = false;
|
|
632
|
-
const done = (alive) => {
|
|
633
|
-
if (settled) return;
|
|
634
|
-
settled = true;
|
|
635
|
-
clearTimeout(timer);
|
|
636
|
-
resolve(alive);
|
|
637
|
-
};
|
|
638
|
-
const timer = setTimeout(() => {
|
|
639
|
-
if (this.#nativePing || this.#waiters.size > 0) done(false);
|
|
640
|
-
}, this.#opts.pingTimeoutMs);
|
|
641
|
-
ping.then(done);
|
|
642
|
-
});
|
|
643
|
-
if (gen !== this.#gen) return;
|
|
644
|
-
if (!ok) {
|
|
645
|
-
this.#abortCurrentPing();
|
|
646
|
-
this.#opts.closeWs();
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
#waitForNativePing(ws) {
|
|
650
|
-
return new Promise((resolve) => {
|
|
651
|
-
let settled = false;
|
|
652
|
-
const finish = (alive) => {
|
|
653
|
-
if (settled) return;
|
|
654
|
-
settled = true;
|
|
655
|
-
this.#nativePing = void 0;
|
|
656
|
-
ws.off?.("pong", onPong);
|
|
657
|
-
resolve(alive);
|
|
658
|
-
};
|
|
659
|
-
const onPong = () => finish(true);
|
|
660
|
-
this.#nativePing = { abort: () => finish(false) };
|
|
661
|
-
if (typeof ws.on === "function" && typeof ws.off === "function") ws.on("pong", onPong);
|
|
662
|
-
else if (typeof ws.once === "function") ws.once("pong", onPong);
|
|
663
|
-
else ws.on("pong", onPong);
|
|
664
|
-
try {
|
|
665
|
-
ws.ping();
|
|
666
|
-
} catch {
|
|
667
|
-
finish(false);
|
|
668
|
-
}
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
#waitForDummyPing() {
|
|
672
|
-
return new Promise((resolve) => {
|
|
673
|
-
const id = this.#opts.nextSubId("__ping__");
|
|
674
|
-
this.#waiters.set(id, { resolve });
|
|
675
|
-
try {
|
|
676
|
-
this.#opts.send([
|
|
677
|
-
"REQ",
|
|
678
|
-
id,
|
|
679
|
-
PING_DUMMY_FILTER
|
|
680
|
-
]);
|
|
681
|
-
} catch {
|
|
682
|
-
this.#waiters.delete(id);
|
|
683
|
-
resolve(false);
|
|
684
|
-
}
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
};
|
|
688
|
-
//#endregion
|
|
689
697
|
//#region src/relay/relay.ts
|
|
698
|
+
/** Relay lifecycle states. */
|
|
690
699
|
const RelayStatus = {
|
|
691
700
|
Initialized: "initialized",
|
|
692
701
|
Connecting: "connecting",
|
|
@@ -703,9 +712,10 @@ const DEFAULT_BACKOFF = [
|
|
|
703
712
|
3e4,
|
|
704
713
|
6e4
|
|
705
714
|
];
|
|
715
|
+
const noop = () => {};
|
|
706
716
|
/**
|
|
707
|
-
* Single-relay NIP-01 client.
|
|
708
|
-
*
|
|
717
|
+
* Single-relay NIP-01 client. Connection lifecycle, REQ/CLOSE, EVENT publish ACK, AUTH, COUNT,
|
|
718
|
+
* NIP-77, optional reconnect.
|
|
709
719
|
*/
|
|
710
720
|
var Relay = class Relay {
|
|
711
721
|
url;
|
|
@@ -734,6 +744,10 @@ var Relay = class Relay {
|
|
|
734
744
|
#serial = 0;
|
|
735
745
|
#challenge;
|
|
736
746
|
#authedChallenge;
|
|
747
|
+
/** Challenge value already answered on this connection; duplicates are not re-signed. */
|
|
748
|
+
#answeredChallenge;
|
|
749
|
+
/** Settled OK verdict for `#answeredChallenge`; set only once the relay replies. */
|
|
750
|
+
#answeredResult;
|
|
737
751
|
#authPromise;
|
|
738
752
|
#authSigner;
|
|
739
753
|
#intentionalClose = false;
|
|
@@ -744,12 +758,12 @@ var Relay = class Relay {
|
|
|
744
758
|
#deathHandled = false;
|
|
745
759
|
#enablePing;
|
|
746
760
|
#ping;
|
|
747
|
-
onnotice
|
|
748
|
-
onclose
|
|
761
|
+
onnotice;
|
|
762
|
+
onclose;
|
|
749
763
|
/** Fired when the relay sends a NIP-42 AUTH challenge. */
|
|
750
|
-
onauth
|
|
764
|
+
onauth;
|
|
751
765
|
/** Fired after a successful reconnect (not the initial connect). */
|
|
752
|
-
onreconnect
|
|
766
|
+
onreconnect;
|
|
753
767
|
constructor(url, opts = {}) {
|
|
754
768
|
this.url = normalizeURL(url);
|
|
755
769
|
this.#WS = opts.websocketImplementation ?? getWebSocketImplementation();
|
|
@@ -805,6 +819,10 @@ var Relay = class Relay {
|
|
|
805
819
|
get subscriptionCount() {
|
|
806
820
|
return this.#subs.size;
|
|
807
821
|
}
|
|
822
|
+
/** One-shot requests still awaiting a relay reply (EVENT ACK, COUNT, NEG). */
|
|
823
|
+
get inFlightCount() {
|
|
824
|
+
return this.#publishes.size + this.#counts.size + this.#neg.size;
|
|
825
|
+
}
|
|
808
826
|
static async connect(url, opts) {
|
|
809
827
|
const relay = new Relay(url, opts);
|
|
810
828
|
await relay.connect({ signal: opts?.signal });
|
|
@@ -812,7 +830,11 @@ var Relay = class Relay {
|
|
|
812
830
|
}
|
|
813
831
|
async connect(opts) {
|
|
814
832
|
if (this.#connected) return;
|
|
815
|
-
if (this.#connecting)
|
|
833
|
+
if (this.#connecting) {
|
|
834
|
+
await raceSignal(this.#connecting, opts?.signal);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
throwIfAborted(opts?.signal);
|
|
816
838
|
const gen = ++this.#gen;
|
|
817
839
|
this.#intentionalClose = false;
|
|
818
840
|
this.#skipReconnect = false;
|
|
@@ -821,84 +843,64 @@ var Relay = class Relay {
|
|
|
821
843
|
this.#status = RelayStatus.Connecting;
|
|
822
844
|
const timeoutMs = opts?.timeoutMs ?? this.#connectTimeoutMs;
|
|
823
845
|
const isReconnect = this.#reconnectAttempts > 0;
|
|
824
|
-
let resolveConnect =
|
|
825
|
-
let rejectConnect =
|
|
846
|
+
let resolveConnect = noop;
|
|
847
|
+
let rejectConnect = noop;
|
|
826
848
|
let settled = false;
|
|
827
|
-
let timer;
|
|
828
849
|
let ws;
|
|
829
|
-
let handlers;
|
|
830
850
|
const connecting = new Promise((resolve, reject) => {
|
|
831
851
|
resolveConnect = resolve;
|
|
832
852
|
rejectConnect = reject;
|
|
833
853
|
});
|
|
834
854
|
const release = () => {
|
|
835
|
-
if (
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
if (this.#socketHandlers === handlers) this.#socketHandlers = void 0;
|
|
844
|
-
}
|
|
855
|
+
if (ws === void 0) return;
|
|
856
|
+
try {
|
|
857
|
+
ws.removeEventListener("open", handlers.onOpen);
|
|
858
|
+
ws.removeEventListener("error", handlers.onError);
|
|
859
|
+
ws.removeEventListener("close", handlers.onClose);
|
|
860
|
+
ws.removeEventListener("message", handlers.onMessage);
|
|
861
|
+
} catch {}
|
|
862
|
+
if (this.#socketHandlers === handlers) this.#socketHandlers = void 0;
|
|
845
863
|
try {
|
|
846
864
|
if (ws.readyState !== this.#WS.CLOSED && ws.readyState !== this.#WS.CLOSING) ws.close();
|
|
847
865
|
} catch {}
|
|
848
866
|
if (this.#ws === ws) this.#ws = void 0;
|
|
849
867
|
};
|
|
850
|
-
const onAbort = () => {
|
|
851
|
-
if (gen !== this.#gen) {
|
|
852
|
-
release();
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
this.#intentionalClose = true;
|
|
856
|
-
this.#skipReconnect = true;
|
|
857
|
-
release();
|
|
858
|
-
finish(new RelayConnectionError("connection aborted", this.url));
|
|
859
|
-
if (gen === this.#gen) this.#handleSocketDeath("connection aborted", {
|
|
860
|
-
fromConnectAttempt: true,
|
|
861
|
-
gen
|
|
862
|
-
});
|
|
863
|
-
};
|
|
864
868
|
const finish = (err) => {
|
|
865
869
|
if (settled) return;
|
|
866
870
|
settled = true;
|
|
867
|
-
|
|
871
|
+
clearTimeout(timer);
|
|
868
872
|
if (this.#connectTimer === timer) this.#connectTimer = void 0;
|
|
869
|
-
opts?.signal?.removeEventListener("abort", onAbort);
|
|
870
873
|
if (this.#connectFinish === finish) this.#connectFinish = void 0;
|
|
871
874
|
if (this.#connecting === connecting) this.#connecting = void 0;
|
|
872
|
-
if (err
|
|
873
|
-
else
|
|
875
|
+
if (err === void 0) resolveConnect();
|
|
876
|
+
else rejectConnect(err);
|
|
874
877
|
};
|
|
875
878
|
this.#connecting = connecting;
|
|
876
879
|
this.#connectFinish = finish;
|
|
877
|
-
timer = setTimeout(() => {
|
|
880
|
+
const timer = setTimeout(() => {
|
|
878
881
|
if (gen !== this.#gen) {
|
|
879
882
|
release();
|
|
880
883
|
return;
|
|
881
884
|
}
|
|
882
885
|
if (!isReconnect && !this.#enableReconnect) this.#skipReconnect = true;
|
|
883
886
|
release();
|
|
884
|
-
finish(new
|
|
887
|
+
finish(new RelayTimeoutError("connection timed out", this.url));
|
|
885
888
|
if (gen === this.#gen) this.#handleSocketDeath("connection timed out", {
|
|
886
889
|
fromConnectAttempt: true,
|
|
887
890
|
gen
|
|
888
891
|
});
|
|
889
892
|
}, timeoutMs);
|
|
890
893
|
this.#connectTimer = timer;
|
|
891
|
-
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
892
894
|
try {
|
|
893
895
|
ws = new this.#WS(this.url);
|
|
894
|
-
} catch (
|
|
896
|
+
} catch (error) {
|
|
895
897
|
if (!isReconnect && !this.#enableReconnect) this.#skipReconnect = true;
|
|
896
|
-
finish(
|
|
898
|
+
finish(error);
|
|
897
899
|
if (gen === this.#gen) this.#handleSocketDeath("connection failed", {
|
|
898
900
|
fromConnectAttempt: true,
|
|
899
901
|
gen
|
|
900
902
|
});
|
|
901
|
-
await connecting;
|
|
903
|
+
await raceSignal(connecting, opts?.signal);
|
|
902
904
|
return;
|
|
903
905
|
}
|
|
904
906
|
this.#ws = ws;
|
|
@@ -914,6 +916,8 @@ var Relay = class Relay {
|
|
|
914
916
|
this.#challenge = void 0;
|
|
915
917
|
this.#authPromise = void 0;
|
|
916
918
|
this.#authedChallenge = void 0;
|
|
919
|
+
this.#answeredChallenge = void 0;
|
|
920
|
+
this.#answeredResult = void 0;
|
|
917
921
|
if (!resubscribeAll(this.#live)) {
|
|
918
922
|
this.#connected = false;
|
|
919
923
|
this.#status = RelayStatus.Disconnected;
|
|
@@ -926,7 +930,7 @@ var Relay = class Relay {
|
|
|
926
930
|
this.#reconnectAttempts = 0;
|
|
927
931
|
if (this.#enablePing) this.#ping.start();
|
|
928
932
|
else this.#ping.stop();
|
|
929
|
-
if (wasReconnect) this.onreconnect?.();
|
|
933
|
+
if (wasReconnect) invokeSafely(() => this.onreconnect?.());
|
|
930
934
|
finish();
|
|
931
935
|
};
|
|
932
936
|
const onError = () => {
|
|
@@ -964,7 +968,7 @@ var Relay = class Relay {
|
|
|
964
968
|
const data = typeof ev === "object" && ev !== null && "data" in ev ? ev.data : ev;
|
|
965
969
|
this.#onMessage(String(data));
|
|
966
970
|
};
|
|
967
|
-
handlers = {
|
|
971
|
+
const handlers = {
|
|
968
972
|
onOpen,
|
|
969
973
|
onError,
|
|
970
974
|
onClose,
|
|
@@ -976,7 +980,7 @@ var Relay = class Relay {
|
|
|
976
980
|
ws.addEventListener("error", onError);
|
|
977
981
|
ws.addEventListener("close", onClose);
|
|
978
982
|
ws.addEventListener("message", onMessage);
|
|
979
|
-
await connecting;
|
|
983
|
+
await raceSignal(connecting, opts?.signal);
|
|
980
984
|
}
|
|
981
985
|
#detachSocketHandlers() {
|
|
982
986
|
const h = this.#socketHandlers;
|
|
@@ -1011,7 +1015,7 @@ var Relay = class Relay {
|
|
|
1011
1015
|
this.#detachSocketHandlers();
|
|
1012
1016
|
this.#teardownSocket();
|
|
1013
1017
|
this.#connected = false;
|
|
1014
|
-
this.onclose?.();
|
|
1018
|
+
invokeSafely(() => this.onclose?.());
|
|
1015
1019
|
}
|
|
1016
1020
|
}
|
|
1017
1021
|
#teardownSocket() {
|
|
@@ -1044,9 +1048,7 @@ var Relay = class Relay {
|
|
|
1044
1048
|
for (const session of this.#neg.values()) failNegSession(session, err);
|
|
1045
1049
|
this.#neg.clear();
|
|
1046
1050
|
}
|
|
1047
|
-
/**
|
|
1048
|
-
* Unexpected socket death. Keep subscriptions if reconnecting.
|
|
1049
|
-
*/
|
|
1051
|
+
/** Unexpected socket death. Keep subscriptions if reconnecting. */
|
|
1050
1052
|
#handleSocketDeath(reason, opts) {
|
|
1051
1053
|
if (opts.gen !== this.#gen) return;
|
|
1052
1054
|
if (this.#deathHandled) return;
|
|
@@ -1064,9 +1066,9 @@ var Relay = class Relay {
|
|
|
1064
1066
|
return;
|
|
1065
1067
|
}
|
|
1066
1068
|
if (!this.#intentionalClose) this.#status = RelayStatus.Disconnected;
|
|
1067
|
-
if (
|
|
1069
|
+
if (opts.fromConnectAttempt !== true || this.#subs.size > 0) {
|
|
1068
1070
|
closeAllSubscriptions(this.#live, reason);
|
|
1069
|
-
if (!this.#intentionalClose) this.onclose?.();
|
|
1071
|
+
if (!this.#intentionalClose) invokeSafely(() => this.onclose?.());
|
|
1070
1072
|
}
|
|
1071
1073
|
this.#status = RelayStatus.Closed;
|
|
1072
1074
|
}
|
|
@@ -1078,9 +1080,13 @@ var Relay = class Relay {
|
|
|
1078
1080
|
this.#reconnectTimer = setTimeout(() => {
|
|
1079
1081
|
this.#reconnectTimer = void 0;
|
|
1080
1082
|
if (this.#intentionalClose || this.#connected) return;
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1083
|
+
(async () => {
|
|
1084
|
+
try {
|
|
1085
|
+
await this.connect();
|
|
1086
|
+
} catch {
|
|
1087
|
+
if (this.#enableReconnect && !this.#intentionalClose && !this.#skipReconnect && this.#subs.size > 0 && !this.#connected) this.#scheduleReconnect();
|
|
1088
|
+
}
|
|
1089
|
+
})();
|
|
1084
1090
|
}, delay);
|
|
1085
1091
|
}
|
|
1086
1092
|
#send(message) {
|
|
@@ -1135,7 +1141,7 @@ var Relay = class Relay {
|
|
|
1135
1141
|
const [, eventId, ok, message] = msg;
|
|
1136
1142
|
const waiter = this.#publishes.get(eventId);
|
|
1137
1143
|
if (!waiter) return;
|
|
1138
|
-
if (!ok && isAuthRequired(message) && waiter.event &&
|
|
1144
|
+
if (!ok && isAuthRequired(message) && waiter.event !== void 0 && waiter.authRetried !== true && this.#authSigner) {
|
|
1139
1145
|
waiter.authRetried = true;
|
|
1140
1146
|
this.#authThenRepublish(waiter, eventId, message);
|
|
1141
1147
|
return;
|
|
@@ -1169,14 +1175,23 @@ var Relay = class Relay {
|
|
|
1169
1175
|
failNegErr(session, reason);
|
|
1170
1176
|
break;
|
|
1171
1177
|
}
|
|
1172
|
-
case "NOTICE":
|
|
1173
|
-
|
|
1178
|
+
case "NOTICE": {
|
|
1179
|
+
const [, notice] = msg;
|
|
1180
|
+
invokeSafely(() => this.onnotice?.(notice));
|
|
1174
1181
|
break;
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
this.#
|
|
1179
|
-
|
|
1182
|
+
}
|
|
1183
|
+
case "AUTH": {
|
|
1184
|
+
const [, authChallenge] = msg;
|
|
1185
|
+
if (authChallenge !== this.#challenge) {
|
|
1186
|
+
this.#authPromise = void 0;
|
|
1187
|
+
this.#authedChallenge = void 0;
|
|
1188
|
+
this.#answeredChallenge = void 0;
|
|
1189
|
+
this.#answeredResult = void 0;
|
|
1190
|
+
}
|
|
1191
|
+
this.#challenge = authChallenge;
|
|
1192
|
+
invokeSafely(() => this.onauth?.(authChallenge));
|
|
1193
|
+
break;
|
|
1194
|
+
}
|
|
1180
1195
|
}
|
|
1181
1196
|
}
|
|
1182
1197
|
/** Low-level REQ with callbacks. Survives reconnect when enableReconnect is on. */
|
|
@@ -1191,27 +1206,25 @@ var Relay = class Relay {
|
|
|
1191
1206
|
if (this.#verifyDead) return false;
|
|
1192
1207
|
try {
|
|
1193
1208
|
return this.#verify(event);
|
|
1194
|
-
} catch (
|
|
1195
|
-
if (
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
if (error instanceof WasmVerifyPoisonedError || error instanceof WebAssembly.RuntimeError) {
|
|
1196
1211
|
this.#verifyDead = true;
|
|
1197
|
-
this.onnotice?.("verify-poisoned: wasm instance aborted");
|
|
1212
|
+
invokeSafely(() => this.onnotice?.("verify-poisoned: wasm instance aborted"));
|
|
1198
1213
|
return false;
|
|
1199
1214
|
}
|
|
1200
|
-
throw
|
|
1215
|
+
throw error;
|
|
1201
1216
|
}
|
|
1202
1217
|
}
|
|
1203
1218
|
/** AsyncIterable of events for filters until the subscription is closed. */
|
|
1204
1219
|
stream(filters, opts) {
|
|
1205
1220
|
return streamFilters((f, o) => this.subscribe(f, o), filters, opts);
|
|
1206
1221
|
}
|
|
1207
|
-
/**
|
|
1208
|
-
* One-shot query: collect events until EOSE or timeout, then close.
|
|
1209
|
-
*/
|
|
1222
|
+
/** One-shot query: collect events until EOSE or timeout, then close. */
|
|
1210
1223
|
async fetch(filters, opts) {
|
|
1211
1224
|
if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
|
|
1212
|
-
|
|
1225
|
+
const canonical = canonicalizeFilters(filters);
|
|
1213
1226
|
if (!this.#connected) await this.connect({ signal: opts?.signal });
|
|
1214
|
-
return fetchFilters((f, o) => this.subscribe(f, o),
|
|
1227
|
+
return fetchFilters((f, o) => this.subscribe(f, o), canonical, {
|
|
1215
1228
|
timeoutMs: opts?.timeoutMs ?? 4400,
|
|
1216
1229
|
signal: opts?.signal,
|
|
1217
1230
|
id: opts?.id,
|
|
@@ -1225,7 +1238,7 @@ var Relay = class Relay {
|
|
|
1225
1238
|
return await new Promise((resolve, reject) => {
|
|
1226
1239
|
const timer = setTimeout(() => {
|
|
1227
1240
|
this.#publishes.delete(event.id);
|
|
1228
|
-
reject(new
|
|
1241
|
+
reject(new RelayTimeoutError("publish timed out", this.url));
|
|
1229
1242
|
}, timeoutMs);
|
|
1230
1243
|
this.#publishes.set(event.id, {
|
|
1231
1244
|
resolve,
|
|
@@ -1236,25 +1249,25 @@ var Relay = class Relay {
|
|
|
1236
1249
|
});
|
|
1237
1250
|
try {
|
|
1238
1251
|
this.#send(["EVENT", event]);
|
|
1239
|
-
} catch (
|
|
1252
|
+
} catch (error) {
|
|
1240
1253
|
clearTimeout(timer);
|
|
1241
1254
|
this.#publishes.delete(event.id);
|
|
1242
|
-
reject(
|
|
1255
|
+
reject(error instanceof Error ? error : new RelayPublishError("publish failed", this.url));
|
|
1243
1256
|
}
|
|
1244
1257
|
});
|
|
1245
1258
|
}
|
|
1246
1259
|
/**
|
|
1247
|
-
* NIP-45 COUNT: ask the relay how many events match `filters`.
|
|
1248
|
-
*
|
|
1260
|
+
* NIP-45 COUNT: ask the relay how many events match `filters`. Resolves with the COUNT payload
|
|
1261
|
+
* (count / optional approximate / optional hll).
|
|
1249
1262
|
*/
|
|
1250
1263
|
async count(filters, opts) {
|
|
1251
1264
|
if (!this.#connected) throw new RelayClosedError("not connected", this.url);
|
|
1252
1265
|
if (filters.length === 0) throw new RelayError("COUNT requires at least one filter", this.url);
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
const id = opts?.id
|
|
1266
|
+
throwIfAborted(opts?.signal);
|
|
1267
|
+
const canonical = canonicalizeFilters(filters);
|
|
1268
|
+
const id = opts?.id === void 0 ? this.nextSubId("count") : createSubscriptionId(opts.id);
|
|
1256
1269
|
const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
|
|
1257
|
-
return
|
|
1270
|
+
return new Promise((resolve, reject) => {
|
|
1258
1271
|
const cleanup = () => {
|
|
1259
1272
|
opts?.signal?.removeEventListener("abort", onAbort);
|
|
1260
1273
|
};
|
|
@@ -1274,13 +1287,15 @@ var Relay = class Relay {
|
|
|
1274
1287
|
reject(err);
|
|
1275
1288
|
},
|
|
1276
1289
|
timer: void 0,
|
|
1277
|
-
filters: [...
|
|
1290
|
+
filters: [...canonical],
|
|
1278
1291
|
authRetried: false,
|
|
1279
1292
|
timeoutMs
|
|
1280
1293
|
};
|
|
1281
|
-
const onAbort = () =>
|
|
1294
|
+
const onAbort = () => {
|
|
1295
|
+
if (opts?.signal) waiter.reject(abortReason(opts.signal));
|
|
1296
|
+
};
|
|
1282
1297
|
waiter.timer = setTimeout(() => {
|
|
1283
|
-
waiter.reject(new
|
|
1298
|
+
waiter.reject(new RelayTimeoutError("count timed out", this.url));
|
|
1284
1299
|
}, timeoutMs);
|
|
1285
1300
|
this.#counts.set(id, waiter);
|
|
1286
1301
|
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
@@ -1288,41 +1303,56 @@ var Relay = class Relay {
|
|
|
1288
1303
|
this.#send([
|
|
1289
1304
|
"COUNT",
|
|
1290
1305
|
id,
|
|
1291
|
-
...
|
|
1306
|
+
...canonical
|
|
1292
1307
|
]);
|
|
1293
|
-
} catch (
|
|
1294
|
-
waiter.reject(
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
waiter.reject(error instanceof Error ? error : new RelayPublishError("count failed", this.url));
|
|
1295
1310
|
}
|
|
1296
1311
|
});
|
|
1297
1312
|
}
|
|
1298
|
-
/**
|
|
1299
|
-
* NIP-42 AUTH: sign the current challenge and wait for OK.
|
|
1300
|
-
*/
|
|
1313
|
+
/** NIP-42 AUTH: sign the current challenge and wait for OK. */
|
|
1301
1314
|
async auth(sign, opts) {
|
|
1302
1315
|
const challenge = this.#challenge;
|
|
1303
|
-
if (
|
|
1316
|
+
if (challenge === void 0 || challenge === "") throw new RelayError("no AUTH challenge received from relay", this.url);
|
|
1304
1317
|
if (this.#authPromise) return this.#authPromise;
|
|
1318
|
+
if (this.#answeredChallenge === challenge && this.#answeredResult !== void 0) return this.#answeredResult;
|
|
1305
1319
|
const pending = (async () => {
|
|
1306
|
-
const
|
|
1320
|
+
const template = makeAuthEvent(this.url, challenge);
|
|
1321
|
+
let event;
|
|
1322
|
+
try {
|
|
1323
|
+
event = await sign(template);
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
if (error instanceof NoSignerError) return {
|
|
1326
|
+
ok: false,
|
|
1327
|
+
message: "auth: no signer"
|
|
1328
|
+
};
|
|
1329
|
+
throw error;
|
|
1330
|
+
}
|
|
1307
1331
|
if (!this.#connected) throw new RelayClosedError("not connected", this.url);
|
|
1308
1332
|
const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
|
|
1309
|
-
return
|
|
1333
|
+
return new Promise((resolve, reject) => {
|
|
1310
1334
|
const timer = setTimeout(() => {
|
|
1311
1335
|
this.#publishes.delete(event.id);
|
|
1312
|
-
reject(new
|
|
1336
|
+
reject(new RelayTimeoutError("auth timed out", this.url));
|
|
1313
1337
|
}, timeoutMs);
|
|
1314
1338
|
this.#publishes.set(event.id, {
|
|
1315
|
-
resolve
|
|
1339
|
+
resolve: (result) => {
|
|
1340
|
+
if (this.#challenge === challenge) {
|
|
1341
|
+
this.#answeredChallenge = challenge;
|
|
1342
|
+
this.#answeredResult = result;
|
|
1343
|
+
}
|
|
1344
|
+
resolve(result);
|
|
1345
|
+
},
|
|
1316
1346
|
reject,
|
|
1317
1347
|
timer,
|
|
1318
1348
|
timeoutMs
|
|
1319
1349
|
});
|
|
1320
1350
|
try {
|
|
1321
1351
|
this.#send(["AUTH", event]);
|
|
1322
|
-
} catch (
|
|
1352
|
+
} catch (error) {
|
|
1323
1353
|
clearTimeout(timer);
|
|
1324
1354
|
this.#publishes.delete(event.id);
|
|
1325
|
-
reject(
|
|
1355
|
+
reject(error instanceof Error ? error : new RelayPublishError("auth send failed", this.url));
|
|
1326
1356
|
}
|
|
1327
1357
|
});
|
|
1328
1358
|
})();
|
|
@@ -1335,17 +1365,32 @@ var Relay = class Relay {
|
|
|
1335
1365
|
if (this.#authPromise === pending) this.#authPromise = void 0;
|
|
1336
1366
|
}
|
|
1337
1367
|
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Clear a cached AUTH rejection for the current challenge so a later `auth()` signs again. A
|
|
1370
|
+
* successful auth and in-flight answers are kept. When a challenge is pending and unanswered,
|
|
1371
|
+
* `onauth` is re-fired so the pool's automatic auth can run once more.
|
|
1372
|
+
*/
|
|
1373
|
+
resetAuth() {
|
|
1374
|
+
if (this.#answeredResult !== void 0 && !this.#answeredResult.ok) {
|
|
1375
|
+
this.#answeredChallenge = void 0;
|
|
1376
|
+
this.#answeredResult = void 0;
|
|
1377
|
+
}
|
|
1378
|
+
if (this.#challenge !== void 0 && this.#challenge !== this.#authedChallenge) {
|
|
1379
|
+
const challenge = this.#challenge;
|
|
1380
|
+
invokeSafely(() => this.onauth?.(challenge));
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1338
1383
|
async #ensureAuthed() {
|
|
1339
1384
|
const signer = this.#authSigner;
|
|
1340
1385
|
if (!signer) return false;
|
|
1341
1386
|
for (let i = 0; i < 3; i++) {
|
|
1342
|
-
if (
|
|
1387
|
+
if (this.#challenge === void 0 || this.#challenge === "") return false;
|
|
1343
1388
|
if (this.#authedChallenge === this.#challenge) return true;
|
|
1344
1389
|
const signed = this.#challenge;
|
|
1345
1390
|
const result = await this.auth(signer);
|
|
1346
1391
|
if (this.#authedChallenge === this.#challenge) return true;
|
|
1347
1392
|
if (!result.ok) {
|
|
1348
|
-
if (
|
|
1393
|
+
if (this.#challenge === void 0 || this.#challenge === "" || this.#challenge === signed) return false;
|
|
1349
1394
|
continue;
|
|
1350
1395
|
}
|
|
1351
1396
|
}
|
|
@@ -1393,16 +1438,16 @@ var Relay = class Relay {
|
|
|
1393
1438
|
return;
|
|
1394
1439
|
}
|
|
1395
1440
|
waiter.timer = setTimeout(() => {
|
|
1396
|
-
waiter.reject(new
|
|
1441
|
+
waiter.reject(new RelayTimeoutError("count timed out", this.url));
|
|
1397
1442
|
}, waiter.timeoutMs);
|
|
1398
1443
|
this.#send([
|
|
1399
1444
|
"COUNT",
|
|
1400
1445
|
id,
|
|
1401
1446
|
...waiter.filters
|
|
1402
1447
|
]);
|
|
1403
|
-
} catch (
|
|
1448
|
+
} catch (error) {
|
|
1404
1449
|
if (!this.#counts.has(id)) return;
|
|
1405
|
-
waiter.reject(
|
|
1450
|
+
waiter.reject(error instanceof Error ? error : new RelayClosedError(reason || "COUNT closed", this.url));
|
|
1406
1451
|
}
|
|
1407
1452
|
}
|
|
1408
1453
|
async #authThenRepublish(waiter, eventId, message) {
|
|
@@ -1441,7 +1486,7 @@ var Relay = class Relay {
|
|
|
1441
1486
|
}
|
|
1442
1487
|
waiter.timer = setTimeout(() => {
|
|
1443
1488
|
this.#publishes.delete(eventId);
|
|
1444
|
-
waiter.reject(new
|
|
1489
|
+
waiter.reject(new RelayTimeoutError("publish timed out", this.url));
|
|
1445
1490
|
}, waiter.timeoutMs);
|
|
1446
1491
|
this.#send(["EVENT", waiter.event]);
|
|
1447
1492
|
} catch {
|
|
@@ -1453,19 +1498,17 @@ var Relay = class Relay {
|
|
|
1453
1498
|
}
|
|
1454
1499
|
}
|
|
1455
1500
|
/**
|
|
1456
|
-
* NIP-77: run Negentropy reconciliation against this relay.
|
|
1457
|
-
*
|
|
1458
|
-
* Does not upload or download events.
|
|
1501
|
+
* NIP-77: run Negentropy reconciliation against this relay. Returns the local-only (`have`) and
|
|
1502
|
+
* remote-only (`need`) event ids. Does not upload or download events.
|
|
1459
1503
|
*
|
|
1460
|
-
* `timeoutMs` is a single wall-clock deadline for the whole session
|
|
1461
|
-
*
|
|
1462
|
-
* Default: {@link RelayOptions.publishTimeoutMs}.
|
|
1504
|
+
* `timeoutMs` is a single wall-clock deadline for the whole session (`NEG-OPEN` through the last
|
|
1505
|
+
* `NEG-MSG`), not a per-message budget. Default: {@link RelayOptions.publishTimeoutMs}.
|
|
1463
1506
|
*/
|
|
1464
1507
|
async negReconcile(filter, storage, opts) {
|
|
1465
1508
|
if (!this.#connected) throw new RelayClosedError("not connected", this.url);
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
const id = opts?.id
|
|
1509
|
+
throwIfAborted(opts?.signal);
|
|
1510
|
+
const canonical = canonicalizeFilter(filter);
|
|
1511
|
+
const id = opts?.id === void 0 ? this.nextSubId("neg") : assertSubscriptionId(opts.id);
|
|
1469
1512
|
const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
|
|
1470
1513
|
const prev = this.#neg.get(id);
|
|
1471
1514
|
if (prev) failNegSession(prev, new Nip77Error("closed: replaced by new NEG-OPEN"));
|
|
@@ -1475,7 +1518,7 @@ var Relay = class Relay {
|
|
|
1475
1518
|
return await runWiredNegSession({
|
|
1476
1519
|
session,
|
|
1477
1520
|
storage,
|
|
1478
|
-
filter,
|
|
1521
|
+
filter: canonical,
|
|
1479
1522
|
id,
|
|
1480
1523
|
timeoutMs,
|
|
1481
1524
|
signal: opts?.signal,
|
|
@@ -1500,9 +1543,8 @@ var Relay = class Relay {
|
|
|
1500
1543
|
//#endregion
|
|
1501
1544
|
//#region src/relay/fan-in.ts
|
|
1502
1545
|
/**
|
|
1503
|
-
* Aggregate EOSE/close across routed jobs. One `seen` set.
|
|
1504
|
-
* `
|
|
1505
|
-
* `pendingEose` counts unique `jobIndex:url` keys.
|
|
1546
|
+
* Aggregate EOSE/close across routed jobs. One `seen` set. `pending` counts URL list entries
|
|
1547
|
+
* (duplicates included). `pendingEose` counts unique `jobIndex:url` keys.
|
|
1506
1548
|
*/
|
|
1507
1549
|
function fanIn(pool, jobs, opts = {}) {
|
|
1508
1550
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1521,7 +1563,7 @@ function fanIn(pool, jobs, opts = {}) {
|
|
|
1521
1563
|
clearTimeout(eoseTimer);
|
|
1522
1564
|
eoseTimer = void 0;
|
|
1523
1565
|
}
|
|
1524
|
-
opts.oneose?.();
|
|
1566
|
+
invokeSafely(() => opts.oneose?.());
|
|
1525
1567
|
};
|
|
1526
1568
|
const markEose = (jobIndex, url) => {
|
|
1527
1569
|
const key = `${jobIndex}:${url}`;
|
|
@@ -1541,24 +1583,39 @@ function fanIn(pool, jobs, opts = {}) {
|
|
|
1541
1583
|
if (closed) return;
|
|
1542
1584
|
settleClose();
|
|
1543
1585
|
for (const c of closers) c.close(reason);
|
|
1544
|
-
opts.onclose?.(reason ?? "closed by client");
|
|
1586
|
+
invokeSafely(() => opts.onclose?.(reason ?? "closed by client"));
|
|
1545
1587
|
};
|
|
1546
|
-
if (opts.signal?.aborted) {
|
|
1588
|
+
if (opts.signal?.aborted === true) {
|
|
1547
1589
|
closed = true;
|
|
1548
|
-
opts.onclose?.("aborted");
|
|
1590
|
+
invokeSafely(() => opts.onclose?.("aborted"));
|
|
1549
1591
|
return { close: closeAll };
|
|
1550
1592
|
}
|
|
1551
1593
|
opts.signal?.addEventListener("abort", () => closeAll("aborted"), { once: true });
|
|
1594
|
+
const failUrl = (jobIndex, key) => {
|
|
1595
|
+
markEose(jobIndex, key);
|
|
1596
|
+
pending -= 1;
|
|
1597
|
+
if (pending <= 0 && closers.length === 0 && !closed) {
|
|
1598
|
+
settleClose();
|
|
1599
|
+
invokeSafely(() => opts.onclose?.("all relays failed"));
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1552
1602
|
const attach = (relay, job, jobIndex) => {
|
|
1553
1603
|
if (closed) return;
|
|
1604
|
+
const received = opts.receivedEvent;
|
|
1554
1605
|
const sub = relay.subscribe([...job.filters], {
|
|
1555
1606
|
id: jobs.length === 1 ? job.id : void 0,
|
|
1556
1607
|
closeOnEose: opts.closeOnEose,
|
|
1557
|
-
alreadyHaveEvent: (id) =>
|
|
1558
|
-
|
|
1608
|
+
alreadyHaveEvent: (id) => {
|
|
1609
|
+
let have = seen.has(id);
|
|
1610
|
+
invokeSafely(() => {
|
|
1611
|
+
have = have || Boolean(opts.alreadyHaveEvent?.(id));
|
|
1612
|
+
});
|
|
1613
|
+
return have;
|
|
1614
|
+
},
|
|
1615
|
+
receivedEvent: received === void 0 ? void 0 : (id) => invokeSafely(() => received(id, relay.url)),
|
|
1559
1616
|
onevent: (event) => {
|
|
1560
1617
|
seen.add(event.id);
|
|
1561
|
-
opts.onevent?.(event);
|
|
1618
|
+
invokeSafely(() => opts.onevent?.(event, relay.url));
|
|
1562
1619
|
},
|
|
1563
1620
|
oneose: () => markEose(jobIndex, relay.url),
|
|
1564
1621
|
onclose: (reason) => {
|
|
@@ -1566,14 +1623,14 @@ function fanIn(pool, jobs, opts = {}) {
|
|
|
1566
1623
|
pending -= 1;
|
|
1567
1624
|
if (pending <= 0 && !closed) {
|
|
1568
1625
|
settleClose();
|
|
1569
|
-
opts.onclose?.(reason);
|
|
1626
|
+
invokeSafely(() => opts.onclose?.(reason));
|
|
1570
1627
|
}
|
|
1571
1628
|
}
|
|
1572
1629
|
});
|
|
1573
1630
|
closers.push(sub);
|
|
1574
1631
|
};
|
|
1575
|
-
for (
|
|
1576
|
-
const
|
|
1632
|
+
for (const [jobIndex, job] of jobs.entries()) {
|
|
1633
|
+
const jobUrls = /* @__PURE__ */ new Set();
|
|
1577
1634
|
for (const url of job.urls) {
|
|
1578
1635
|
let key;
|
|
1579
1636
|
try {
|
|
@@ -1581,42 +1638,43 @@ function fanIn(pool, jobs, opts = {}) {
|
|
|
1581
1638
|
} catch {
|
|
1582
1639
|
key = url;
|
|
1583
1640
|
}
|
|
1641
|
+
if (jobUrls.has(key)) continue;
|
|
1642
|
+
jobUrls.add(key);
|
|
1584
1643
|
pending += 1;
|
|
1585
1644
|
const eoseKey = `${jobIndex}:${key}`;
|
|
1586
1645
|
if (!eoseAttempted.has(eoseKey)) {
|
|
1587
1646
|
eoseAttempted.add(eoseKey);
|
|
1588
1647
|
pendingEose += 1;
|
|
1589
1648
|
}
|
|
1590
|
-
const failUrl = () => {
|
|
1591
|
-
markEose(jobIndex, key);
|
|
1592
|
-
pending -= 1;
|
|
1593
|
-
if (pending <= 0 && closers.length === 0 && !closed) {
|
|
1594
|
-
settleClose();
|
|
1595
|
-
opts.onclose?.("all relays failed");
|
|
1596
|
-
}
|
|
1597
|
-
};
|
|
1598
1649
|
const tryAttach = (relay) => {
|
|
1599
1650
|
try {
|
|
1600
1651
|
attach(relay, job, jobIndex);
|
|
1601
|
-
} catch (
|
|
1602
|
-
if (
|
|
1603
|
-
failUrl();
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
if (error instanceof RelayClosedError) {
|
|
1654
|
+
failUrl(jobIndex, key);
|
|
1604
1655
|
return;
|
|
1605
1656
|
}
|
|
1606
|
-
throw
|
|
1657
|
+
throw error;
|
|
1607
1658
|
}
|
|
1608
1659
|
};
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1660
|
+
(async () => {
|
|
1661
|
+
let relay;
|
|
1662
|
+
try {
|
|
1663
|
+
relay = await pool.ensureRelay(key, {
|
|
1664
|
+
signal: opts.signal,
|
|
1665
|
+
timeoutMs: opts.connectTimeoutMs
|
|
1666
|
+
});
|
|
1667
|
+
} catch {
|
|
1668
|
+
const existing = pool.getRelay(key);
|
|
1669
|
+
if (existing !== void 0) {
|
|
1670
|
+
tryAttach(existing);
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
failUrl(jobIndex, key);
|
|
1616
1674
|
return;
|
|
1617
1675
|
}
|
|
1618
|
-
|
|
1619
|
-
});
|
|
1676
|
+
tryAttach(relay);
|
|
1677
|
+
})();
|
|
1620
1678
|
}
|
|
1621
1679
|
}
|
|
1622
1680
|
if (opts.eoseTimeoutMs !== void 0 && pending > 0) eoseTimer = setTimeout(() => {
|
|
@@ -1626,31 +1684,53 @@ function fanIn(pool, jobs, opts = {}) {
|
|
|
1626
1684
|
if (pending === 0) queueMicrotask(() => {
|
|
1627
1685
|
if (closed) return;
|
|
1628
1686
|
settleClose();
|
|
1629
|
-
opts.onclose?.("no relays");
|
|
1687
|
+
invokeSafely(() => opts.onclose?.("no relays"));
|
|
1630
1688
|
});
|
|
1631
1689
|
return { close: closeAll };
|
|
1632
1690
|
}
|
|
1633
1691
|
async function fetchRouted(pool, jobs, opts = {}) {
|
|
1692
|
+
throwIfAborted(opts.signal);
|
|
1634
1693
|
const byId = /* @__PURE__ */ new Map();
|
|
1635
|
-
await Promise.all(jobs.flatMap((job) =>
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
}
|
|
1694
|
+
await Promise.all(jobs.flatMap((job) => {
|
|
1695
|
+
const urls = [];
|
|
1696
|
+
for (const raw of job.urls) {
|
|
1697
|
+
let key;
|
|
1698
|
+
try {
|
|
1699
|
+
key = normalizeURL(raw);
|
|
1700
|
+
} catch {
|
|
1701
|
+
key = raw;
|
|
1702
|
+
}
|
|
1703
|
+
if (!urls.includes(key)) urls.push(key);
|
|
1704
|
+
}
|
|
1705
|
+
return urls.map(async (url) => {
|
|
1706
|
+
let batch;
|
|
1707
|
+
let relayUrl;
|
|
1708
|
+
try {
|
|
1709
|
+
const relay = await pool.ensureRelay(url, {
|
|
1710
|
+
signal: opts.signal,
|
|
1711
|
+
timeoutMs: opts.connectTimeoutMs
|
|
1712
|
+
});
|
|
1713
|
+
relayUrl = relay.url;
|
|
1714
|
+
batch = await relay.fetch([...job.filters], {
|
|
1715
|
+
timeoutMs: opts.timeoutMs,
|
|
1716
|
+
signal: opts.signal
|
|
1717
|
+
});
|
|
1718
|
+
} catch {
|
|
1719
|
+
if (opts.signal?.aborted === true) throw abortReason(opts.signal);
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1644
1722
|
for (const event of batch) if (!byId.has(event.id)) byId.set(event.id, event);
|
|
1645
|
-
|
|
1646
|
-
|
|
1723
|
+
for (const event of batch) invokeSafely(() => opts.onevent?.(event, relayUrl));
|
|
1724
|
+
});
|
|
1725
|
+
}));
|
|
1647
1726
|
return [...byId.values()];
|
|
1648
1727
|
}
|
|
1649
1728
|
//#endregion
|
|
1650
1729
|
//#region src/relay/pool.ts
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1730
|
+
function isIdle(relay) {
|
|
1731
|
+
return relay.status !== RelayStatus.Connecting && relay.subscriptionCount === 0 && relay.inFlightCount === 0;
|
|
1732
|
+
}
|
|
1733
|
+
/** Multi-relay coordinator: connection reuse, cross-relay event dedup, fan-out publish. */
|
|
1654
1734
|
var Pool = class {
|
|
1655
1735
|
#relays = /* @__PURE__ */ new Map();
|
|
1656
1736
|
#opts;
|
|
@@ -1659,10 +1739,12 @@ var Pool = class {
|
|
|
1659
1739
|
#idleTimer;
|
|
1660
1740
|
#allowInsecure;
|
|
1661
1741
|
#trustedInsecure;
|
|
1742
|
+
#pinned;
|
|
1662
1743
|
constructor(opts = {}) {
|
|
1663
1744
|
this.#opts = opts;
|
|
1664
|
-
this.#allowInsecure = opts.allowInsecure ??
|
|
1745
|
+
this.#allowInsecure = opts.allowInsecure ?? false;
|
|
1665
1746
|
this.#trustedInsecure = new Set((opts.trustedInsecureUrls ?? []).map(normalizeURL));
|
|
1747
|
+
this.#pinned = new Set((opts.pinnedUrls ?? []).map(normalizeURL));
|
|
1666
1748
|
this.#idleTimeoutMs = opts.idleTimeoutMs ?? 0;
|
|
1667
1749
|
if (this.#idleTimeoutMs > 0) this.#idleTimer = setInterval(() => this.cleanIdleRelays(), opts.idleCleanupIntervalMs ?? 3e4);
|
|
1668
1750
|
}
|
|
@@ -1672,19 +1754,29 @@ var Pool = class {
|
|
|
1672
1754
|
setTrustedInsecureUrls(urls) {
|
|
1673
1755
|
this.#trustedInsecure = new Set(urls.map(normalizeURL));
|
|
1674
1756
|
}
|
|
1757
|
+
setPinnedUrls(urls) {
|
|
1758
|
+
this.#pinned = new Set(urls.map(normalizeURL));
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Drop cached AUTH rejections and re-fire pending challenges on every pooled relay, so a later
|
|
1762
|
+
* `setSigner` takes effect without reconnect.
|
|
1763
|
+
*/
|
|
1764
|
+
resetAuth() {
|
|
1765
|
+
for (const relay of this.#relays.values()) relay.resetAuth();
|
|
1766
|
+
}
|
|
1675
1767
|
cleanIdleRelays() {
|
|
1676
1768
|
if (this.#idleTimeoutMs <= 0) return;
|
|
1677
1769
|
const now = Date.now();
|
|
1678
1770
|
const idle = [];
|
|
1679
1771
|
for (const [url, relay] of this.#relays) {
|
|
1680
|
-
if (
|
|
1772
|
+
if (this.#pinned.has(url) || !isIdle(relay)) continue;
|
|
1681
1773
|
const last = this.#lastActivity.get(url) ?? 0;
|
|
1682
1774
|
if (!relay.connected || now - last >= this.#idleTimeoutMs) idle.push(url);
|
|
1683
1775
|
}
|
|
1684
1776
|
for (const url of this.#lastActivity.keys()) if (!this.#relays.has(url)) this.#lastActivity.delete(url);
|
|
1685
1777
|
if (idle.length === 0) return;
|
|
1686
1778
|
this.close(idle);
|
|
1687
|
-
this.#opts.onIdleRelaysClosed?.(idle);
|
|
1779
|
+
invokeSafely(() => this.#opts.onIdleRelaysClosed?.(idle));
|
|
1688
1780
|
}
|
|
1689
1781
|
#touch(url) {
|
|
1690
1782
|
this.#lastActivity.set(url, Date.now());
|
|
@@ -1700,11 +1792,35 @@ var Pool = class {
|
|
|
1700
1792
|
clearInterval(this.#idleTimer);
|
|
1701
1793
|
this.#idleTimer = void 0;
|
|
1702
1794
|
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Soft cap: closing the least-recently-used idle non-pinned relay when a new non-pinned relay
|
|
1797
|
+
* would push the count past `maxRelays`. Busy relays are never evicted; with none idle the cap is
|
|
1798
|
+
* exceeded rather than failing.
|
|
1799
|
+
*/
|
|
1800
|
+
#enforceMaxRelays(incoming) {
|
|
1801
|
+
const cap = this.#opts.maxRelays;
|
|
1802
|
+
if (cap === void 0 || this.#pinned.has(incoming)) return;
|
|
1803
|
+
let count = 0;
|
|
1804
|
+
let oldestUrl;
|
|
1805
|
+
let oldestAt = Number.POSITIVE_INFINITY;
|
|
1806
|
+
for (const [url, relay] of this.#relays) {
|
|
1807
|
+
if (this.#pinned.has(url)) continue;
|
|
1808
|
+
count += 1;
|
|
1809
|
+
if (!isIdle(relay)) continue;
|
|
1810
|
+
const at = this.#lastActivity.get(url) ?? 0;
|
|
1811
|
+
if (at < oldestAt) {
|
|
1812
|
+
oldestAt = at;
|
|
1813
|
+
oldestUrl = url;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (count >= cap && oldestUrl !== void 0) this.close([oldestUrl]);
|
|
1817
|
+
}
|
|
1703
1818
|
async ensureRelay(url, opts) {
|
|
1704
1819
|
const norm = normalizeURL(url);
|
|
1705
1820
|
this.#rejectInsecure(url, norm);
|
|
1706
1821
|
let relay = this.#relays.get(norm);
|
|
1707
1822
|
if (!relay) {
|
|
1823
|
+
this.#enforceMaxRelays(norm);
|
|
1708
1824
|
const signFn = this.#opts.automaticallyAuth?.(norm) ?? void 0;
|
|
1709
1825
|
const created = new Relay(norm, {
|
|
1710
1826
|
websocketImplementation: this.#opts.websocketImplementation,
|
|
@@ -1723,7 +1839,11 @@ var Pool = class {
|
|
|
1723
1839
|
this.#lastActivity.delete(norm);
|
|
1724
1840
|
};
|
|
1725
1841
|
if (signFn) created.onauth = () => {
|
|
1726
|
-
|
|
1842
|
+
(async () => {
|
|
1843
|
+
try {
|
|
1844
|
+
await created.auth(signFn);
|
|
1845
|
+
} catch {}
|
|
1846
|
+
})();
|
|
1727
1847
|
};
|
|
1728
1848
|
this.#relays.set(norm, created);
|
|
1729
1849
|
relay = created;
|
|
@@ -1734,12 +1854,12 @@ var Pool = class {
|
|
|
1734
1854
|
signal: opts?.signal,
|
|
1735
1855
|
timeoutMs: opts?.timeoutMs ?? this.#opts.connectTimeoutMs
|
|
1736
1856
|
});
|
|
1737
|
-
} catch (
|
|
1738
|
-
if (
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
if (this.#opts.enableReconnect !== true) {
|
|
1739
1859
|
this.#relays.delete(norm);
|
|
1740
1860
|
this.#lastActivity.delete(norm);
|
|
1741
1861
|
}
|
|
1742
|
-
throw
|
|
1862
|
+
throw error;
|
|
1743
1863
|
}
|
|
1744
1864
|
return relay;
|
|
1745
1865
|
}
|
|
@@ -1759,16 +1879,16 @@ var Pool = class {
|
|
|
1759
1879
|
}
|
|
1760
1880
|
}
|
|
1761
1881
|
/**
|
|
1762
|
-
* Subscribe across relays. Deduplicates by event id.
|
|
1763
|
-
*
|
|
1882
|
+
* Subscribe across relays. Deduplicates by event id. Returns a closer; callbacks receive every
|
|
1883
|
+
* new event once.
|
|
1764
1884
|
*/
|
|
1765
1885
|
subscribe(relays, filters, opts = {}) {
|
|
1766
1886
|
if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
|
|
1767
1887
|
if (opts.id !== void 0) assertSubscriptionId(opts.id);
|
|
1768
|
-
|
|
1888
|
+
const canonical = canonicalizeFilters(filters);
|
|
1769
1889
|
return fanIn(this, [{
|
|
1770
1890
|
urls: relays,
|
|
1771
|
-
filters,
|
|
1891
|
+
filters: canonical,
|
|
1772
1892
|
id: opts.id
|
|
1773
1893
|
}], {
|
|
1774
1894
|
onevent: opts.onevent,
|
|
@@ -1791,7 +1911,8 @@ var Pool = class {
|
|
|
1791
1911
|
}], {
|
|
1792
1912
|
timeoutMs: opts?.timeoutMs,
|
|
1793
1913
|
signal: opts?.signal,
|
|
1794
|
-
connectTimeoutMs: this.#opts.connectTimeoutMs
|
|
1914
|
+
connectTimeoutMs: this.#opts.connectTimeoutMs,
|
|
1915
|
+
onevent: opts?.onevent
|
|
1795
1916
|
});
|
|
1796
1917
|
}
|
|
1797
1918
|
/** Publish to all listed relays; returns per-relay outcomes. */
|
|
@@ -1805,10 +1926,10 @@ var Pool = class {
|
|
|
1805
1926
|
url: relay.url,
|
|
1806
1927
|
result
|
|
1807
1928
|
};
|
|
1808
|
-
} catch (
|
|
1929
|
+
} catch (error) {
|
|
1809
1930
|
return {
|
|
1810
1931
|
url,
|
|
1811
|
-
error:
|
|
1932
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1812
1933
|
};
|
|
1813
1934
|
}
|
|
1814
1935
|
}));
|
|
@@ -1827,11 +1948,12 @@ var Pool = class {
|
|
|
1827
1948
|
}));
|
|
1828
1949
|
}
|
|
1829
1950
|
/**
|
|
1830
|
-
* NIP-45 COUNT across relays. Per-relay outcomes; failures do not throw.
|
|
1831
|
-
*
|
|
1951
|
+
* NIP-45 COUNT across relays. Per-relay outcomes; failures do not throw. Counts are not summed —
|
|
1952
|
+
* each relay reports independently (may overlap).
|
|
1832
1953
|
*/
|
|
1833
1954
|
async count(relays, filters, opts) {
|
|
1834
|
-
|
|
1955
|
+
throwIfAborted(opts?.signal);
|
|
1956
|
+
const canonical = canonicalizeFilters(filters);
|
|
1835
1957
|
return await Promise.all(relays.map(async (url) => {
|
|
1836
1958
|
try {
|
|
1837
1959
|
const relay = await this.ensureRelay(url, {
|
|
@@ -1839,20 +1961,22 @@ var Pool = class {
|
|
|
1839
1961
|
timeoutMs: this.#opts.connectTimeoutMs
|
|
1840
1962
|
});
|
|
1841
1963
|
this.#touch(relay.url);
|
|
1842
|
-
const payload = await relay.count(
|
|
1964
|
+
const payload = await relay.count(canonical, {
|
|
1843
1965
|
timeoutMs: opts?.timeoutMs,
|
|
1844
1966
|
signal: opts?.signal
|
|
1845
1967
|
});
|
|
1846
|
-
|
|
1968
|
+
const out = {
|
|
1847
1969
|
url: relay.url,
|
|
1848
|
-
count: payload.count
|
|
1849
|
-
approximate: payload.approximate,
|
|
1850
|
-
hll: payload.hll
|
|
1970
|
+
count: payload.count
|
|
1851
1971
|
};
|
|
1852
|
-
|
|
1972
|
+
if (payload.approximate !== void 0) out.approximate = payload.approximate;
|
|
1973
|
+
if (payload.hll !== void 0) out.hll = payload.hll;
|
|
1974
|
+
return out;
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
if (opts?.signal?.aborted === true) throw abortReason(opts.signal);
|
|
1853
1977
|
return {
|
|
1854
1978
|
url,
|
|
1855
|
-
error:
|
|
1979
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1856
1980
|
};
|
|
1857
1981
|
}
|
|
1858
1982
|
}));
|
|
@@ -1876,6 +2000,6 @@ var Pool = class {
|
|
|
1876
2000
|
}
|
|
1877
2001
|
};
|
|
1878
2002
|
//#endregion
|
|
1879
|
-
export { RelayStatus as a, isInsecureRelayUrl as c, RelayClosedError as d, RelayConnectionError as f, 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 };
|
|
2003
|
+
export { RelayStatus as a, isInsecureRelayUrl as c, RelayClosedError as d, RelayConnectionError as f, RelayTimeoutError 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 };
|
|
1880
2004
|
|
|
1881
|
-
//# sourceMappingURL=pool-
|
|
2005
|
+
//# sourceMappingURL=pool-XmAGaufS.mjs.map
|