@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.
- package/README.md +41 -10
- package/core/composite_key.mjs +5 -5
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/storage.mjs +36 -36
- package/discovery/advert_peer_hints.mjs +1 -1
- package/discovery/adverts.mjs +109 -0
- package/discovery/bt/index.mjs +153 -112
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +2 -9
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +394 -59
- package/federation/chunk_fetch_pending.mjs +10 -13
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +29 -28
- package/files/manifest_fetch.mjs +16 -3
- package/files/public_manifest.mjs +11 -11
- package/files/transfer_key_registry.mjs +2 -2
- package/index.mjs +86 -11
- package/infra/cli.mjs +62 -0
- package/infra/debug_log.mjs +56 -0
- package/infra/default_node_dir.mjs +22 -0
- package/infra/priority.mjs +56 -0
- package/infra/service.mjs +140 -0
- package/infra/tunables.json +5 -0
- package/link/frame.mjs +9 -16
- package/link/handshake.mjs +14 -15
- package/link/pipe.mjs +8 -8
- package/link/providers/ble_gatt.mjs +6 -6
- package/link/rtc.mjs +3 -3
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +5 -5
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +37 -5
- package/node/reputation_store.mjs +4 -4
- package/node/reputation_sync.mjs +318 -0
- package/node/routing_profile.mjs +21 -0
- package/node/signaling_config.mjs +30 -11
- package/overlay/index.mjs +22 -1
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -166
- package/transport/advert_ingest.mjs +11 -14
- package/transport/group_link_set.mjs +149 -99
- package/transport/link_registry.mjs +211 -60
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +45 -48
- package/transport/peer_pool.mjs +116 -14
- package/transport/remote_user_room.mjs +6 -9
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +170 -108
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +83 -158
- package/trust_graph/build.mjs +0 -2
- package/trust_graph/cache.mjs +12 -3
- package/trust_graph/registry.mjs +6 -6
- package/trust_graph/send.mjs +0 -3
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +3 -3
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/wire/part_ingress.mjs +32 -28
- package/wire/part_query.mjs +26 -20
- package/discovery/mdns.mjs +0 -197
- package/transport/signal_crypto.mjs +0 -104
package/transport/user_room.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
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
5
|
import { shuffleInPlace } from '../utils/shuffle.mjs'
|
|
7
|
-
import { attachPartWire } from '../wire/part_ingress.mjs'
|
|
8
|
-
import { attachPartQueryWire } from '../wire/part_query.mjs'
|
|
9
6
|
|
|
10
|
-
import { listLinks, sendToNodeLink
|
|
7
|
+
import { getLinkRegistry, listLinks, sendToNodeLink } from './link_registry.mjs'
|
|
8
|
+
import {
|
|
9
|
+
attachUserRoomDefaultWires,
|
|
10
|
+
ensureNodeScope,
|
|
11
|
+
} from './node_scope.mjs'
|
|
11
12
|
import { USER_ROOM_SCOPE } from './room_scopes.mjs'
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -20,117 +21,35 @@ import { USER_ROOM_SCOPE } from './room_scopes.mjs'
|
|
|
20
21
|
const sendNodeAction = (peerId, action, payload) =>
|
|
21
22
|
sendToNodeLink(peerId, { scope: 'node', action, payload })
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 在 node scope action 表上注册 fed_chunk_get / fed_chunk_data handler,
|
|
26
|
-
* 供用户房间(本地 + 远端)双向 chunk 传输使用。
|
|
27
|
-
* @param {string} username 副本用户名 用户名
|
|
28
|
-
* @param {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire action 表
|
|
29
|
-
* @returns {void}
|
|
30
|
-
*/
|
|
31
|
-
function attachUserRoomChunkHandlers(username, wire) {
|
|
32
|
-
import('../files/chunk_responder.mjs').then(({ attachNodeScopeFedChunkResponder }) => {
|
|
33
|
-
attachNodeScopeFedChunkResponder(username, wire)
|
|
34
|
-
}).catch(error => console.error('p2p: failed to attach chunk handlers to node scope', error))
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** @type {Promise<UserRoomSlot | null> | null} */
|
|
24
|
+
/** @type {Promise<UserRoomSlot> | null} */
|
|
38
25
|
let userRoomInflight = null
|
|
39
26
|
|
|
40
27
|
/** @type {UserRoomSlot | null} */
|
|
41
|
-
|
|
28
|
+
let userRoomSlot = null
|
|
42
29
|
|
|
43
|
-
/** @type {
|
|
44
|
-
|
|
45
|
-
/** @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>} */
|
|
46
|
-
const nodeScopeWireHooks = new Set()
|
|
47
|
-
/** @type {string} */
|
|
48
|
-
let nodeScopeReplicaUsername = ''
|
|
49
|
-
/** @type {ReturnType<typeof createNodeScopeWire> | null} */
|
|
50
|
-
let nodeScopeWire = null
|
|
51
|
-
let nodeScopeCleanup = null
|
|
30
|
+
/** @type {(() => void) | null} */
|
|
31
|
+
let userRoomDefaultWiresDispose = null
|
|
52
32
|
|
|
53
33
|
/**
|
|
54
|
-
*
|
|
55
|
-
* @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 注册回调
|
|
56
|
-
* @returns {() => void} 取消注册
|
|
34
|
+
* @returns {UserRoomSlot | null} 已创建的用户房间槽,未 ensure 时为 null
|
|
57
35
|
*/
|
|
58
|
-
export function
|
|
59
|
-
|
|
60
|
-
if (nodeScopeWire)
|
|
61
|
-
try { hook(nodeScopeReplicaUsername, nodeScopeWire) } catch { /* ignore */ }
|
|
62
|
-
return () => nodeScopeWireHooks.delete(hook)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* 创建 node scope 的 on/send wire 表。
|
|
67
|
-
* @returns {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire 接口
|
|
68
|
-
*/
|
|
69
|
-
function createNodeScopeWire() {
|
|
70
|
-
return {
|
|
71
|
-
/**
|
|
72
|
-
* 注册 node scope action handler。
|
|
73
|
-
* @param {string} name action 名称
|
|
74
|
-
* @param {(payload: unknown, peerId: string) => void} handler 入站回调
|
|
75
|
-
* @returns {void}
|
|
76
|
-
*/
|
|
77
|
-
on(name, handler) {
|
|
78
|
-
if (!nodeActionHandlers.has(name)) nodeActionHandlers.set(name, new Set())
|
|
79
|
-
nodeActionHandlers.get(name).add(handler)
|
|
80
|
-
},
|
|
81
|
-
/**
|
|
82
|
-
* 向指定 peer 发送 node scope action。
|
|
83
|
-
* @param {string} name action 名称
|
|
84
|
-
* @param {unknown} payload 载荷
|
|
85
|
-
* @param {string | null} peerId 目标 peer id
|
|
86
|
-
* @returns {void}
|
|
87
|
-
*/
|
|
88
|
-
send(name, payload, peerId) {
|
|
89
|
-
if (!peerId) return
|
|
90
|
-
void sendNodeAction(peerId, name, payload).catch(() => { })
|
|
91
|
-
},
|
|
92
|
-
}
|
|
36
|
+
export function getUserRoomSlot() {
|
|
37
|
+
return userRoomSlot
|
|
93
38
|
}
|
|
94
39
|
|
|
95
40
|
/**
|
|
96
41
|
* 返回当前所有活跃链路的 roster。
|
|
97
|
-
* @returns {Array<{ peerId: string, remoteNodeHash: string }>}
|
|
42
|
+
* @returns {Array<{ peerId: string, remoteNodeHash: string }>} 活跃链路列表
|
|
98
43
|
*/
|
|
99
|
-
function activeLinkRoster() {
|
|
44
|
+
export function activeLinkRoster() {
|
|
100
45
|
return listLinks().map(({ nodeHash }) => ({ peerId: nodeHash, remoteNodeHash: nodeHash }))
|
|
101
46
|
}
|
|
102
47
|
|
|
103
48
|
/**
|
|
104
|
-
* 初始化 node scope 订阅与 wire 派发(幂等)。
|
|
105
|
-
* @param {{ replicaUsername?: string }} wireContext 入站上下文
|
|
106
|
-
* @returns {Promise<void>}
|
|
107
|
-
*/
|
|
108
|
-
async function ensureNodeScopeRuntime(wireContext) {
|
|
109
|
-
if (nodeScopeCleanup) return
|
|
110
|
-
nodeScopeReplicaUsername = wireContext.replicaUsername || nodeScopeReplicaUsername || ''
|
|
111
|
-
nodeScopeCleanup = subscribeScope('node', (senderNodeHash, envelope) => {
|
|
112
|
-
const handlers = nodeActionHandlers.get(envelope.action)
|
|
113
|
-
if (!handlers?.size) return
|
|
114
|
-
for (const handler of handlers)
|
|
115
|
-
try { handler(envelope.payload, senderNodeHash) } catch { /* ignore */ }
|
|
116
|
-
})
|
|
117
|
-
const wire = createNodeScopeWire()
|
|
118
|
-
nodeScopeWire = wire
|
|
119
|
-
attachPartWire({ replicaUsername: wireContext.replicaUsername }, wire)
|
|
120
|
-
attachPartQueryWire({ replicaUsername: wireContext.replicaUsername }, wire)
|
|
121
|
-
attachMailboxWire({ replicaUsername: wireContext.replicaUsername }, wire)
|
|
122
|
-
attachUserRoomChunkHandlers(wireContext.replicaUsername || '', wire)
|
|
123
|
-
for (const hook of nodeScopeWireHooks)
|
|
124
|
-
try { hook(wireContext.replicaUsername || '', wire) } catch { /* ignore */ }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* 用户级 node scope 房间(`fount-node-{nodeHash}`),单节点单实例。
|
|
129
|
-
*
|
|
130
49
|
* @typedef {{
|
|
131
50
|
* roomId: string
|
|
132
51
|
* roomSecret: string
|
|
133
|
-
* room: object
|
|
52
|
+
* room: object | null
|
|
134
53
|
* sendToPeer: (peerId: string, actionName: string, payload: unknown) => void
|
|
135
54
|
* getRoster: () => Array<{ peerId: string, remoteNodeHash: string | undefined }>
|
|
136
55
|
* getPeerIdByNodeHash: (nodeHash: string) => string | null
|
|
@@ -138,28 +57,31 @@ async function ensureNodeScopeRuntime(wireContext) {
|
|
|
138
57
|
*/
|
|
139
58
|
|
|
140
59
|
registerFederationRoomProvider('user-room', () => {
|
|
141
|
-
|
|
60
|
+
const slot = getUserRoomSlot()
|
|
61
|
+
if (!slot) return []
|
|
142
62
|
return [{
|
|
143
63
|
groupId: USER_ROOM_SCOPE,
|
|
144
|
-
/** @returns {Array<{ peerId: string, remoteNodeHash: string | undefined }>} 房间 roster 列表 */
|
|
145
|
-
getRoster: () => userRoomSlot.getRoster(),
|
|
146
64
|
/**
|
|
147
|
-
* @
|
|
148
|
-
|
|
65
|
+
* @returns {Array<{ peerId: string, remoteNodeHash: string | undefined }>} 当前 roster
|
|
66
|
+
*/
|
|
67
|
+
getRoster: () => slot.getRoster(),
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} nodeHash - 远端节点 hash
|
|
70
|
+
* @returns {string | null} 已连接时返回 peerId,否则 null
|
|
149
71
|
*/
|
|
150
|
-
getPeerIdByNodeHash: nodeHash =>
|
|
72
|
+
getPeerIdByNodeHash: nodeHash => slot.getPeerIdByNodeHash(nodeHash),
|
|
151
73
|
/**
|
|
152
|
-
* @param {string} peerId 目标 peer
|
|
153
|
-
* @param {string} actionName
|
|
154
|
-
* @param {unknown} payload 载荷
|
|
74
|
+
* @param {string} peerId - 目标 peer
|
|
75
|
+
* @param {string} actionName - node scope action 名
|
|
76
|
+
* @param {unknown} payload - 载荷
|
|
155
77
|
* @returns {void}
|
|
156
78
|
*/
|
|
157
|
-
sendToPeer: (peerId, actionName, payload) =>
|
|
79
|
+
sendToPeer: (peerId, actionName, payload) => slot.sendToPeer(peerId, actionName, payload),
|
|
158
80
|
}]
|
|
159
81
|
})
|
|
160
82
|
|
|
161
83
|
/**
|
|
162
|
-
* @returns {{ appId: string, password: string, roomId: string, nodeHash: string }}
|
|
84
|
+
* @returns {{ appId: string, password: string, roomId: string, nodeHash: string }} user room rendezvous 凭据
|
|
163
85
|
*/
|
|
164
86
|
export function resolveUserRoomCredentials() {
|
|
165
87
|
const nodeHash = getNodeHash()
|
|
@@ -173,61 +95,62 @@ export function resolveUserRoomCredentials() {
|
|
|
173
95
|
}
|
|
174
96
|
|
|
175
97
|
/**
|
|
176
|
-
*
|
|
177
|
-
* @
|
|
98
|
+
* 用户房间槽 + runtime;默认不挂业务 wire(用 `attachUserRoomDefaultWires` / `attachDefaultWires: true`)。
|
|
99
|
+
* @param {{ replicaUsername?: string, attachDefaultWires?: boolean }} [options] - 副本用户名与是否挂载默认 wire
|
|
100
|
+
* @returns {Promise<UserRoomSlot>} 用户房间槽
|
|
178
101
|
*/
|
|
179
|
-
export async function ensureUserRoom(
|
|
180
|
-
|
|
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
|
+
}
|
|
181
111
|
if (userRoomInflight) return await userRoomInflight
|
|
182
112
|
|
|
183
113
|
userRoomInflight = (async () => {
|
|
184
114
|
ensureNodeDefaults()
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
getPeerIdByNodeHash(nodeHash) {
|
|
215
|
-
return getLinkRegistry().getLink(nodeHash) ? nodeHash : null
|
|
216
|
-
},
|
|
217
|
-
}
|
|
218
|
-
return userRoomSlot
|
|
219
|
-
}
|
|
220
|
-
catch (error) {
|
|
221
|
-
console.error('p2p: node scope init failed', error)
|
|
222
|
-
userRoomSlot = null
|
|
223
|
-
return null
|
|
224
|
-
}
|
|
225
|
-
finally {
|
|
226
|
-
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
|
+
},
|
|
227
144
|
}
|
|
145
|
+
return userRoomSlot
|
|
228
146
|
})()
|
|
229
147
|
|
|
230
|
-
|
|
148
|
+
try {
|
|
149
|
+
return await userRoomInflight
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
userRoomInflight = null
|
|
153
|
+
}
|
|
231
154
|
}
|
|
232
155
|
|
|
233
156
|
/**
|
|
@@ -236,16 +159,15 @@ export async function ensureUserRoom(wireContext = {}) {
|
|
|
236
159
|
* @param {unknown} payload 载荷
|
|
237
160
|
* @param {string | null} [exceptPeerId] 跳过的 peer
|
|
238
161
|
* @param {number} [limit] 最多转发 peer 数
|
|
239
|
-
* @returns {Promise<number>}
|
|
162
|
+
* @returns {Promise<number>} 成功转发的 peer 数
|
|
240
163
|
*/
|
|
241
164
|
export async function deliverToUserRoomPeers(username, actionName, payload, exceptPeerId = null, limit) {
|
|
165
|
+
void username
|
|
242
166
|
const { USER_ROOM_PEER_FANOUT_DEFAULT } = await import('../wire/part_common.mjs')
|
|
243
167
|
const fanoutLimit = limit ?? USER_ROOM_PEER_FANOUT_DEFAULT
|
|
244
|
-
const slot = await ensureUserRoom({ replicaUsername: username })
|
|
245
|
-
if (!slot) return 0
|
|
246
168
|
const body = { ...payload, nodeHash: getNodeHash() }
|
|
247
169
|
let sent = 0
|
|
248
|
-
const peers = shuffleInPlace(
|
|
170
|
+
const peers = shuffleInPlace(activeLinkRoster()
|
|
249
171
|
.filter(({ peerId }) => peerId && peerId !== exceptPeerId))
|
|
250
172
|
for (const { peerId } of peers)
|
|
251
173
|
try {
|
|
@@ -257,3 +179,6 @@ export async function deliverToUserRoomPeers(username, actionName, payload, exce
|
|
|
257
179
|
|
|
258
180
|
return sent
|
|
259
181
|
}
|
|
182
|
+
|
|
183
|
+
/** 再导出:node-scope 订阅与默认 wires(见 `node_scope.mjs`)。 */
|
|
184
|
+
export { attachUserRoomDefaultWires, ensureNodeScope } from './node_scope.mjs'
|
package/trust_graph/build.mjs
CHANGED
|
@@ -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)),
|
package/trust_graph/cache.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
|
|
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 (
|
|
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
|
}
|
package/trust_graph/registry.mjs
CHANGED
|
@@ -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}
|
|
12
|
+
* @param {import('./registry.mjs').TrustGraphProvider} implementation 信任图实现
|
|
13
13
|
* @returns {void}
|
|
14
14
|
*/
|
|
15
|
-
export function registerTrustGraphProvider(ownerId,
|
|
16
|
-
providersByOwner.set(String(ownerId),
|
|
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
|
|
30
|
-
if (!
|
|
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
|
|
32
|
+
return implementation
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** @returns {import('./registry.mjs').TrustGraphProvider} 默认信任图实现 */
|
package/trust_graph/send.mjs
CHANGED
|
@@ -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,7 +22,6 @@ 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
27
|
if (await sendToNodeLink(target, { scope: 'node', action: actionName, payload }))
|
|
@@ -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()
|
package/utils/async_mutex.mjs
CHANGED
|
@@ -34,11 +34,11 @@ function releaseMutex(lockKey, state) {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* @param {string} key 锁键
|
|
37
|
-
* @param {() => Promise<T>}
|
|
38
|
-
* @returns {Promise<T>} `
|
|
37
|
+
* @param {() => Promise<T>} criticalSection 临界区
|
|
38
|
+
* @returns {Promise<T>} `criticalSection` 的解析结果
|
|
39
39
|
* @template T
|
|
40
40
|
*/
|
|
41
|
-
export async function withAsyncMutex(key,
|
|
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(
|
|
50
|
+
.then(criticalSection)
|
|
51
51
|
.then(resolve, reject)
|
|
52
52
|
.finally(() => releaseMutex(lockKey, state))
|
|
53
53
|
}
|
package/utils/emit_safe.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 调用一组 listener,单次抛错不影响其余。
|
|
3
3
|
* @param {Iterable<Function>} listeners 回调集合
|
|
4
|
-
* @param {...unknown}
|
|
4
|
+
* @param {...unknown} listenerArguments 传给每个 listener 的参数
|
|
5
5
|
* @returns {void}
|
|
6
6
|
*/
|
|
7
|
-
export function emitSafe(listeners, ...
|
|
7
|
+
export function emitSafe(listeners, ...listenerArguments) {
|
|
8
8
|
for (const listener of listeners)
|
|
9
|
-
try { listener(...
|
|
9
|
+
try { listener(...listenerArguments) }
|
|
10
10
|
catch { /* ignore */ }
|
|
11
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 (
|
|
15
|
-
if (/** @type {NodeJS.ErrnoException} */
|
|
16
|
-
throw
|
|
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
|
|
28
|
-
await fsp.writeFile(
|
|
29
|
-
await fsp.rename(
|
|
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 (
|
|
42
|
-
if (/** @type {NodeJS.ErrnoException} */
|
|
43
|
-
throw
|
|
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
|
|
55
|
-
fs.writeFileSync(
|
|
56
|
-
fs.renameSync(
|
|
54
|
+
const temporaryPath = `${filePath}.tmp`
|
|
55
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
|
|
56
|
+
fs.renameSync(temporaryPath, filePath)
|
|
57
57
|
}
|
package/utils/map_pool.mjs
CHANGED
|
@@ -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>}
|
|
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,
|
|
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
|
|
20
|
-
if (
|
|
21
|
-
results[
|
|
19
|
+
const index = nextIndex++
|
|
20
|
+
if (index >= items.length) return
|
|
21
|
+
results[index] = await mapper(items[index], index)
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
package/wire/part_ingress.mjs
CHANGED
|
@@ -18,7 +18,7 @@ export const pendingPartInvoke = new Map()
|
|
|
18
18
|
/**
|
|
19
19
|
* @typedef {{
|
|
20
20
|
* send: (name: string, payload: unknown, peerId: string | null) => void
|
|
21
|
-
* on: (name: string, handler: (payload: unknown, peerId: string) => void) => void
|
|
21
|
+
* on: (name: string, handler: (payload: unknown, peerId: string) => void) => (() => void) | void
|
|
22
22
|
* }} PartWireAdapter
|
|
23
23
|
*/
|
|
24
24
|
|
|
@@ -73,35 +73,39 @@ async function dispatchPartInvoke(wireContext, payload) {
|
|
|
73
73
|
* @param {PartWireContext} wireContext 入站上下文
|
|
74
74
|
* @param {PartWireAdapter} wire Trystero 适配器
|
|
75
75
|
* @param {{ allowPartInvoke?: (payload: object) => boolean }} [options] 入站过滤
|
|
76
|
-
* @returns {void}
|
|
76
|
+
* @returns {() => void} 取消挂载的 dispose
|
|
77
77
|
*/
|
|
78
78
|
export function attachPartWire(wireContext, wire, options = {}) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
79
|
+
const offs = [
|
|
80
|
+
wire.on('part_timeline_put', data => {
|
|
81
|
+
if (!isPlainObject(data)) return
|
|
82
|
+
const partpath = normalizePartpath(data.partpath)
|
|
83
|
+
if (!partpath) return
|
|
84
|
+
const message = parsePartTimelinePut(data, partpath)
|
|
85
|
+
if (!message) return
|
|
86
|
+
void dispatchDeliveryInbound({
|
|
87
|
+
replicaUsername: wireContext.replicaUsername,
|
|
88
|
+
requesterNodeHash: data.nodeHash ? String(data.nodeHash).trim() : null,
|
|
89
|
+
}, message)
|
|
90
|
+
}),
|
|
91
|
+
wire.on('part_invoke', (data, peerId) => {
|
|
92
|
+
if (!isPlainObject(data)) return
|
|
93
|
+
if (options.allowPartInvoke?.(data) === false) return
|
|
94
|
+
const payload = { ...data, peerId }
|
|
95
|
+
if (payload.requestId)
|
|
96
|
+
void handleIncomingPartInvokeRequest(wireContext, payload, wire, peerId)
|
|
97
|
+
else
|
|
98
|
+
void handleIncomingPartInvokeFireAndForget(wireContext, payload, wire, peerId)
|
|
99
|
+
}),
|
|
100
|
+
wire.on('part_invoke_response', (data, peerId) => {
|
|
101
|
+
if (!isPlainObject(data)) return
|
|
102
|
+
handleIncomingPartInvokeResponse(data, peerId)
|
|
103
|
+
}),
|
|
104
|
+
]
|
|
105
|
+
return () => {
|
|
106
|
+
for (const off of offs)
|
|
107
|
+
try { off?.() } catch { /* ignore */ }
|
|
108
|
+
}
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
/**
|