@steve02081504/fount-p2p 0.0.12 → 0.0.13

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 (75) hide show
  1. package/README.md +41 -10
  2. package/core/composite_key.mjs +5 -5
  3. package/crypto/crypto.mjs +3 -3
  4. package/crypto/key.mjs +12 -12
  5. package/dag/storage.mjs +36 -36
  6. package/discovery/advert_peer_hints.mjs +1 -1
  7. package/discovery/adverts.mjs +109 -0
  8. package/discovery/bt/index.mjs +153 -112
  9. package/discovery/bt/probe_child.mjs +2 -1
  10. package/discovery/bt/runtime.mjs +2 -9
  11. package/discovery/index.mjs +267 -62
  12. package/discovery/internal/signal_crypto.mjs +109 -0
  13. package/discovery/lan.mjs +228 -0
  14. package/discovery/nostr.mjs +394 -59
  15. package/federation/chunk_fetch_pending.mjs +10 -13
  16. package/federation/manifest_fetch_pending.mjs +1 -3
  17. package/files/assemble.mjs +14 -14
  18. package/files/assemble_stream.mjs +6 -6
  19. package/files/chunk_responder.mjs +29 -20
  20. package/files/evfs.mjs +29 -28
  21. package/files/manifest_fetch.mjs +16 -3
  22. package/files/public_manifest.mjs +11 -11
  23. package/files/transfer_key_registry.mjs +2 -2
  24. package/index.mjs +86 -11
  25. package/infra/cli.mjs +62 -0
  26. package/infra/debug_log.mjs +56 -0
  27. package/infra/default_node_dir.mjs +22 -0
  28. package/infra/priority.mjs +56 -0
  29. package/infra/service.mjs +140 -0
  30. package/infra/tunables.json +5 -0
  31. package/link/frame.mjs +9 -16
  32. package/link/handshake.mjs +14 -15
  33. package/link/pipe.mjs +8 -8
  34. package/link/providers/ble_gatt.mjs +6 -6
  35. package/link/rtc.mjs +3 -3
  36. package/mailbox/consumer_registry.mjs +2 -2
  37. package/mailbox/deliver_or_store.mjs +5 -5
  38. package/mailbox/wire.mjs +28 -24
  39. package/node/entity_store.mjs +6 -6
  40. package/node/instance.mjs +46 -27
  41. package/node/local_data_revision.mjs +26 -0
  42. package/node/log.mjs +56 -0
  43. package/node/network.mjs +37 -5
  44. package/node/reputation_store.mjs +4 -4
  45. package/node/reputation_sync.mjs +318 -0
  46. package/node/routing_profile.mjs +21 -0
  47. package/node/signaling_config.mjs +30 -11
  48. package/overlay/index.mjs +22 -1
  49. package/package.json +13 -2
  50. package/rooms/scoped_link.mjs +17 -166
  51. package/transport/advert_ingest.mjs +11 -14
  52. package/transport/group_link_set.mjs +149 -99
  53. package/transport/link_registry.mjs +211 -60
  54. package/transport/mesh_keepalive.mjs +217 -0
  55. package/transport/node_scope.mjs +289 -0
  56. package/transport/offer_answer.mjs +45 -48
  57. package/transport/peer_pool.mjs +116 -14
  58. package/transport/remote_user_room.mjs +6 -9
  59. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  60. package/transport/runtime_bootstrap.mjs +170 -108
  61. package/transport/tunables.json +11 -0
  62. package/transport/tunables.mjs +18 -0
  63. package/transport/user_room.mjs +83 -158
  64. package/trust_graph/build.mjs +0 -2
  65. package/trust_graph/cache.mjs +12 -3
  66. package/trust_graph/registry.mjs +6 -6
  67. package/trust_graph/send.mjs +0 -3
  68. package/utils/async_mutex.mjs +4 -4
  69. package/utils/emit_safe.mjs +3 -3
  70. package/utils/json_io.mjs +12 -12
  71. package/utils/map_pool.mjs +5 -5
  72. package/wire/part_ingress.mjs +32 -28
  73. package/wire/part_query.mjs +26 -20
  74. package/discovery/mdns.mjs +0 -197
  75. package/transport/signal_crypto.mjs +0 -104
@@ -0,0 +1,289 @@
1
+ import { attachNodeScopeFedChunkResponder } from '../files/chunk_responder.mjs'
2
+ import { attachMailboxWire } from '../mailbox/wire.mjs'
3
+ import { attachPartWire } from '../wire/part_ingress.mjs'
4
+ import { attachPartQueryWire } from '../wire/part_query.mjs'
5
+
6
+ import { sendToNodeLink, subscribeScope } from './link_registry.mjs'
7
+
8
+ /** @type {Map<string, Set<(payload: unknown, peerId: string) => void>>} */
9
+ const nodeActionHandlers = new Map()
10
+
11
+ /** @type {Set<(context: NodeScopeContext, wire: NodeScopeWire) => void>} */
12
+ const nodeScopeWireHooks = new Set()
13
+
14
+ /** @type {NodeScopeContext} */
15
+ const nodeScopeContext = { replicaUsername: '' }
16
+
17
+ /** @type {NodeScopeWire | null} */
18
+ let nodeScopeWire = null
19
+
20
+ /** @type {(() => void) | null} */
21
+ let nodeScopeSubscribeCleanup = null
22
+
23
+ /** @type {Set<() => void>} */
24
+ const nodeScopeFeatureDisposers = new Set()
25
+
26
+ /** @type {Map<string, { count: number, disposeCore: () => void }>} */
27
+ const featureAttachRefs = new Map()
28
+
29
+ /**
30
+ * @typedef {{ replicaUsername: string }} NodeScopeContext
31
+ */
32
+
33
+ /**
34
+ * @typedef {{
35
+ * on: (name: string, handler: (payload: unknown, peerId: string) => void) => () => void
36
+ * send: (name: string, payload: unknown, peerId: string | null) => void
37
+ * }} NodeScopeWire
38
+ */
39
+
40
+ /**
41
+ * @param {string} peerId 对端 nodeHash
42
+ * @param {string} action 动作名
43
+ * @param {unknown} payload 载荷
44
+ * @returns {Promise<boolean>} 是否成功发出
45
+ */
46
+ const sendNodeAction = (peerId, action, payload) =>
47
+ sendToNodeLink(peerId, { scope: 'node', action, payload })
48
+
49
+ /**
50
+ * @returns {NodeScopeWire} node scope 的 wire 适配器
51
+ */
52
+ function createNodeScopeWire() {
53
+ return {
54
+ /**
55
+ * @param {string} name - action 名
56
+ * @param {(payload: unknown, peerId: string) => void} handler - 入站处理器
57
+ * @returns {() => void} 取消注册的 dispose
58
+ */
59
+ on(name, handler) {
60
+ if (!nodeActionHandlers.has(name)) nodeActionHandlers.set(name, new Set())
61
+ nodeActionHandlers.get(name).add(handler)
62
+ return () => {
63
+ const set = nodeActionHandlers.get(name)
64
+ if (!set) return
65
+ set.delete(handler)
66
+ if (!set.size) nodeActionHandlers.delete(name)
67
+ }
68
+ },
69
+ /**
70
+ * @param {string} name - action 名
71
+ * @param {unknown} payload - 出站载荷
72
+ * @param {string | null} peerId - 目标 peer,null 时忽略
73
+ * @returns {void}
74
+ */
75
+ send(name, payload, peerId) {
76
+ if (!peerId) return
77
+ void sendNodeAction(peerId, name, payload).catch(() => { })
78
+ },
79
+ }
80
+ }
81
+
82
+ /**
83
+ * @returns {boolean} 是否已订阅 node scope
84
+ */
85
+ export function isNodeScopeSubscribed() {
86
+ return nodeScopeSubscribeCleanup != null
87
+ }
88
+
89
+ /**
90
+ * @param {string} action - action 名
91
+ * @returns {boolean} 是否已挂载处理器
92
+ */
93
+ export function hasNodeScopeAction(action) {
94
+ return (nodeActionHandlers.get(action)?.size ?? 0) > 0
95
+ }
96
+
97
+ /**
98
+ * @param {string} action - action 名
99
+ * @returns {number} 已注册的处理器数量
100
+ */
101
+ export function countNodeScopeActionHandlers(action) {
102
+ return nodeActionHandlers.get(action)?.size ?? 0
103
+ }
104
+
105
+ /**
106
+ * 测试/调试:直接派发已挂载的 node action。
107
+ * @param {string} action - action 名
108
+ * @param {unknown} payload - 载荷
109
+ * @param {string} peerId - 发送方 nodeHash
110
+ * @returns {boolean} 是否有处理器被调用
111
+ */
112
+ export function dispatchNodeScopeAction(action, payload, peerId) {
113
+ const handlers = nodeActionHandlers.get(action)
114
+ if (!handlers?.size) return false
115
+ for (const handler of handlers)
116
+ try { handler(payload, peerId) } catch { /* ignore */ }
117
+ return true
118
+ }
119
+
120
+ /**
121
+ * @returns {NodeScopeWire | null} 当前 wire,未 ensure 时为 null
122
+ */
123
+ export function getNodeScopeWire() {
124
+ return nodeScopeWire
125
+ }
126
+
127
+ /**
128
+ * @returns {NodeScopeContext} 可变 node scope 上下文
129
+ */
130
+ export function getNodeScopeContext() {
131
+ return nodeScopeContext
132
+ }
133
+
134
+ /**
135
+ * 只订阅 node scope 派发,不挂任何 feature。
136
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
137
+ * @returns {() => void} 取消订阅的 dispose
138
+ */
139
+ export function ensureNodeScope(options = {}) {
140
+ if (options.replicaUsername != null)
141
+ nodeScopeContext.replicaUsername = String(options.replicaUsername)
142
+ if (nodeScopeSubscribeCleanup) return nodeScopeSubscribeCleanup
143
+ nodeScopeSubscribeCleanup = subscribeScope('node', (senderNodeHash, envelope) => {
144
+ const handlers = nodeActionHandlers.get(envelope.action)
145
+ if (!handlers?.size) return
146
+ for (const handler of handlers)
147
+ try { handler(envelope.payload, senderNodeHash) } catch { /* ignore */ }
148
+ })
149
+ if (!nodeScopeWire) {
150
+ nodeScopeWire = createNodeScopeWire()
151
+ for (const hook of nodeScopeWireHooks)
152
+ try { hook(nodeScopeContext, nodeScopeWire) } catch { /* ignore */ }
153
+ }
154
+ return nodeScopeSubscribeCleanup
155
+ }
156
+
157
+ /**
158
+ * @param {(context: NodeScopeContext, wire: NodeScopeWire) => void} hook - wire 创建时回调
159
+ * @returns {() => void} 取消注册的 dispose
160
+ */
161
+ export function registerNodeScopeWireHook(hook) {
162
+ nodeScopeWireHooks.add(hook)
163
+ if (nodeScopeWire)
164
+ try { hook(nodeScopeContext, nodeScopeWire) } catch { /* ignore */ }
165
+ return () => nodeScopeWireHooks.delete(hook)
166
+ }
167
+
168
+ /**
169
+ * @param {() => void} dispose - feature 卸载函数
170
+ * @returns {() => void} 包装后的 dispose(同时从跟踪集移除)
171
+ */
172
+ function trackFeatureDisposer(dispose) {
173
+ nodeScopeFeatureDisposers.add(dispose)
174
+ return () => {
175
+ dispose()
176
+ nodeScopeFeatureDisposers.delete(dispose)
177
+ }
178
+ }
179
+
180
+ /**
181
+ * 同一 feature 多次 attach 共享一份 wire;dispose 引用计数归零才卸。
182
+ * @param {string} key - feature 去重键
183
+ * @param {() => () => void} attachCore - 首次 attach 时执行,返回核心 dispose
184
+ * @returns {() => void} 引用计数包装的 dispose
185
+ */
186
+ function attachFeatureRefCounted(key, attachCore) {
187
+ let entry = featureAttachRefs.get(key)
188
+ if (!entry) {
189
+ entry = { count: 0, disposeCore: attachCore() }
190
+ featureAttachRefs.set(key, entry)
191
+ }
192
+ entry.count++
193
+ return trackFeatureDisposer(() => {
194
+ const cur = featureAttachRefs.get(key)
195
+ if (!cur) return
196
+ cur.count--
197
+ if (cur.count > 0) return
198
+ cur.disposeCore()
199
+ featureAttachRefs.delete(key)
200
+ })
201
+ }
202
+
203
+ /**
204
+ * 自定义 node scope feature(与 mailbox/part 相同 refcount 语义)。
205
+ * @param {string} key - feature 去重键
206
+ * @param {(wire: NodeScopeWire, context: NodeScopeContext) => () => void} attachCore - 首次 attach 注册 handler,返回核心 dispose
207
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
208
+ * @returns {() => void} 引用计数包装的 dispose
209
+ */
210
+ export function attachNodeScopeFeature(key, attachCore, options = {}) {
211
+ ensureNodeScope(options)
212
+ return attachFeatureRefCounted(key, () => attachCore(nodeScopeWire, nodeScopeContext))
213
+ }
214
+
215
+ /**
216
+ * refcount 挂载 mailbox wire。
217
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
218
+ * @returns {() => void} 取消挂载的 dispose
219
+ */
220
+ export function attachNodeScopeMailbox(options = {}) {
221
+ ensureNodeScope(options)
222
+ return attachFeatureRefCounted('mailbox', () => attachMailboxWire(nodeScopeContext, nodeScopeWire))
223
+ }
224
+
225
+ /**
226
+ * refcount 挂载 part ingress wire。
227
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
228
+ * @returns {() => void} 取消挂载的 dispose
229
+ */
230
+ export function attachNodeScopePart(options = {}) {
231
+ ensureNodeScope(options)
232
+ return attachFeatureRefCounted('part', () => attachPartWire(nodeScopeContext, nodeScopeWire))
233
+ }
234
+
235
+ /**
236
+ * refcount 挂载 part_query wire。
237
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
238
+ * @returns {() => void} 取消挂载的 dispose
239
+ */
240
+ export function attachNodeScopePartQuery(options = {}) {
241
+ ensureNodeScope(options)
242
+ return attachFeatureRefCounted('partQuery', () => attachPartQueryWire(nodeScopeContext, nodeScopeWire))
243
+ }
244
+
245
+ /**
246
+ * refcount 挂载 fed chunk responder。
247
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
248
+ * @returns {() => void} 取消挂载的 dispose
249
+ */
250
+ export function attachNodeScopeChunks(options = {}) {
251
+ ensureNodeScope(options)
252
+ return attachFeatureRefCounted('chunks', () =>
253
+ attachNodeScopeFedChunkResponder(() => nodeScopeContext.replicaUsername, nodeScopeWire))
254
+ }
255
+
256
+ /**
257
+ * 全业务 preset(等价旧 ensureNodeScopeRuntime 默认行为)。
258
+ * @param {{ replicaUsername?: string }} [options] - 可选副本用户名
259
+ * @returns {() => void} 取消全部 preset 挂载的 dispose
260
+ */
261
+ export function attachUserRoomDefaultWires(options = {}) {
262
+ const disposers = [
263
+ attachNodeScopePart(options),
264
+ attachNodeScopePartQuery(options),
265
+ attachNodeScopeMailbox(options),
266
+ attachNodeScopeChunks(options),
267
+ ]
268
+ return () => {
269
+ for (const dispose of disposers) dispose()
270
+ }
271
+ }
272
+
273
+ /**
274
+ * 卸掉 feature 挂载;可选保留 node scope 订阅。
275
+ * @param {{ keepSubscribe?: boolean }} [options] - true 时保留 node scope 订阅
276
+ * @returns {void}
277
+ */
278
+ export function stopNodeScopeRuntime(options = {}) {
279
+ for (const dispose of [...nodeScopeFeatureDisposers]) dispose()
280
+ nodeScopeFeatureDisposers.clear()
281
+ for (const entry of featureAttachRefs.values())
282
+ try { entry.disposeCore() } catch { /* ignore */ }
283
+ featureAttachRefs.clear()
284
+ if (options.keepSubscribe) return
285
+ nodeScopeSubscribeCleanup?.()
286
+ nodeScopeSubscribeCleanup = null
287
+ nodeScopeWire = null
288
+ nodeActionHandlers.clear()
289
+ }
@@ -1,18 +1,16 @@
1
1
  import { randomBytes } from 'node:crypto'
2
2
 
3
3
  import { normalizeHex64 } from '../core/hexIds.mjs'
4
- import { sendSignal } from '../discovery/index.mjs'
4
+ import { decryptNodeSignalPacket, sendNodeSignalPacket } from '../discovery/index.mjs'
5
5
  import { listLinkProviders } from '../link/providers/index.mjs'
6
+ import { nodeDebug, shortHash } from '../node/log.mjs'
6
7
 
7
- import { decryptSignalPacket, encryptSignalPacket, nodeRendezvousTopic } from './signal_crypto.mjs'
8
-
9
- /** accept/dial 挂起期间 ICE 信令 backlog 上限,防无 handler 时无限堆积 */
8
+ /** accept/dial 挂起期间 ICE 信令 backlog 上限 */
10
9
  const SIGNAL_BACKLOG_MAX = 64
11
10
 
12
11
  /**
13
- * 创建带 backlog 的缓冲信令会话。
14
12
  * @param {(message: unknown) => Promise<void>} sendRemote 远端发送回调
15
- * @returns {{ send: (message: unknown) => Promise<void>, onRemote: (handler: (message: unknown) => void) => () => void, deliver: (message: unknown) => void, clear: () => void }} 信令会话
13
+ * @returns {object} 信令会话
16
14
  */
17
15
  export function createBufferedSignalSession(sendRemote) {
18
16
  /** @type {Set<(message: unknown) => void>} */
@@ -21,17 +19,15 @@ export function createBufferedSignalSession(sendRemote) {
21
19
  const backlog = []
22
20
  return {
23
21
  /**
24
- * 发送信令消息到远端。
25
- * @param {unknown} message 信令消息
22
+ * @param {unknown} message 待发送信令消息
26
23
  * @returns {Promise<void>}
27
24
  */
28
25
  async send(message) {
29
26
  await sendRemote(message)
30
27
  },
31
28
  /**
32
- * 注册远端信令 handler(含 backlog 回放)。
33
- * @param {(message: unknown) => void} handler 入站回调
34
- * @returns {() => void} 取消订阅函数
29
+ * @param {(message: unknown) => void} handler 远端消息回调
30
+ * @returns {() => void} 取消订阅
35
31
  */
36
32
  onRemote(handler) {
37
33
  handlers.add(handler)
@@ -40,8 +36,7 @@ export function createBufferedSignalSession(sendRemote) {
40
36
  return () => handlers.delete(handler)
41
37
  },
42
38
  /**
43
- * 投递信令消息;无 handler 时入 backlog(有界)。
44
- * @param {unknown} message 信令消息
39
+ * @param {unknown} message 入站信令消息
45
40
  * @returns {void}
46
41
  */
47
42
  deliver(message) {
@@ -54,7 +49,6 @@ export function createBufferedSignalSession(sendRemote) {
54
49
  handler(message)
55
50
  },
56
51
  /**
57
- * 清空 backlog 与 handler。
58
52
  * @returns {void}
59
53
  */
60
54
  clear() {
@@ -65,9 +59,8 @@ export function createBufferedSignalSession(sendRemote) {
65
59
  }
66
60
 
67
61
  /**
68
- * 判断信令 body 是否为发起方 offer(据此决定入站是否新建应答 PC)。
69
62
  * @param {unknown} body 信令 body
70
- * @returns {boolean} offer 则 true
63
+ * @returns {boolean} 是否为 WebRTC offer description
71
64
  */
72
65
  function isOfferSignalBody(body) {
73
66
  return !!body && typeof body === 'object'
@@ -76,30 +69,19 @@ function isOfferSignalBody(body) {
76
69
  }
77
70
 
78
71
  /**
79
- * 找已注册的 offer/answer provider(按 level 降序的第一个)。
80
- * @returns {import('../link/providers/index.mjs').LinkProvider | null} 命中的 provider;无则 null
72
+ * @returns {import('../link/providers/index.mjs').LinkProvider | null} 首个需要 offer/answer provider
81
73
  */
82
74
  export function findOfferAnswerProvider() {
83
75
  return listLinkProviders().find(provider => provider.caps?.needsOfferAnswer) ?? null
84
76
  }
85
77
 
86
78
  /**
87
- * Offer/answer glare 拨号控制器(WebRTC 等 needsOfferAnswer provider)。
88
- * @param {object} deps 依赖注入
89
- * @param {{ nodeHash: string, nodePubKey: string, secretKey: Uint8Array }} deps.localIdentity 本地身份
90
- * @param {RTCConfiguration['iceServers']} deps.iceServers ICE 服务器列表
91
- * @param {string} deps.selfTopic 本机 rendezvous topic
92
- * @param {Map<string, ReturnType<typeof createBufferedSignalSession>>} deps.signalSessions 按 connId 索引的信令会话
93
- * @param {(remoteNodeHash: string, link: object) => Promise<void>} deps.registerResolvedLink 注册规范链
94
- * @param {() => Promise<void>} deps.trimToBudget 超预算驱逐
95
- * @param {(remoteNodeHash: string) => object | null | undefined} deps.getCanonicalLink 读取当前规范链
96
- * @returns {{ handleIncomingSignal: (bytes: Uint8Array) => Promise<void>, dialOfferAnswer: (provider: import('../link/providers/index.mjs').LinkProvider, remoteNodeHash: string) => Promise<object | null> }} 入站信令与主动拨号
79
+ * @param {object} deps 依赖
80
+ * @returns {object} offer/answer 拨号
97
81
  */
98
82
  export function createOfferAnswerDial(deps) {
99
83
  const {
100
84
  localIdentity,
101
- iceServers,
102
- selfTopic,
103
85
  signalSessions,
104
86
  registerResolvedLink,
105
87
  trimToBudget,
@@ -107,28 +89,30 @@ export function createOfferAnswerDial(deps) {
107
89
  } = deps
108
90
 
109
91
  /**
110
- * 为单条 PC 创建按 connId 标记的信令会话。
111
- * @param {string} remoteNodeHash 远端节点 64 hex
112
- * @param {string} connId 连接标识
113
- * @returns {ReturnType<typeof createBufferedSignalSession>} 信令会话
92
+ * @param {string} remoteNodeHash 远端
93
+ * @param {string} connId 连接 id
94
+ * @returns {ReturnType<typeof createBufferedSignalSession>} 该连接的缓冲信令会话
114
95
  */
115
96
  function createConnSession(remoteNodeHash, connId) {
116
97
  const normalized = normalizeHex64(remoteNodeHash)
117
- const topic = nodeRendezvousTopic(normalized)
118
98
  return createBufferedSignalSession(async message => {
119
- await sendSignal(topic, normalized, encryptSignalPacket(topic, {
99
+ await sendNodeSignalPacket(normalized, {
120
100
  type: 'signal',
121
101
  from: localIdentity.nodeHash,
122
102
  connId,
123
103
  body: message,
124
- }))
104
+ })
125
105
  })
126
106
  }
127
107
 
128
108
  /**
129
- * Offer/answer provider:为一条 connId 会话建链并走 glare 择一。
130
- * @param {{ provider: import('../link/providers/index.mjs').LinkProvider, remoteNodeHash: string, connId: string, session: ReturnType<typeof createBufferedSignalSession>, initiator: boolean }} options 建链参数
131
- * @returns {Promise<object | null>} 当前规范链;失败 null
109
+ * @param {object} options 建链参数
110
+ * @param {import('../link/providers/index.mjs').LinkProvider} options.provider 链路提供者
111
+ * @param {string} options.remoteNodeHash 远端 nodeHash
112
+ * @param {string} options.connId 连接 id
113
+ * @param {ReturnType<typeof createBufferedSignalSession>} options.session 信令会话
114
+ * @param {boolean} options.initiator 是否主动拨号
115
+ * @returns {Promise<object | null>} 建链成功返回规范链路,否则 null
132
116
  */
133
117
  async function buildConnLink({ provider, remoteNodeHash, connId, session, initiator }) {
134
118
  try {
@@ -136,11 +120,16 @@ export function createOfferAnswerDial(deps) {
136
120
  const link = await (initiator ? provider.dial : provider.accept)({
137
121
  nodeHash: remoteNodeHash,
138
122
  signal: session,
139
- iceServers,
123
+ iceServers: deps.iceServers,
140
124
  localIdentity,
141
125
  })
142
126
  if (!link) {
143
127
  signalSessions.delete(connId)
128
+ nodeDebug('p2p:webrtc null', {
129
+ peer: shortHash(remoteNodeHash),
130
+ provider: provider.id,
131
+ role: initiator ? 'dial' : 'accept',
132
+ })
144
133
  return null
145
134
  }
146
135
  link.onDown(() => signalSessions.delete(connId))
@@ -148,19 +137,24 @@ export function createOfferAnswerDial(deps) {
148
137
  await registerResolvedLink(remoteNodeHash, link)
149
138
  return getCanonicalLink(remoteNodeHash) ?? null
150
139
  }
151
- catch {
140
+ catch (error) {
152
141
  signalSessions.delete(connId)
142
+ nodeDebug('p2p:webrtc fail', {
143
+ peer: shortHash(remoteNodeHash),
144
+ provider: provider.id,
145
+ role: initiator ? 'dial' : 'accept',
146
+ err: String(error?.message || error),
147
+ })
153
148
  return null
154
149
  }
155
150
  }
156
151
 
157
152
  /**
158
- * 处理入站加密信令并可能发起被动建链。
159
- * @param {Uint8Array} bytes 加密信令字节
153
+ * @param {Uint8Array} bytes 加密信令
160
154
  * @returns {Promise<void>}
161
155
  */
162
156
  async function handleIncomingSignal(bytes) {
163
- const packet = decryptSignalPacket(selfTopic, bytes)
157
+ const packet = decryptNodeSignalPacket(localIdentity.nodeHash, bytes)
164
158
  if (packet?.type !== 'signal') return
165
159
  const remoteNodeHash = normalizeHex64(packet.from)
166
160
  const connId = String(packet.connId || '')
@@ -170,6 +164,10 @@ export function createOfferAnswerDial(deps) {
170
164
  if (!isOfferSignalBody(packet.body)) return
171
165
  const provider = findOfferAnswerProvider()
172
166
  if (!provider) return
167
+ nodeDebug('p2p:webrtc inbound offer', {
168
+ peer: shortHash(remoteNodeHash),
169
+ provider: provider.id,
170
+ })
173
171
  session = createConnSession(remoteNodeHash, connId)
174
172
  signalSessions.set(connId, session)
175
173
  void buildConnLink({ provider, remoteNodeHash, connId, session, initiator: false })
@@ -178,10 +176,9 @@ export function createOfferAnswerDial(deps) {
178
176
  }
179
177
 
180
178
  /**
181
- * 经 offer/answer provider 拨号(discovery signal + glare)。
182
179
  * @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
183
- * @param {string} remoteNodeHash 远端 64 hex
184
- * @returns {Promise<object | null>} 当前规范链;失败 null
180
+ * @param {string} remoteNodeHash 远端 nodeHash
181
+ * @returns {Promise<object | null>} 建链成功返回规范链路,否则 null
185
182
  */
186
183
  async function dialOfferAnswer(provider, remoteNodeHash) {
187
184
  const connId = randomBytes(16).toString('hex')
@@ -1,9 +1,11 @@
1
1
  /**
2
- * 稀疏连接池纯计算逻辑(§0、§4):
3
- * Top-K 信任连接 + M 随机探索,可被 chat shell 与 subfounts 共用。
4
- * 不含文件 I/O;I/O 层由调用方注入。
2
+ * 连接池纯计算:
3
+ * - 群联邦稀疏拨号:trustedSlots + exploreSlots(groupSettings)
4
+ * - 节点 mesh 保活:N / K_max(routing profile + transport tunables)
5
+ * 不含文件 I/O;I/O 由调用方注入。
5
6
  */
6
7
 
8
+ import { compareHex64Asc } from '../core/hexIds.mjs'
7
9
  import { loadPeerPoolView, mergeNetworkPeerPools } from '../node/network.mjs'
8
10
  import { loadReputation } from '../node/reputation_store.mjs'
9
11
  import { isQuarantinedPure } from '../reputation/engine.mjs'
@@ -97,9 +99,9 @@ export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxP
97
99
  /** @type {Map<string, string[]>} */
98
100
  const bySource = new Map()
99
101
  for (const id of exploreIds) {
100
- const src = exploreSources.get(id) || 'unknown'
101
- if (!bySource.has(src)) bySource.set(src, [])
102
- bySource.get(src).push(id)
102
+ const source = exploreSources.get(id) || 'unknown'
103
+ if (!bySource.has(source)) bySource.set(source, [])
104
+ bySource.get(source).push(id)
103
105
  }
104
106
  for (const ids of bySource.values()) shuffleInPlace(ids)
105
107
  const out = []
@@ -107,12 +109,12 @@ export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxP
107
109
  const picked = new Map()
108
110
  while (out.length < k) {
109
111
  let progressed = false
110
- for (const [src, ids] of bySource) {
112
+ for (const [source, ids] of bySource) {
111
113
  if (out.length >= k) break
112
- const idx = picked.get(src) ?? 0
113
- if (idx >= maxPerSource || idx >= ids.length) continue
114
- out.push(ids[idx])
115
- picked.set(src, idx + 1)
114
+ const index = picked.get(source) ?? 0
115
+ if (index >= maxPerSource || index >= ids.length) continue
116
+ out.push(ids[index])
117
+ picked.set(source, index + 1)
116
118
  progressed = true
117
119
  }
118
120
  if (!progressed) break
@@ -133,7 +135,7 @@ export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxP
133
135
  * inRoomNodeHashes?: Set<string> | string[] 群内在线 node_id;有则优先,仅全不可达时用 explore 中非房内节点
134
136
  * hintSources?: Map<string, string> explore 节点来源(用于配额)
135
137
  * }} options 选取参数(roster、peers、rep、limits、selfNodeHash)
136
- * @returns {string[]} 目标 Trystero peerId 列表(去重,长度 ≤ maxPeers)
138
+ * @returns {string[]} 目标 peerId 列表(去重,长度 ≤ maxPeers)
137
139
  */
138
140
  export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash, inRoomNodeHashes, hintSources }) {
139
141
  const blocked = new Set(peers.blockedPeers)
@@ -292,10 +294,10 @@ export function applyRosterToPeerPool(options) {
292
294
  /**
293
295
  * 稀疏连接池:优先 trusted,再 explore,再其余在线节点。
294
296
  * @param {string} groupId 群
295
- * @param {{ peerId: string, remoteNodeHash?: string }[]} roster Trystero 在线表
297
+ * @param {{ peerId: string, remoteNodeHash?: string }[]} roster 在线表
296
298
  * @param {object} groupSettings 物化群设置
297
299
  * @param {string} selfNodeHash 本机 node_id
298
- * @returns {string[]} 目标 Trystero peerId(去重)
300
+ * @returns {string[]} 目标 peerId(去重)
299
301
  */
300
302
  export function pickFederationTargetPeerIds(groupId, roster, groupSettings, selfNodeHash) {
301
303
  const limits = resolveFederationPoolLimits(groupSettings)
@@ -342,3 +344,103 @@ export function reconcilePeerPoolFromRoster(groupId, roster, groupSettings) {
342
344
  const { trustedPeers, explorePeers } = applyRosterToPeerPool({ peers, rep, roster, limits })
343
345
  mergeNetworkPeerPools({ trustedPeers, explorePeers })
344
346
  }
347
+
348
+ /**
349
+ * Mesh 保活 N/K 槽位(routing profile 可缩 N 与 K_max)。
350
+ * @param {'default' | 'low'} [routingProfile='default'] 路由 profile
351
+ * @param {object} [tunables={}] transport tunables
352
+ * @returns {{ N: number, K_max: number }} mesh 槽位上限 N 与熟人槽 K_max
353
+ */
354
+ export function resolveMeshPoolLimits(routingProfile = 'default', tunables = {}) {
355
+ const low = routingProfile === 'low'
356
+ const N = Math.max(1, Math.min(32, Number(low ? tunables.meshNLow : tunables.meshN) || (low ? 4 : 8)))
357
+ const K_max = Math.max(0, Math.min(N, Number(low ? tunables.meshKMaxLow : tunables.meshKMax) || (low ? 2 : 5)))
358
+ return { N, K_max }
359
+ }
360
+
361
+ /**
362
+ * 选取 mesh 拨号目标:补齐 K 熟人(可超出当前空位,由调用方先踢探索),再按 N−K 探索配额填空。
363
+ * @param {{
364
+ * selfNodeHash: string,
365
+ * trustedPeers: string[],
366
+ * exploreCandidates: string[],
367
+ * hintSources?: Map<string, string>,
368
+ * limits: ReturnType<typeof resolveMeshPoolLimits>,
369
+ * connectedHashes: Set<string>,
370
+ * rep: { byNodeHash?: Record<string, { score?: number, quarantinedUntil?: number }> },
371
+ * blockedPeers?: string[],
372
+ * now?: number,
373
+ * }} options 选取参数
374
+ * @returns {string[]} 应拨号的 nodeHash(熟人可导致需先驱逐探索)
375
+ */
376
+ export function selectMeshLinkTargets(options) {
377
+ const {
378
+ selfNodeHash,
379
+ trustedPeers = [],
380
+ exploreCandidates = [],
381
+ hintSources,
382
+ limits,
383
+ connectedHashes,
384
+ rep,
385
+ blockedPeers = [],
386
+ now = Date.now(),
387
+ } = options
388
+ const blocked = new Set(blockedPeers)
389
+ const connected = connectedHashes
390
+
391
+ const eligibleTrusted = [...new Set(trustedPeers)]
392
+ .filter(id => id && id !== selfNodeHash && !blocked.has(id))
393
+ .filter(id => !isQuarantinedPure(rep, id, now))
394
+ const trustedSet = new Set(eligibleTrusted)
395
+ /** 目标组成:K 熟人槽 + (N−K) 探索槽 */
396
+ const K = Math.min(limits.K_max, eligibleTrusted.length)
397
+ let connectedTrusted = 0
398
+ let connectedExplore = 0
399
+ for (const id of connected)
400
+ if (trustedSet.has(id)) connectedTrusted++
401
+ else connectedExplore++
402
+ const trustedSlotsLeft = Math.max(0, K - connectedTrusted)
403
+ const trustedRanked = eligibleTrusted
404
+ .filter(id => !connected.has(id))
405
+ .sort((a, b) => repScore(b, rep) - repScore(a, rep))
406
+ // 熟人缺口优先补齐,即使当前已满 N(调用方应先踢探索腾位)
407
+ const trustedPick = trustedRanked.slice(0, trustedSlotsLeft)
408
+
409
+ const exploreQuota = Math.max(0, limits.N - K)
410
+ const exploreSlotsLeft = Math.max(0, exploreQuota - connectedExplore)
411
+ const freeAfterTrustedDial = Math.max(0, limits.N - connected.size - trustedPick.length)
412
+ const exploreNeed = Math.min(exploreSlotsLeft, freeAfterTrustedDial)
413
+ const explorePool = [...new Set(exploreCandidates)]
414
+ .filter(id => id && id !== selfNodeHash && !blocked.has(id) && !connected.has(id) && !trustedSet.has(id))
415
+ .filter(id => !isQuarantinedPure(rep, id, now))
416
+ const explorePick = selectExploreWithSourceQuota(explorePool, hintSources, exploreNeed)
417
+ return [...trustedPick, ...explorePick]
418
+ }
419
+
420
+ /**
421
+ * mesh trim:探索链优先驱逐,同档取 scope 权重低、nodeHash 小。
422
+ * @param {string[]} linkHashes 当前链路 nodeHash
423
+ * @param {Set<string>} exploreLinkHashes 探索链集合
424
+ * @param {string[]} trustedPeers 熟人池
425
+ * @param {(nodeHash: string) => number} scopeWeightFn scope 权重
426
+ * @returns {string | null} 应驱逐的 nodeHash
427
+ */
428
+ export function pickMeshEvictionVictim(linkHashes, exploreLinkHashes, trustedPeers, scopeWeightFn) {
429
+ const trustedSet = new Set(trustedPeers)
430
+ let victimHash = null
431
+ let victimScore = Infinity
432
+ for (const nodeHash of linkHashes) {
433
+ const isExplore = exploreLinkHashes.has(nodeHash) && !trustedSet.has(nodeHash)
434
+ const weight = scopeWeightFn(nodeHash)
435
+ const score = (isExplore ? 0 : 1000) + weight
436
+ if (
437
+ victimHash == null
438
+ || score < victimScore
439
+ || (score === victimScore && compareHex64Asc(nodeHash, victimHash) < 0)
440
+ ) {
441
+ victimHash = nodeHash
442
+ victimScore = score
443
+ }
444
+ }
445
+ return victimHash
446
+ }