@steve02081504/fount-p2p 0.0.11 → 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 (102) hide show
  1. package/README.md +42 -11
  2. package/core/bytes_codec.mjs +65 -10
  3. package/core/composite_key.mjs +5 -5
  4. package/core/tcp_port.mjs +1 -1
  5. package/crypto/checkpoint_sign.mjs +2 -2
  6. package/crypto/crypto.mjs +3 -3
  7. package/crypto/key.mjs +12 -12
  8. package/dag/canonicalize_row.mjs +3 -3
  9. package/dag/index.mjs +2 -3
  10. package/dag/storage.mjs +36 -36
  11. package/discovery/advert_peer_hints.mjs +1 -1
  12. package/discovery/adverts.mjs +109 -0
  13. package/discovery/bt/index.mjs +154 -113
  14. package/discovery/bt/probe_child.mjs +2 -1
  15. package/discovery/bt/runtime.mjs +5 -12
  16. package/discovery/index.mjs +267 -62
  17. package/discovery/internal/signal_crypto.mjs +109 -0
  18. package/discovery/lan.mjs +228 -0
  19. package/discovery/nostr.mjs +430 -141
  20. package/federation/chunk_fetch_pending.mjs +14 -18
  21. package/federation/dag_order_cache.mjs +1 -1
  22. package/federation/entity_key_chain.mjs +3 -4
  23. package/federation/manifest_fetch_pending.mjs +1 -3
  24. package/federation/topo_order_memo.mjs +11 -4
  25. package/files/assemble.mjs +14 -14
  26. package/files/assemble_stream.mjs +6 -6
  27. package/files/chunk_fetch.mjs +2 -2
  28. package/files/chunk_responder.mjs +29 -20
  29. package/files/evfs.mjs +31 -30
  30. package/files/manifest_fetch.mjs +16 -3
  31. package/files/public_manifest.mjs +11 -11
  32. package/files/transfer_key_registry.mjs +2 -2
  33. package/index.mjs +86 -11
  34. package/infra/cli.mjs +62 -0
  35. package/infra/debug_log.mjs +56 -0
  36. package/infra/default_node_dir.mjs +22 -0
  37. package/infra/priority.mjs +56 -0
  38. package/infra/service.mjs +140 -0
  39. package/infra/tunables.json +5 -0
  40. package/link/channel_mux.mjs +10 -10
  41. package/link/frame.mjs +76 -131
  42. package/link/handshake.mjs +19 -20
  43. package/link/pipe.mjs +53 -79
  44. package/link/providers/ble_gatt.mjs +19 -31
  45. package/link/providers/lan_tcp.mjs +17 -32
  46. package/link/providers/link_id_pipe.mjs +46 -0
  47. package/link/providers/webrtc.mjs +42 -51
  48. package/link/rtc.mjs +24 -12
  49. package/mailbox/consumer_registry.mjs +2 -2
  50. package/mailbox/deliver_or_store.mjs +6 -6
  51. package/mailbox/wire.mjs +28 -24
  52. package/node/entity_store.mjs +6 -6
  53. package/node/identity.mjs +4 -4
  54. package/node/instance.mjs +46 -27
  55. package/node/local_data_revision.mjs +26 -0
  56. package/node/log.mjs +56 -0
  57. package/node/network.mjs +46 -14
  58. package/node/reputation_store.mjs +4 -4
  59. package/node/reputation_sync.mjs +318 -0
  60. package/node/routing_profile.mjs +21 -0
  61. package/node/signaling_config.mjs +30 -11
  62. package/overlay/index.mjs +35 -14
  63. package/package.json +13 -2
  64. package/rooms/scoped_link.mjs +17 -182
  65. package/schemas/discovery.mjs +1 -2
  66. package/schemas/federation_pull.mjs +2 -2
  67. package/schemas/part_query.mjs +1 -1
  68. package/schemas/remote_event.mjs +1 -1
  69. package/transport/advert_ingest.mjs +17 -0
  70. package/transport/group_link_set.mjs +165 -134
  71. package/transport/ice_servers.mjs +2 -2
  72. package/transport/link_registry.mjs +283 -124
  73. package/transport/mesh_keepalive.mjs +217 -0
  74. package/transport/node_scope.mjs +289 -0
  75. package/transport/offer_answer.mjs +48 -47
  76. package/transport/peer_pool.mjs +169 -78
  77. package/transport/remote_user_room.mjs +18 -23
  78. package/transport/rtc_connection_budget.mjs +18 -4
  79. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  80. package/transport/runtime_bootstrap.mjs +172 -104
  81. package/transport/tunables.json +11 -0
  82. package/transport/tunables.mjs +18 -0
  83. package/transport/user_room.mjs +93 -161
  84. package/trust_graph/build.mjs +0 -2
  85. package/trust_graph/cache.mjs +12 -3
  86. package/trust_graph/registry.mjs +6 -6
  87. package/trust_graph/send.mjs +1 -4
  88. package/utils/async_mutex.mjs +4 -4
  89. package/utils/emit_safe.mjs +11 -0
  90. package/utils/json_io.mjs +12 -12
  91. package/utils/map_pool.mjs +5 -5
  92. package/utils/shuffle.mjs +13 -0
  93. package/utils/ttl_map.mjs +29 -4
  94. package/wire/ingress.mjs +1 -1
  95. package/wire/part_ingress.mjs +38 -36
  96. package/wire/part_invoke.mjs +4 -4
  97. package/wire/part_query.mjs +28 -23
  98. package/wire/part_query.tunables.json +6 -1
  99. package/wire/part_query_cache.mjs +1 -1
  100. package/wire/volatile_signature.mjs +1 -1
  101. package/discovery/mdns.mjs +0 -191
  102. package/transport/signal_crypto.mjs +0 -82
@@ -1,125 +1,55 @@
1
1
  import { createHash } from 'node:crypto'
2
2
 
3
- import { attachMailboxWire } from '../mailbox/wire.mjs'
4
3
  import { ensureNodeDefaults, getNodeHash } from '../node/identity.mjs'
5
4
  import { registerFederationRoomProvider } from '../registries/room_provider.mjs'
6
- import { attachPartWire } from '../wire/part_ingress.mjs'
7
- import { attachPartQueryWire } from '../wire/part_query.mjs'
5
+ import { shuffleInPlace } from '../utils/shuffle.mjs'
8
6
 
9
- import { listLinks, sendToNodeLink, subscribeScope, getLinkRegistry } from './link_registry.mjs'
7
+ import { getLinkRegistry, listLinks, sendToNodeLink } from './link_registry.mjs'
8
+ import {
9
+ attachUserRoomDefaultWires,
10
+ ensureNodeScope,
11
+ } from './node_scope.mjs'
10
12
  import { USER_ROOM_SCOPE } from './room_scopes.mjs'
11
13
 
12
14
  /**
13
- * node scope action 表上注册 fed_chunk_get / fed_chunk_data handler,
14
- * 供用户房间(本地 + 远端)双向 chunk 传输使用。
15
- * @param {string} username 副本用户名 用户名
16
- * @param {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire action 表
17
- * @returns {void}
15
+ * node scope 向对端发 action
16
+ * @param {string} peerId 对端 nodeHash
17
+ * @param {string} action 动作名
18
+ * @param {unknown} payload 载荷
19
+ * @returns {Promise<boolean>} 是否发出
18
20
  */
19
- function attachUserRoomChunkHandlers(username, wire) {
20
- import('../files/chunk_responder.mjs').then(({ attachNodeScopeFedChunkResponder }) => {
21
- attachNodeScopeFedChunkResponder(username, wire)
22
- }).catch(error => console.error('p2p: failed to attach chunk handlers to node scope', error))
23
- }
21
+ const sendNodeAction = (peerId, action, payload) =>
22
+ sendToNodeLink(peerId, { scope: 'node', action, payload })
24
23
 
25
- /** @type {Promise<UserRoomSlot | null> | null} */
24
+ /** @type {Promise<UserRoomSlot> | null} */
26
25
  let userRoomInflight = null
27
26
 
28
27
  /** @type {UserRoomSlot | null} */
29
- export let userRoomSlot = null
30
-
31
- /** @type {Map<string, Set<(payload: unknown, peerId: string) => void>>} */
32
- const nodeActionHandlers = new Map()
33
- /** @type {Set<(username: string, wire: { on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }) => void>} */
34
- const nodeScopeWireHooks = new Set()
35
- /** @type {string} */
36
- let nodeScopeReplicaUsername = ''
37
- /** @type {ReturnType<typeof createNodeScopeWire> | null} */
38
- let nodeScopeWire = null
39
- let nodeScopeCleanup = null
28
+ let userRoomSlot = null
40
29
 
41
- /**
42
- * Chat 等非 P2P 模块可向 node scope 注册 fed_emoji 等 handler(避免 p2p→shell 硬依赖)。
43
- * @param {(username: string, wire: { on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }) => void} hook 注册回调
44
- * @returns {() => void} 取消注册
45
- */
46
- export function registerUserRoomNodeScopeHook(hook) {
47
- nodeScopeWireHooks.add(hook)
48
- if (nodeScopeWire)
49
- try { hook(nodeScopeReplicaUsername, nodeScopeWire) } catch { /* ignore */ }
50
- return () => nodeScopeWireHooks.delete(hook)
51
- }
30
+ /** @type {(() => void) | null} */
31
+ let userRoomDefaultWiresDispose = null
52
32
 
53
33
  /**
54
- * 创建 node scope on/send wire 表。
55
- * @returns {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire 接口
34
+ * @returns {UserRoomSlot | null} 已创建的用户房间槽,未 ensure 时为 null
56
35
  */
57
- function createNodeScopeWire() {
58
- return {
59
- /**
60
- * 注册 node scope action handler。
61
- * @param {string} name action 名称
62
- * @param {(payload: unknown, peerId: string) => void} handler 入站回调
63
- * @returns {void}
64
- */
65
- on(name, handler) {
66
- const key = String(name)
67
- if (!nodeActionHandlers.has(key)) nodeActionHandlers.set(key, new Set())
68
- nodeActionHandlers.get(key).add(handler)
69
- },
70
- /**
71
- * 向指定 peer 发送 node scope action。
72
- * @param {string} name action 名称
73
- * @param {unknown} payload 载荷
74
- * @param {string | null} peerId 目标 peer id
75
- * @returns {void}
76
- */
77
- send(name, payload, peerId) {
78
- if (!peerId) return
79
- void sendToNodeLink(peerId, { scope: 'node', action: String(name), payload }).catch(() => { })
80
- },
81
- }
36
+ export function getUserRoomSlot() {
37
+ return userRoomSlot
82
38
  }
83
39
 
84
40
  /**
85
41
  * 返回当前所有活跃链路的 roster。
86
- * @returns {Array<{ peerId: string, remoteNodeHash: string }>} 在线 peer 列表
42
+ * @returns {Array<{ peerId: string, remoteNodeHash: string }>} 活跃链路列表
87
43
  */
88
- function activeLinkRoster() {
44
+ export function activeLinkRoster() {
89
45
  return listLinks().map(({ nodeHash }) => ({ peerId: nodeHash, remoteNodeHash: nodeHash }))
90
46
  }
91
47
 
92
48
  /**
93
- * 初始化 node scope 订阅与 wire 派发(幂等)。
94
- * @param {{ replicaUsername?: string }} wireContext 入站上下文
95
- * @returns {Promise<void>}
96
- */
97
- async function ensureNodeScopeRuntime(wireContext) {
98
- if (nodeScopeCleanup) return
99
- nodeScopeReplicaUsername = String(wireContext.replicaUsername || nodeScopeReplicaUsername || '')
100
- nodeScopeCleanup = subscribeScope('node', (senderNodeHash, envelope) => {
101
- const handlers = nodeActionHandlers.get(String(envelope?.action || ''))
102
- if (!handlers?.size) return
103
- for (const handler of handlers)
104
- try { handler(envelope.payload, senderNodeHash) } catch { /* ignore */ }
105
- })
106
- const wire = createNodeScopeWire()
107
- nodeScopeWire = wire
108
- attachPartWire({ replicaUsername: wireContext.replicaUsername }, wire)
109
- attachPartQueryWire({ replicaUsername: wireContext.replicaUsername }, wire)
110
- attachMailboxWire({ replicaUsername: wireContext.replicaUsername }, wire)
111
- attachUserRoomChunkHandlers(wireContext.replicaUsername || '', wire)
112
- for (const hook of nodeScopeWireHooks)
113
- try { hook(wireContext.replicaUsername || '', wire) } catch { /* ignore */ }
114
- }
115
-
116
- /**
117
- * 用户级 node scope 房间(`fount-node-{nodeHash}`),单节点单实例。
118
- *
119
49
  * @typedef {{
120
50
  * roomId: string
121
51
  * roomSecret: string
122
- * room: object
52
+ * room: object | null
123
53
  * sendToPeer: (peerId: string, actionName: string, payload: unknown) => void
124
54
  * getRoster: () => Array<{ peerId: string, remoteNodeHash: string | undefined }>
125
55
  * getPeerIdByNodeHash: (nodeHash: string) => string | null
@@ -127,28 +57,31 @@ async function ensureNodeScopeRuntime(wireContext) {
127
57
  */
128
58
 
129
59
  registerFederationRoomProvider('user-room', () => {
130
- if (!userRoomSlot) return []
60
+ const slot = getUserRoomSlot()
61
+ if (!slot) return []
131
62
  return [{
132
63
  groupId: USER_ROOM_SCOPE,
133
- /** @returns {Array<{ peerId: string, remoteNodeHash: string | undefined }>} 房间 roster 列表 */
134
- getRoster: () => userRoomSlot.getRoster(),
135
64
  /**
136
- * @param {string} nodeHash 64 位十六进制
137
- * @returns {string | null} 对端 id
65
+ * @returns {Array<{ peerId: string, remoteNodeHash: string | undefined }>} 当前 roster
138
66
  */
139
- getPeerIdByNodeHash: nodeHash => userRoomSlot.getPeerIdByNodeHash(nodeHash),
67
+ getRoster: () => slot.getRoster(),
140
68
  /**
141
- * @param {string} peerId 目标 peer
142
- * @param {string} actionName 节点 scope action
143
- * @param {unknown} payload 载荷
69
+ * @param {string} nodeHash - 远端节点 hash
70
+ * @returns {string | null} 已连接时返回 peerId,否则 null
71
+ */
72
+ getPeerIdByNodeHash: nodeHash => slot.getPeerIdByNodeHash(nodeHash),
73
+ /**
74
+ * @param {string} peerId - 目标 peer
75
+ * @param {string} actionName - node scope action 名
76
+ * @param {unknown} payload - 载荷
144
77
  * @returns {void}
145
78
  */
146
- sendToPeer: (peerId, actionName, payload) => userRoomSlot.sendToPeer(peerId, actionName, payload),
79
+ sendToPeer: (peerId, actionName, payload) => slot.sendToPeer(peerId, actionName, payload),
147
80
  }]
148
81
  })
149
82
 
150
83
  /**
151
- * @returns {{ appId: string, password: string, roomId: string, nodeHash: string }} 用户房间参数
84
+ * @returns {{ appId: string, password: string, roomId: string, nodeHash: string }} user room rendezvous 凭据
152
85
  */
153
86
  export function resolveUserRoomCredentials() {
154
87
  const nodeHash = getNodeHash()
@@ -162,61 +95,62 @@ export function resolveUserRoomCredentials() {
162
95
  }
163
96
 
164
97
  /**
165
- * @param {{ replicaUsername?: string }} [wireContext] 入站上下文(part/mailbox 派发用)
166
- * @returns {Promise<UserRoomSlot | null>} 用户级联邦房间槽
98
+ * 用户房间槽 + runtime;默认不挂业务 wire(用 `attachUserRoomDefaultWires` / `attachDefaultWires: true`)。
99
+ * @param {{ replicaUsername?: string, attachDefaultWires?: boolean }} [options] - 副本用户名与是否挂载默认 wire
100
+ * @returns {Promise<UserRoomSlot>} 用户房间槽
167
101
  */
168
- export async function ensureUserRoom(wireContext = {}) {
169
- if (userRoomSlot) return userRoomSlot
102
+ export async function ensureUserRoom(options = {}) {
103
+ const { attachDefaultWires = false } = options
104
+ if (options.replicaUsername != null)
105
+ ensureNodeScope({ replicaUsername: options.replicaUsername })
106
+ if (userRoomSlot) {
107
+ if (attachDefaultWires && !userRoomDefaultWiresDispose)
108
+ userRoomDefaultWiresDispose = attachUserRoomDefaultWires({ replicaUsername: options.replicaUsername })
109
+ return userRoomSlot
110
+ }
170
111
  if (userRoomInflight) return await userRoomInflight
171
112
 
172
113
  userRoomInflight = (async () => {
173
114
  ensureNodeDefaults()
174
- try {
175
- await getLinkRegistry().ensureRuntime()
176
- await ensureNodeScopeRuntime(wireContext)
177
- const creds = resolveUserRoomCredentials()
178
- /** @type {UserRoomSlot} */
179
- userRoomSlot = {
180
- roomId: creds.roomId,
181
- roomSecret: creds.password,
182
- room: null,
183
- /**
184
- * node scope peer 发送 action。
185
- * @param {string} peerId 目标 peer id
186
- * @param {string} actionName action 名称
187
- * @param {unknown} payload 载荷
188
- * @returns {void}
189
- */
190
- sendToPeer(peerId, actionName, payload) {
191
- void sendToNodeLink(peerId, { scope: 'node', action: String(actionName), payload }).catch(() => { })
192
- },
193
- /**
194
- * 返回当前活跃链路 roster。
195
- * @returns {Array<{ peerId: string, remoteNodeHash: string }>} 在线 peer 列表
196
- */
197
- getRoster: () => activeLinkRoster(),
198
- /**
199
- * 按 nodeHash 查找 peer id。
200
- * @param {string} nodeHash 目标节点 64 hex
201
- * @returns {string | null} 对端 id;无链路时为 null
202
- */
203
- getPeerIdByNodeHash(nodeHash) {
204
- return getLinkRegistry().getLink(nodeHash) ? String(nodeHash) : null
205
- },
206
- }
207
- return userRoomSlot
208
- }
209
- catch (error) {
210
- console.error('p2p: node scope init failed', error)
211
- userRoomSlot = null
212
- return null
213
- }
214
- finally {
215
- userRoomInflight = null
115
+ await getLinkRegistry().ensureRuntime()
116
+ ensureNodeScope({ replicaUsername: options.replicaUsername })
117
+ if (attachDefaultWires && !userRoomDefaultWiresDispose)
118
+ userRoomDefaultWiresDispose = attachUserRoomDefaultWires({ replicaUsername: options.replicaUsername })
119
+ const creds = resolveUserRoomCredentials()
120
+ userRoomSlot = {
121
+ roomId: creds.roomId,
122
+ roomSecret: creds.password,
123
+ room: null,
124
+ /**
125
+ * @param {string} peerId - 目标 peer
126
+ * @param {string} actionName - node scope action 名
127
+ * @param {unknown} payload - 载荷
128
+ * @returns {void}
129
+ */
130
+ sendToPeer(peerId, actionName, payload) {
131
+ void sendNodeAction(peerId, actionName, payload).catch(() => { })
132
+ },
133
+ /**
134
+ * @returns {Array<{ peerId: string, remoteNodeHash: string }>} 当前活跃链路 roster
135
+ */
136
+ getRoster: () => activeLinkRoster(),
137
+ /**
138
+ * @param {string} nodeHash - 远端节点 hash
139
+ * @returns {string | null} 已连接时返回 peerId,否则 null
140
+ */
141
+ getPeerIdByNodeHash(nodeHash) {
142
+ return getLinkRegistry().getLink(nodeHash) ? nodeHash : null
143
+ },
216
144
  }
145
+ return userRoomSlot
217
146
  })()
218
147
 
219
- return await userRoomInflight
148
+ try {
149
+ return await userRoomInflight
150
+ }
151
+ finally {
152
+ userRoomInflight = null
153
+ }
220
154
  }
221
155
 
222
156
  /**
@@ -225,24 +159,19 @@ export async function ensureUserRoom(wireContext = {}) {
225
159
  * @param {unknown} payload 载荷
226
160
  * @param {string | null} [exceptPeerId] 跳过的 peer
227
161
  * @param {number} [limit] 最多转发 peer 数
228
- * @returns {Promise<number>} 实际转发的 peer 数
162
+ * @returns {Promise<number>} 成功转发的 peer 数
229
163
  */
230
164
  export async function deliverToUserRoomPeers(username, actionName, payload, exceptPeerId = null, limit) {
165
+ void username
231
166
  const { USER_ROOM_PEER_FANOUT_DEFAULT } = await import('../wire/part_common.mjs')
232
167
  const fanoutLimit = limit ?? USER_ROOM_PEER_FANOUT_DEFAULT
233
- const slot = await ensureUserRoom({ replicaUsername: username })
234
- if (!slot) return 0
235
168
  const body = { ...payload, nodeHash: getNodeHash() }
236
169
  let sent = 0
237
- const peers = [...slot.getRoster()
238
- .filter(({ peerId }) => peerId && peerId !== exceptPeerId)]
239
- for (let swapIndex = peers.length - 1; swapIndex > 0; swapIndex--) {
240
- const pickIndex = Math.floor(Math.random() * (swapIndex + 1))
241
- ;[peers[swapIndex], peers[pickIndex]] = [peers[pickIndex], peers[swapIndex]]
242
- }
170
+ const peers = shuffleInPlace(activeLinkRoster()
171
+ .filter(({ peerId }) => peerId && peerId !== exceptPeerId))
243
172
  for (const { peerId } of peers)
244
173
  try {
245
- if (await sendToNodeLink(peerId, { scope: 'node', action: String(actionName), payload: body }))
174
+ if (await sendNodeAction(peerId, actionName, body))
246
175
  sent++
247
176
  if (sent >= fanoutLimit) break
248
177
  }
@@ -250,3 +179,6 @@ export async function deliverToUserRoomPeers(username, actionName, payload, exce
250
179
 
251
180
  return sent
252
181
  }
182
+
183
+ /** 再导出:node-scope 订阅与默认 wires(见 `node_scope.mjs`)。 */
184
+ export { attachUserRoomDefaultWires, ensureNodeScope } from './node_scope.mjs'
@@ -3,7 +3,6 @@ import { loadNetwork } from '../node/network.mjs'
3
3
  import { loadReputation } from '../node/reputation_store.mjs'
4
4
  import { listFederationRoomSlots } from '../registries/room_provider.mjs'
5
5
  import { isQuarantinedPure } from '../reputation/engine.mjs'
6
- import { ensureUserRoom } from '../transport/user_room.mjs'
7
6
 
8
7
  import { getCachedTrustGraph } from './cache.mjs'
9
8
  import { mergeGraph, pickTopFromGraph } from './engine.mjs'
@@ -90,7 +89,6 @@ export async function buildMergedGraph(username) {
90
89
  * @returns {Promise<TrustNode[]>} 按信誉降序
91
90
  */
92
91
  export async function pickTopNodes(username, limit = trustGraphTunables.pickTopNodesDefaultLimit) {
93
- await ensureUserRoom({ replicaUsername: username })
94
92
  const rep = loadReputation()
95
93
  const quarantined = new Set(
96
94
  Object.keys(rep.byNodeHash || {}).filter(id => isQuarantinedPure(rep, id)),
@@ -1,10 +1,13 @@
1
- /** @type {Map<string, { graph: Map<string, object>, builtAt: number, revision: number }>} */
1
+ import { getLocalDataRevision } from '../node/local_data_revision.mjs'
2
+
3
+ /** @type {Map<string, { graph: Map<string, object>, builtAt: number, revision: number, dataRevision: number }>} */
2
4
  const cacheByUsername = new Map()
3
5
  let revision = 0
4
6
 
5
7
  const DEFAULT_TTL_MS = 30_000
6
8
 
7
9
  /**
10
+ * 显式失效(测试 / 特殊路径);常态依赖 local_data_revision。
8
11
  * @returns {void}
9
12
  */
10
13
  export function invalidateTrustGraphCache() {
@@ -21,11 +24,17 @@ export function invalidateTrustGraphCache() {
21
24
  export async function getCachedTrustGraph(username, build, ttlMs = DEFAULT_TTL_MS) {
22
25
  const key = String(username || '')
23
26
  const now = Date.now()
27
+ const dataRevision = getLocalDataRevision()
24
28
  const cached = cacheByUsername.get(key)
25
- if (cached && cached.revision === revision && now - cached.builtAt < ttlMs)
29
+ if (
30
+ cached
31
+ && cached.revision === revision
32
+ && cached.dataRevision === dataRevision
33
+ && now - cached.builtAt < ttlMs
34
+ )
26
35
  return cached.graph
27
36
 
28
37
  const graph = await build()
29
- cacheByUsername.set(key, { graph, builtAt: now, revision })
38
+ cacheByUsername.set(key, { graph, builtAt: now, revision, dataRevision })
30
39
  return graph
31
40
  }
@@ -9,11 +9,11 @@ export const DEFAULT_TRUST_GRAPH_OWNER = 'default'
9
9
 
10
10
  /**
11
11
  * @param {string} ownerId 注册方(如 chat)
12
- * @param {import('./registry.mjs').TrustGraphProvider} impl 信任图实现
12
+ * @param {import('./registry.mjs').TrustGraphProvider} implementation 信任图实现
13
13
  * @returns {void}
14
14
  */
15
- export function registerTrustGraphProvider(ownerId, impl) {
16
- providersByOwner.set(String(ownerId), impl)
15
+ export function registerTrustGraphProvider(ownerId, implementation) {
16
+ providersByOwner.set(String(ownerId), implementation)
17
17
  }
18
18
 
19
19
  /** @returns {void} */
@@ -26,10 +26,10 @@ export function clearTrustGraphProvider() {
26
26
  * @returns {import('./registry.mjs').TrustGraphProvider} 已注册实现
27
27
  */
28
28
  export function requireTrustGraphProvider(ownerId = DEFAULT_TRUST_GRAPH_OWNER) {
29
- const impl = providersByOwner.get(String(ownerId))
30
- if (!impl)
29
+ const implementation = providersByOwner.get(String(ownerId))
30
+ if (!implementation)
31
31
  throw new Error(`p2p: registerTrustGraphProvider('${ownerId}') must run before trust graph fanout`)
32
- return impl
32
+ return implementation
33
33
  }
34
34
 
35
35
  /** @returns {import('./registry.mjs').TrustGraphProvider} 默认信任图实现 */
@@ -5,7 +5,6 @@ import { listFederationRoomSlots } from '../registries/room_provider.mjs'
5
5
  import { isQuarantinedPure } from '../reputation/engine.mjs'
6
6
  import { sendToNodeLink } from '../transport/link_registry.mjs'
7
7
  import { USER_ROOM_SCOPE } from '../transport/room_scopes.mjs'
8
- import { ensureUserRoom } from '../transport/user_room.mjs'
9
8
 
10
9
  import { buildMergedGraph } from './build.mjs'
11
10
  import { pickTopFromGraph } from './engine.mjs'
@@ -23,10 +22,9 @@ import trustGraphTunables from './tunables.json' with { type: 'json' }
23
22
  export async function sendToNode(username, targetNodeHash, actionName, payload, graph) {
24
23
  const target = normalizeHex64(targetNodeHash) || String(targetNodeHash || '').trim().toLowerCase()
25
24
  if (!isHex64(target)) return false
26
- await ensureUserRoom({ replicaUsername: username })
27
25
 
28
26
  // 已直连 peer 不经 trust-graph scope 也应能收发 node scope action(非成员 CAS chunk / follow hint 等)
29
- if (await sendToNodeLink(target, { scope: 'node', action: String(actionName), payload }))
27
+ if (await sendToNodeLink(target, { scope: 'node', action: actionName, payload }))
30
28
  return true
31
29
 
32
30
  const merged = graph ?? await buildMergedGraph(username)
@@ -73,7 +71,6 @@ export async function sendToNode(username, targetNodeHash, actionName, payload,
73
71
  * @returns {Promise<number>} 发送次数
74
72
  */
75
73
  export async function fanoutToTopNodes(username, actionName, payload, limit) {
76
- await ensureUserRoom({ replicaUsername: username })
77
74
  const graph = await buildMergedGraph(username)
78
75
  const k = limit ?? resolveFederationFanoutTopK(graph.size, trustGraphTunables)
79
76
  const rep = loadReputation()
@@ -34,11 +34,11 @@ function releaseMutex(lockKey, state) {
34
34
 
35
35
  /**
36
36
  * @param {string} key 锁键
37
- * @param {() => Promise<T>} fn 临界区
38
- * @returns {Promise<T>} `fn` 的解析结果
37
+ * @param {() => Promise<T>} criticalSection 临界区
38
+ * @returns {Promise<T>} `criticalSection` 的解析结果
39
39
  * @template T
40
40
  */
41
- export async function withAsyncMutex(key, fn) {
41
+ export async function withAsyncMutex(key, criticalSection) {
42
42
  const lockKey = String(key)
43
43
  const state = mutexState(lockKey)
44
44
  return new Promise((resolve, reject) => {
@@ -47,7 +47,7 @@ export async function withAsyncMutex(key, fn) {
47
47
  */
48
48
  const run = () => {
49
49
  Promise.resolve()
50
- .then(fn)
50
+ .then(criticalSection)
51
51
  .then(resolve, reject)
52
52
  .finally(() => releaseMutex(lockKey, state))
53
53
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 调用一组 listener,单次抛错不影响其余。
3
+ * @param {Iterable<Function>} listeners 回调集合
4
+ * @param {...unknown} listenerArguments 传给每个 listener 的参数
5
+ * @returns {void}
6
+ */
7
+ export function emitSafe(listeners, ...listenerArguments) {
8
+ for (const listener of listeners)
9
+ try { listener(...listenerArguments) }
10
+ catch { /* ignore */ }
11
+ }
package/utils/json_io.mjs CHANGED
@@ -11,9 +11,9 @@ export async function readJsonFile(filePath) {
11
11
  const raw = await fsp.readFile(filePath, 'utf8')
12
12
  return JSON.parse(raw)
13
13
  }
14
- catch (err) {
15
- if (/** @type {NodeJS.ErrnoException} */ err.code === 'ENOENT') return null
16
- throw err
14
+ catch (error) {
15
+ if (/** @type {NodeJS.ErrnoException} */ error.code === 'ENOENT') return null
16
+ throw error
17
17
  }
18
18
  }
19
19
 
@@ -24,9 +24,9 @@ export async function readJsonFile(filePath) {
24
24
  */
25
25
  export async function writeJsonFile(filePath, data) {
26
26
  await fsp.mkdir(path.dirname(filePath), { recursive: true })
27
- const tmp = `${filePath}.tmp`
28
- await fsp.writeFile(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
29
- await fsp.rename(tmp, filePath)
27
+ const temporaryPath = `${filePath}.tmp`
28
+ await fsp.writeFile(temporaryPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
29
+ await fsp.rename(temporaryPath, filePath)
30
30
  }
31
31
 
32
32
  /**
@@ -38,9 +38,9 @@ export function readJsonFileSync(filePath) {
38
38
  const raw = fs.readFileSync(filePath, 'utf8')
39
39
  return JSON.parse(raw)
40
40
  }
41
- catch (err) {
42
- if (/** @type {NodeJS.ErrnoException} */ err.code === 'ENOENT') return null
43
- throw err
41
+ catch (error) {
42
+ if (/** @type {NodeJS.ErrnoException} */ error.code === 'ENOENT') return null
43
+ throw error
44
44
  }
45
45
  }
46
46
 
@@ -51,7 +51,7 @@ export function readJsonFileSync(filePath) {
51
51
  */
52
52
  export function writeJsonFileSync(filePath, data) {
53
53
  fs.mkdirSync(path.dirname(filePath), { recursive: true })
54
- const tmp = `${filePath}.tmp`
55
- fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
56
- fs.renameSync(tmp, filePath)
54
+ const temporaryPath = `${filePath}.tmp`
55
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
56
+ fs.renameSync(temporaryPath, filePath)
57
57
  }
@@ -2,11 +2,11 @@
2
2
  * 有限并发 map(进程内,无外部依赖)。
3
3
  * @template T, R
4
4
  * @param {T[]} items 待处理项
5
- * @param {(item: T, index: number) => Promise<R>} fn 异步映射
5
+ * @param {(item: T, index: number) => Promise<R>} mapper 异步映射
6
6
  * @param {number} concurrency 并发上限
7
7
  * @returns {Promise<R[]>} 与 items 同序的结果
8
8
  */
9
- export async function mapPool(items, fn, concurrency) {
9
+ export async function mapPool(items, mapper, concurrency) {
10
10
  if (!items.length) return []
11
11
  const limit = Math.max(1, Math.min(concurrency, items.length))
12
12
  /** @type {R[]} */
@@ -16,9 +16,9 @@ export async function mapPool(items, fn, concurrency) {
16
16
  /** @returns {Promise<void>} */
17
17
  const worker = async () => {
18
18
  for (; ;) {
19
- const i = nextIndex++
20
- if (i >= items.length) return
21
- results[i] = await fn(items[i], i)
19
+ const index = nextIndex++
20
+ if (index >= items.length) return
21
+ results[index] = await mapper(items[index], index)
22
22
  }
23
23
  }
24
24
 
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Fisher–Yates 原地洗牌。
3
+ * @template T
4
+ * @param {T[]} arr 待洗牌数组(原地修改)
5
+ * @returns {T[]} 同一数组引用
6
+ */
7
+ export function shuffleInPlace(arr) {
8
+ for (let i = arr.length - 1; i > 0; i--) {
9
+ const j = Math.floor(Math.random() * (i + 1))
10
+ ;[arr[i], arr[j]] = [arr[j], arr[i]]
11
+ }
12
+ return arr
13
+ }
package/utils/ttl_map.mjs CHANGED
@@ -1,21 +1,46 @@
1
1
  /**
2
- * 带 TTL 的 Map:get 时惰性过期删除。
2
+ * 带 TTL 的 Map:get 时惰性过期;set 时在超上限下先清过期再 LRU 驱逐。
3
3
  * @template T
4
4
  * @param {number} ttlMs 存活毫秒
5
- * @returns {{ ttlMs: number, set: (key: string, value: T) => void, get: (key: string, now?: number) => T | null, clear: () => void }} TTL Map 句柄
5
+ * @param {number} [maxSize=4096] 最大条目(防只写不读时过期项堆积)
6
+ * @returns {{ ttlMs: number, maxSize: number, size: () => number, set: (key: string, value: T) => void, get: (key: string, now?: number) => T | null, clear: () => void }} TTL Map 句柄
6
7
  */
7
- export function createTtlMap(ttlMs) {
8
+ export function createTtlMap(ttlMs, maxSize = 4096) {
9
+ const cap = Math.max(1, Math.floor(Number(maxSize) || 4096))
8
10
  /** @type {Map<string, { value: T, seenAt: number }>} */
9
11
  const map = new Map()
12
+
13
+ /**
14
+ * 删除过期项;若仍满则按插入序驱逐最旧。
15
+ * @param {number} now 当前时间
16
+ * @returns {void}
17
+ */
18
+ function prune(now) {
19
+ for (const [key, entry] of map)
20
+ if (now - entry.seenAt > ttlMs) map.delete(key)
21
+ while (map.size >= cap) {
22
+ const oldest = map.keys().next().value
23
+ map.delete(oldest)
24
+ }
25
+ }
26
+
10
27
  return {
11
28
  ttlMs,
29
+ maxSize: cap,
30
+ /**
31
+ * @returns {number} 当前条目数(含未 get 的过期项)
32
+ */
33
+ size: () => map.size,
12
34
  /**
13
35
  * @param {string} key 键
14
36
  * @param {T} value 值
15
37
  * @returns {void}
16
38
  */
17
39
  set(key, value) {
18
- map.set(key, { value, seenAt: Date.now() })
40
+ const now = Date.now()
41
+ if (map.has(key)) map.delete(key)
42
+ if (map.size >= cap) prune(now)
43
+ map.set(key, { value, seenAt: now })
19
44
  },
20
45
  /**
21
46
  * @param {string} key 键
package/wire/ingress.mjs CHANGED
@@ -19,7 +19,7 @@ export function isPlainObject(value) {
19
19
  * @returns {Record<string, unknown> | null} 解析失败或非对象时为 null
20
20
  */
21
21
  export function parseInboundJson(raw) {
22
- if (raw == null) return null
22
+ if (!raw) return null
23
23
  const text = typeof raw === 'string'
24
24
  ? raw
25
25
  : Buffer.isBuffer(raw)