@steve02081504/fount-p2p 0.0.0

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 (154) hide show
  1. package/README.md +62 -0
  2. package/core/bytes_codec.mjs +51 -0
  3. package/core/canonical_json.mjs +21 -0
  4. package/core/composite_key.mjs +91 -0
  5. package/core/constants.mjs +53 -0
  6. package/core/entity_id.mjs +53 -0
  7. package/core/entity_id_parse.mjs +40 -0
  8. package/core/evfs_logical_path.mjs +15 -0
  9. package/core/hexIds.mjs +73 -0
  10. package/core/hlc.mjs +114 -0
  11. package/core/random_id.mjs +9 -0
  12. package/crypto/channel.mjs +101 -0
  13. package/crypto/checkpoint_sign.mjs +67 -0
  14. package/crypto/crypto.mjs +158 -0
  15. package/crypto/key.mjs +431 -0
  16. package/dag/canonicalize_row.mjs +65 -0
  17. package/dag/event_query.mjs +15 -0
  18. package/dag/index.mjs +335 -0
  19. package/dag/storage.mjs +333 -0
  20. package/dag/strip_extensions.mjs +18 -0
  21. package/dag/tunables.json +6 -0
  22. package/discovery/bt.mjs +260 -0
  23. package/discovery/index.mjs +134 -0
  24. package/discovery/mdns.mjs +138 -0
  25. package/discovery/nostr.mjs +273 -0
  26. package/entity/files/acl.mjs +38 -0
  27. package/entity/files/evfs.mjs +189 -0
  28. package/entity/files/evfs_ref.mjs +37 -0
  29. package/entity/files/manifest_acl_registry.mjs +93 -0
  30. package/entity/files/replica_host_cache.mjs +46 -0
  31. package/entity/files/url.mjs +19 -0
  32. package/entity/hosting_registry.mjs +47 -0
  33. package/entity/localized_core.mjs +141 -0
  34. package/entity/logical_entity.mjs +25 -0
  35. package/entity/logical_entity_id_registry.mjs +40 -0
  36. package/entity/node_hash.mjs +15 -0
  37. package/entity/presentation_registry.mjs +44 -0
  38. package/entity/profile.mjs +256 -0
  39. package/entity/replica.mjs +20 -0
  40. package/entity/session_snapshot_registry.mjs +38 -0
  41. package/federation/chunk_fetch_pending.mjs +128 -0
  42. package/federation/chunk_fetch_scheduler.mjs +97 -0
  43. package/federation/dag_order_cache.mjs +124 -0
  44. package/federation/dedupe_slot.mjs +33 -0
  45. package/federation/message_rate_limit.mjs +23 -0
  46. package/federation/operator_key_chain.mjs +200 -0
  47. package/federation/topo_order_memo.mjs +44 -0
  48. package/federation/volatile_streams.mjs +52 -0
  49. package/federation/want_ids.mjs +158 -0
  50. package/files/assemble.mjs +220 -0
  51. package/files/assemble_stream.mjs +129 -0
  52. package/files/chunk_fetch.mjs +113 -0
  53. package/files/chunk_fetch_verify.mjs +23 -0
  54. package/files/chunk_provider_registry.mjs +70 -0
  55. package/files/chunk_responder.mjs +79 -0
  56. package/files/chunk_store.mjs +87 -0
  57. package/files/manifest.mjs +99 -0
  58. package/files/transfer_key.mjs +87 -0
  59. package/files/transfer_key_registry.mjs +108 -0
  60. package/governance/branch.mjs +230 -0
  61. package/governance/join_pow.mjs +165 -0
  62. package/governance/owner_succession_ballot.mjs +57 -0
  63. package/governance/tunables.json +7 -0
  64. package/index.mjs +39 -0
  65. package/link/channel_mux.mjs +222 -0
  66. package/link/frame.mjs +246 -0
  67. package/link/handshake.mjs +175 -0
  68. package/link/link.mjs +617 -0
  69. package/link/rtc.mjs +109 -0
  70. package/link/sdp_fingerprint.mjs +25 -0
  71. package/mailbox/consumer_registry.mjs +71 -0
  72. package/mailbox/deliver_or_store.mjs +190 -0
  73. package/mailbox/importance.mjs +72 -0
  74. package/mailbox/parse.mjs +73 -0
  75. package/mailbox/prune.mjs +106 -0
  76. package/mailbox/rate.mjs +86 -0
  77. package/mailbox/settings.mjs +53 -0
  78. package/mailbox/store.mjs +256 -0
  79. package/mailbox/tunables.json +12 -0
  80. package/mailbox/wire.mjs +40 -0
  81. package/node/denylist.mjs +309 -0
  82. package/node/entity_store.mjs +214 -0
  83. package/node/identity.mjs +99 -0
  84. package/node/instance.mjs +115 -0
  85. package/node/network.mjs +306 -0
  86. package/node/personal_block.mjs +286 -0
  87. package/node/reputation_store.mjs +326 -0
  88. package/node/retention_policy.mjs +82 -0
  89. package/node/signaling_config.mjs +32 -0
  90. package/node/storage.mjs +30 -0
  91. package/node/storage_plugins.mjs +71 -0
  92. package/node/user_paths.mjs +10 -0
  93. package/overlay/index.mjs +191 -0
  94. package/package.json +73 -0
  95. package/permissions/bitmask.mjs +35 -0
  96. package/permissions/evaluator.mjs +90 -0
  97. package/permissions/index.mjs +12 -0
  98. package/permissions/layered.mjs +39 -0
  99. package/registries/action.mjs +94 -0
  100. package/registries/event_type.mjs +75 -0
  101. package/registries/inbound.mjs +90 -0
  102. package/registries/p2p_viewer.mjs +35 -0
  103. package/registries/part_path.mjs +35 -0
  104. package/registries/room_provider.mjs +47 -0
  105. package/reputation/engine.mjs +458 -0
  106. package/reputation/math.mjs +73 -0
  107. package/reputation/pick_score.mjs +11 -0
  108. package/reputation/relay_dedupe.mjs +22 -0
  109. package/reputation/tunables.json +36 -0
  110. package/rooms/scoped_link.mjs +185 -0
  111. package/schemas/discovery.mjs +110 -0
  112. package/schemas/federation_pull.mjs +102 -0
  113. package/schemas/mailbox.mjs +37 -0
  114. package/schemas/remote_event.mjs +17 -0
  115. package/timeline/append_core.mjs +54 -0
  116. package/timeline/materialize_runner.mjs +26 -0
  117. package/timeline/prune.mjs +39 -0
  118. package/timeline/retention.mjs +54 -0
  119. package/timeline/retention_runner.mjs +35 -0
  120. package/timeline/verify_remote.mjs +25 -0
  121. package/transport/group_link_set.mjs +324 -0
  122. package/transport/ice_servers.mjs +72 -0
  123. package/transport/link_registry.mjs +749 -0
  124. package/transport/peer_identity_maps.mjs +94 -0
  125. package/transport/peer_pool.mjs +355 -0
  126. package/transport/remote_user_room.mjs +109 -0
  127. package/transport/room_scopes.mjs +2 -0
  128. package/transport/room_wire_action.mjs +19 -0
  129. package/transport/rtc_connection_budget.mjs +180 -0
  130. package/transport/rtc_mdns_filter.mjs +118 -0
  131. package/transport/stale_peer_log.mjs +50 -0
  132. package/transport/user_room.mjs +263 -0
  133. package/trust_graph/build.mjs +99 -0
  134. package/trust_graph/cache.mjs +44 -0
  135. package/trust_graph/engine.mjs +184 -0
  136. package/trust_graph/registry.mjs +38 -0
  137. package/trust_graph/resolve.mjs +105 -0
  138. package/trust_graph/send.mjs +88 -0
  139. package/trust_graph/tunables.json +13 -0
  140. package/utils/async_mutex.mjs +69 -0
  141. package/utils/debug_log.mjs +16 -0
  142. package/utils/duration.mjs +28 -0
  143. package/utils/json_io.mjs +57 -0
  144. package/utils/lru.mjs +22 -0
  145. package/utils/map_pool.mjs +27 -0
  146. package/wire/group_part.mjs +43 -0
  147. package/wire/ingress.mjs +58 -0
  148. package/wire/part_common.mjs +55 -0
  149. package/wire/part_fanout.mjs +71 -0
  150. package/wire/part_ingress.mjs +172 -0
  151. package/wire/part_invoke.mjs +81 -0
  152. package/wire/rate_bucket.mjs +49 -0
  153. package/wire/volatile_signature.mjs +76 -0
  154. package/wire/wait.mjs +127 -0
@@ -0,0 +1,99 @@
1
+ import { isPeerKeyBlocked, isSubjectBlocked } from '../node/denylist.mjs'
2
+ import { loadNetwork } from '../node/network.mjs'
3
+ import { loadReputation } from '../node/reputation_store.mjs'
4
+ import { listFederationRoomSlots } from '../registries/room_provider.mjs'
5
+ import { isQuarantinedPure } from '../reputation/engine.mjs'
6
+ import { ensureUserRoom } from '../transport/user_room.mjs'
7
+
8
+ import { getCachedTrustGraph } from './cache.mjs'
9
+ import { mergeGraph, pickTopFromGraph } from './engine.mjs'
10
+ import trustGraphTunables from './tunables.json' with { type: 'json' }
11
+
12
+ /**
13
+ * @typedef {import('./engine.mjs').TrustNode} TrustNode
14
+ */
15
+
16
+ /**
17
+ * @param {string} nodeHash 64 位十六进制
18
+ * @returns {boolean} 是否拉黑
19
+ */
20
+ function isNodeBlocked(nodeHash) {
21
+ return isPeerKeyBlocked('', nodeHash) || isSubjectBlocked({ nodeHash })
22
+ }
23
+
24
+ /**
25
+ * @param {string} username 副本用户名 登录名(联邦房间枚举仍按用户)
26
+ * @returns {Promise<Map<string, TrustNode>>} nodeHash → 节点
27
+ */
28
+ export async function buildMergedGraph(username) {
29
+ return getCachedTrustGraph(username, async () => {
30
+ const net = loadNetwork()
31
+ const rep = loadReputation()
32
+ const blocked = new Set()
33
+ const quarantined = new Set()
34
+ for (const nodeHash of [...net.trustedPeers, ...net.explorePeers, ...net.hints.map(h => h.nodeHash)]) {
35
+ if (isNodeBlocked(nodeHash)) blocked.add(nodeHash)
36
+ if (isQuarantinedPure(rep, nodeHash)) quarantined.add(nodeHash)
37
+ }
38
+
39
+ /**
40
+ * @param {string} nodeHash 64 位十六进制
41
+ * @returns {number} 信誉分
42
+ */
43
+ function scoreOf(nodeHash) {
44
+ return Number(rep.byNodeHash?.[nodeHash]?.score ?? 0)
45
+ }
46
+
47
+ const rooms = await listFederationRoomSlots(username)
48
+ /** @type {import('./engine.mjs').TrustGraphInputs['roomRosters']} */
49
+ const roomRosters = []
50
+ for (const room of rooms) {
51
+ const nodeHashes = []
52
+ for (const { remoteNodeHash } of room.getRoster()) {
53
+ if (!remoteNodeHash) continue
54
+ if (isNodeBlocked(remoteNodeHash)) blocked.add(remoteNodeHash)
55
+ else if (isQuarantinedPure(rep, remoteNodeHash)) quarantined.add(remoteNodeHash)
56
+ else nodeHashes.push(remoteNodeHash)
57
+ }
58
+ /**
59
+ * @param {string} remoteNodeHash 64 位十六进制
60
+ * @returns {number} 本地主观信誉分;从未打分的新人退回 rosterDefaultScore
61
+ */
62
+ function rosterScoreOf(remoteNodeHash) {
63
+ const row = rep.byNodeHash?.[remoteNodeHash]
64
+ return row && Number.isFinite(Number(row.score))
65
+ ? Number(row.score)
66
+ : trustGraphTunables.rosterDefaultScore
67
+ }
68
+ roomRosters.push({
69
+ scopeId: room.groupId,
70
+ nodeHashes,
71
+ scoreOf: rosterScoreOf,
72
+ })
73
+ }
74
+
75
+ return mergeGraph({
76
+ trustedPeers: net.trustedPeers,
77
+ explorePeers: net.explorePeers,
78
+ hints: net.hints,
79
+ roomRosters,
80
+ blockedNodeHashes: blocked,
81
+ quarantinedNodeHashes: quarantined,
82
+ scoreOf,
83
+ })
84
+ })
85
+ }
86
+
87
+ /**
88
+ * @param {string} username 副本用户名 登录名
89
+ * @param {number} [limit=12] 最多返回节点数
90
+ * @returns {Promise<TrustNode[]>} 按信誉降序
91
+ */
92
+ export async function pickTopNodes(username, limit = trustGraphTunables.pickTopNodesDefaultLimit) {
93
+ await ensureUserRoom({ replicaUsername: username })
94
+ const rep = loadReputation()
95
+ const quarantined = new Set(
96
+ Object.keys(rep.byNodeHash || {}).filter(id => isQuarantinedPure(rep, id)),
97
+ )
98
+ return pickTopFromGraph(await buildMergedGraph(username), limit, trustGraphTunables, quarantined)
99
+ }
@@ -0,0 +1,44 @@
1
+ /** @type {Map<string, { graph: Map<string, object>, builtAt: number, revision: number }>} */
2
+ const cacheByUsername = new Map()
3
+ let revision = 0
4
+
5
+ const DEFAULT_TTL_MS = 30_000
6
+
7
+ /**
8
+ * @returns {number} 当前 revision
9
+ */
10
+ export function getTrustGraphRevision() {
11
+ return revision
12
+ }
13
+
14
+ /**
15
+ * @returns {void}
16
+ */
17
+ export function invalidateTrustGraphCache() {
18
+ cacheByUsername.clear()
19
+ revision++
20
+ }
21
+
22
+ /**
23
+ * @param {string} username 副本用户名 登录名(缓存键,与 buildMergedGraph 一致)
24
+ * @param {() => Promise<Map<string, object>>} build 构建函数
25
+ * @param {number} [ttlMs=30000] TTL
26
+ * @returns {Promise<Map<string, object>>} 合并后的信任图
27
+ */
28
+ export async function getCachedTrustGraph(username, build, ttlMs = DEFAULT_TTL_MS) {
29
+ const key = String(username || '')
30
+ const now = Date.now()
31
+ const cached = cacheByUsername.get(key)
32
+ if (cached && cached.revision === revision && now - cached.builtAt < ttlMs)
33
+ return cached.graph
34
+
35
+ const graph = await build()
36
+ cacheByUsername.set(key, { graph, builtAt: now, revision })
37
+ return graph
38
+ }
39
+
40
+ /** @returns {void} 测试用 */
41
+ export function clearTrustGraphCache() {
42
+ cacheByUsername.clear()
43
+ revision = 0
44
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * TrustGraph 纯图合并/选择(模拟器与 trust_graph.mjs 共用)。
3
+ */
4
+ import { resolveFederationFanoutTopK } from './resolve.mjs'
5
+ import trustGraphTunables from './tunables.json' with { type: 'json' }
6
+
7
+ /**
8
+ * @typedef {{ nodeHash: string, score: number, scopeIds: string[] }} TrustNode
9
+ * @typedef {{
10
+ * trustedPeers?: string[],
11
+ * explorePeers?: string[],
12
+ * hints?: Array<{ nodeHash: string, source?: string, weight?: number, expiresAt?: number }>,
13
+ * roomRosters?: Array<{ scopeId: string, nodeHashes: string[], scoreOf?: (nodeHash: string) => number }>,
14
+ * blockedNodeHashes?: Set<string>,
15
+ * quarantinedNodeHashes?: Set<string>,
16
+ * now?: number,
17
+ * }} TrustGraphInputs
18
+ */
19
+
20
+ /**
21
+ * @returns {typeof trustGraphTunables} 默认 tunables
22
+ */
23
+ export function defaultTrustGraphTunables() {
24
+ return trustGraphTunables
25
+ }
26
+
27
+ /**
28
+ * @param {Map<string, TrustNode>} byNode 累积图
29
+ * @param {string} scopeId scope 标识
30
+ * @param {string} nodeHash 64 位十六进制
31
+ * @param {number} score 信誉分
32
+ */
33
+ export function mergeTrustNode(byNode, scopeId, nodeHash, score) {
34
+ const previous = byNode.get(nodeHash)
35
+ if (previous) {
36
+ const seenCount = previous.scopeIds.length
37
+ previous.score = (previous.score * seenCount + score) / (seenCount + 1)
38
+ if (!previous.scopeIds.includes(scopeId)) previous.scopeIds.push(scopeId)
39
+ return
40
+ }
41
+ byNode.set(nodeHash, { nodeHash, score, scopeIds: [scopeId] })
42
+ }
43
+
44
+ /**
45
+ * @param {TrustGraphInputs} inputs 图输入
46
+ * @param {typeof trustGraphTunables} [tunables] tunables
47
+ * @returns {Map<string, TrustNode>} nodeHash → 节点
48
+ */
49
+ export function mergeGraph(inputs, tunables = trustGraphTunables) {
50
+ /**
51
+ * @typedef {{
52
+ * scopeIds: Set<string>,
53
+ * networkScores: number[],
54
+ * rosterScores: number[],
55
+ * hintWeightSum: number,
56
+ * hintSources: Set<string>,
57
+ * }} NodeEvidence
58
+ */
59
+ /** @type {Map<string, NodeEvidence>} */
60
+ const evidenceByNode = new Map()
61
+ const blocked = inputs.blockedNodeHashes || new Set()
62
+ const quarantined = inputs.quarantinedNodeHashes || new Set()
63
+ const damp = Number(tunables.quarantineTrustDamp ?? 0.35)
64
+ const now = inputs.now ?? Date.now()
65
+ /**
66
+ * @param {string} nodeHash 64 位十六进制
67
+ * @returns {boolean} 是否拉黑
68
+ */
69
+ function isBlocked(nodeHash) {
70
+ return blocked.has(nodeHash)
71
+ }
72
+
73
+ /**
74
+ * @param {string} nodeHash 64 位十六进制
75
+ * @returns {boolean} 是否本地隔离
76
+ */
77
+ function isQuarantined(nodeHash) {
78
+ return quarantined.has(nodeHash)
79
+ }
80
+
81
+ /**
82
+ * @param {string} nodeHash 64 位十六进制
83
+ * @returns {NodeEvidence} 节点证据容器
84
+ */
85
+ function nodeEvidence(nodeHash) {
86
+ let ev = evidenceByNode.get(nodeHash)
87
+ if (!ev) {
88
+ ev = {
89
+ scopeIds: new Set(),
90
+ networkScores: [],
91
+ rosterScores: [],
92
+ hintWeightSum: 0,
93
+ hintSources: new Set(),
94
+ }
95
+ evidenceByNode.set(nodeHash, ev)
96
+ }
97
+ return ev
98
+ }
99
+
100
+ for (const nodeHash of [...inputs.trustedPeers || [], ...inputs.explorePeers || []]) {
101
+ if (isBlocked(nodeHash)) continue
102
+ const score = inputs.scoreOf?.(nodeHash) ?? 0
103
+ const ev = nodeEvidence(nodeHash)
104
+ ev.scopeIds.add('network')
105
+ ev.networkScores.push(Number(score))
106
+ }
107
+
108
+ for (const hint of inputs.hints || []) {
109
+ if (hint.expiresAt && hint.expiresAt <= now) continue
110
+ if (isBlocked(hint.nodeHash)) continue
111
+ const ev = nodeEvidence(hint.nodeHash)
112
+ ev.scopeIds.add(`hint:${hint.source ?? 'unknown'}`)
113
+ const rawWeight = Number(hint.weight ?? tunables.hintDefaultWeight)
114
+ if (Number.isFinite(rawWeight) && rawWeight > 0)
115
+ ev.hintWeightSum += rawWeight
116
+ ev.hintSources.add(String(hint.source ?? 'unknown'))
117
+ }
118
+
119
+ for (const room of inputs.roomRosters || [])
120
+ for (const remoteNodeHash of room.nodeHashes) {
121
+ if (!remoteNodeHash || isBlocked(remoteNodeHash)) continue
122
+ // 名册只能告诉你「这个节点存在于该 scope」;信任分一律取本地主观信誉,
123
+ // 仅当本地从未给它打过分(新人)时才退回 rosterDefaultScore。
124
+ const local = room.scoreOf?.(remoteNodeHash) ?? inputs.scoreOf?.(remoteNodeHash)
125
+ const score = Number.isFinite(Number(local)) ? Number(local) : tunables.rosterDefaultScore
126
+ const ev = nodeEvidence(remoteNodeHash)
127
+ ev.scopeIds.add(room.scopeId)
128
+ ev.rosterScores.push(score)
129
+ }
130
+
131
+ /** @type {Map<string, TrustNode>} */
132
+ const byNode = new Map()
133
+ for (const [nodeHash, ev] of evidenceByNode.entries()) {
134
+ const networkMean = ev.networkScores.length
135
+ ? ev.networkScores.reduce((s, n) => s + n, 0) / ev.networkScores.length
136
+ : NaN
137
+ const rosterMean = ev.rosterScores.length
138
+ ? ev.rosterScores.reduce((s, n) => s + n, 0) / ev.rosterScores.length
139
+ : NaN
140
+ const baseScores = [networkMean, rosterMean].filter(Number.isFinite)
141
+ const baseScore = baseScores.length
142
+ ? baseScores.reduce((s, n) => s + n, 0) / baseScores.length
143
+ : 0
144
+
145
+ // Hint 只做“发现增益”:收益按总权重指数饱和,防止多源投毒线性抬分。
146
+ const hintScale = 0.1
147
+ const saturatedHintLift = tunables.hintMaxBonus * (1 - Math.exp(-ev.hintWeightSum / hintScale))
148
+ const hasHardEvidence = ev.networkScores.length + ev.rosterScores.length > 0
149
+ const hintReliability = hasHardEvidence ? 1 : 0.35
150
+ let score = baseScore + saturatedHintLift * hintReliability
151
+ if (isQuarantined(nodeHash))
152
+ score *= damp
153
+
154
+ byNode.set(nodeHash, { nodeHash, score, scopeIds: [...ev.scopeIds] })
155
+ }
156
+ return byNode
157
+ }
158
+
159
+ /**
160
+ * @param {Map<string, TrustNode>} graph 合并图
161
+ * @param {number} [limit] 最多返回节点数
162
+ * @param {typeof trustGraphTunables} [tunables] tunables
163
+ * @param {Set<string>} [quarantinedNodeHashes] 本地隔离节点(踢出 topK)
164
+ * @returns {TrustNode[]} 按信誉降序
165
+ */
166
+ export function pickTopFromGraph(graph, limit = trustGraphTunables.pickTopNodesDefaultLimit, tunables = trustGraphTunables, quarantinedNodeHashes = new Set()) {
167
+ const k = limit ?? tunables.pickTopNodesDefaultLimit
168
+ return [...graph.values()]
169
+ .filter(node => !quarantinedNodeHashes.has(node.nodeHash))
170
+ .sort((a, b) => b.score - a.score)
171
+ .slice(0, Math.max(1, k))
172
+ }
173
+
174
+ /**
175
+ * @param {TrustGraphInputs} inputs 图输入
176
+ * @param {number} [limit] fanout K
177
+ * @param {typeof trustGraphTunables} [tunables] tunables
178
+ * @returns {TrustNode[]} Top-K 节点
179
+ */
180
+ export function pickTop(inputs, limit, tunables = trustGraphTunables) {
181
+ const graph = mergeGraph(inputs, tunables)
182
+ const k = limit ?? resolveFederationFanoutTopK(graph.size, tunables)
183
+ return pickTopFromGraph(graph, k, tunables, inputs.quarantinedNodeHashes || new Set())
184
+ }
@@ -0,0 +1,38 @@
1
+ import { buildMergedGraph, pickTopNodes } from './build.mjs'
2
+ import { fanoutToTopNodes, sendToNode } from './send.mjs'
3
+
4
+ /** @type {Map<string, import('./registry.mjs').TrustGraphProvider>} */
5
+ const providersByOwner = new Map()
6
+
7
+ /** 用户级 P2P trust graph(registerTrustGraphProvider 注册) */
8
+ export const DEFAULT_TRUST_GRAPH_OWNER = 'default'
9
+
10
+ /**
11
+ * @param {string} ownerId 注册方(如 chat)
12
+ * @param {import('./registry.mjs').TrustGraphProvider} impl 信任图实现
13
+ * @returns {void}
14
+ */
15
+ export function registerTrustGraphProvider(ownerId, impl) {
16
+ providersByOwner.set(String(ownerId), impl)
17
+ }
18
+
19
+ /** @returns {void} */
20
+ export function clearTrustGraphProvider() {
21
+ providersByOwner.clear()
22
+ }
23
+
24
+ /**
25
+ * @param {string} [ownerId=DEFAULT_TRUST_GRAPH_OWNER] 注册方 ID
26
+ * @returns {import('./registry.mjs').TrustGraphProvider} 已注册实现
27
+ */
28
+ export function requireTrustGraphProvider(ownerId = DEFAULT_TRUST_GRAPH_OWNER) {
29
+ const impl = providersByOwner.get(String(ownerId))
30
+ if (!impl)
31
+ throw new Error(`p2p: registerTrustGraphProvider('${ownerId}') must run before trust graph fanout`)
32
+ return impl
33
+ }
34
+
35
+ /** @returns {import('./registry.mjs').TrustGraphProvider} 默认信任图实现 */
36
+ export function createDefaultTrustGraphProvider() {
37
+ return { buildMergedGraph, pickTopNodes, sendToNode, fanoutToTopNodes }
38
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * 共享 tunables 缩放:ratio + floor + cap,仿真与运行时共用。
3
+ */
4
+
5
+ /** @typedef {{ floor: number, ratio: number, cap?: number }} ScaleSpec */
6
+
7
+ /**
8
+ * @param {number} n 分母(在场人数 / 候选 peer 数等)
9
+ * @param {ScaleSpec} spec floor、ratio、可选 cap
10
+ * @returns {number} 有效整数阈值 ≥ floor
11
+ */
12
+ export function scaleCount(n, { floor, ratio, cap = Infinity }) {
13
+ const safeN = Math.max(0, Math.floor(Number(n) || 0))
14
+ const scaled = Math.ceil(safeN * Number(ratio) || 0)
15
+ const capped = Number.isFinite(cap) ? Math.min(scaled, Math.floor(cap)) : scaled
16
+ const bounded = safeN > 0 ? Math.min(capped, safeN) : Math.max(1, Math.floor(floor))
17
+ return Math.max(Math.max(1, Math.floor(floor)), bounded)
18
+ }
19
+
20
+ /**
21
+ * @param {number} n 群 activeMembers 或联邦应答 peer 数
22
+ * @param {object} tunables 归档 tunables
23
+ * @returns {number} 收集阶段 quorum peerMin
24
+ */
25
+ export function resolveArchiveQuorumPeerMin(n, tunables) {
26
+ return scaleCount(n, {
27
+ floor: tunables.archiveQuorumPeerMinFloor ?? tunables.archiveQuorumPeerMin ?? 2,
28
+ ratio: tunables.archiveQuorumPeerMinRatio ?? 0.25,
29
+ })
30
+ }
31
+
32
+ /**
33
+ * @param {number} n 群 activeMembers 或联邦应答 peer 数
34
+ * @param {object} tunables 归档 tunables
35
+ * @returns {number} 无正信誉时 strictMin(硬钳 ≥2)
36
+ */
37
+ export function resolveArchiveQuorumPeerStrictMin(n, tunables) {
38
+ const raw = scaleCount(n, {
39
+ floor: tunables.archiveQuorumPeerStrictMinFloor ?? tunables.archiveQuorumPeerStrictMin ?? 2,
40
+ ratio: tunables.archiveQuorumPeerStrictMinRatio ?? 0.5,
41
+ })
42
+ return Math.max(2, raw)
43
+ }
44
+
45
+ /**
46
+ * @param {number} peerCount 已知在线 relay 候选数
47
+ * @param {object} tunables 邮箱 tunables
48
+ * @returns {number} 受信层 relay fanout
49
+ */
50
+ export function resolveMailboxRelayFanout(peerCount, tunables) {
51
+ if (Number.isFinite(tunables.relayFanoutTrusted))
52
+ return Math.max(1, Math.min(peerCount > 0 ? peerCount : Infinity, Math.floor(tunables.relayFanoutTrusted)))
53
+ return scaleCount(peerCount, {
54
+ floor: tunables.relayFanoutTrustedFloor ?? 3,
55
+ ratio: tunables.relayFanoutTrustedRatio ?? 0.3,
56
+ cap: tunables.relayFanoutTrustedCap ?? 32,
57
+ })
58
+ }
59
+
60
+ /**
61
+ * @param {number} peerCount 已知在线 relay 候选数
62
+ * @param {object} tunables 邮箱 tunables
63
+ * @returns {number} want 广播 fanout
64
+ */
65
+ export function resolveMailboxWantFanout(peerCount, tunables) {
66
+ if (Number.isFinite(tunables.wantFanout))
67
+ return Math.max(1, Math.min(peerCount > 0 ? peerCount : Infinity, Math.floor(tunables.wantFanout)))
68
+ return scaleCount(peerCount, {
69
+ floor: tunables.wantFanoutFloor ?? 3,
70
+ ratio: tunables.wantFanoutRatio ?? 0.4,
71
+ cap: tunables.wantFanoutCap ?? 32,
72
+ })
73
+ }
74
+
75
+ /**
76
+ * @param {number} rosterSize 信任图 roster 大小
77
+ * @param {object} tunables 信任图 tunables
78
+ * @returns {number} 联邦 fanout Top-K
79
+ */
80
+ export function resolveFederationFanoutTopK(rosterSize, tunables) {
81
+ if (Number.isFinite(tunables.federationFanoutTopK))
82
+ return Math.max(1, Math.min(rosterSize > 0 ? rosterSize : Infinity, Math.floor(tunables.federationFanoutTopK)))
83
+ return scaleCount(rosterSize, {
84
+ floor: tunables.federationFanoutTopKFloor ?? 3,
85
+ ratio: tunables.federationFanoutTopKRatio ?? 0.35,
86
+ cap: tunables.federationFanoutTopKCap ?? 16,
87
+ })
88
+ }
89
+
90
+ /**
91
+ * @param {object} tunables 归档 tunables
92
+ * @param {number} [activeMemberCount] 群 active 成员数
93
+ * @param {number} [candidatePeerCount] 联邦候选 peer 数
94
+ * @returns {{ peerMin: number, strictMin: number }} 归档 quorum 阈值
95
+ */
96
+ export function resolveArchiveQuorumThresholds(tunables, activeMemberCount, candidatePeerCount) {
97
+ const n = Math.max(
98
+ Number(activeMemberCount) || 0,
99
+ Number(candidatePeerCount) || 0,
100
+ )
101
+ return {
102
+ peerMin: resolveArchiveQuorumPeerMin(n, tunables),
103
+ strictMin: resolveArchiveQuorumPeerStrictMin(n, tunables),
104
+ }
105
+ }
@@ -0,0 +1,88 @@
1
+ import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
2
+ import { getNodeHash } from '../node/identity.mjs'
3
+ import { loadReputation } from '../node/reputation_store.mjs'
4
+ import { listFederationRoomSlots } from '../registries/room_provider.mjs'
5
+ import { isQuarantinedPure } from '../reputation/engine.mjs'
6
+ import { sendToNodeLink } from '../transport/link_registry.mjs'
7
+ import { USER_ROOM_SCOPE } from '../transport/room_scopes.mjs'
8
+ import { ensureUserRoom } from '../transport/user_room.mjs'
9
+
10
+ import { buildMergedGraph } from './build.mjs'
11
+ import { pickTopFromGraph } from './engine.mjs'
12
+ import { resolveFederationFanoutTopK } from './resolve.mjs'
13
+ import trustGraphTunables from './tunables.json' with { type: 'json' }
14
+
15
+ /**
16
+ * @param {string} username 副本用户名 登录名
17
+ * @param {string} targetNodeHash 64 位十六进制
18
+ * @param {string} actionName Trystero 动作
19
+ * @param {unknown} payload 载荷
20
+ * @param {Map<string, object>} [graph] 已构建信任图(省略时内部构建)
21
+ * @returns {Promise<boolean>} 是否已发送
22
+ */
23
+ export async function sendToNode(username, targetNodeHash, actionName, payload, graph) {
24
+ const target = normalizeHex64(targetNodeHash) || String(targetNodeHash || '').trim().toLowerCase()
25
+ if (!isHex64(target)) return false
26
+ await ensureUserRoom({ replicaUsername: username })
27
+
28
+ // 已直连 peer 不经 trust-graph scope 也应能收发 node scope action(非成员 CAS chunk / follow hint 等)
29
+ if (await sendToNodeLink(target, { scope: 'node', action: String(actionName), payload }))
30
+ return true
31
+
32
+ const merged = graph ?? await buildMergedGraph(username)
33
+ const targetNode = merged.get(target)
34
+ if (!targetNode?.scopeIds.length) return false
35
+ const selfNodeHash = getNodeHash()
36
+
37
+ const rooms = await listFederationRoomSlots(username)
38
+ const userRooms = rooms.filter(room => room.groupId === USER_ROOM_SCOPE)
39
+ const groupRooms = rooms.filter(room => room.groupId !== USER_ROOM_SCOPE)
40
+
41
+ for (const userRoom of userRooms) {
42
+ const peerId = userRoom.getPeerIdByNodeHash(target)
43
+ if (peerId) {
44
+ userRoom.sendToPeer(peerId, actionName, payload)
45
+ return true
46
+ }
47
+ }
48
+
49
+ for (const room of groupRooms) {
50
+ if (!targetNode.scopeIds.includes(room.groupId)) continue
51
+ const peerId = room.getPeerIdByNodeHash(targetNodeHash)
52
+ if (peerId) {
53
+ room.sendToPeer(peerId, actionName, payload)
54
+ return true
55
+ }
56
+ if (room.pickFallbackPeerIds) {
57
+ const targets = await room.pickFallbackPeerIds(selfNodeHash)
58
+ if (targets.length) {
59
+ for (const targetPeerId of targets.slice(0, trustGraphTunables.sendFallbackPeerLimit))
60
+ room.sendToPeer(targetPeerId, actionName, payload)
61
+ return true
62
+ }
63
+ }
64
+ }
65
+ return false
66
+ }
67
+
68
+ /**
69
+ * @param {string} username 副本用户名 登录名
70
+ * @param {string} actionName action 名
71
+ * @param {unknown} payload 载荷
72
+ * @param {number} [limit] K(省略时按 roster 规模缩放)
73
+ * @returns {Promise<number>} 发送次数
74
+ */
75
+ export async function fanoutToTopNodes(username, actionName, payload, limit) {
76
+ await ensureUserRoom({ replicaUsername: username })
77
+ const graph = await buildMergedGraph(username)
78
+ const k = limit ?? resolveFederationFanoutTopK(graph.size, trustGraphTunables)
79
+ const rep = loadReputation()
80
+ const quarantined = new Set(
81
+ Object.keys(rep.byNodeHash || {}).filter(id => isQuarantinedPure(rep, id)),
82
+ )
83
+ const topNodes = pickTopFromGraph(graph, k, trustGraphTunables, quarantined)
84
+ let sent = 0
85
+ for (const { nodeHash } of topNodes)
86
+ if (await sendToNode(username, nodeHash, actionName, payload, graph)) sent++
87
+ return sent
88
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "federationFanoutTopKFloor": 3,
3
+ "federationFanoutTopKRatio": 0.35,
4
+ "federationFanoutTopKCap": 16,
5
+ "federationChunkFetchFanoutK": 6,
6
+ "federationChunkMaxBytes": 524288,
7
+ "hintDefaultWeight": 0.1,
8
+ "hintMaxBonus": 0.15,
9
+ "rosterDefaultScore": 0.1,
10
+ "sendFallbackPeerLimit": 4,
11
+ "pickTopNodesDefaultLimit": 12,
12
+ "quarantineTrustDamp": 0.35
13
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * 进程内 per-key 异步互斥锁(队列式,避免 Promise 链堆积)。
3
+ */
4
+
5
+ /** @type {Map<string, { locked: boolean, queue: Array<() => void> }>} */
6
+ const mutexes = new Map()
7
+
8
+ /**
9
+ * @param {string} lockKey 锁键
10
+ * @returns {{ locked: boolean, queue: Array<() => void> }} 互斥状态
11
+ */
12
+ function mutexState(lockKey) {
13
+ let state = mutexes.get(lockKey)
14
+ if (!state) {
15
+ state = { locked: false, queue: [] }
16
+ mutexes.set(lockKey, state)
17
+ }
18
+ return state
19
+ }
20
+
21
+ /**
22
+ * @param {string} lockKey 锁键
23
+ * @param {{ locked: boolean, queue: Array<() => void> }} state 互斥状态
24
+ * @returns {void}
25
+ */
26
+ function releaseMutex(lockKey, state) {
27
+ const next = state.queue.shift()
28
+ if (next) setTimeout(next, 0)
29
+ else {
30
+ state.locked = false
31
+ mutexes.delete(lockKey)
32
+ }
33
+ }
34
+
35
+ /**
36
+ * @param {string} key 锁键
37
+ * @param {() => Promise<T>} fn 临界区
38
+ * @returns {Promise<T>} `fn` 的解析结果
39
+ * @template T
40
+ */
41
+ export async function withAsyncMutex(key, fn) {
42
+ const lockKey = String(key)
43
+ const state = mutexState(lockKey)
44
+ return new Promise((resolve, reject) => {
45
+ /**
46
+ * @returns {void}
47
+ */
48
+ const run = () => {
49
+ Promise.resolve()
50
+ .then(fn)
51
+ .then(resolve, reject)
52
+ .finally(() => releaseMutex(lockKey, state))
53
+ }
54
+ if (state.locked) state.queue.push(run)
55
+ else {
56
+ state.locked = true
57
+ run()
58
+ }
59
+ })
60
+ }
61
+
62
+ /**
63
+ * @param {string} keyPrefix 前缀
64
+ * @returns {(key: string, fn: () => Promise<T>) => Promise<T>} 带前缀的 mutex 函数
65
+ * @template T
66
+ */
67
+ export function asyncMutexForPrefix(keyPrefix) {
68
+ return (key, fn) => withAsyncMutex(`${keyPrefix}:${key}`, fn)
69
+ }
@@ -0,0 +1,16 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ const dir = path.resolve(process.cwd(), 'debug_logs')
5
+
6
+ /**
7
+ * P2P 观测落盘:写入进程 cwd 下 `debug_logs/`(与 monorepo `debugLog` 行为对齐)。
8
+ * @param {string} name 日志文件名(不含扩展名)
9
+ * @param {unknown} data 调试数据
10
+ * @returns {Promise<void>}
11
+ */
12
+ export async function debugLog(name, data) {
13
+ const text = Object(data) instanceof String ? data : JSON.stringify(data)
14
+ await fs.promises.mkdir(dir, { recursive: true })
15
+ await fs.promises.appendFile(path.join(dir, `${name}.log`), text)
16
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 将持续时间字符串转换为毫秒。
3
+ * @param {string|number} duration - 持续时间字符串 (例如, "1d", "2h", "30m", "10s") 或毫秒数。
4
+ * @returns {number} 持续时间(以毫秒为单位)。
5
+ */
6
+ export function ms(duration) {
7
+ if (Object(duration) instanceof Number) return duration
8
+
9
+ const match = /(\d+)\s*(\w+)/.exec(duration)
10
+ if (!match)
11
+ throw new Error('Invalid duration format')
12
+
13
+ const value = Number(match[1])
14
+ const unit = match[2]
15
+
16
+ switch (unit) {
17
+ case 's':
18
+ return value * 1000
19
+ case 'm':
20
+ return value * 60 * 1000
21
+ case 'h':
22
+ return value * 60 * 60 * 1000
23
+ case 'd':
24
+ return value * 24 * 60 * 60 * 1000
25
+ default:
26
+ throw new Error('Invalid duration unit')
27
+ }
28
+ }