@steve02081504/fount-p2p 0.0.13 → 0.0.15
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/README.md +8 -0
- package/deno.json +4 -1
- package/discovery/advert_peer_hints.mjs +8 -2
- package/discovery/adverts.mjs +5 -0
- package/discovery/bt/index.mjs +4 -1
- package/discovery/lan.mjs +73 -14
- package/discovery/lan_interfaces.mjs +78 -0
- package/discovery/lan_peer_hints.mjs +22 -5
- package/discovery/nostr.mjs +384 -136
- package/discovery/peer_clue.mjs +23 -0
- package/link/handshake.mjs +15 -7
- package/link/providers/lan_tcp.mjs +62 -33
- package/link/providers/webrtc.mjs +15 -1
- package/link/rtc.mjs +14 -5
- package/package.json +1 -1
- package/transport/link_registry.mjs +37 -1
- package/transport/runtime_bootstrap.mjs +4 -1
package/README.md
CHANGED
|
@@ -44,6 +44,14 @@ npx @steve02081504/fount-p2p
|
|
|
44
44
|
|
|
45
45
|
Default `nodeDir`: Windows `%LOCALAPPDATA%/fount-p2p/node`; elsewhere `~/.local/share/fount-p2p/node`.
|
|
46
46
|
|
|
47
|
+
Deno (optional; WebRTC needs explicit native scripts — **not** blanket `--allow-scripts`):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
deno run -A --minimum-dependency-age=0 --node-modules-dir=auto --allow-scripts=npm:node-datachannel npm:@steve02081504/fount-p2p
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
See [docs/runtime.md](./docs/runtime.md).
|
|
54
|
+
|
|
47
55
|
```javascript
|
|
48
56
|
import { initNode, startNode, startInfra, stopInfra, setInfraPriority } from '@steve02081504/fount-p2p'
|
|
49
57
|
|
package/deno.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { normalizeTcpPort } from '../core/tcp_port.mjs'
|
|
2
2
|
|
|
3
3
|
import { noteBtPeerHint } from './bt/peer_hints.mjs'
|
|
4
|
+
import { normalizeLanHosts } from './lan_interfaces.mjs'
|
|
4
5
|
import { noteLanPeerHint } from './lan_peer_hints.mjs'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* 从已验证的 discovery advert + provider meta 写入 LAN/BT peer hints。
|
|
8
9
|
* 任意 discovery 路径(node / group / scoped)收到 advert 时都应调用。
|
|
10
|
+
* meta.address(观测地址)优先于 body.lanHosts(自报地址)。
|
|
9
11
|
* @param {string} verifiedNodeHash 验签通过的 nodeHash
|
|
10
|
-
* @param {{ tcpPort?: unknown } | null | undefined} body advert body
|
|
12
|
+
* @param {{ tcpPort?: unknown, lanHosts?: unknown } | null | undefined} body advert body
|
|
11
13
|
* @param {{ address?: unknown, peripheralId?: unknown } | null | undefined} meta discovery provider meta
|
|
12
14
|
* @returns {void}
|
|
13
15
|
*/
|
|
@@ -15,7 +17,11 @@ export function noteAdvertPeerHints(verifiedNodeHash, body, meta) {
|
|
|
15
17
|
if (meta?.peripheralId)
|
|
16
18
|
noteBtPeerHint(verifiedNodeHash, meta.peripheralId)
|
|
17
19
|
const tcpPort = normalizeTcpPort(body?.tcpPort)
|
|
20
|
+
if (tcpPort == null) return
|
|
21
|
+
// lanHosts 先写(差→优,unshift 后优在前),再写观测 address,使 address 最优先
|
|
22
|
+
for (const host of normalizeLanHosts(body?.lanHosts).toReversed())
|
|
23
|
+
noteLanPeerHint(verifiedNodeHash, { host, port: tcpPort })
|
|
18
24
|
const address = String(meta?.address || '').trim()
|
|
19
|
-
if (
|
|
25
|
+
if (address)
|
|
20
26
|
noteLanPeerHint(verifiedNodeHash, { host: address, port: tcpPort })
|
|
21
27
|
}
|
package/discovery/adverts.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
networkRendezvousKey,
|
|
9
9
|
nodeRendezvousKey,
|
|
10
10
|
} from './internal/signal_crypto.mjs'
|
|
11
|
+
import { listMulticastIpv4Addresses } from './lan_interfaces.mjs'
|
|
11
12
|
|
|
12
13
|
/** @typedef {'node' | 'network' | { roomSecret: string }} AdvertScope */
|
|
13
14
|
|
|
@@ -33,9 +34,13 @@ export function rendezvousKeyForScope(scope, selfNodeHash) {
|
|
|
33
34
|
*/
|
|
34
35
|
export async function buildSignedAdvertForScope(scope, localIdentity, tcpPort) {
|
|
35
36
|
const key = rendezvousKeyForScope(scope, localIdentity.nodeHash)
|
|
37
|
+
const lanHosts = scope === 'network' && tcpPort != null
|
|
38
|
+
? listMulticastIpv4Addresses()
|
|
39
|
+
: []
|
|
36
40
|
return await buildSignedAdvert(key, Date.now(), {
|
|
37
41
|
...localIdentity,
|
|
38
42
|
...tcpPort != null ? { tcpPort } : {},
|
|
43
|
+
...lanHosts.length ? { lanHosts } : {},
|
|
39
44
|
})
|
|
40
45
|
}
|
|
41
46
|
|
package/discovery/bt/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { isHex64, normalizeHex64 } from '../../core/hexIds.mjs'
|
|
|
4
4
|
import { nodeDebug, shortHash } from '../../node/log.mjs'
|
|
5
5
|
import { noteAdvertPeerHints } from '../advert_peer_hints.mjs'
|
|
6
6
|
import { ingestNetworkAdvert } from '../adverts.mjs'
|
|
7
|
+
import { noteDiscoveryPeerClue } from '../peer_clue.mjs'
|
|
7
8
|
|
|
8
9
|
import { getBtPeerHint } from './peer_hints.mjs'
|
|
9
10
|
import { canUseBluetoothRuntime, loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from './runtime.mjs'
|
|
@@ -106,11 +107,13 @@ export async function acceptBtScannedPresence(bytes, meta) {
|
|
|
106
107
|
const firstSeen = !visibleByHash.has(ingested.verifiedNodeHash)
|
|
107
108
|
noteBtVisibleNode(ingested.verifiedNodeHash)
|
|
108
109
|
noteAdvertPeerHints(ingested.verifiedNodeHash, ingested.body, meta)
|
|
109
|
-
if (firstSeen)
|
|
110
|
+
if (firstSeen) {
|
|
111
|
+
noteDiscoveryPeerClue(ingested.verifiedNodeHash)
|
|
110
112
|
nodeDebug('p2p:bt peer visible', {
|
|
111
113
|
peer: shortHash(ingested.verifiedNodeHash),
|
|
112
114
|
peripheralId,
|
|
113
115
|
})
|
|
116
|
+
}
|
|
114
117
|
return ingested
|
|
115
118
|
}
|
|
116
119
|
|
package/discovery/lan.mjs
CHANGED
|
@@ -5,8 +5,11 @@ import { base64ToBytes, bytesToBase64 } from '../core/bytes_codec.mjs'
|
|
|
5
5
|
import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
6
6
|
import { nodeDebug, shortHash } from '../node/log.mjs'
|
|
7
7
|
|
|
8
|
+
import { noteAdvertPeerHints } from './advert_peer_hints.mjs'
|
|
8
9
|
import { ingestNetworkAdvert } from './adverts.mjs'
|
|
9
|
-
import {
|
|
10
|
+
import { listMulticastIpv4Addresses } from './lan_interfaces.mjs'
|
|
11
|
+
import { getLanPeerHint } from './lan_peer_hints.mjs'
|
|
12
|
+
import { noteDiscoveryPeerClue } from './peer_clue.mjs'
|
|
10
13
|
|
|
11
14
|
const DEFAULT_PORT = 53531
|
|
12
15
|
const DEFAULT_GROUP = '239.255.42.99'
|
|
@@ -51,31 +54,33 @@ export function clearLanVisibleNodes() {
|
|
|
51
54
|
/**
|
|
52
55
|
* Untrusted ingress:验签 network advert 后写入可见池 / peer hint。
|
|
53
56
|
* @param {Uint8Array} advertBytes 加密 network advert
|
|
54
|
-
* @param {{ address?: string }} [meta] 发送方地址
|
|
57
|
+
* @param {{ address?: string, skipNodeHash?: string }} [meta] 发送方地址 / 本机 nodeHash(过滤自回环)
|
|
55
58
|
* @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签结果
|
|
56
59
|
*/
|
|
57
60
|
export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
|
|
58
61
|
if (!advertBytes?.byteLength) return null
|
|
59
62
|
const ingested = await ingestNetworkAdvert(advertBytes, meta)
|
|
60
63
|
if (!ingested) return null
|
|
64
|
+
const skipHash = meta.skipNodeHash ? normalizeHex64(meta.skipNodeHash) : null
|
|
65
|
+
if (skipHash && ingested.verifiedNodeHash === skipHash) return ingested
|
|
61
66
|
const firstSeen = !visibleByHash.has(ingested.verifiedNodeHash)
|
|
62
67
|
noteLanVisibleNode(ingested.verifiedNodeHash)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (firstSeen)
|
|
68
|
+
noteAdvertPeerHints(ingested.verifiedNodeHash, ingested.body, meta)
|
|
69
|
+
if (firstSeen) {
|
|
70
|
+
noteDiscoveryPeerClue(ingested.verifiedNodeHash)
|
|
71
|
+
const host = String(meta.address || '').trim()
|
|
68
72
|
nodeDebug('p2p:lan peer visible', {
|
|
69
73
|
peer: shortHash(ingested.verifiedNodeHash),
|
|
70
74
|
host: host || undefined,
|
|
71
|
-
tcpPort: tcpPort
|
|
75
|
+
tcpPort: ingested.body?.tcpPort,
|
|
72
76
|
})
|
|
77
|
+
}
|
|
73
78
|
return ingested
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
/**
|
|
77
82
|
* LAN UDP presence:段内 beacon,非 topic 订阅模型。
|
|
78
|
-
* @param {{ port?: number, group?: string }} [options] 配置
|
|
83
|
+
* @param {{ port?: number, group?: string, localNodeHash?: string }} [options] 配置
|
|
79
84
|
* @returns {import('./index.mjs').DiscoveryProvider} LAN 发现提供者
|
|
80
85
|
*/
|
|
81
86
|
export function createLanDiscoveryProvider(options = {}) {
|
|
@@ -88,9 +93,33 @@ export function createLanDiscoveryProvider(options = {}) {
|
|
|
88
93
|
let refs = 0
|
|
89
94
|
/** @type {ReturnType<typeof setInterval> | null} */
|
|
90
95
|
let beaconTimer = null
|
|
96
|
+
const seededSelf = normalizeHex64(options.localNodeHash)
|
|
97
|
+
/** @type {string | null} */
|
|
98
|
+
let selfNodeHash = isHex64(seededSelf) ? seededSelf : null
|
|
99
|
+
/** @type {Set<string>} */
|
|
100
|
+
const joinedAddresses = new Set()
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 在每个本机 IPv4 接口上加入组播组;全部失败则回退到默认接口。
|
|
104
|
+
* @param {import('node:dgram').Socket} sock UDP socket
|
|
105
|
+
* @returns {void}
|
|
106
|
+
*/
|
|
107
|
+
function ensureMemberships(sock) {
|
|
108
|
+
for (const addr of listMulticastIpv4Addresses()) {
|
|
109
|
+
if (joinedAddresses.has(addr)) continue
|
|
110
|
+
try {
|
|
111
|
+
sock.addMembership(group, addr)
|
|
112
|
+
joinedAddresses.add(addr)
|
|
113
|
+
}
|
|
114
|
+
catch { /* ignore per-interface join failure */ }
|
|
115
|
+
}
|
|
116
|
+
if (!joinedAddresses.size)
|
|
117
|
+
try { sock.addMembership(group) } catch { /* ignore */ }
|
|
118
|
+
}
|
|
91
119
|
|
|
92
120
|
/**
|
|
93
|
-
*
|
|
121
|
+
* 懒创建复用的 udp4 socket(reuseAddr,便于同机多实例绑同一 port)。
|
|
122
|
+
* @returns {import('node:dgram').Socket} 组播 presence socket
|
|
94
123
|
*/
|
|
95
124
|
function getSocket() {
|
|
96
125
|
return socket ||= dgram.createSocket({ type: 'udp4', reuseAddr: true })
|
|
@@ -108,7 +137,7 @@ export function createLanDiscoveryProvider(options = {}) {
|
|
|
108
137
|
sock.once('error', reject)
|
|
109
138
|
sock.bind(port, '0.0.0.0', () => {
|
|
110
139
|
sock.off('error', reject)
|
|
111
|
-
sock
|
|
140
|
+
ensureMemberships(sock)
|
|
112
141
|
sock.setMulticastTTL(1)
|
|
113
142
|
resolve()
|
|
114
143
|
})
|
|
@@ -117,6 +146,7 @@ export function createLanDiscoveryProvider(options = {}) {
|
|
|
117
146
|
try { sock.close() } catch { /* ignore */ }
|
|
118
147
|
if (socket === sock) socket = null
|
|
119
148
|
bound = false
|
|
149
|
+
joinedAddresses.clear()
|
|
120
150
|
return
|
|
121
151
|
}
|
|
122
152
|
sock.on('message', (raw, rinfo) => {
|
|
@@ -131,8 +161,11 @@ export function createLanDiscoveryProvider(options = {}) {
|
|
|
131
161
|
}
|
|
132
162
|
catch { return }
|
|
133
163
|
if (!advertBytes?.byteLength) return
|
|
134
|
-
void acceptLanPresenceAdvert(advertBytes, {
|
|
135
|
-
|
|
164
|
+
void acceptLanPresenceAdvert(advertBytes, {
|
|
165
|
+
address: rinfo?.address,
|
|
166
|
+
provider: 'lan',
|
|
167
|
+
skipNodeHash: selfNodeHash || undefined,
|
|
168
|
+
}).catch(() => { })
|
|
136
169
|
})
|
|
137
170
|
bound = true
|
|
138
171
|
})().finally(() => {
|
|
@@ -148,10 +181,33 @@ export function createLanDiscoveryProvider(options = {}) {
|
|
|
148
181
|
async function multicastBeacon(beacon) {
|
|
149
182
|
await ensureBound()
|
|
150
183
|
const sock = getSocket()
|
|
184
|
+
ensureMemberships(sock)
|
|
151
185
|
const packet = Buffer.from(JSON.stringify({ type: 'presence', ...beacon }))
|
|
152
|
-
|
|
186
|
+
const addrs = listMulticastIpv4Addresses()
|
|
187
|
+
/**
|
|
188
|
+
* @param {string | undefined} ifaceAddr 组播出口地址
|
|
189
|
+
* @returns {Promise<void>}
|
|
190
|
+
*/
|
|
191
|
+
const sendOnce = ifaceAddr => new Promise((resolve, reject) => {
|
|
192
|
+
if (ifaceAddr) sock.setMulticastInterface(ifaceAddr)
|
|
153
193
|
sock.send(packet, port, group, error => error ? reject(error) : resolve())
|
|
154
194
|
})
|
|
195
|
+
if (!addrs.length) {
|
|
196
|
+
await sendOnce()
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
let sent = 0
|
|
200
|
+
/** @type {unknown} */
|
|
201
|
+
let lastError = null
|
|
202
|
+
for (const addr of addrs)
|
|
203
|
+
try {
|
|
204
|
+
await sendOnce(addr)
|
|
205
|
+
sent++
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
lastError = error
|
|
209
|
+
}
|
|
210
|
+
if (!sent) throw lastError || new Error('p2p: lan multicast send failed')
|
|
155
211
|
}
|
|
156
212
|
|
|
157
213
|
/**
|
|
@@ -164,6 +220,8 @@ export function createLanDiscoveryProvider(options = {}) {
|
|
|
164
220
|
refs = Math.max(0, refs - 1)
|
|
165
221
|
if (refs > 0 || !socket) return
|
|
166
222
|
if (beaconTimer) { clearInterval(beaconTimer); beaconTimer = null }
|
|
223
|
+
selfNodeHash = null
|
|
224
|
+
joinedAddresses.clear()
|
|
167
225
|
try { socket.close() } catch { /* ignore */ }
|
|
168
226
|
socket = null
|
|
169
227
|
bound = false
|
|
@@ -206,6 +264,7 @@ export function createLanDiscoveryProvider(options = {}) {
|
|
|
206
264
|
const send = async () => {
|
|
207
265
|
const body = await getBeacon?.()
|
|
208
266
|
if (!body) return
|
|
267
|
+
if (body.nodeHash) selfNodeHash = normalizeHex64(body.nodeHash)
|
|
209
268
|
const advertBytes = body.advertBytes?.byteLength
|
|
210
269
|
? body.advertBytes
|
|
211
270
|
: null
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
|
|
3
|
+
/** advert / 组播 beacon 携带的 LAN IPv4 上限 */
|
|
4
|
+
export const MAX_LAN_HOSTS = 4
|
|
5
|
+
|
|
6
|
+
const IPV4_RE = /^(?:\d{1,3}\.){3}\d{1,3}$/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* untrusted ingress:清洗 advert body 中的 LAN IPv4 列表。
|
|
10
|
+
* @param {unknown} input 原始 lanHosts
|
|
11
|
+
* @returns {string[]} 去重后的 IPv4 列表
|
|
12
|
+
*/
|
|
13
|
+
export function normalizeLanHosts(input) {
|
|
14
|
+
if (!input) return []
|
|
15
|
+
const arr = Array.isArray(input) ? input : [input]
|
|
16
|
+
const seen = new Set()
|
|
17
|
+
/** @type {string[]} */
|
|
18
|
+
const out = []
|
|
19
|
+
for (const item of arr) {
|
|
20
|
+
const host = String(item || '').trim()
|
|
21
|
+
if (!host || !IPV4_RE.test(host) || seen.has(host)) continue
|
|
22
|
+
seen.add(host)
|
|
23
|
+
out.push(host)
|
|
24
|
+
if (out.length >= MAX_LAN_HOSTS) break
|
|
25
|
+
}
|
|
26
|
+
return out
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} addr IPv4
|
|
31
|
+
* @returns {number} 排序权重(越小越优先)
|
|
32
|
+
*/
|
|
33
|
+
function lanAddressRank(addr) {
|
|
34
|
+
if (addr.startsWith('169.254.')) return 100
|
|
35
|
+
if (addr.startsWith('192.168.56.')) return 90
|
|
36
|
+
if (addr.startsWith('192.168.')) return 10
|
|
37
|
+
if (addr.startsWith('10.')) return 20
|
|
38
|
+
const match = /^172\.(\d+)\./u.exec(addr)
|
|
39
|
+
if (match && Number(match[1]) >= 16 && Number(match[1]) <= 31) return 30
|
|
40
|
+
return 50
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string[]} addrs IPv4 列表
|
|
45
|
+
* @returns {string[]} 按 LAN 可达性优先排序
|
|
46
|
+
*/
|
|
47
|
+
function prioritizeLanAddresses(addrs) {
|
|
48
|
+
return [...addrs].sort((left, right) => lanAddressRank(left) - lanAddressRank(right))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string | number} family os.networkInterfaces family
|
|
53
|
+
* @returns {boolean} 是否 IPv4
|
|
54
|
+
*/
|
|
55
|
+
function isIpv4Family(family) {
|
|
56
|
+
return family === 'IPv4' || family === 4
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 本机可用于 LAN 组播 / advert 的非 internal IPv4 地址。
|
|
61
|
+
* @returns {string[]} 去重、排序后的地址列表
|
|
62
|
+
*/
|
|
63
|
+
export function listMulticastIpv4Addresses() {
|
|
64
|
+
const seen = new Set()
|
|
65
|
+
/** @type {string[]} */
|
|
66
|
+
const addrs = []
|
|
67
|
+
for (const ifaces of Object.values(os.networkInterfaces())) {
|
|
68
|
+
if (!ifaces) continue
|
|
69
|
+
for (const iface of ifaces) {
|
|
70
|
+
if (iface.internal || !isIpv4Family(iface.family)) continue
|
|
71
|
+
const addr = String(iface.address || '').trim()
|
|
72
|
+
if (!addr || seen.has(addr)) continue
|
|
73
|
+
seen.add(addr)
|
|
74
|
+
addrs.push(addr)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return prioritizeLanAddresses(addrs).slice(0, MAX_LAN_HOSTS)
|
|
78
|
+
}
|
|
@@ -5,11 +5,15 @@ import { createTtlMap } from '../utils/ttl_map.mjs'
|
|
|
5
5
|
/** LAN peer hint 存活时间。 */
|
|
6
6
|
export const LAN_PEER_HINT_TTL_MS = 5 * 60_000
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/** 单 peer 保留的 endpoint 上限。 */
|
|
9
|
+
const MAX_ENDPOINTS = 8
|
|
10
|
+
|
|
11
|
+
/** @type {ReturnType<typeof createTtlMap<{ endpoints: { host: string, port: number }[] }>>} */
|
|
9
12
|
const hints = createTtlMap(LAN_PEER_HINT_TTL_MS)
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* 记录 LAN 上观察到的 nodeHash → host:port。
|
|
16
|
+
* 最新写入排在最前(dial / getLanPeerHint 优先用新观测)。
|
|
13
17
|
* @param {string} nodeHash 节点 64 hex
|
|
14
18
|
* @param {{ host: string, port: number }} endpoint 端点
|
|
15
19
|
* @returns {void}
|
|
@@ -19,19 +23,32 @@ export function noteLanPeerHint(nodeHash, endpoint) {
|
|
|
19
23
|
const host = String(endpoint?.host || '').trim()
|
|
20
24
|
const port = normalizeTcpPort(endpoint?.port)
|
|
21
25
|
if (!hash || !host || !port) return
|
|
22
|
-
hints.
|
|
26
|
+
const existing = hints.get(hash)?.endpoints ?? []
|
|
27
|
+
const next = existing.filter(item => !(item.host === host && item.port === port))
|
|
28
|
+
next.unshift({ host, port })
|
|
29
|
+
hints.set(hash, { endpoints: next.slice(0, MAX_ENDPOINTS) })
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/**
|
|
26
|
-
*
|
|
33
|
+
* 查询未过期的首个 LAN peer hint(最新观测)。
|
|
27
34
|
* @param {string} nodeHash 节点 64 hex
|
|
28
35
|
* @param {number} [now=Date.now()] 当前时间(测试可注入)
|
|
29
36
|
* @returns {{ host: string, port: number } | null} hint 或 null
|
|
30
37
|
*/
|
|
31
38
|
export function getLanPeerHint(nodeHash, now = Date.now()) {
|
|
39
|
+
return listLanPeerHints(nodeHash, now)[0] ?? null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 查询未过期的全部 LAN peer hint(最新在前)。
|
|
44
|
+
* @param {string} nodeHash 节点 64 hex
|
|
45
|
+
* @param {number} [now=Date.now()] 当前时间(测试可注入)
|
|
46
|
+
* @returns {{ host: string, port: number }[]} hint 列表
|
|
47
|
+
*/
|
|
48
|
+
export function listLanPeerHints(nodeHash, now = Date.now()) {
|
|
32
49
|
const hash = normalizeHex64(nodeHash)
|
|
33
|
-
if (!hash) return
|
|
34
|
-
return hints.get(hash, now)
|
|
50
|
+
if (!hash) return []
|
|
51
|
+
return hints.get(hash, now)?.endpoints ?? []
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
/**
|