@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
@@ -80,9 +80,9 @@ export function createFsEntityStore(baseDir) {
80
80
  const entries = await fsp.readdir(root, { withFileTypes: true })
81
81
  return entries.filter(e => e.isDirectory()).map(e => e.name.toLowerCase()).filter(h => parseEntityHash(h))
82
82
  }
83
- catch (err) {
84
- if (/** @type {NodeJS.ErrnoException} */ err.code === 'ENOENT') return []
85
- throw err
83
+ catch (error) {
84
+ if (/** @type {NodeJS.ErrnoException} */ error.code === 'ENOENT') return []
85
+ throw error
86
86
  }
87
87
  },
88
88
 
@@ -115,9 +115,9 @@ export function createFsEntityStore(baseDir) {
115
115
  try {
116
116
  return await fsp.readFile(filePath)
117
117
  }
118
- catch (err) {
119
- if (/** @type {NodeJS.ErrnoException} */ err.code === 'ENOENT') return null
120
- throw err
118
+ catch (error) {
119
+ if (/** @type {NodeJS.ErrnoException} */ error.code === 'ENOENT') return null
120
+ throw error
121
121
  }
122
122
  },
123
123
 
package/node/identity.mjs CHANGED
@@ -66,9 +66,9 @@ export function getNodeHash() {
66
66
  */
67
67
  export function getNodeTransportSettings() {
68
68
  const data = loadNodeFile()
69
- const relayUrls = Array.isArray(data.relayUrls)
70
- ? data.relayUrls.map(url => String(url).trim()).filter(url => url.startsWith('wss://'))
71
- : []
69
+ const relayUrls = (data.relayUrls || [])
70
+ .map(url => url.trim())
71
+ .filter(url => url.startsWith('wss://'))
72
72
  const batterySaver = !!data.batterySaver
73
73
  const mailbox = normalizeMailboxSettings(data.mailbox || {})
74
74
  return { relayUrls, batterySaver, mailbox }
@@ -82,7 +82,7 @@ export function saveNodeTransportSettings(patch) {
82
82
  const data = loadNodeFile()
83
83
  if (patch.batterySaver != null) data.batterySaver = !!patch.batterySaver
84
84
  if (patch.relayUrls)
85
- data.relayUrls = patch.relayUrls.map(url => String(url).trim()).filter(url => url.startsWith('wss://'))
85
+ data.relayUrls = patch.relayUrls.map(url => url.trim()).filter(url => url.startsWith('wss://'))
86
86
  if (patch.mailbox)
87
87
  data.mailbox = normalizeMailboxSettings({ ...data.mailbox, ...patch.mailbox })
88
88
  saveNodeFile(data)
package/node/instance.mjs CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path'
3
3
  import { createFsEntityStore } from './entity_store.mjs'
4
4
  import { defaultSignalingRuntimeConfig, resolveSignalingRuntimeConfig } from './signaling_config.mjs'
5
5
 
6
- /** @typedef {{ warn?: (...args: unknown[]) => void, error?: (...args: unknown[]) => void }} NodeLogger */
6
+ /** @typedef {{ warn?: (...args: unknown[]) => void, error?: (...args: unknown[]) => void, info?: (...args: unknown[]) => void, log?: (...args: unknown[]) => void }} NodeLogger */
7
7
 
8
8
  /** @typedef {import('./signaling_config.mjs').SignalingRuntimeConfig} SignalingRuntimeConfig */
9
9
 
@@ -11,7 +11,7 @@ import { defaultSignalingRuntimeConfig, resolveSignalingRuntimeConfig } from './
11
11
  * @typedef {{
12
12
  * nodeDir: string
13
13
  * entityStore: import('./entity_store.mjs').EntityStore
14
- * logger: NodeLogger
14
+ * logger: NodeLogger | null
15
15
  * signaling: SignalingRuntimeConfig
16
16
  * }} NodeRuntime
17
17
  */
@@ -22,37 +22,28 @@ let runtime = null
22
22
  /** @type {Set<(event: string, payload?: unknown) => void>} */
23
23
  const changeListeners = new Set()
24
24
 
25
- /** @type {NodeLogger} */
26
- const noopLogger = {
27
- /**
28
- *
29
- */
30
- warn() { },
31
- /**
32
- *
33
- */
34
- error() { },
35
- }
36
-
37
25
  /**
38
- * @param {{ nodeDir: string, entityStore?: import('./entity_store.mjs').EntityStore, logger?: NodeLogger, signaling?: SignalingRuntimeConfig }} options 节点目录与可选注入
39
- * @returns {NodeRuntime} 单节点运行时句柄
26
+ * @param {{ nodeDir: string, entityStore?: import('./entity_store.mjs').EntityStore }} options - 节点目录与可选 entity store
27
+ * @returns {NodeRuntime} 初始化后的运行时
40
28
  */
41
29
  export function initNode(options) {
30
+ if (runtime) throw new Error('p2p: initNode already called — use setNodeLogger / setSignalingRuntimeConfig or resetNodeForTests')
31
+ if (options?.logger !== undefined || options?.signaling !== undefined)
32
+ throw new Error('p2p: initNode only accepts nodeDir/entityStore — use setNodeLogger / setSignalingRuntimeConfig')
42
33
  const nodeDir = path.resolve(String(options.nodeDir || '').trim())
43
34
  if (!nodeDir) throw new Error('p2p: initNode requires nodeDir')
44
35
  const entityStore = options.entityStore ?? createFsEntityStore(path.join(nodeDir, 'entities'))
45
36
  runtime = {
46
37
  nodeDir,
47
38
  entityStore,
48
- logger: options.logger ?? console,
49
- signaling: options.signaling ?? resolveSignalingRuntimeConfig(),
39
+ logger: console,
40
+ signaling: resolveSignalingRuntimeConfig(),
50
41
  }
51
42
  return runtime
52
43
  }
53
44
 
54
45
  /**
55
- * @returns {NodeRuntime} 已初始化的节点运行时
46
+ * @returns {NodeRuntime} 当前节点运行时
56
47
  */
57
48
  export function getNode() {
58
49
  if (!runtime) throw new Error('p2p: node not initialized — call initNode() first')
@@ -67,7 +58,26 @@ export function isNodeInitialized() {
67
58
  }
68
59
 
69
60
  /**
70
- * @returns {SignalingRuntimeConfig} 信令传输策略(未 init 时返回生产默认,不读测试 env)
61
+ * @param {NodeLogger | null} logger - 节点日志器,null 表示静默
62
+ * @returns {void}
63
+ */
64
+ export function setNodeLogger(logger) {
65
+ if (!runtime) throw new Error('p2p: setNodeLogger requires initNode')
66
+ runtime.logger = logger ?? null
67
+ }
68
+
69
+ /**
70
+ * @param {Partial<SignalingRuntimeConfig>} config - signaling 运行时补丁
71
+ * @returns {void}
72
+ */
73
+ export function setSignalingRuntimeConfig(config) {
74
+ if (!runtime) throw new Error('p2p: setSignalingRuntimeConfig requires initNode')
75
+ runtime.signaling = resolveSignalingRuntimeConfig({ ...runtime.signaling, ...config })
76
+ emitNodeChange('signaling-changed', runtime.signaling)
77
+ }
78
+
79
+ /**
80
+ * @returns {SignalingRuntimeConfig} 当前 signaling 配置
71
81
  */
72
82
  export function getSignalingRuntimeConfig() {
73
83
  return runtime?.signaling ?? defaultSignalingRuntimeConfig()
@@ -81,22 +91,22 @@ export function getNodeDir() {
81
91
  }
82
92
 
83
93
  /**
84
- * @returns {import('./entity_store.mjs').EntityStore} 注入的 EntityStore
94
+ * @returns {import('./entity_store.mjs').EntityStore} 当前 entity store
85
95
  */
86
96
  export function getEntityStore() {
87
97
  return getNode().entityStore
88
98
  }
89
99
 
90
100
  /**
91
- * @returns {NodeLogger} 节点日志器(未 init 时为 no-op)
101
+ * @returns {NodeLogger | null} 当前节点日志器
92
102
  */
93
103
  export function getNodeLogger() {
94
- return runtime?.logger ?? noopLogger
104
+ return runtime?.logger ?? null
95
105
  }
96
106
 
97
107
  /**
98
- * @param {string} event 事件名
99
- * @param {unknown} [payload] 载荷
108
+ * @param {string} event - 变更事件名
109
+ * @param {unknown} [payload] - 事件载荷
100
110
  * @returns {void}
101
111
  */
102
112
  export function emitNodeChange(event, payload) {
@@ -106,10 +116,19 @@ export function emitNodeChange(event, payload) {
106
116
  }
107
117
 
108
118
  /**
109
- * @param {(event: string, payload?: unknown) => void} listener 回调
110
- * @returns {() => void} 取消订阅
119
+ * @param {(event: string, payload?: unknown) => void} listener - 变更回调
120
+ * @returns {() => void} 取消监听的 dispose
111
121
  */
112
122
  export function onNodeChange(listener) {
113
123
  changeListeners.add(listener)
114
124
  return () => changeListeners.delete(listener)
115
125
  }
126
+
127
+ /**
128
+ * 测试专用:重置节点运行时。
129
+ * @returns {void}
130
+ */
131
+ export function resetNodeForTests() {
132
+ runtime = null
133
+ changeListeners.clear()
134
+ }
@@ -0,0 +1,26 @@
1
+ /** 本机落盘数据 revision:信任图等订阅方只读,store 不反向依赖 trust_graph。 */
2
+
3
+ let revision = 0
4
+
5
+ /**
6
+ * 本机数据 revision +1(信任图等订阅方)。
7
+ * @returns {void}
8
+ */
9
+ export function bumpLocalDataRevision() {
10
+ revision++
11
+ }
12
+
13
+ /**
14
+ * @returns {number} 当前本机数据 revision
15
+ */
16
+ export function getLocalDataRevision() {
17
+ return revision
18
+ }
19
+
20
+ /**
21
+ * 测试用:清零 revision。
22
+ * @returns {void}
23
+ */
24
+ export function resetLocalDataRevisionForTests() {
25
+ revision = 0
26
+ }
package/node/log.mjs ADDED
@@ -0,0 +1,56 @@
1
+ import { getNodeLogger } from './instance.mjs'
2
+
3
+ /**
4
+ * 连接/发现诊断日志开关。
5
+ * CLI(非 --quiet)默认打开;非 CLI 用 `setConnectivityDebug(true)`。
6
+ * @type {boolean}
7
+ */
8
+ let connectivityDebug = false
9
+
10
+ /**
11
+ * @param {boolean} enabled 是否输出连接诊断
12
+ * @returns {void}
13
+ */
14
+ export function setConnectivityDebug(enabled) {
15
+ connectivityDebug = !!enabled
16
+ }
17
+
18
+ /**
19
+ * @returns {boolean} 当前是否输出连接诊断
20
+ */
21
+ export function isConnectivityDebug() {
22
+ return connectivityDebug
23
+ }
24
+
25
+ /**
26
+ * 缩短 nodeHash 便于对照两边日志。
27
+ * @param {string | null | undefined} hash 完整 hash
28
+ * @param {number} [n=8] 前缀长度
29
+ * @returns {string} 短 hash
30
+ */
31
+ export function shortHash(hash, n = 8) {
32
+ const value = String(hash || '')
33
+ return value.length <= n ? value : value.slice(0, n)
34
+ }
35
+
36
+ /**
37
+ * 连接诊断 info(未开启或无 logger 时静默)。
38
+ * @param {string} message 消息
39
+ * @param {object} [extra] 附加字段
40
+ * @returns {void}
41
+ */
42
+ export function nodeDebug(message, extra) {
43
+ if (!connectivityDebug) return
44
+ const logger = getNodeLogger()
45
+ if (!logger?.info) return
46
+ if (extra === undefined) logger.info(message)
47
+ else logger.info(message, extra)
48
+ }
49
+
50
+ /**
51
+ * 测试专用:关掉连接诊断。
52
+ * @returns {void}
53
+ */
54
+ export function resetConnectivityDebugForTests() {
55
+ connectivityDebug = false
56
+ }
package/node/network.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { isEntityHash128 } from '../core/entity_id.mjs'
2
2
  import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
3
- import { invalidateTrustGraphCache } from '../trust_graph/cache.mjs'
4
3
 
5
4
  import { loadDenylist } from './denylist.mjs'
6
5
  import { getNodeDir, isNodeInitialized } from './instance.mjs'
6
+ import { bumpLocalDataRevision } from './local_data_revision.mjs'
7
7
  import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
8
8
 
9
9
 
@@ -28,6 +28,7 @@ import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
28
28
 
29
29
  const DATA_NAME = 'network'
30
30
  const MAX_EXPLORE = 500
31
+ const MAX_TRUSTED = 64
31
32
  const MAX_HINTS = 256
32
33
  const MAX_HINTS_PER_SOURCE = 12
33
34
  const DEFAULT_EXPLORE_TTL_MS = 7 * 24 * 60 * 60 * 1000
@@ -52,18 +53,18 @@ export function normalizeNetwork(raw) {
52
53
  * @returns {string[]} 去重 nodeHash 列表
53
54
  */
54
55
  const pickIds = key => [...new Set(
55
- (Array.isArray(file[key]) ? file[key] : [])
56
- .map(id => normalizeHex64(id) || String(id).trim())
56
+ (file[key] || [])
57
+ .map(id => normalizeHex64(id) || id.trim())
57
58
  .filter(id => isHex64(id)),
58
59
  )]
59
- const hints = (Array.isArray(file.hints) ? file.hints : [])
60
+ const hints = (file.hints || [])
60
61
  .map(hint => ({
61
- nodeHash: normalizeHex64(hint?.nodeHash) || '',
62
- source: String(hint?.source || '').trim(),
63
- kind: String(hint?.kind || '').trim(),
64
- weight: Number.isFinite(Number(hint?.weight)) ? Number(hint.weight) : 0.1,
65
- expiresAt: Number(hint?.expiresAt) || 0,
66
- ...hint?.groupId ? { groupId: String(hint.groupId).trim() } : {},
62
+ nodeHash: normalizeHex64(hint.nodeHash) || '',
63
+ source: hint.source?.trim() || '',
64
+ kind: hint.kind?.trim() || '',
65
+ weight: Number.isFinite(Number(hint.weight)) ? Number(hint.weight) : 0.1,
66
+ expiresAt: Number(hint.expiresAt) || 0,
67
+ ...hint.groupId ? { groupId: hint.groupId.trim() } : {},
67
68
  }))
68
69
  .filter(hint => isHex64(hint.nodeHash))
69
70
  return {
@@ -97,10 +98,10 @@ export function capHintsBySource(hints, maxPerSource = MAX_HINTS_PER_SOURCE) {
97
98
  const counts = new Map()
98
99
  const out = []
99
100
  for (const hint of [...hints].reverse()) {
100
- const src = String(hint.source || 'unknown')
101
- const n = counts.get(src) ?? 0
101
+ const source = String(hint.source || 'unknown')
102
+ const n = counts.get(source) ?? 0
102
103
  if (n >= maxPerSource) continue
103
- counts.set(src, n + 1)
104
+ counts.set(source, n + 1)
104
105
  out.unshift(hint)
105
106
  }
106
107
  return out
@@ -115,10 +116,11 @@ export function saveNetwork(data) {
115
116
  const now = Date.now()
116
117
  clean.hints = capHintsBySource(clean.hints.filter(h => !h.expiresAt || h.expiresAt > now)).slice(-MAX_HINTS)
117
118
  clean.explorePeers = clean.explorePeers.slice(-MAX_EXPLORE)
119
+ clean.trustedPeers = clean.trustedPeers.slice(-MAX_TRUSTED)
118
120
  writeNodeJsonSync(DATA_NAME, clean)
119
121
  networkCache = clean
120
122
  networkCacheNodeDir = isNodeInitialized() ? getNodeDir() : ''
121
- invalidateTrustGraphCache()
123
+ bumpLocalDataRevision()
122
124
  }
123
125
 
124
126
  /**
@@ -196,6 +198,36 @@ export function mergeNetworkPeerPools(patch = {}) {
196
198
  saveNetwork(net)
197
199
  }
198
200
 
201
+ /**
202
+ * 稳定探索对端升入熟人池(从 explore 移除并追加 trusted)。
203
+ * @param {string} nodeHash 对端 nodeHash
204
+ * @returns {void}
205
+ */
206
+ export function promoteExplorePeer(nodeHash) {
207
+ const net = loadNetwork()
208
+ const id = normalizeHex64(nodeHash)
209
+ if (!isHex64(id)) return
210
+ net.explorePeers = net.explorePeers.filter(peer => peer !== id)
211
+ if (!net.trustedPeers.includes(id)) net.trustedPeers.push(id)
212
+ net.lastRosterAt = Date.now()
213
+ saveNetwork(net)
214
+ }
215
+
216
+ /**
217
+ * 整表替换 trusted/explore 池(可缩池)。
218
+ * @param {{ trustedPeers?: string[], explorePeers?: string[] }} pools - 要替换的 peer 池
219
+ * @returns {void}
220
+ */
221
+ export function replaceNetworkPeerPools(pools = {}) {
222
+ const net = loadNetwork()
223
+ if (Array.isArray(pools.trustedPeers))
224
+ net.trustedPeers = pools.trustedPeers.map(id => normalizeHex64(id)).filter(id => isHex64(id))
225
+ if (Array.isArray(pools.explorePeers))
226
+ net.explorePeers = pools.explorePeers.map(id => normalizeHex64(id)).filter(id => isHex64(id))
227
+ net.lastRosterAt = Date.now()
228
+ saveNetwork(net)
229
+ }
230
+
199
231
  /**
200
232
  * @param {string} groupId 群 scope
201
233
  * @param {'node' | 'subject' | 'entity'} scope denylist 作用域
@@ -18,10 +18,10 @@ import {
18
18
  } from '../reputation/engine.mjs'
19
19
  import { pickNodeScoreFromReputation } from '../reputation/pick_score.mjs'
20
20
  import reputationTunables from '../reputation/tunables.json' with { type: 'json' }
21
- import { invalidateTrustGraphCache } from '../trust_graph/cache.mjs'
22
21
  import { withAsyncMutex } from '../utils/async_mutex.mjs'
23
22
 
24
23
  import { getNodeDir, getNodeLogger, isNodeInitialized } from './instance.mjs'
24
+ import { bumpLocalDataRevision } from './local_data_revision.mjs'
25
25
  import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
26
26
 
27
27
  const DATA_NAME = 'reputation'
@@ -88,7 +88,7 @@ export function saveReputation(data) {
88
88
  pruneReputationFile(data)
89
89
  writeNodeJsonSync(DATA_NAME, data)
90
90
  reputationCache = data
91
- invalidateTrustGraphCache()
91
+ bumpLocalDataRevision()
92
92
  }
93
93
 
94
94
  /**
@@ -211,7 +211,7 @@ export async function applyVolatileSlashAlert(alert) {
211
211
  * @param {object} [groupSettings] 群设置
212
212
  * @returns {object} VOLATILE 载荷
213
213
  */
214
- export function buildAndApplyUnverifiedSlashAlert(senderPubKeyHash, content, groupSettings = {}) {
214
+ export function buildUnverifiedSlashAlert(senderPubKeyHash, content, groupSettings = {}) {
215
215
  const targetPubKeyHash = assertHex64(content.targetPubKeyHash, 'slash target')
216
216
  const claim = Number.isFinite(Number(content.claim)) ? Number(content.claim) : reputationTunables.slashDefaultClaim
217
217
  const sender = assertHex64(senderPubKeyHash, 'slash sender')
@@ -268,7 +268,7 @@ export function applyDecayCollusionAfterSlash(targetPubKeyHash, inviteEdges) {
268
268
  void mutateReputation(data => {
269
269
  const applied = applyDecayCollusionAfterSlashPure(data, targetPubKeyHash, inviteEdges)
270
270
  if (applied.length)
271
- getNodeLogger().warn?.('reputation: collusion decay after slash', {
271
+ getNodeLogger()?.warn?.('reputation: collusion decay after slash', {
272
272
  target: targetPubKeyHash.trim().toLowerCase(),
273
273
  upstreamCount: applied.length,
274
274
  hops: applied.map(row => row.hop),