@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
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
import { S as toError, _ as MessageError, u as normalizeURL } from "./util-DMF9AIO0.mjs";
|
|
2
|
+
import { n as canonicalizeFilters } from "./filter-BQl3GlHt.mjs";
|
|
3
|
+
import { t as assertSubscriptionId } from "./message-oNr7t0SJ.mjs";
|
|
4
|
+
import { t as invokeSafely } from "./report-CHld4uWj.mjs";
|
|
5
|
+
import { c as RelayConnectionError, d as RelaySuspendedError, n as Relay, r as RelayStatus, s as RelayClosedError, u as RelayPublishError } from "./relay-quCs7MHD.mjs";
|
|
6
|
+
import { i as throwIfAborted, n as onAbort, t as abortReason } from "./abort-DBq2STPj.mjs";
|
|
7
|
+
import { t as Emitter } from "./emitter-DkCY6NkU.mjs";
|
|
8
|
+
//#region src/relay/url.ts
|
|
9
|
+
/**
|
|
10
|
+
* {@link normalizeURL}, or the raw string when it is not a valid relay URL (ensureRelay then fails
|
|
11
|
+
* it like a dead relay).
|
|
12
|
+
*/
|
|
13
|
+
function relayUrlKey(url) {
|
|
14
|
+
try {
|
|
15
|
+
return normalizeURL(url);
|
|
16
|
+
} catch {
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Unique relay keys in first-seen order. */
|
|
21
|
+
function uniqueRelayUrls(urls) {
|
|
22
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23
|
+
const unique = [];
|
|
24
|
+
for (const url of urls) {
|
|
25
|
+
const key = relayUrlKey(url);
|
|
26
|
+
if (seen.has(key)) continue;
|
|
27
|
+
seen.add(key);
|
|
28
|
+
unique.push(key);
|
|
29
|
+
}
|
|
30
|
+
return unique;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* True for `ws:` / `http:` URLs that are not `.onion`. Local-network `ws://` is still insecure;
|
|
34
|
+
* allow those via a trusted set.
|
|
35
|
+
*/
|
|
36
|
+
function isInsecureRelayUrl(url) {
|
|
37
|
+
try {
|
|
38
|
+
const parsed = new URL(url);
|
|
39
|
+
if (parsed.hostname.endsWith(".onion")) return false;
|
|
40
|
+
return parsed.protocol === "ws:" || parsed.protocol === "http:";
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/relay/fan-in.ts
|
|
47
|
+
/**
|
|
48
|
+
* Aggregate EOSE/close across routed jobs. One `seen` set. `pending` counts URL list entries
|
|
49
|
+
* (duplicates included). `pendingEose` counts unique `jobIndex:url` keys.
|
|
50
|
+
*/
|
|
51
|
+
function fanIn(pool, jobs, opts = {}) {
|
|
52
|
+
const seen = /* @__PURE__ */ new Set();
|
|
53
|
+
const closers = [];
|
|
54
|
+
let closed = false;
|
|
55
|
+
let eoseFired = false;
|
|
56
|
+
let sawEose = false;
|
|
57
|
+
let eoseTimer;
|
|
58
|
+
const eoseDone = /* @__PURE__ */ new Set();
|
|
59
|
+
const eoseAttempted = /* @__PURE__ */ new Set();
|
|
60
|
+
let pendingEose = 0;
|
|
61
|
+
let pending = 0;
|
|
62
|
+
const fireEose = () => {
|
|
63
|
+
if (closed || eoseFired) return;
|
|
64
|
+
eoseFired = true;
|
|
65
|
+
if (eoseTimer !== void 0) {
|
|
66
|
+
clearTimeout(eoseTimer);
|
|
67
|
+
eoseTimer = void 0;
|
|
68
|
+
}
|
|
69
|
+
invokeSafely(() => opts.oneose?.());
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Remove one relay from the pending-EOSE set. `eosed` credits a real EOSE; a CLOSED or a connect
|
|
73
|
+
* failure only drops the wait — the aggregate `oneose` fires once every remaining relay ended AND
|
|
74
|
+
* at least one actually EOSE'd.
|
|
75
|
+
*/
|
|
76
|
+
const settleEose = (jobIndex, url, eosed) => {
|
|
77
|
+
const key = `${jobIndex}:${url}`;
|
|
78
|
+
if (eoseDone.has(key)) return;
|
|
79
|
+
eoseDone.add(key);
|
|
80
|
+
if (eosed) sawEose = true;
|
|
81
|
+
pendingEose -= 1;
|
|
82
|
+
if (pendingEose === 0 && sawEose) fireEose();
|
|
83
|
+
};
|
|
84
|
+
const settleClose = () => {
|
|
85
|
+
closed = true;
|
|
86
|
+
disposeAbort();
|
|
87
|
+
if (eoseTimer !== void 0) {
|
|
88
|
+
clearTimeout(eoseTimer);
|
|
89
|
+
eoseTimer = void 0;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const closeAll = (reason) => {
|
|
93
|
+
if (closed) return;
|
|
94
|
+
settleClose();
|
|
95
|
+
for (const c of closers) c.close(reason);
|
|
96
|
+
invokeSafely(() => opts.onclose?.(reason ?? "closed by client"));
|
|
97
|
+
};
|
|
98
|
+
if (opts.signal?.aborted === true) {
|
|
99
|
+
closed = true;
|
|
100
|
+
invokeSafely(() => opts.onclose?.("aborted"));
|
|
101
|
+
return { close: closeAll };
|
|
102
|
+
}
|
|
103
|
+
const disposeAbort = onAbort(opts.signal, () => closeAll("aborted"));
|
|
104
|
+
const failUrl = (jobIndex, key) => {
|
|
105
|
+
settleEose(jobIndex, key, false);
|
|
106
|
+
pending -= 1;
|
|
107
|
+
if (pending <= 0 && closers.length === 0 && !closed) {
|
|
108
|
+
settleClose();
|
|
109
|
+
invokeSafely(() => opts.onclose?.("all relays failed"));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const attach = (relay, job, jobIndex) => {
|
|
113
|
+
if (closed) return;
|
|
114
|
+
const received = opts.receivedEvent;
|
|
115
|
+
const sub = relay.subscribe([...job.filters], {
|
|
116
|
+
id: jobs.length === 1 ? job.id : void 0,
|
|
117
|
+
closeOnEose: opts.closeOnEose,
|
|
118
|
+
alreadyHaveEvent: (id) => {
|
|
119
|
+
let have = seen.has(id);
|
|
120
|
+
invokeSafely(() => {
|
|
121
|
+
have = have || Boolean(opts.alreadyHaveEvent?.(id));
|
|
122
|
+
});
|
|
123
|
+
return have;
|
|
124
|
+
},
|
|
125
|
+
receivedEvent: received === void 0 ? void 0 : (id) => invokeSafely(() => received(id, relay.url)),
|
|
126
|
+
onevent: (event) => {
|
|
127
|
+
seen.add(event.id);
|
|
128
|
+
invokeSafely(() => opts.onevent?.(event, relay.url));
|
|
129
|
+
},
|
|
130
|
+
oneose: () => settleEose(jobIndex, relay.url, true),
|
|
131
|
+
onclose: (reason) => {
|
|
132
|
+
settleEose(jobIndex, relay.url, false);
|
|
133
|
+
pending -= 1;
|
|
134
|
+
if (pending <= 0 && !closed) {
|
|
135
|
+
settleClose();
|
|
136
|
+
invokeSafely(() => opts.onclose?.(reason));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
closers.push(sub);
|
|
141
|
+
};
|
|
142
|
+
for (const [jobIndex, job] of jobs.entries()) for (const key of uniqueRelayUrls(job.urls)) {
|
|
143
|
+
pending += 1;
|
|
144
|
+
const eoseKey = `${jobIndex}:${key}`;
|
|
145
|
+
if (!eoseAttempted.has(eoseKey)) {
|
|
146
|
+
eoseAttempted.add(eoseKey);
|
|
147
|
+
pendingEose += 1;
|
|
148
|
+
}
|
|
149
|
+
const tryAttach = (relay) => {
|
|
150
|
+
try {
|
|
151
|
+
attach(relay, job, jobIndex);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof RelayClosedError) {
|
|
154
|
+
failUrl(jobIndex, key);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
(async () => {
|
|
161
|
+
let relay;
|
|
162
|
+
try {
|
|
163
|
+
relay = await pool.ensureRelay(key, {
|
|
164
|
+
signal: opts.signal,
|
|
165
|
+
timeoutMs: opts.connectTimeoutMs
|
|
166
|
+
});
|
|
167
|
+
} catch {
|
|
168
|
+
const existing = pool.getRelay(key);
|
|
169
|
+
if (existing !== void 0) {
|
|
170
|
+
tryAttach(existing);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
failUrl(jobIndex, key);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
tryAttach(relay);
|
|
177
|
+
})();
|
|
178
|
+
}
|
|
179
|
+
if (opts.eoseTimeoutMs !== void 0 && pending > 0) eoseTimer = setTimeout(() => {
|
|
180
|
+
eoseTimer = void 0;
|
|
181
|
+
fireEose();
|
|
182
|
+
}, opts.eoseTimeoutMs);
|
|
183
|
+
if (pending === 0) queueMicrotask(() => {
|
|
184
|
+
if (closed) return;
|
|
185
|
+
settleClose();
|
|
186
|
+
invokeSafely(() => opts.onclose?.("no relays"));
|
|
187
|
+
});
|
|
188
|
+
return { close: closeAll };
|
|
189
|
+
}
|
|
190
|
+
async function fetchRouted(pool, jobs, opts = {}) {
|
|
191
|
+
throwIfAborted(opts.signal);
|
|
192
|
+
const byId = /* @__PURE__ */ new Map();
|
|
193
|
+
await Promise.all(jobs.flatMap((job) => {
|
|
194
|
+
return uniqueRelayUrls(job.urls).map(async (url) => {
|
|
195
|
+
let batch;
|
|
196
|
+
let relayUrl;
|
|
197
|
+
try {
|
|
198
|
+
const relay = await pool.ensureRelay(url, {
|
|
199
|
+
signal: opts.signal,
|
|
200
|
+
timeoutMs: opts.connectTimeoutMs
|
|
201
|
+
});
|
|
202
|
+
relayUrl = relay.url;
|
|
203
|
+
batch = (await relay.fetch([...job.filters], {
|
|
204
|
+
timeoutMs: opts.timeoutMs,
|
|
205
|
+
signal: opts.signal
|
|
206
|
+
})).events;
|
|
207
|
+
} catch {
|
|
208
|
+
if (opts.signal?.aborted === true) throw abortReason(opts.signal);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const event of batch) if (!byId.has(event.id)) byId.set(event.id, event);
|
|
212
|
+
for (const event of batch) invokeSafely(() => opts.onevent?.(event, relayUrl));
|
|
213
|
+
});
|
|
214
|
+
}));
|
|
215
|
+
return [...byId.values()];
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/relay/pool.ts
|
|
219
|
+
const countOk = (url, payload) => ({
|
|
220
|
+
url,
|
|
221
|
+
status: "ok",
|
|
222
|
+
...payload
|
|
223
|
+
});
|
|
224
|
+
function isIdle(relay) {
|
|
225
|
+
return relay.status !== RelayStatus.Connecting && relay.subscriptionCount === 0 && relay.inFlightCount === 0;
|
|
226
|
+
}
|
|
227
|
+
/** Multi-relay coordinator: connection reuse, cross-relay event dedup, fan-out publish. */
|
|
228
|
+
var Pool = class {
|
|
229
|
+
#relays = /* @__PURE__ */ new Map();
|
|
230
|
+
#relayOptions;
|
|
231
|
+
#automaticallyAuth;
|
|
232
|
+
#maxRelays;
|
|
233
|
+
#invalidEventPolicy;
|
|
234
|
+
#events = new Emitter();
|
|
235
|
+
#lastActivity = /* @__PURE__ */ new Map();
|
|
236
|
+
#idleTimeoutMs;
|
|
237
|
+
#idleTimer;
|
|
238
|
+
#allowInsecure;
|
|
239
|
+
#trustedInsecure;
|
|
240
|
+
#pinned;
|
|
241
|
+
/** Per-relay timestamps of verification failures inside the sliding window. */
|
|
242
|
+
#invalidEvents = /* @__PURE__ */ new Map();
|
|
243
|
+
/** Normalized URL → epoch ms when its suspension lifts. */
|
|
244
|
+
#suspendedUntil = /* @__PURE__ */ new Map();
|
|
245
|
+
#resumeTimers = /* @__PURE__ */ new Map();
|
|
246
|
+
constructor(opts = {}) {
|
|
247
|
+
const { automaticallyAuth, allowInsecure, trustedInsecureUrls, idleTimeoutMs, idleCleanupIntervalMs, maxRelays, pinnedUrls, invalidEventPolicy, ...relayOptions } = opts;
|
|
248
|
+
this.#relayOptions = relayOptions;
|
|
249
|
+
this.#automaticallyAuth = automaticallyAuth;
|
|
250
|
+
this.#maxRelays = maxRelays;
|
|
251
|
+
this.#invalidEventPolicy = invalidEventPolicy;
|
|
252
|
+
this.#allowInsecure = allowInsecure ?? false;
|
|
253
|
+
this.#trustedInsecure = new Set((trustedInsecureUrls ?? []).map(normalizeURL));
|
|
254
|
+
this.#pinned = new Set((pinnedUrls ?? []).map(normalizeURL));
|
|
255
|
+
this.#idleTimeoutMs = idleTimeoutMs ?? 0;
|
|
256
|
+
if (this.#idleTimeoutMs > 0) this.#idleTimer = setInterval(() => this.cleanIdleRelays(), idleCleanupIntervalMs ?? 3e4);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Listen for a pool event. Multiple listeners per type are allowed and fire in registration
|
|
260
|
+
* order; a throwing listener is reported and does not break the dispatch. Returns an unsubscribe
|
|
261
|
+
* function.
|
|
262
|
+
*/
|
|
263
|
+
on(type, listener) {
|
|
264
|
+
return this.#events.on(type, listener);
|
|
265
|
+
}
|
|
266
|
+
setAllowInsecure(allow) {
|
|
267
|
+
this.#allowInsecure = allow;
|
|
268
|
+
}
|
|
269
|
+
setTrustedInsecureUrls(urls) {
|
|
270
|
+
this.#trustedInsecure = new Set(urls.map(normalizeURL));
|
|
271
|
+
}
|
|
272
|
+
setPinnedUrls(urls) {
|
|
273
|
+
this.#pinned = new Set(urls.map(normalizeURL));
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Drop cached AUTH rejections and re-fire pending challenges on every pooled relay, so a later
|
|
277
|
+
* `setSigner` takes effect without reconnect.
|
|
278
|
+
*/
|
|
279
|
+
resetAuth() {
|
|
280
|
+
for (const relay of this.#relays.values()) relay.resetAuth();
|
|
281
|
+
}
|
|
282
|
+
cleanIdleRelays() {
|
|
283
|
+
if (this.#idleTimeoutMs <= 0) return;
|
|
284
|
+
const now = Date.now();
|
|
285
|
+
const idle = [];
|
|
286
|
+
for (const [url, relay] of this.#relays) {
|
|
287
|
+
if (this.#pinned.has(url) || !isIdle(relay)) continue;
|
|
288
|
+
const last = this.#lastActivity.get(url) ?? 0;
|
|
289
|
+
if (!relay.connected || now - last >= this.#idleTimeoutMs) idle.push(url);
|
|
290
|
+
}
|
|
291
|
+
for (const url of this.#lastActivity.keys()) if (!this.#relays.has(url)) this.#lastActivity.delete(url);
|
|
292
|
+
if (idle.length === 0) return;
|
|
293
|
+
this.closeRelays(idle);
|
|
294
|
+
this.#events.emit("idle", idle);
|
|
295
|
+
}
|
|
296
|
+
#touch(url) {
|
|
297
|
+
this.#lastActivity.set(url, Date.now());
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Count one verification failure for `norm`; on the `limit + 1`-th inside `windowMs` suspend the
|
|
301
|
+
* relay: drop its connection (subscriptions kept), record `until`, and emit `suspend`.
|
|
302
|
+
*/
|
|
303
|
+
#noteInvalidEvent(norm) {
|
|
304
|
+
const policy = this.#invalidEventPolicy;
|
|
305
|
+
if (policy === void 0) return;
|
|
306
|
+
const now = Date.now();
|
|
307
|
+
const window = (this.#invalidEvents.get(norm) ?? []).filter((at) => now - at < policy.windowMs);
|
|
308
|
+
window.push(now);
|
|
309
|
+
if (window.length <= policy.limit) {
|
|
310
|
+
this.#invalidEvents.set(norm, window);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.#invalidEvents.delete(norm);
|
|
314
|
+
if (this.#suspendedUntil.has(norm)) return;
|
|
315
|
+
const until = now + policy.cooldownMs;
|
|
316
|
+
this.#suspendedUntil.set(norm, until);
|
|
317
|
+
this.#relays.get(norm)?.disconnect();
|
|
318
|
+
const resume = setTimeout(() => {
|
|
319
|
+
this.#resumeTimers.delete(norm);
|
|
320
|
+
this.#suspendedUntil.delete(norm);
|
|
321
|
+
const relay = this.#relays.get(norm);
|
|
322
|
+
if (relay === void 0 || relay.connected || relay.subscriptionCount === 0) return;
|
|
323
|
+
(async () => {
|
|
324
|
+
try {
|
|
325
|
+
await relay.connect();
|
|
326
|
+
} catch {}
|
|
327
|
+
})();
|
|
328
|
+
}, policy.cooldownMs);
|
|
329
|
+
this.#resumeTimers.set(norm, resume);
|
|
330
|
+
this.#events.emit("suspend", {
|
|
331
|
+
url: norm,
|
|
332
|
+
until
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
#rejectInsecure(url, norm) {
|
|
336
|
+
if (this.#allowInsecure) return;
|
|
337
|
+
if (!isInsecureRelayUrl(url)) return;
|
|
338
|
+
if (this.#trustedInsecure.has(norm)) return;
|
|
339
|
+
throw new RelayConnectionError("insecure relay connection blocked", norm);
|
|
340
|
+
}
|
|
341
|
+
#stopIdleCleanup() {
|
|
342
|
+
if (this.#idleTimer === void 0) return;
|
|
343
|
+
clearInterval(this.#idleTimer);
|
|
344
|
+
this.#idleTimer = void 0;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Soft cap: closing the least-recently-used idle non-pinned relay when a new non-pinned relay
|
|
348
|
+
* would push the count past `maxRelays`. Busy relays are never evicted; with none idle the cap is
|
|
349
|
+
* exceeded rather than failing.
|
|
350
|
+
*/
|
|
351
|
+
#enforceMaxRelays(incoming) {
|
|
352
|
+
const cap = this.#maxRelays;
|
|
353
|
+
if (cap === void 0 || this.#pinned.has(incoming)) return;
|
|
354
|
+
let count = 0;
|
|
355
|
+
let oldestUrl;
|
|
356
|
+
let oldestAt = Number.POSITIVE_INFINITY;
|
|
357
|
+
for (const [url, relay] of this.#relays) {
|
|
358
|
+
if (this.#pinned.has(url)) continue;
|
|
359
|
+
count += 1;
|
|
360
|
+
if (!isIdle(relay)) continue;
|
|
361
|
+
const at = this.#lastActivity.get(url) ?? 0;
|
|
362
|
+
if (at < oldestAt) {
|
|
363
|
+
oldestAt = at;
|
|
364
|
+
oldestUrl = url;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (count >= cap && oldestUrl !== void 0) this.closeRelays([oldestUrl]);
|
|
368
|
+
}
|
|
369
|
+
async ensureRelay(url, opts) {
|
|
370
|
+
const norm = normalizeURL(url);
|
|
371
|
+
this.#rejectInsecure(url, norm);
|
|
372
|
+
const suspendedUntil = this.#suspendedUntil.get(norm);
|
|
373
|
+
if (suspendedUntil !== void 0) {
|
|
374
|
+
if (Date.now() < suspendedUntil) throw new RelaySuspendedError(norm, suspendedUntil);
|
|
375
|
+
this.#suspendedUntil.delete(norm);
|
|
376
|
+
}
|
|
377
|
+
let relay = this.#relays.get(norm);
|
|
378
|
+
if (!relay) {
|
|
379
|
+
this.#enforceMaxRelays(norm);
|
|
380
|
+
const signFn = this.#automaticallyAuth?.(norm) ?? void 0;
|
|
381
|
+
const created = new Relay(norm, {
|
|
382
|
+
...this.#relayOptions,
|
|
383
|
+
authSigner: signFn
|
|
384
|
+
});
|
|
385
|
+
created.on("close", () => {
|
|
386
|
+
this.#relays.delete(norm);
|
|
387
|
+
this.#lastActivity.delete(norm);
|
|
388
|
+
});
|
|
389
|
+
created.on("invalidevent", () => {
|
|
390
|
+
this.#noteInvalidEvent(norm);
|
|
391
|
+
});
|
|
392
|
+
if (signFn) created.on("auth", () => {
|
|
393
|
+
(async () => {
|
|
394
|
+
try {
|
|
395
|
+
await created.auth(signFn);
|
|
396
|
+
} catch {}
|
|
397
|
+
})();
|
|
398
|
+
});
|
|
399
|
+
this.#relays.set(norm, created);
|
|
400
|
+
relay = created;
|
|
401
|
+
}
|
|
402
|
+
this.#touch(norm);
|
|
403
|
+
if (!relay.connected) try {
|
|
404
|
+
await relay.connect({
|
|
405
|
+
signal: opts?.signal,
|
|
406
|
+
timeoutMs: opts?.timeoutMs ?? this.#relayOptions.connectTimeoutMs
|
|
407
|
+
});
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if (this.#relayOptions.enableReconnect !== true) {
|
|
410
|
+
this.#relays.delete(norm);
|
|
411
|
+
this.#lastActivity.delete(norm);
|
|
412
|
+
}
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
return relay;
|
|
416
|
+
}
|
|
417
|
+
/** Close every pooled relay and clear all pool state. */
|
|
418
|
+
close() {
|
|
419
|
+
this.#stopIdleCleanup();
|
|
420
|
+
for (const timer of this.#resumeTimers.values()) clearTimeout(timer);
|
|
421
|
+
this.#resumeTimers.clear();
|
|
422
|
+
this.#suspendedUntil.clear();
|
|
423
|
+
this.#invalidEvents.clear();
|
|
424
|
+
for (const relay of this.#relays.values()) relay.close();
|
|
425
|
+
this.#relays.clear();
|
|
426
|
+
this.#lastActivity.clear();
|
|
427
|
+
}
|
|
428
|
+
/** Close and forget the listed relays; other pooled relays are untouched. */
|
|
429
|
+
closeRelays(urls) {
|
|
430
|
+
for (const url of urls) {
|
|
431
|
+
const norm = normalizeURL(url);
|
|
432
|
+
const timer = this.#resumeTimers.get(norm);
|
|
433
|
+
if (timer !== void 0) {
|
|
434
|
+
clearTimeout(timer);
|
|
435
|
+
this.#resumeTimers.delete(norm);
|
|
436
|
+
}
|
|
437
|
+
this.#relays.get(norm)?.close();
|
|
438
|
+
this.#relays.delete(norm);
|
|
439
|
+
this.#lastActivity.delete(norm);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Subscribe across relays. Deduplicates by event id. Returns a closer; callbacks receive every
|
|
444
|
+
* new event once.
|
|
445
|
+
*/
|
|
446
|
+
subscribe(relays, filters, opts = {}) {
|
|
447
|
+
if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
|
|
448
|
+
if (opts.id !== void 0) assertSubscriptionId(opts.id);
|
|
449
|
+
const canonical = canonicalizeFilters(filters);
|
|
450
|
+
return fanIn(this, [{
|
|
451
|
+
urls: relays,
|
|
452
|
+
filters: canonical,
|
|
453
|
+
id: opts.id
|
|
454
|
+
}], {
|
|
455
|
+
onevent: opts.onevent,
|
|
456
|
+
oneose: opts.oneose,
|
|
457
|
+
onclose: opts.onclose,
|
|
458
|
+
signal: opts.signal,
|
|
459
|
+
eoseTimeoutMs: opts.eoseTimeoutMs,
|
|
460
|
+
alreadyHaveEvent: opts.alreadyHaveEvent,
|
|
461
|
+
receivedEvent: opts.receivedEvent,
|
|
462
|
+
closeOnEose: opts.closeOnEose,
|
|
463
|
+
connectTimeoutMs: this.#relayOptions.connectTimeoutMs
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* One-shot fetch per relay: each entry reports its own events and end reason (`eose`, `closed`,
|
|
468
|
+
* `timeout`), or `failed` when the connect or REQ itself errored. An abort rejects the whole
|
|
469
|
+
* call. Events are not deduped across relays — use {@link fetch} for that.
|
|
470
|
+
*/
|
|
471
|
+
async fetchEach(relays, filters, opts) {
|
|
472
|
+
if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
|
|
473
|
+
throwIfAborted(opts?.signal);
|
|
474
|
+
const canonical = canonicalizeFilters(filters);
|
|
475
|
+
return Promise.all(uniqueRelayUrls(relays).map(async (url) => {
|
|
476
|
+
try {
|
|
477
|
+
const relay = await this.ensureRelay(url, {
|
|
478
|
+
signal: opts?.signal,
|
|
479
|
+
timeoutMs: this.#relayOptions.connectTimeoutMs
|
|
480
|
+
});
|
|
481
|
+
this.#touch(relay.url);
|
|
482
|
+
const result = await relay.fetch([...canonical], {
|
|
483
|
+
timeoutMs: opts?.timeoutMs,
|
|
484
|
+
signal: opts?.signal
|
|
485
|
+
});
|
|
486
|
+
return {
|
|
487
|
+
url,
|
|
488
|
+
events: result.events,
|
|
489
|
+
end: result.end
|
|
490
|
+
};
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (opts?.signal?.aborted === true) throw abortReason(opts.signal);
|
|
493
|
+
return {
|
|
494
|
+
url,
|
|
495
|
+
events: [],
|
|
496
|
+
end: {
|
|
497
|
+
type: "failed",
|
|
498
|
+
error: toError(error)
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
/** Fetch events until each connected relay EOSE or timeout; dedupe by id. */
|
|
505
|
+
async fetch(relays, filters, opts) {
|
|
506
|
+
if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
|
|
507
|
+
return fetchRouted(this, [{
|
|
508
|
+
urls: relays,
|
|
509
|
+
filters: canonicalizeFilters(filters)
|
|
510
|
+
}], {
|
|
511
|
+
timeoutMs: opts?.timeoutMs,
|
|
512
|
+
signal: opts?.signal,
|
|
513
|
+
connectTimeoutMs: this.#relayOptions.connectTimeoutMs,
|
|
514
|
+
onevent: opts?.onevent
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
/** Publish to all listed relays; returns per-relay outcomes. */
|
|
518
|
+
async publish(relays, event, opts) {
|
|
519
|
+
return Promise.all(uniqueRelayUrls(relays).map(async (url) => {
|
|
520
|
+
try {
|
|
521
|
+
const relay = await this.ensureRelay(url, { timeoutMs: this.#relayOptions.connectTimeoutMs });
|
|
522
|
+
this.#touch(relay.url);
|
|
523
|
+
const result = await relay.publish(event, { timeoutMs: opts?.timeoutMs });
|
|
524
|
+
return result.ok ? {
|
|
525
|
+
url,
|
|
526
|
+
status: "ok",
|
|
527
|
+
message: result.message
|
|
528
|
+
} : {
|
|
529
|
+
url,
|
|
530
|
+
status: "rejected",
|
|
531
|
+
message: result.message
|
|
532
|
+
};
|
|
533
|
+
} catch (error) {
|
|
534
|
+
return {
|
|
535
|
+
url,
|
|
536
|
+
status: "failed",
|
|
537
|
+
error: toError(error)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
542
|
+
/** First successful publish (Promise.any semantics). */
|
|
543
|
+
async publishAny(relays, event, opts) {
|
|
544
|
+
return Promise.any(uniqueRelayUrls(relays).map(async (url) => {
|
|
545
|
+
const relay = await this.ensureRelay(url, { timeoutMs: this.#relayOptions.connectTimeoutMs });
|
|
546
|
+
this.#touch(relay.url);
|
|
547
|
+
const result = await relay.publish(event, { timeoutMs: opts?.timeoutMs });
|
|
548
|
+
if (!result.ok) throw new RelayPublishError(result.message || "rejected", relay.url);
|
|
549
|
+
return {
|
|
550
|
+
url,
|
|
551
|
+
status: "ok",
|
|
552
|
+
message: result.message
|
|
553
|
+
};
|
|
554
|
+
}));
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* NIP-45 COUNT across relays. Per-relay outcomes; failures do not throw. Counts are not summed —
|
|
558
|
+
* each relay reports independently (may overlap).
|
|
559
|
+
*/
|
|
560
|
+
async count(relays, filters, opts) {
|
|
561
|
+
throwIfAborted(opts?.signal);
|
|
562
|
+
const canonical = canonicalizeFilters(filters);
|
|
563
|
+
return Promise.all(uniqueRelayUrls(relays).map(async (url) => {
|
|
564
|
+
try {
|
|
565
|
+
const relay = await this.ensureRelay(url, {
|
|
566
|
+
signal: opts?.signal,
|
|
567
|
+
timeoutMs: this.#relayOptions.connectTimeoutMs
|
|
568
|
+
});
|
|
569
|
+
this.#touch(relay.url);
|
|
570
|
+
const payload = await relay.count(canonical, {
|
|
571
|
+
timeoutMs: opts?.timeoutMs,
|
|
572
|
+
signal: opts?.signal
|
|
573
|
+
});
|
|
574
|
+
return countOk(url, payload);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
if (opts?.signal?.aborted === true) throw abortReason(opts.signal);
|
|
577
|
+
return {
|
|
578
|
+
url,
|
|
579
|
+
status: "failed",
|
|
580
|
+
error: toError(error)
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
listRelays() {
|
|
586
|
+
return [...this.#relays.keys()];
|
|
587
|
+
}
|
|
588
|
+
/** Lookup a pooled relay, including disconnected reconnecting entries. */
|
|
589
|
+
getRelay(url) {
|
|
590
|
+
try {
|
|
591
|
+
return this.#relays.get(normalizeURL(url));
|
|
592
|
+
} catch {
|
|
593
|
+
return this.#relays.get(url);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/** Currently connected URLs. Unlike listRelays(), excludes reconnecting/disconnected entries. */
|
|
597
|
+
connectedUrls() {
|
|
598
|
+
const urls = [];
|
|
599
|
+
for (const [url, relay] of this.#relays) if (relay.connected) urls.push(url);
|
|
600
|
+
return urls;
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
//#endregion
|
|
604
|
+
export { isInsecureRelayUrl as i, fanIn as n, fetchRouted as r, Pool as t };
|
|
605
|
+
|
|
606
|
+
//# sourceMappingURL=pool-vlw74C4t.mjs.map
|