@steve02081504/fount-p2p 0.0.5 → 0.0.7

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.
Files changed (47) hide show
  1. package/core/tcp_port.mjs +11 -0
  2. package/dag/canonicalize_row.mjs +5 -5
  3. package/discovery/advert_peer_hints.mjs +21 -0
  4. package/discovery/{bt.mjs → bt/index.mjs} +151 -55
  5. package/discovery/bt/peer_hints.mjs +41 -0
  6. package/discovery/bt/runtime.mjs +46 -0
  7. package/discovery/lan_peer_hints.mjs +43 -0
  8. package/discovery/mdns.mjs +10 -6
  9. package/discovery/nostr.mjs +3 -3
  10. package/federation/chunk_fetch_pending.mjs +3 -3
  11. package/federation/chunk_fetch_scheduler.mjs +3 -3
  12. package/federation/dag_order_cache.mjs +3 -3
  13. package/federation/topo_order_memo.mjs +3 -3
  14. package/files/chunk_responder.mjs +1 -1
  15. package/files/evfs.mjs +26 -26
  16. package/files/transfer_key.mjs +9 -9
  17. package/files/transfer_key_registry.mjs +9 -9
  18. package/governance/join_pow.mjs +17 -17
  19. package/index.mjs +3 -2
  20. package/link/channel_mux.mjs +7 -7
  21. package/link/frame.mjs +5 -5
  22. package/link/handshake.mjs +63 -36
  23. package/link/pipe.mjs +404 -0
  24. package/link/providers/ble_gatt.mjs +339 -0
  25. package/link/providers/index.mjs +90 -0
  26. package/link/providers/lan_tcp.mjs +342 -0
  27. package/link/providers/levels.mjs +9 -0
  28. package/link/providers/webrtc.mjs +385 -0
  29. package/mailbox/deliver_or_store.mjs +13 -13
  30. package/mailbox/wire.mjs +4 -4
  31. package/node/reputation_store.mjs +5 -5
  32. package/node/retention_policy.mjs +8 -8
  33. package/overlay/index.mjs +6 -6
  34. package/package.json +3 -2
  35. package/registries/inbound.mjs +8 -8
  36. package/rooms/scoped_link.mjs +23 -21
  37. package/timeline/append_core.mjs +3 -3
  38. package/transport/group_link_set.mjs +35 -33
  39. package/transport/link_registry.mjs +250 -63
  40. package/transport/peer_pool.mjs +4 -4
  41. package/transport/user_room.mjs +12 -14
  42. package/utils/ttl_map.mjs +41 -0
  43. package/wire/group_part.mjs +3 -3
  44. package/wire/part_ingress.mjs +14 -14
  45. package/wire/part_query.mjs +81 -82
  46. package/wire/part_query_cache.mjs +7 -0
  47. package/link/link.mjs +0 -617
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 规范化 TCP 端口(advert / peer hint 共用)。
3
+ * @param {unknown} port 原始端口
4
+ * @returns {number | null} 有效端口或 null(未提供 / 非法)
5
+ */
6
+ export function normalizeTcpPort(port) {
7
+ if (port == null || port === '') return null
8
+ const value = Number(port)
9
+ if (!Number.isInteger(value) || value < 1 || value > 65535) return null
10
+ return value
11
+ }
@@ -47,19 +47,19 @@ export function canonicalizeRowContent(content, hexKeys, entityHashKeys = new Se
47
47
  * prepare?: (event: object) => object,
48
48
  * contentHexKeys?: ReadonlySet<string>,
49
49
  * entityHashKeys?: ReadonlySet<string>,
50
- * }} [opts] 各域字段集
50
+ * }} [options] 各域字段集
51
51
  * @returns {object} canonical 行
52
52
  */
53
- export function canonicalizeSignedRow(event, opts = {}) {
54
- const out = opts.prepare ? opts.prepare({ ...event }) : { ...event }
53
+ export function canonicalizeSignedRow(event, options = {}) {
54
+ const out = options.prepare ? options.prepare({ ...event }) : { ...event }
55
55
  out.id = assertHex64(out.id, 'id')
56
56
  out.sender = assertHex64(out.sender, 'sender')
57
57
  if (Array.isArray(out.prev_event_ids))
58
58
  out.prev_event_ids = out.prev_event_ids.map((id, index) =>
59
59
  assertHex64(id, `prev_event_ids[${index}]`),
60
60
  )
61
- const hexKeys = opts.contentHexKeys
61
+ const hexKeys = options.contentHexKeys
62
62
  if (out.content && hexKeys?.size)
63
- out.content = canonicalizeRowContent(out.content, hexKeys, opts.entityHashKeys)
63
+ out.content = canonicalizeRowContent(out.content, hexKeys, options.entityHashKeys)
64
64
  return out
65
65
  }
@@ -0,0 +1,21 @@
1
+ import { normalizeTcpPort } from '../core/tcp_port.mjs'
2
+
3
+ import { noteBtPeerHint } from './bt/peer_hints.mjs'
4
+ import { noteLanPeerHint } from './lan_peer_hints.mjs'
5
+
6
+ /**
7
+ * 从已验证的 discovery advert + provider meta 写入 LAN/BT peer hints。
8
+ * 任意 discovery 路径(node / group / scoped topic)收到 advert 时都应调用。
9
+ * @param {string} verifiedNodeHash 验签通过的 nodeHash
10
+ * @param {{ tcpPort?: unknown } | null | undefined} body advert body
11
+ * @param {{ address?: unknown, peripheralId?: unknown } | null | undefined} meta discovery provider meta
12
+ * @returns {void}
13
+ */
14
+ export function noteAdvertPeerHints(verifiedNodeHash, body, meta) {
15
+ if (meta?.peripheralId)
16
+ noteBtPeerHint(verifiedNodeHash, meta.peripheralId)
17
+ const tcpPort = normalizeTcpPort(body?.tcpPort)
18
+ const address = String(meta?.address || '').trim()
19
+ if (tcpPort != null && address)
20
+ noteLanPeerHint(verifiedNodeHash, { host: address, port: tcpPort })
21
+ }
@@ -1,56 +1,19 @@
1
1
  import { Buffer } from 'node:buffer'
2
- import process from 'node:process'
2
+
3
+ import { getBtPeerHint } from './peer_hints.mjs'
4
+ import { loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from './runtime.mjs'
5
+
6
+ /** 重导出 waitPoweredOn,供 discovery 调用方使用。 */
7
+ export { waitPoweredOn } from './runtime.mjs'
3
8
 
4
9
  const BT_SERVICE_UUID = 'f017f017f017f017f017f017f017f017'
5
10
  const BT_CHARACTERISTIC_UUID = 'f017f017f017f017f017f017f017f018'
11
+ const BT_SIGNAL_CHAR_UUID = 'f017f017f017f017f017f017f017f01b'
6
12
  const BT_DEVICE_NAME = 'fount-bt'
7
13
  const MAX_ADVERT_BLOB_BYTES = 12 * 1024
14
+ const MAX_SIGNAL_BLOB_BYTES = 8 * 1024
8
15
  const PERIPHERAL_RESCAN_MS = 15_000
9
16
 
10
- /**
11
- * 解析 Bluetooth 发现角色(scan / dual)。
12
- * @returns {'scan' | 'dual'} 当前平台或环境变量指定的角色
13
- */
14
- function resolveBtRole() {
15
- const override = String(process.env.FOUNT_BT_DISCOVERY_ROLE || '').trim().toLowerCase()
16
- if (override === 'dual') return 'dual'
17
- if (override === 'scan') return 'scan'
18
- return process.platform === 'win32' ? 'scan' : 'dual'
19
- }
20
-
21
- /**
22
- * 加载 Noble BLE central 库。
23
- * @returns {Promise<any>} Noble 运行时实例
24
- */
25
- async function loadNoble() {
26
- const mod = await import('@stoprocent/noble')
27
- if (typeof mod.withBindings === 'function') return mod.withBindings('default')
28
- return mod.default ?? mod
29
- }
30
-
31
- /**
32
- * 加载 Bleno BLE peripheral 库。
33
- * @returns {Promise<any>} Bleno 运行时实例
34
- */
35
- async function loadBleno() {
36
- const mod = await import('@stoprocent/bleno')
37
- if (typeof mod.withBindings === 'function') return mod.withBindings('default')
38
- return mod.default ?? mod
39
- }
40
-
41
- /**
42
- * 等待 BLE 运行时进入 poweredOn(兼容 noble v1/v2 与 bleno)。
43
- * @param {*} runtime noble 或 bleno 实例
44
- * @param {number} [timeout] 超时毫秒
45
- * @returns {Promise<void>}
46
- */
47
- export async function waitPoweredOn(runtime, timeout) {
48
- const wait = runtime.waitForPoweredOnAsync ?? runtime.waitForPoweredOn
49
- if (typeof wait !== 'function')
50
- throw new Error('p2p: bluetooth runtime missing waitForPoweredOn(Async)')
51
- return wait.call(runtime, timeout)
52
- }
53
-
54
17
  /**
55
18
  * 探测本机 noble 运行时是否具备 BT 扫描所需 API。
56
19
  * @returns {Promise<boolean>} noble 可加载且具备 startScanningAsync 与 waitForPoweredOn(Async) 时为 true
@@ -125,6 +88,7 @@ function addListener(bucket, topic, listener) {
125
88
  * - 默认在 Windows 上只启用 scan 侧发现(单适配器 central+peripheral 常冲突)
126
89
  * - 其他平台默认 dual:advertise + scan
127
90
  * - 通过固定 BLE service + read characteristic 传输完整 advert 列表,避免 31-byte 广告包限制
91
+ * - dual 下额外暴露 write characteristic 传短信令;central 可按 peer hint 写信令
128
92
  *
129
93
  * @returns {import('./index.mjs').DiscoveryProvider} Bluetooth 发现提供者
130
94
  */
@@ -134,6 +98,8 @@ export function createBluetoothDiscoveryProvider() {
134
98
  const adverts = new Map()
135
99
  /** @type {Map<string, Set<Function>>} */
136
100
  const advertListeners = new Map()
101
+ /** @type {Map<string, Set<Function>>} */
102
+ const signalListeners = new Map()
137
103
  /** @type {Map<string, number>} */
138
104
  const inspectedAt = new Map()
139
105
  let nobleRuntime = null
@@ -149,17 +115,17 @@ export function createBluetoothDiscoveryProvider() {
149
115
  if (role === 'scan') return null
150
116
  if (blenoRuntime) return blenoRuntime
151
117
  const bleno = await loadBleno()
152
- const characteristic = new bleno.Characteristic({
118
+ const advertCharacteristic = new bleno.Characteristic({
153
119
  uuid: BT_CHARACTERISTIC_UUID,
154
120
  properties: ['read'],
155
121
  /**
156
- * BLE characteristic 读请求回调。
157
- * @param {*} _handle Bleno handle(未使用)
158
- * @param {number} offset 读取偏移
159
- * @param {Function} callback Bleno 结果回调
122
+ * stoprocent/bleno onReadRequest(connection, offset, callback)
123
+ * @param {*} _connection 连接句柄
124
+ * @param {number} offset 偏移
125
+ * @param {Function} callback 结果
160
126
  * @returns {void}
161
127
  */
162
- onReadRequest(_handle, offset, callback) {
128
+ onReadRequest(_connection, offset, callback) {
163
129
  try {
164
130
  const blob = serializeAdvertBlob(adverts)
165
131
  if (offset > blob.length) {
@@ -173,11 +139,39 @@ export function createBluetoothDiscoveryProvider() {
173
139
  }
174
140
  },
175
141
  })
142
+ const signalCharacteristic = new bleno.Characteristic({
143
+ uuid: BT_SIGNAL_CHAR_UUID,
144
+ properties: ['write', 'writeWithoutResponse'],
145
+ /**
146
+ * stoprocent/bleno onWriteRequest(connection, data, offset, withoutResponse, callback)
147
+ * @param {*} _connection 连接句柄
148
+ * @param {Buffer} data 信令 blob
149
+ * @param {number} _offset 偏移
150
+ * @param {boolean} _withoutResponse 无响应写
151
+ * @param {Function} callback 结果
152
+ * @returns {void}
153
+ */
154
+ onWriteRequest(_connection, data, _offset, _withoutResponse, callback) {
155
+ try {
156
+ const parsed = JSON.parse(Buffer.from(data).toString('utf8'))
157
+ const topic = String(parsed?.topic || '')
158
+ const bytes = Uint8Array.from(Buffer.from(String(parsed?.data || ''), 'base64'))
159
+ if (topic && bytes.byteLength)
160
+ for (const listener of signalListeners.get(topic) || [])
161
+ listener(bytes, { provider: 'bt' })
162
+
163
+ callback(bleno.Characteristic.RESULT_SUCCESS)
164
+ }
165
+ catch {
166
+ callback(bleno.Characteristic.RESULT_UNLIKELY_ERROR)
167
+ }
168
+ },
169
+ })
176
170
  await waitPoweredOn(bleno, 5_000)
177
171
  await bleno.setServicesAsync([
178
172
  new bleno.PrimaryService({
179
173
  uuid: BT_SERVICE_UUID,
180
- characteristics: [characteristic],
174
+ characteristics: [advertCharacteristic, signalCharacteristic],
181
175
  }),
182
176
  ])
183
177
  blenoRuntime = bleno
@@ -192,14 +186,14 @@ export function createBluetoothDiscoveryProvider() {
192
186
  if (role === 'scan') return
193
187
  const bleno = await ensurePeripheralRuntime()
194
188
  if (!bleno) return
195
- if (!adverts.size) {
189
+ if (!adverts.size && !signalListeners.size) {
196
190
  if (advertisingStarted) {
197
191
  await bleno.stopAdvertisingAsync().catch(() => { })
198
192
  advertisingStarted = false
199
193
  }
200
194
  return
201
195
  }
202
- serializeAdvertBlob(adverts)
196
+ if (adverts.size) serializeAdvertBlob(adverts)
203
197
  if (!advertisingStarted) {
204
198
  await bleno.startAdvertisingAsync(BT_DEVICE_NAME, [BT_SERVICE_UUID])
205
199
  advertisingStarted = true
@@ -256,10 +250,86 @@ export function createBluetoothDiscoveryProvider() {
256
250
  scanningStarted = true
257
251
  }
258
252
 
253
+ /**
254
+ * Central:按 peer hint 连接并对端 signal characteristic 写短包。
255
+ * @param {string} topic 信令 topic
256
+ * @param {string} to 目标 nodeHash
257
+ * @param {Uint8Array} bytes 载荷
258
+ * @returns {Promise<void>}
259
+ */
260
+ async function sendSignalViaGatt(topic, to, bytes) {
261
+ const hint = getBtPeerHint(to)
262
+ if (!hint) throw new Error('p2p: bt signal missing peer hint')
263
+ const blob = Buffer.from(JSON.stringify({
264
+ topic: String(topic),
265
+ to: String(to),
266
+ data: Buffer.from(bytes).toString('base64'),
267
+ }), 'utf8')
268
+ if (blob.byteLength > MAX_SIGNAL_BLOB_BYTES)
269
+ throw new Error(`p2p: bt signal blob exceeds ${MAX_SIGNAL_BLOB_BYTES} bytes`)
270
+ await ensureScanRuntime()
271
+ const noble = nobleRuntime
272
+ const wantId = hint.peripheralId
273
+ /**
274
+ * 先查 noble 已缓存的 peripheral,避免只等下一次 advertise 漏报。
275
+ * @returns {*|null} 已缓存的 peripheral,未命中为 null
276
+ */
277
+ function cachedPeripheral() {
278
+ const table = noble?._peripherals
279
+ if (!table || typeof table !== 'object') return null
280
+ for (const found of Object.values(table)) {
281
+ const id = String(found?.id || found?.address || '')
282
+ if (id === wantId) return found
283
+ }
284
+ return null
285
+ }
286
+ const peripheral = cachedPeripheral() ?? await new Promise((resolve, reject) => {
287
+ const deadline = setTimeout(() => {
288
+ cleanup()
289
+ reject(new Error('p2p: bt signal peripheral timeout'))
290
+ }, 8_000)
291
+ /**
292
+ * @returns {void}
293
+ */
294
+ function cleanup() {
295
+ clearTimeout(deadline)
296
+ noble.removeListener('discover', onDiscover)
297
+ }
298
+ /**
299
+ * @param {*} found peripheral
300
+ * @returns {void}
301
+ */
302
+ function onDiscover(found) {
303
+ const id = String(found?.id || found?.address || '')
304
+ if (id !== wantId) return
305
+ cleanup()
306
+ resolve(found)
307
+ }
308
+ noble.on('discover', onDiscover)
309
+ const again = cachedPeripheral()
310
+ if (again) {
311
+ cleanup()
312
+ resolve(again)
313
+ }
314
+ })
315
+ try {
316
+ await peripheral.connectAsync()
317
+ const { characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync(
318
+ [BT_SERVICE_UUID],
319
+ [BT_SIGNAL_CHAR_UUID],
320
+ )
321
+ if (!characteristics?.length) throw new Error('p2p: bt signal characteristic missing')
322
+ await characteristics[0].writeAsync(blob, false)
323
+ }
324
+ finally {
325
+ try { await peripheral.disconnectAsync() } catch { /* ignore */ }
326
+ }
327
+ }
328
+
259
329
  return {
260
330
  id: 'bt',
261
331
  priority: 20,
262
- caps: { canDiscover: true, canSignal: false, canRelay: false },
332
+ caps: { canDiscover: true, canSignal: true, canRelay: false },
263
333
  /**
264
334
  * 广播指定 topic 的 advert。
265
335
  * @param {string} topic advert 主题
@@ -285,5 +355,31 @@ export function createBluetoothDiscoveryProvider() {
285
355
  await ensureScanRuntime()
286
356
  return addListener(advertListeners, String(topic), onAdvert)
287
357
  },
358
+ /**
359
+ * 经 GATT 向近场 peer 发送信令。
360
+ * @param {string} topic 信令 topic
361
+ * @param {string} to 目标 nodeHash
362
+ * @param {Uint8Array} bytes 载荷
363
+ * @returns {Promise<void>}
364
+ */
365
+ async sendSignal(topic, to, bytes) {
366
+ await sendSignalViaGatt(topic, to, bytes)
367
+ },
368
+ /**
369
+ * 监听经本机 peripheral signal characteristic 写入的信令。
370
+ * @param {string} topic 信令 topic
371
+ * @param {Function} onSignal 回调
372
+ * @returns {Promise<() => void>} 取消订阅
373
+ */
374
+ async onSignal(topic, onSignal) {
375
+ if (role === 'scan') return () => { }
376
+ await ensurePeripheralRuntime()
377
+ await refreshAdvertising()
378
+ const stop = addListener(signalListeners, String(topic), onSignal)
379
+ return () => {
380
+ stop()
381
+ void refreshAdvertising().catch(() => { })
382
+ }
383
+ },
288
384
  }
289
385
  }
@@ -0,0 +1,41 @@
1
+ import { normalizeHex64 } from '../../core/hexIds.mjs'
2
+ import { createTtlMap } from '../../utils/ttl_map.mjs'
3
+
4
+ /** BT peer hint 存活时间。 */
5
+ export const BT_PEER_HINT_TTL_MS = 5 * 60_000
6
+
7
+ /** @type {ReturnType<typeof createTtlMap<{ peripheralId: string }>>} */
8
+ const hints = createTtlMap(BT_PEER_HINT_TTL_MS)
9
+
10
+ /**
11
+ * 记录近场 BT 扫描到的 nodeHash → peripheral 映射。
12
+ * @param {string} nodeHash 节点 64 hex
13
+ * @param {string} peripheralId noble peripheral id / address
14
+ * @returns {void}
15
+ */
16
+ export function noteBtPeerHint(nodeHash, peripheralId) {
17
+ const hash = normalizeHex64(nodeHash)
18
+ const id = String(peripheralId || '').trim()
19
+ if (!hash || !id) return
20
+ hints.set(hash, { peripheralId: id })
21
+ }
22
+
23
+ /**
24
+ * 查询未过期的 BT peer hint。
25
+ * @param {string} nodeHash 节点 64 hex
26
+ * @param {number} [now=Date.now()] 当前时间(测试可注入)
27
+ * @returns {{ peripheralId: string } | null} hint 或 null
28
+ */
29
+ export function getBtPeerHint(nodeHash, now = Date.now()) {
30
+ const hash = normalizeHex64(nodeHash)
31
+ if (!hash) return null
32
+ return hints.get(hash, now)
33
+ }
34
+
35
+ /**
36
+ * 清空全部 BT peer hints(测试用)。
37
+ * @returns {void}
38
+ */
39
+ export function clearBtPeerHints() {
40
+ hints.clear()
41
+ }
@@ -0,0 +1,46 @@
1
+ import process from 'node:process'
2
+
3
+ /**
4
+ * 解析 Bluetooth 角色(scan / dual)。
5
+ * Win32 默认 scan(单适配器 central+peripheral 常冲突);可用 FOUNT_BT_DISCOVERY_ROLE 覆盖。
6
+ * @returns {'scan' | 'dual'} 生效角色
7
+ */
8
+ export function resolveBtRole() {
9
+ const override = String(process.env.FOUNT_BT_DISCOVERY_ROLE || '').trim().toLowerCase()
10
+ if (override === 'dual') return 'dual'
11
+ if (override === 'scan') return 'scan'
12
+ return process.platform === 'win32' ? 'scan' : 'dual'
13
+ }
14
+
15
+ /**
16
+ * 加载 Noble BLE central。
17
+ * @returns {Promise<any>} noble 运行时
18
+ */
19
+ export async function loadNoble() {
20
+ const mod = await import('@stoprocent/noble')
21
+ if (typeof mod.withBindings === 'function') return mod.withBindings('default')
22
+ return mod.default ?? mod
23
+ }
24
+
25
+ /**
26
+ * 加载 Bleno BLE peripheral。
27
+ * @returns {Promise<any>} bleno 运行时
28
+ */
29
+ export async function loadBleno() {
30
+ const mod = await import('@stoprocent/bleno')
31
+ if (typeof mod.withBindings === 'function') return mod.withBindings('default')
32
+ return mod.default ?? mod
33
+ }
34
+
35
+ /**
36
+ * 等待 BLE 运行时 poweredOn(兼容 noble/bleno v1/v2)。
37
+ * @param {*} runtime noble 或 bleno
38
+ * @param {number} [timeout] 超时毫秒
39
+ * @returns {Promise<void>}
40
+ */
41
+ export async function waitPoweredOn(runtime, timeout) {
42
+ const wait = runtime.waitForPoweredOnAsync ?? runtime.waitForPoweredOn
43
+ if (typeof wait !== 'function')
44
+ throw new Error('p2p: bluetooth runtime missing waitForPoweredOn(Async)')
45
+ return wait.call(runtime, timeout)
46
+ }
@@ -0,0 +1,43 @@
1
+ import { normalizeHex64 } from '../core/hexIds.mjs'
2
+ import { normalizeTcpPort } from '../core/tcp_port.mjs'
3
+ import { createTtlMap } from '../utils/ttl_map.mjs'
4
+
5
+ /** LAN peer hint 存活时间。 */
6
+ export const LAN_PEER_HINT_TTL_MS = 5 * 60_000
7
+
8
+ /** @type {ReturnType<typeof createTtlMap<{ host: string, port: number }>>} */
9
+ const hints = createTtlMap(LAN_PEER_HINT_TTL_MS)
10
+
11
+ /**
12
+ * 记录 LAN 上观察到的 nodeHash → host:port。
13
+ * @param {string} nodeHash 节点 64 hex
14
+ * @param {{ host: string, port: number }} endpoint 端点
15
+ * @returns {void}
16
+ */
17
+ export function noteLanPeerHint(nodeHash, endpoint) {
18
+ const hash = normalizeHex64(nodeHash)
19
+ const host = String(endpoint?.host || '').trim()
20
+ const port = normalizeTcpPort(endpoint?.port)
21
+ if (!hash || !host || !port) return
22
+ hints.set(hash, { host, port })
23
+ }
24
+
25
+ /**
26
+ * 查询未过期的 LAN peer hint。
27
+ * @param {string} nodeHash 节点 64 hex
28
+ * @param {number} [now=Date.now()] 当前时间(测试可注入)
29
+ * @returns {{ host: string, port: number } | null} hint 或 null
30
+ */
31
+ export function getLanPeerHint(nodeHash, now = Date.now()) {
32
+ const hash = normalizeHex64(nodeHash)
33
+ if (!hash) return null
34
+ return hints.get(hash, now)
35
+ }
36
+
37
+ /**
38
+ * 清空全部 LAN peer hints(测试用)。
39
+ * @returns {void}
40
+ */
41
+ export function clearLanPeerHints() {
42
+ hints.clear()
43
+ }
@@ -7,12 +7,12 @@ const DEFAULT_GROUP = '239.255.42.99'
7
7
  /**
8
8
  * 轻量 multicast 发现插件:不做完整 DNS-SD,只复用 mDNS 的 LAN multicast 发现思路。
9
9
  *
10
- * @param {{ port?: number, group?: string }} [opts] 组播端口与组地址
10
+ * @param {{ port?: number, group?: string }} [options] 组播端口与组地址
11
11
  * @returns {import('./index.mjs').DiscoveryProvider} mDNS 发现提供者
12
12
  */
13
- export function createMdnsDiscoveryProvider(opts = {}) {
14
- const port = Number(opts.port) || DEFAULT_PORT
15
- const group = String(opts.group || DEFAULT_GROUP)
13
+ export function createMdnsDiscoveryProvider(options = {}) {
14
+ const port = Number(options.port) || DEFAULT_PORT
15
+ const group = String(options.group || DEFAULT_GROUP)
16
16
  const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true })
17
17
  let bound = false
18
18
  let bindPromise = null
@@ -38,7 +38,7 @@ export function createMdnsDiscoveryProvider(opts = {}) {
38
38
  resolve()
39
39
  })
40
40
  })
41
- socket.on('message', raw => {
41
+ socket.on('message', (raw, rinfo) => {
42
42
  let packet
43
43
  try { packet = JSON.parse(String(raw)) } catch { return }
44
44
  const listeners = packet.type === 'advert'
@@ -46,8 +46,12 @@ export function createMdnsDiscoveryProvider(opts = {}) {
46
46
  : signalListeners.get(String(packet.topic || ''))
47
47
  if (!listeners?.size) return
48
48
  const bytes = Uint8Array.from(Buffer.from(String(packet.data || ''), 'base64'))
49
+ const meta = {
50
+ provider: 'mdns',
51
+ address: String(rinfo?.address || ''),
52
+ }
49
53
  for (const listener of listeners)
50
- listener(bytes, { provider: 'mdns' })
54
+ listener(bytes, meta)
51
55
  })
52
56
  bound = true
53
57
  })()
@@ -142,11 +142,11 @@ async function publishEvent(relayUrls, event) {
142
142
 
143
143
  /**
144
144
  * 创建 Nostr discovery provider。
145
- * @param {{ relayUrls?: string[] }} [opts] 可选中继 URL 覆盖
145
+ * @param {{ relayUrls?: string[] }} [options] 可选中继 URL 覆盖
146
146
  * @returns {import('./index.mjs').DiscoveryProvider} Nostr 发现提供者
147
147
  */
148
- export function createNostrDiscoveryProvider(opts = {}) {
149
- const relayUrls = mergeSignalingRelayUrls(opts.relayUrls)
148
+ export function createNostrDiscoveryProvider(options = {}) {
149
+ const relayUrls = mergeSignalingRelayUrls(options.relayUrls)
150
150
  const secretKey = randomBytes(32)
151
151
  return {
152
152
  id: 'nostr',
@@ -14,10 +14,10 @@ export const MAX_PENDING_CHUNK_FETCHES = 2048
14
14
  * @param {string} key 唯一等待键
15
15
  * @param {string} expectedHash 期望 64 hex 密文哈希
16
16
  * @param {number} timeoutMs 超时毫秒
17
- * @param {{ rejectOnTimeout?: boolean }} [opts] rejectOnTimeout 时 Promise 以 Error 拒绝
17
+ * @param {{ rejectOnTimeout?: boolean }} [options] rejectOnTimeout 时 Promise 以 Error 拒绝
18
18
  * @returns {{ done: Promise<Uint8Array | null>, cancel: () => void }} 等待 Promise 与取消函数
19
19
  */
20
- export function registerChunkFetchWait(key, expectedHash, timeoutMs, opts = {}) {
20
+ export function registerChunkFetchWait(key, expectedHash, timeoutMs, options = {}) {
21
21
  if (!key || pendingChunkFetches.size >= MAX_PENDING_CHUNK_FETCHES)
22
22
  return {
23
23
  done: Promise.resolve(null), /**
@@ -41,7 +41,7 @@ export function registerChunkFetchWait(key, expectedHash, timeoutMs, opts = {})
41
41
  })
42
42
  const timer = setTimeout(() => {
43
43
  pendingChunkFetches.delete(key)
44
- if (opts.rejectOnTimeout)
44
+ if (options.rejectOnTimeout)
45
45
  settle(new Error('chunk fetch timeout'))
46
46
  else
47
47
  settle(null)
@@ -23,11 +23,11 @@ export function assignChunksToPeers(chunkHashes, peerIds) {
23
23
  * @param {Map<string, { state: ChunkFetchState, peerId?: string, attempts: number }>} table 状态表
24
24
  * @param {string[]} chunkHashes 待拉取块
25
25
  * @param {string[]} peerIds 可用 peer
26
- * @param {{ maxAttempts?: number }} [opts] 选项
26
+ * @param {{ maxAttempts?: number }} [options] 选项
27
27
  * @returns {{ assignments: Map<string, string>, broadcast: string[] }} 分配与需广播块
28
28
  */
29
- export function planChunkFetches(table, chunkHashes, peerIds, opts = {}) {
30
- const maxAttempts = Math.max(1, Number(opts.maxAttempts) || 3)
29
+ export function planChunkFetches(table, chunkHashes, peerIds, options = {}) {
30
+ const maxAttempts = Math.max(1, Number(options.maxAttempts) || 3)
31
31
  const assignments = assignChunksToPeers(chunkHashes, peerIds)
32
32
  /** @type {string[]} */
33
33
  const broadcast = []
@@ -43,12 +43,12 @@ export function mergeTopologicalOrder(cachedOrder, events) {
43
43
  /**
44
44
  * @param {object[]} events 全量事件
45
45
  * @param {object | null} cache 磁盘缓存
46
- * @param {{ forceFull?: boolean }} [opts] 选项
46
+ * @param {{ forceFull?: boolean }} [options] 选项
47
47
  * @returns {string[]} 拓扑序 id 列表
48
48
  */
49
- export function resolveEventTopologicalOrder(events, cache, opts = {}) {
49
+ export function resolveEventTopologicalOrder(events, cache, options = {}) {
50
50
  if (!events.length) return []
51
- if (opts.forceFull)
51
+ if (options.forceFull)
52
52
  return topologicalCanonicalOrder(eventsToMetas(events))
53
53
 
54
54
  const byId = new Map(events.map(event => [event.id, event]))
@@ -6,11 +6,11 @@ const memoByKey = new Map()
6
6
  * @param {string} memoKey 缓存键
7
7
  * @param {string} fingerprint 文件 stat + 事件数指纹
8
8
  * @param {() => string[]} resolveOrder 实际求序(含磁盘层)
9
- * @param {{ force?: boolean }} [opts] 强制重算
9
+ * @param {{ force?: boolean }} [options] 强制重算
10
10
  * @returns {string[]} 拓扑序 event id
11
11
  */
12
- export function resolveTopologicalOrderMemoCached(memoKey, fingerprint, resolveOrder, opts = {}) {
13
- if (!opts.force) {
12
+ export function resolveTopologicalOrderMemoCached(memoKey, fingerprint, resolveOrder, options = {}) {
13
+ if (!options.force) {
14
14
  const cached = memoByKey.get(memoKey)
15
15
  if (cached?.fp === fingerprint && cached.order.length)
16
16
  return cached.order
@@ -71,7 +71,7 @@ export function attachNodeScopeFedChunkResponder(username, wire) {
71
71
  * Trystero room:注册带 requestId 的 fed_chunk_* + fed_manifest_*(TrustGraph 全局 miss)。
72
72
  * @param {string} username 用户
73
73
  * @param {object} room Trystero room
74
- * @param {{ enqueue: (prio: number, fn: () => void) => void }} [fedOut] 出站队列
74
+ * @param {{ enqueue: (prio: number, cleanup: () => void) => void }} [fedOut] 出站队列
75
75
  * @param {(roomKey: string, action: string, rtcLimits: object) => boolean} [guardGet] RTC 负载守卫
76
76
  * @param {object} [rtcLimits] RTC 限额
77
77
  * @param {string} [roomKey] 房间键