@steve02081504/fount-p2p 0.0.48 → 0.0.50
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/AGENTS.md
CHANGED
|
@@ -54,6 +54,7 @@ Deno / native / BT: [runtime.md](docs/runtime.md).
|
|
|
54
54
|
- Assertions: `test/helpers/assert.mjs`
|
|
55
55
|
- Fixed-seed identity: `test/helpers/identity.mjs`
|
|
56
56
|
- Mock discovery: `test/helpers/mock_discovery.mjs`
|
|
57
|
+
- Transport fakes must mirror the backend's failure signalling: a fake `RTCPeerConnection` whose `close()` stays silent (no `connectionstatechange`) hides re-entrancy bugs where a replaced connection closes the live one (fount-p2p#39, `link/providers/webrtc.mjs`).
|
|
57
58
|
|
|
58
59
|
## Static checks (`scripts/checks/`, ported from fount's check standard)
|
|
59
60
|
|
|
@@ -103,7 +103,7 @@ let discoveryEnabled = true
|
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* 规范化 Nostr relay URL(唯一入站入口)。
|
|
106
|
-
* -
|
|
106
|
+
* - `wss://` 不限地址;`ws://` 只接受本机回环(`localhost` / `::1` / `127.x`),私网与公网一律拒绝。
|
|
107
107
|
* - hostname 小写,去除默认端口,去除尾部斜杠,保留非空 path。
|
|
108
108
|
* @param {unknown} raw 原始 URL
|
|
109
109
|
* @returns {string | null} 规范化字符串,无效返回 null
|
|
@@ -250,7 +250,8 @@ function currentTrustedRelayUrls() {
|
|
|
250
250
|
|
|
251
251
|
/**
|
|
252
252
|
* 判定该 relay 是否允许作为连接目的地(probe/发布/订阅)。
|
|
253
|
-
*
|
|
253
|
+
* 受信 relay(本机显式配置、提供方注册、NIP-66 引导集、pinned,见 [currentTrustedRelayUrls])不受公网限制;
|
|
254
|
+
* 其余须为公网 hostname 且 DNS 全部解析为公网地址。
|
|
254
255
|
* @param {string} relayUrl 规范化 relay URL
|
|
255
256
|
* @returns {Promise<boolean>} 允许连接为 true
|
|
256
257
|
*/
|
|
@@ -261,20 +262,32 @@ export async function isRelayDestinationAllowed(relayUrl) {
|
|
|
261
262
|
return relayUrlResolvesPublic(relayUrl)
|
|
262
263
|
}
|
|
263
264
|
|
|
265
|
+
/**
|
|
266
|
+
* 最近一次尝试是否失败。时间戳与 lastSuccess 打平按失败算(同一毫秒内的并发尝试里,成功者会记下更晚的时间戳)。
|
|
267
|
+
* @param {number} lastFailure 最近一次失败时间戳
|
|
268
|
+
* @param {number} lastSuccess 最近一次成功时间戳
|
|
269
|
+
* @returns {boolean} 最近一次尝试失败为 true
|
|
270
|
+
*/
|
|
271
|
+
function latestAttemptFailed(lastFailure, lastSuccess) {
|
|
272
|
+
return lastFailure >= lastSuccess
|
|
273
|
+
}
|
|
274
|
+
|
|
264
275
|
/**
|
|
265
276
|
* 计算 relay 健康分(越低越优)。
|
|
277
|
+
* 最近一次尝试失败(或从未成功过)时 RTT 与失败率都取最差值,分数必定高于任何活着的 relay。
|
|
266
278
|
* @param {Partial<RelayPoolEntry>} entry 条目
|
|
267
279
|
* @returns {number} 健康分
|
|
268
280
|
*/
|
|
269
281
|
export function computeRelayHealth(entry) {
|
|
270
|
-
const { rttMs, successCount, failureCount, lastProbe } = entry
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
const { rttMs, successCount = 0, failureCount = 0, lastProbe = 0, lastSuccess = 0, lastFailure = 0 } = entry
|
|
283
|
+
const failed = failureCount > 0 && (!successCount || latestAttemptFailed(lastFailure, lastSuccess))
|
|
284
|
+
const total = successCount + failureCount
|
|
285
|
+
const failureRate = failed ? 1 : total > 0 ? failureCount / total : 0
|
|
286
|
+
// 失败的 relay 最后一次测量已被作废:没有可信 RTT 可依,按上限算。
|
|
287
|
+
const measuredRtt = failed ? MAX_RTT_MS : rttMs
|
|
288
|
+
const rtt = Math.max(1, Math.min(MAX_RTT_MS, Number.isFinite(measuredRtt) && measuredRtt > 0 ? measuredRtt : DEFAULT_RTT_MS))
|
|
276
289
|
let score = rtt * (1 + failureRate * FAILURE_WEIGHT)
|
|
277
|
-
if (Date.now() -
|
|
290
|
+
if (Date.now() - lastProbe > PROBE_STALE_MS) score *= STALE_PENALTY
|
|
278
291
|
return score
|
|
279
292
|
}
|
|
280
293
|
|
|
@@ -512,7 +525,7 @@ export function recordProbeSuccess(url, rttMs) {
|
|
|
512
525
|
const entry = ensureEntry(normalized)
|
|
513
526
|
if (rttMs != null && Number.isFinite(Number(rttMs))) entry.rttMs = Math.max(0, Math.min(MAX_RTT_MS, Math.round(Number(rttMs))))
|
|
514
527
|
entry.successCount++
|
|
515
|
-
entry.lastSuccess = entry.lastProbe = entry.lastSeen = Date.now()
|
|
528
|
+
entry.lastSuccess = entry.lastProbe = entry.lastSeen = Math.max(Date.now(), entry.lastProbe + 1)
|
|
516
529
|
markDirty()
|
|
517
530
|
}
|
|
518
531
|
|
|
@@ -525,7 +538,8 @@ export function recordProbeFailure(url) {
|
|
|
525
538
|
if (!normalized) return
|
|
526
539
|
const entry = ensureEntry(normalized)
|
|
527
540
|
entry.failureCount++
|
|
528
|
-
entry.
|
|
541
|
+
entry.rttMs = null
|
|
542
|
+
entry.lastFailure = entry.lastProbe = entry.lastSeen = Math.max(Date.now(), entry.lastProbe + 1)
|
|
529
543
|
markDirty()
|
|
530
544
|
}
|
|
531
545
|
|
|
@@ -583,17 +597,23 @@ export function clearStale() {
|
|
|
583
597
|
}
|
|
584
598
|
|
|
585
599
|
/**
|
|
586
|
-
* @returns {RelayPoolEntry[]}
|
|
600
|
+
* @returns {RelayPoolEntry[]} 工作集(保留未探测引导项,排除失败或过期项,优先 pinned)
|
|
587
601
|
*/
|
|
588
602
|
export function getWorkingRelays() {
|
|
589
|
-
const
|
|
603
|
+
const now = Date.now()
|
|
604
|
+
// 合格项:最近一次尝试成功且没过期,或压根还没探测过的引导项。
|
|
605
|
+
const eligible = [...poolEntries.values()].filter(entry =>
|
|
606
|
+
entry.successCount === 0
|
|
607
|
+
? entry.lastFailure === 0
|
|
608
|
+
: !latestAttemptFailed(entry.lastFailure, entry.lastSuccess) && now - entry.lastSuccess <= PROBE_STALE_MS)
|
|
609
|
+
const sorted = sortByHealth(eligible)
|
|
590
610
|
const pinned = sorted.filter(isPinned)
|
|
591
611
|
const rest = sorted.filter(entry => !isPinned(entry))
|
|
592
612
|
return dedupeByUrl([...pinned, ...rest.slice(0, Math.max(0, WORKING_RELAYS_COUNT - pinned.length))])
|
|
593
613
|
}
|
|
594
614
|
|
|
595
615
|
/**
|
|
596
|
-
* @returns {RelayPoolEntry[]}
|
|
616
|
+
* @returns {RelayPoolEntry[]} 监听/发布子集(仅含工作集内 public/manual)
|
|
597
617
|
*/
|
|
598
618
|
export function getListenRelays() {
|
|
599
619
|
const working = getWorkingRelays()
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
computeRelayHealth,
|
|
14
14
|
getPeerRoute,
|
|
15
|
-
getPinnedRelays,
|
|
16
15
|
getPoolByUrl,
|
|
17
16
|
getWorkingRelays,
|
|
18
17
|
recordPublishResult,
|
|
@@ -116,7 +115,7 @@ export function expandFromHistory(nodeHash, base, limit) {
|
|
|
116
115
|
|
|
117
116
|
/**
|
|
118
117
|
* 计算到指定节点的当前轮路由目标集。
|
|
119
|
-
* Round 0:对端声称前4(无则本机工作集前4
|
|
118
|
+
* Round 0:对端声称前4(无则本机工作集前4)。
|
|
120
119
|
* Round ≥1:历史成功 + 对端综合分补足 + 加权采样,附退避;总扇出封顶。
|
|
121
120
|
* @param {string} nodeHash 目标节点
|
|
122
121
|
* @param {number} attempt 当前尝试轮次(0 起)
|
|
@@ -139,7 +138,6 @@ export function handshakeTargets(nodeHash, attempt) {
|
|
|
139
138
|
for (const entry of working.slice(0, ROUND0_TARGET_COUNT)) push(entry.url)
|
|
140
139
|
return
|
|
141
140
|
}
|
|
142
|
-
for (const url of getPinnedRelays().slice(0, ROUND0_TARGET_COUNT)) push(url)
|
|
143
141
|
}
|
|
144
142
|
if (attempt <= 0) {
|
|
145
143
|
round0()
|
|
@@ -16,8 +16,10 @@ Source layout (`discovery/nostr/`):
|
|
|
16
16
|
|
|
17
17
|
`relayPool` (all known) → `workingRelays` (top-`WORKING_RELAYS_COUNT` by health) → `listenRelays` (top-`LISTEN_RELAYS_COUNT` working subset used for publish/listen).
|
|
18
18
|
|
|
19
|
-
- `public`/`manual` entries are **pinned**:
|
|
19
|
+
- `public`/`manual` entries are **pinned**: preferred in `working`/`listen` when eligible and never evicted by `clearStale` or pool-cap. Failed or stale pins remain in the pool for retry, but leave the working/listen sets. Pin count may exceed the nominal caps.
|
|
20
20
|
- `nip66`/`peer` entries are disposable: evicted when stale (`PROBE_STALE_MS`) or when the pool exceeds `POOL_CAP`.
|
|
21
|
+
- A relay leaves the working/listen sets when it is **not eligible**: never probed but already failed, or its latest attempt failed, or its last success is older than `PROBE_STALE_MS`. Untested bootstrap entries stay eligible; exclusion never evicts from the pool, so dead relays keep getting retried.
|
|
22
|
+
- Configured `relayUrls` override the automatic listen subset; adverts publish this resolved subscription set as `listenNostrRelays`.
|
|
21
23
|
- Fresh nodes are seeded with `DEFAULT_RELAY_URLS` (`source: 'public'`) so `listenRelays` is never empty at cold start.
|
|
22
24
|
- Persistent connections stay bounded by `workingRelays` (`WORKING_RELAYS_COUNT`, max 32); pins influence selection, not simultaneous connections.
|
|
23
25
|
|
|
@@ -25,20 +27,22 @@ Source layout (`discovery/nostr/`):
|
|
|
25
27
|
|
|
26
28
|
`normalizeNostrRelayUrl` (in `relays.mjs`) is the only entry for every inbound URL (NIP-66 `d` tag, manual config, peer advert):
|
|
27
29
|
|
|
28
|
-
- `wss://`
|
|
30
|
+
- `wss://` to any host; `ws://` to loopback only (`localhost` / `::1` / `127.x` — local dev/tests). Private and public `ws://` are rejected outright.
|
|
31
|
+
- `isRelayDestinationAllowed` then gates dialling: a **trusted** relay (local config, provider registration, the NIP-66 bootstrap set, or any pinned `public`/`manual` entry) may be private; every other relay must be a public hostname whose DNS resolves entirely to public addresses. So a private `wss://` is only reachable when something pinned or configured it.
|
|
29
32
|
- hostname lowercased, default port removed, trailing slashes removed, non-empty path kept.
|
|
30
33
|
- Invalid → `null` → dropped with an audit log (`nodeDebug('invalidRelayUrl', { url, reason })`), never silently cleaned.
|
|
31
34
|
|
|
32
35
|
## Health score
|
|
33
36
|
|
|
34
37
|
```text
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
score
|
|
38
|
+
dead = latest attempt failed, or the entry never succeeded // ties count as failed
|
|
39
|
+
failureRate = dead ? 1 : failureCount / (successCount + failureCount)
|
|
40
|
+
rtt = clamp(dead ? MAX_RTT_MS : rttMs, 1, MAX_RTT_MS) // DEFAULT_RTT_MS when unknown, zero or invalid
|
|
41
|
+
score = rtt * (1 + failureRate * FAILURE_WEIGHT) // FAILURE_WEIGHT = 4
|
|
42
|
+
score *= STALE_PENALTY // ×2 if lastProbe older than PROBE_STALE_MS
|
|
39
43
|
```
|
|
40
44
|
|
|
41
|
-
Lower is better. `recordProbeSuccess` / `recordProbeFailure` / `recordPublishResult` share the same counters. Writes to `nodeDir/nostr/relays.json` are **throttled** (2s debounce).
|
|
45
|
+
Lower is better. A dead entry (`failureCount > 0` and either no successes or a latest failure) scores with `MAX_RTT_MS`, `failureRate = 1`, and its stale RTT discarded, so it can never outrank a live one. `recordProbeSuccess` / `recordProbeFailure` / `recordPublishResult` share the same counters; attempt timestamps advance monotonically so success/failure ordering survives same-millisecond attempts. Writes to `nodeDir/nostr/relays.json` are **throttled** (2s debounce).
|
|
42
46
|
|
|
43
47
|
## Persistence
|
|
44
48
|
|
|
@@ -74,7 +78,7 @@ Lower is better. `recordProbeSuccess` / `recordProbeFailure` / `recordPublishRes
|
|
|
74
78
|
|
|
75
79
|
`handshakeTargets(nodeHash, attempt)`:
|
|
76
80
|
|
|
77
|
-
- **Round 0**: peer-claimed `listenRelays` top 4 by composite score (own health + peer rtt); else local `workingRelays` top 4
|
|
81
|
+
- **Round 0**: peer-claimed `listenRelays` top 4 by composite score (own health + peer rtt); else local `workingRelays` top 4. Empty working sets do not fall back to failed pins.
|
|
78
82
|
- **Round ≥1**: backoff `min(2000 · 2^(attempt−1), 60000)`; base on `lastGoodNostrRelays` (expanded via `expandFromHistory` ≤ 16), or weighted-random sample of `workingRelays` (weight `1/score`); round-0 core always included; fanout capped at `MAX_ROUTING_FANOUT` (64).
|
|
79
83
|
- Retries ≤ `MAX_ROUTING_ATTEMPTS` (4).
|
|
80
84
|
|
|
@@ -135,13 +135,14 @@ export async function createWebRtcLink(options) {
|
|
|
135
135
|
const rungs = iceLocalHostnameLadder(icePolicy)
|
|
136
136
|
const remoteSignalQueue = []
|
|
137
137
|
const seenRemoteSignals = createLruMap(1024)
|
|
138
|
-
/** @type {InstanceType<import('../rtc/polyfill.mjs').LoadedRtcPolyfill['RTCPeerConnection']> & {
|
|
138
|
+
/** @type {InstanceType<import('../rtc/polyfill.mjs').LoadedRtcPolyfill['RTCPeerConnection']> & { ondatachannel?: unknown, onconnectionstatechange?: unknown, candidateCountHandler?: unknown }} */
|
|
139
139
|
let peerConnection = null
|
|
140
140
|
let remoteDescriptionSet = false
|
|
141
141
|
let controlChannel = null
|
|
142
142
|
let bulkChannel = null
|
|
143
143
|
let unlistenRemote = null
|
|
144
144
|
let sendQueues = null
|
|
145
|
+
let linkReady = false
|
|
145
146
|
let controlLowEvents = 0
|
|
146
147
|
let bulkLowEvents = 0
|
|
147
148
|
let reconnectCount = 0
|
|
@@ -216,6 +217,8 @@ export async function createWebRtcLink(options) {
|
|
|
216
217
|
}
|
|
217
218
|
},
|
|
218
219
|
})
|
|
220
|
+
// 数据链路是否已完成握手认证:完成后,发不出去的重协商答复不该再拆掉这条活链路。
|
|
221
|
+
void pipe.ready.then(() => { linkReady = true }, () => { })
|
|
219
222
|
|
|
220
223
|
/**
|
|
221
224
|
* @param {RTCDataChannel} channel RTC 数据通道
|
|
@@ -269,8 +272,12 @@ export async function createWebRtcLink(options) {
|
|
|
269
272
|
remoteSignalQueue.length = 0
|
|
270
273
|
localCandidateCount = 0
|
|
271
274
|
attachPeerConnection(peerConnection)
|
|
272
|
-
if (previous)
|
|
275
|
+
if (previous) {
|
|
276
|
+
// 先摘回调再关:旧连接 close() 会经 connectionstatechange 触发 pipe.close,而
|
|
277
|
+
// closeTransport() 关的是 peerConnection 这个变量(此刻已指向新连接),于是刚建好的一级被自己关掉(fount-p2p#39)。
|
|
278
|
+
detachPeerConnection(previous)
|
|
273
279
|
try { await previous.close() } catch { /* ignore */ }
|
|
280
|
+
}
|
|
274
281
|
nodeDebug('p2p:webrtc ice rung', {
|
|
275
282
|
rung: rungIndex,
|
|
276
283
|
policy: rungs[rungIndex],
|
|
@@ -288,22 +295,29 @@ export async function createWebRtcLink(options) {
|
|
|
288
295
|
|
|
289
296
|
/**
|
|
290
297
|
* 给 peer connection 挂上事件与回调(换级重建后需重新挂)。
|
|
298
|
+
*
|
|
299
|
+
* 回调一律用传入的这个连接,不读 `peerConnection` 闭包变量:换级后旧连接仍可能派发事件,
|
|
300
|
+
* 读变量就会把事件算到新连接头上。
|
|
291
301
|
* @param {RTCPeerConnection} connection peer connection
|
|
292
302
|
* @returns {void}
|
|
293
303
|
*/
|
|
294
304
|
function attachPeerConnection(connection) {
|
|
295
305
|
// 候选随 description 一次性带出(对端要先有 remoteDescription 才吃候选),故这里只计数、不外发。
|
|
296
|
-
// 事件 API 由后端提供(W3C 是 addEventListener);缺失时退化为 onicecandidate
|
|
306
|
+
// 事件 API 由后端提供(W3C 是 addEventListener);缺失时退化为 onicecandidate,摘的时候看 candidateCountHandler 在不在。
|
|
297
307
|
const countCandidate = event => {
|
|
298
308
|
if (event?.candidate) localCandidateCount++
|
|
299
309
|
}
|
|
300
|
-
if (connection.addEventListener)
|
|
310
|
+
if (connection.addEventListener) {
|
|
311
|
+
connection.candidateCountHandler = countCandidate
|
|
312
|
+
connection.addEventListener('icecandidate', countCandidate)
|
|
313
|
+
}
|
|
301
314
|
else {
|
|
302
315
|
const previousHandler = connection.onicecandidate
|
|
303
|
-
connection.
|
|
316
|
+
connection.candidateCountHandler = event => {
|
|
304
317
|
countCandidate(event)
|
|
305
318
|
previousHandler?.(event)
|
|
306
319
|
}
|
|
320
|
+
connection.onicecandidate = connection.candidateCountHandler
|
|
307
321
|
}
|
|
308
322
|
connection.ondatachannel = event => {
|
|
309
323
|
attachChannel(event.channel)
|
|
@@ -317,6 +331,19 @@ export async function createWebRtcLink(options) {
|
|
|
317
331
|
}
|
|
318
332
|
}
|
|
319
333
|
|
|
334
|
+
/**
|
|
335
|
+
* 摘掉某一级 peer connection 上挂的事件与回调(重建前必须调用,见 [buildPeerConnection])。
|
|
336
|
+
* @param {RTCPeerConnection} connection 待摘的连接
|
|
337
|
+
* @returns {void}
|
|
338
|
+
*/
|
|
339
|
+
function detachPeerConnection(connection) {
|
|
340
|
+
if (connection.candidateCountHandler) connection.removeEventListener?.('icecandidate', connection.candidateCountHandler)
|
|
341
|
+
else connection.onicecandidate = null
|
|
342
|
+
delete connection.candidateCountHandler
|
|
343
|
+
connection.ondatachannel = null
|
|
344
|
+
connection.onconnectionstatechange = null
|
|
345
|
+
}
|
|
346
|
+
|
|
320
347
|
/**
|
|
321
348
|
* 等本地候选收齐(停滞判定见 [collectIceGathering]),并回报本次是否产出了候选。
|
|
322
349
|
* @returns {Promise<boolean>} 是否收到过候选
|
|
@@ -396,11 +423,18 @@ export async function createWebRtcLink(options) {
|
|
|
396
423
|
candidates: localCandidateCount,
|
|
397
424
|
hasCandidates,
|
|
398
425
|
})
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
426
|
+
try {
|
|
427
|
+
await sendSignal({
|
|
428
|
+
type: 'description',
|
|
429
|
+
rung: rungIndex,
|
|
430
|
+
description: peerConnection.localDescription?.toJSON?.() ?? peerConnection.localDescription ?? answer,
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
// 已认证的数据链路不应被后续信令发送失败关闭。
|
|
435
|
+
if (!linkReady) throw error
|
|
436
|
+
nodeDebug('p2p:webrtc answer signal send failed after link ready', { reason: formatErrorReason(error) })
|
|
437
|
+
}
|
|
404
438
|
await pipe.maybeSendAuth()
|
|
405
439
|
}
|
|
406
440
|
return
|
|
@@ -469,7 +503,7 @@ export async function createWebRtcLink(options) {
|
|
|
469
503
|
void handleRemoteSignal(message).catch(error => pipe.close(`signal-error:${formatErrorReason(error)}`))
|
|
470
504
|
}) ?? null
|
|
471
505
|
|
|
472
|
-
//
|
|
506
|
+
// 信令随时可能到,故先为当前一级(第 0 级)建好 pc;发起方阶梯的首次迭代直接用它,不重建。
|
|
473
507
|
await ensurePeerConnection()
|
|
474
508
|
|
|
475
509
|
if (options.initiator) {
|
|
@@ -478,7 +512,8 @@ export async function createWebRtcLink(options) {
|
|
|
478
512
|
const ladderDeadline = Date.now() + handshakeTimeoutMs
|
|
479
513
|
let sentOffer = false
|
|
480
514
|
for (let index = 0; index < rungs.length; index++) {
|
|
481
|
-
|
|
515
|
+
// 同一级不重建:换 pc 会牵动并发信令与旧连接的关闭,只在真的换级时才需要(fount-p2p#39)。
|
|
516
|
+
if (rungIndex !== index) await buildPeerConnection(index)
|
|
482
517
|
attachChannel(peerConnection.createDataChannel(CHANNEL_CONTROL))
|
|
483
518
|
attachChannel(peerConnection.createDataChannel(CHANNEL_BULK))
|
|
484
519
|
const offer = await peerConnection.createOffer()
|
|
@@ -491,7 +526,8 @@ export async function createWebRtcLink(options) {
|
|
|
491
526
|
})
|
|
492
527
|
// 候选集为空说明这一级策略把本机候选全滤掉了(例如只产出 mDNS 候选),换更宽松的一级重建。
|
|
493
528
|
// 只在预算内、且还有更宽松的一级时升级;`rewrite-loopback` 起点不会走到这里(阶梯只有它自己)。
|
|
494
|
-
|
|
529
|
+
const escalatable = index + 1 < rungs.length
|
|
530
|
+
if (!hasCandidates && escalatable && Date.now() < ladderDeadline) {
|
|
495
531
|
nodeDebug('p2p:webrtc ice rung escalated', {
|
|
496
532
|
from: rungs[index],
|
|
497
533
|
to: rungs[index + 1],
|
|
@@ -509,7 +545,7 @@ export async function createWebRtcLink(options) {
|
|
|
509
545
|
nodeDebug('p2p:webrtc offer has no usable ice candidates', {
|
|
510
546
|
rung: index,
|
|
511
547
|
policy: rungs[index],
|
|
512
|
-
escalatable
|
|
548
|
+
escalatable,
|
|
513
549
|
iceServers: options.iceServers?.length || 0,
|
|
514
550
|
})
|
|
515
551
|
break
|
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
createNostrDiscoveryProvider,
|
|
16
16
|
resolveNostrRelayUrls,
|
|
17
17
|
} from '../discovery/nostr/index.mjs'
|
|
18
|
-
import {
|
|
18
|
+
import { getWorkingRelays, loadRelayPool, startNostrRelayDiscovery } from '../discovery/nostr/relays.mjs'
|
|
19
19
|
import { createBleGattLinkProvider } from '../link/providers/ble_gatt.mjs'
|
|
20
20
|
import {
|
|
21
21
|
listLinkProviders,
|
|
@@ -188,7 +188,7 @@ export function createRuntimeBootstrap(deps) {
|
|
|
188
188
|
.filter(entry => entry.rttMs != null)
|
|
189
189
|
.slice(0, MAX_ADVERT_RELAY_POOL)
|
|
190
190
|
.map(entry => ({ url: entry.url, rttMs: entry.rttMs })),
|
|
191
|
-
listen:
|
|
191
|
+
listen: resolveNostrRelayUrls(),
|
|
192
192
|
}
|
|
193
193
|
: { pool: [], listen: [] },
|
|
194
194
|
)
|