@steve02081504/fount-p2p 0.0.12 → 0.0.14
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 +41 -10
- package/core/composite_key.mjs +5 -5
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/storage.mjs +36 -36
- package/discovery/advert_peer_hints.mjs +9 -3
- package/discovery/adverts.mjs +115 -0
- package/discovery/bt/index.mjs +153 -112
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +2 -9
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +279 -0
- package/discovery/lan_interfaces.mjs +78 -0
- package/discovery/lan_peer_hints.mjs +22 -5
- package/discovery/nostr.mjs +474 -65
- package/federation/chunk_fetch_pending.mjs +10 -13
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +29 -28
- package/files/manifest_fetch.mjs +16 -3
- package/files/public_manifest.mjs +11 -11
- package/files/transfer_key_registry.mjs +2 -2
- package/index.mjs +86 -11
- package/infra/cli.mjs +62 -0
- package/infra/debug_log.mjs +56 -0
- package/infra/default_node_dir.mjs +22 -0
- package/infra/priority.mjs +56 -0
- package/infra/service.mjs +140 -0
- package/infra/tunables.json +5 -0
- package/link/frame.mjs +9 -16
- package/link/handshake.mjs +25 -18
- package/link/pipe.mjs +8 -8
- package/link/providers/ble_gatt.mjs +6 -6
- package/link/providers/lan_tcp.mjs +62 -33
- package/link/providers/webrtc.mjs +3 -1
- package/link/rtc.mjs +3 -3
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +5 -5
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +37 -5
- package/node/reputation_store.mjs +4 -4
- package/node/reputation_sync.mjs +318 -0
- package/node/routing_profile.mjs +21 -0
- package/node/signaling_config.mjs +30 -11
- package/overlay/index.mjs +22 -1
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -166
- package/transport/advert_ingest.mjs +11 -14
- package/transport/group_link_set.mjs +149 -99
- package/transport/link_registry.mjs +211 -60
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +45 -48
- package/transport/peer_pool.mjs +116 -14
- package/transport/remote_user_room.mjs +6 -9
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +170 -108
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +83 -158
- package/trust_graph/build.mjs +0 -2
- package/trust_graph/cache.mjs +12 -3
- package/trust_graph/registry.mjs +6 -6
- package/trust_graph/send.mjs +0 -3
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +3 -3
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/wire/part_ingress.mjs +32 -28
- package/wire/part_query.mjs +26 -20
- package/discovery/mdns.mjs +0 -197
- package/transport/signal_crypto.mjs +0 -104
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import dgram from 'node:dgram'
|
|
3
|
+
|
|
4
|
+
import { base64ToBytes, bytesToBase64 } from '../core/bytes_codec.mjs'
|
|
5
|
+
import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
6
|
+
import { nodeDebug, shortHash } from '../node/log.mjs'
|
|
7
|
+
|
|
8
|
+
import { noteAdvertPeerHints } from './advert_peer_hints.mjs'
|
|
9
|
+
import { ingestNetworkAdvert } from './adverts.mjs'
|
|
10
|
+
import { listMulticastIpv4Addresses } from './lan_interfaces.mjs'
|
|
11
|
+
import { getLanPeerHint } from './lan_peer_hints.mjs'
|
|
12
|
+
|
|
13
|
+
const DEFAULT_PORT = 53531
|
|
14
|
+
const DEFAULT_GROUP = '239.255.42.99'
|
|
15
|
+
const BEACON_INTERVAL_MS = 30_000
|
|
16
|
+
|
|
17
|
+
/** @type {Map<string, number>} nodeHash → lastSeenAt */
|
|
18
|
+
const visibleByHash = new Map()
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 记入 LAN 可见池。
|
|
22
|
+
* @param {string} nodeHash 节点 hash
|
|
23
|
+
* @param {number} [now=Date.now()] 当前时间
|
|
24
|
+
* @returns {void}
|
|
25
|
+
*/
|
|
26
|
+
export function noteLanVisibleNode(nodeHash, now = Date.now()) {
|
|
27
|
+
const hash = normalizeHex64(nodeHash)
|
|
28
|
+
if (!isHex64(hash)) return
|
|
29
|
+
visibleByHash.set(hash, now)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 列出未过期的 LAN 可见 nodeHash。
|
|
34
|
+
* @param {number} [now=Date.now()] 当前时间
|
|
35
|
+
* @param {number} [ttlMs=BEACON_INTERVAL_MS * 3] TTL
|
|
36
|
+
* @returns {string[]} 可见 nodeHash 列表
|
|
37
|
+
*/
|
|
38
|
+
export function listLanVisibleNodeHashes(now = Date.now(), ttlMs = BEACON_INTERVAL_MS * 3) {
|
|
39
|
+
/** @type {string[]} */
|
|
40
|
+
const out = []
|
|
41
|
+
for (const [hash, seenAt] of visibleByHash)
|
|
42
|
+
if (now - seenAt <= ttlMs) out.push(hash)
|
|
43
|
+
else visibleByHash.delete(hash)
|
|
44
|
+
|
|
45
|
+
return out
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @returns {void} 测试用 */
|
|
49
|
+
export function clearLanVisibleNodes() {
|
|
50
|
+
visibleByHash.clear()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Untrusted ingress:验签 network advert 后写入可见池 / peer hint。
|
|
55
|
+
* @param {Uint8Array} advertBytes 加密 network advert
|
|
56
|
+
* @param {{ address?: string, skipNodeHash?: string }} [meta] 发送方地址 / 本机 nodeHash(过滤自回环)
|
|
57
|
+
* @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签结果
|
|
58
|
+
*/
|
|
59
|
+
export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
|
|
60
|
+
if (!advertBytes?.byteLength) return null
|
|
61
|
+
const ingested = await ingestNetworkAdvert(advertBytes, meta)
|
|
62
|
+
if (!ingested) return null
|
|
63
|
+
const skipHash = meta.skipNodeHash ? normalizeHex64(meta.skipNodeHash) : null
|
|
64
|
+
if (skipHash && ingested.verifiedNodeHash === skipHash) return ingested
|
|
65
|
+
const firstSeen = !visibleByHash.has(ingested.verifiedNodeHash)
|
|
66
|
+
noteLanVisibleNode(ingested.verifiedNodeHash)
|
|
67
|
+
noteAdvertPeerHints(ingested.verifiedNodeHash, ingested.body, meta)
|
|
68
|
+
if (firstSeen) {
|
|
69
|
+
const host = String(meta.address || '').trim()
|
|
70
|
+
nodeDebug('p2p:lan peer visible', {
|
|
71
|
+
peer: shortHash(ingested.verifiedNodeHash),
|
|
72
|
+
host: host || undefined,
|
|
73
|
+
tcpPort: ingested.body?.tcpPort,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
return ingested
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* LAN UDP presence:段内 beacon,非 topic 订阅模型。
|
|
81
|
+
* @param {{ port?: number, group?: string }} [options] 配置
|
|
82
|
+
* @returns {import('./index.mjs').DiscoveryProvider} LAN 发现提供者
|
|
83
|
+
*/
|
|
84
|
+
export function createLanDiscoveryProvider(options = {}) {
|
|
85
|
+
const port = Number(options.port) || DEFAULT_PORT
|
|
86
|
+
const group = String(options.group || DEFAULT_GROUP)
|
|
87
|
+
/** @type {import('node:dgram').Socket | null} */
|
|
88
|
+
let socket = null
|
|
89
|
+
let bound = false
|
|
90
|
+
let bindPromise = null
|
|
91
|
+
let refs = 0
|
|
92
|
+
/** @type {ReturnType<typeof setInterval> | null} */
|
|
93
|
+
let beaconTimer = null
|
|
94
|
+
/** @type {string | null} */
|
|
95
|
+
let selfNodeHash = null
|
|
96
|
+
/** @type {Set<string>} */
|
|
97
|
+
const joinedAddresses = new Set()
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {import('node:dgram').Socket} sock UDP socket
|
|
101
|
+
* @returns {void}
|
|
102
|
+
*/
|
|
103
|
+
function ensureMemberships(sock) {
|
|
104
|
+
for (const addr of listMulticastIpv4Addresses()) {
|
|
105
|
+
if (joinedAddresses.has(addr)) continue
|
|
106
|
+
try {
|
|
107
|
+
sock.addMembership(group, addr)
|
|
108
|
+
joinedAddresses.add(addr)
|
|
109
|
+
}
|
|
110
|
+
catch { /* ignore per-interface join failure */ }
|
|
111
|
+
}
|
|
112
|
+
if (!joinedAddresses.size)
|
|
113
|
+
try { sock.addMembership(group) } catch { /* ignore */ }
|
|
114
|
+
}
|
|
115
|
+
function getSocket() {
|
|
116
|
+
return socket ||= dgram.createSocket({ type: 'udp4', reuseAddr: true })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @returns {Promise<void>}
|
|
121
|
+
*/
|
|
122
|
+
async function ensureBound() {
|
|
123
|
+
if (bound) return
|
|
124
|
+
if (!bindPromise)
|
|
125
|
+
bindPromise = (async () => {
|
|
126
|
+
const sock = getSocket()
|
|
127
|
+
await new Promise((resolve, reject) => {
|
|
128
|
+
sock.once('error', reject)
|
|
129
|
+
sock.bind(port, '0.0.0.0', () => {
|
|
130
|
+
sock.off('error', reject)
|
|
131
|
+
ensureMemberships(sock)
|
|
132
|
+
sock.setMulticastTTL(1)
|
|
133
|
+
resolve()
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
if (refs <= 0) {
|
|
137
|
+
try { sock.close() } catch { /* ignore */ }
|
|
138
|
+
if (socket === sock) socket = null
|
|
139
|
+
bound = false
|
|
140
|
+
joinedAddresses.clear()
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
sock.on('message', (raw, rinfo) => {
|
|
144
|
+
let packet
|
|
145
|
+
try { packet = JSON.parse(String(raw)) } catch { return }
|
|
146
|
+
if (packet?.type !== 'presence') return
|
|
147
|
+
let advertBytes
|
|
148
|
+
try {
|
|
149
|
+
advertBytes = packet.advertBytes
|
|
150
|
+
? base64ToBytes(packet.advertBytes)
|
|
151
|
+
: null
|
|
152
|
+
}
|
|
153
|
+
catch { return }
|
|
154
|
+
if (!advertBytes?.byteLength) return
|
|
155
|
+
void acceptLanPresenceAdvert(advertBytes, {
|
|
156
|
+
address: rinfo?.address,
|
|
157
|
+
provider: 'lan',
|
|
158
|
+
skipNodeHash: selfNodeHash || undefined,
|
|
159
|
+
}).catch(() => { })
|
|
160
|
+
})
|
|
161
|
+
bound = true
|
|
162
|
+
})().finally(() => {
|
|
163
|
+
if (!bound) bindPromise = null
|
|
164
|
+
})
|
|
165
|
+
await bindPromise
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {object} beacon beacon 载荷
|
|
170
|
+
* @returns {Promise<void>}
|
|
171
|
+
*/
|
|
172
|
+
async function multicastBeacon(beacon) {
|
|
173
|
+
await ensureBound()
|
|
174
|
+
const sock = getSocket()
|
|
175
|
+
ensureMemberships(sock)
|
|
176
|
+
const packet = Buffer.from(JSON.stringify({ type: 'presence', ...beacon }))
|
|
177
|
+
const addrs = listMulticastIpv4Addresses()
|
|
178
|
+
/**
|
|
179
|
+
* @param {string | undefined} ifaceAddr 组播出口地址
|
|
180
|
+
* @returns {Promise<void>}
|
|
181
|
+
*/
|
|
182
|
+
const sendOnce = ifaceAddr => new Promise((resolve, reject) => {
|
|
183
|
+
if (ifaceAddr) sock.setMulticastInterface(ifaceAddr)
|
|
184
|
+
sock.send(packet, port, group, error => error ? reject(error) : resolve())
|
|
185
|
+
})
|
|
186
|
+
if (!addrs.length) {
|
|
187
|
+
await sendOnce()
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
let sent = 0
|
|
191
|
+
/** @type {unknown} */
|
|
192
|
+
let lastError = null
|
|
193
|
+
for (const addr of addrs) {
|
|
194
|
+
try {
|
|
195
|
+
await sendOnce(addr)
|
|
196
|
+
sent++
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
lastError = error
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!sent) throw lastError || new Error('p2p: lan multicast send failed')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @returns {() => void} 释放引用;引用归零时关闭 socket
|
|
207
|
+
*/
|
|
208
|
+
function acquire() {
|
|
209
|
+
refs++
|
|
210
|
+
void ensureBound().catch(() => { })
|
|
211
|
+
return () => {
|
|
212
|
+
refs = Math.max(0, refs - 1)
|
|
213
|
+
if (refs > 0 || !socket) return
|
|
214
|
+
if (beaconTimer) { clearInterval(beaconTimer); beaconTimer = null }
|
|
215
|
+
selfNodeHash = null
|
|
216
|
+
joinedAddresses.clear()
|
|
217
|
+
try { socket.close() } catch { /* ignore */ }
|
|
218
|
+
socket = null
|
|
219
|
+
bound = false
|
|
220
|
+
bindPromise = null
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
id: 'lan',
|
|
226
|
+
priority: 10,
|
|
227
|
+
caps: { canDiscover: true, canSignal: false, canRelay: false },
|
|
228
|
+
/**
|
|
229
|
+
* @param {{ limit?: number, roomSecret?: string }} [options] 扫描选项
|
|
230
|
+
* @returns {Promise<string[]>} 可见 nodeHash;群扫描时 LAN 无群语义,返回空
|
|
231
|
+
*/
|
|
232
|
+
async listVisibleNodeHashes(options = {}) {
|
|
233
|
+
if (options.roomSecret) return []
|
|
234
|
+
const limit = Math.max(1, Number(options.limit) || 64)
|
|
235
|
+
await ensureBound().catch(() => { })
|
|
236
|
+
return listLanVisibleNodeHashes().slice(0, limit)
|
|
237
|
+
},
|
|
238
|
+
/**
|
|
239
|
+
* 有 LAN peer hint 则可经 lan_tcp 拨号(门面 dialer 完成建链)。
|
|
240
|
+
* @param {string} nodeHash 目标
|
|
241
|
+
* @returns {Promise<boolean>} 存在 peer hint 时为 true
|
|
242
|
+
*/
|
|
243
|
+
async connectToNode(nodeHash) {
|
|
244
|
+
const hash = normalizeHex64(nodeHash)
|
|
245
|
+
return isHex64(hash) && !!getLanPeerHint(hash)
|
|
246
|
+
},
|
|
247
|
+
/**
|
|
248
|
+
* @param {() => Promise<{ nodeHash?: string, tcpPort?: number, advertBytes?: Uint8Array, advertBody?: object } | null>} getBeacon 本机 beacon
|
|
249
|
+
* @returns {Promise<() => void>} 停止函数
|
|
250
|
+
*/
|
|
251
|
+
async startPresence(getBeacon) {
|
|
252
|
+
const release = acquire()
|
|
253
|
+
/**
|
|
254
|
+
* @returns {Promise<void>}
|
|
255
|
+
*/
|
|
256
|
+
const send = async () => {
|
|
257
|
+
const body = await getBeacon?.()
|
|
258
|
+
if (!body) return
|
|
259
|
+
if (body.nodeHash) selfNodeHash = normalizeHex64(body.nodeHash)
|
|
260
|
+
const advertBytes = body.advertBytes?.byteLength
|
|
261
|
+
? body.advertBytes
|
|
262
|
+
: null
|
|
263
|
+
if (!advertBytes?.byteLength) return
|
|
264
|
+
await multicastBeacon({
|
|
265
|
+
nodeHash: body.nodeHash,
|
|
266
|
+
tcpPort: body.tcpPort,
|
|
267
|
+
advertBytes: bytesToBase64(advertBytes),
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
void send().catch(() => { })
|
|
271
|
+
beaconTimer = setInterval(() => { void send().catch(() => { }) }, BEACON_INTERVAL_MS)
|
|
272
|
+
beaconTimer.unref?.()
|
|
273
|
+
return () => {
|
|
274
|
+
if (beaconTimer) { clearInterval(beaconTimer); beaconTimer = null }
|
|
275
|
+
release()
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -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
|
/**
|