@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.
- package/README.md +42 -11
- package/core/bytes_codec.mjs +65 -10
- package/core/composite_key.mjs +5 -5
- package/core/tcp_port.mjs +1 -1
- package/crypto/checkpoint_sign.mjs +2 -2
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/canonicalize_row.mjs +3 -3
- package/dag/index.mjs +2 -3
- 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 +154 -113
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +5 -12
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +430 -141
- package/federation/chunk_fetch_pending.mjs +14 -18
- package/federation/dag_order_cache.mjs +1 -1
- package/federation/entity_key_chain.mjs +3 -4
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/federation/topo_order_memo.mjs +11 -4
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_fetch.mjs +2 -2
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +31 -30
- 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/channel_mux.mjs +10 -10
- package/link/frame.mjs +76 -131
- package/link/handshake.mjs +19 -20
- package/link/pipe.mjs +53 -79
- package/link/providers/ble_gatt.mjs +19 -31
- package/link/providers/lan_tcp.mjs +17 -32
- package/link/providers/link_id_pipe.mjs +46 -0
- package/link/providers/webrtc.mjs +42 -51
- package/link/rtc.mjs +24 -12
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +6 -6
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/identity.mjs +4 -4
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +46 -14
- 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 +35 -14
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -182
- package/schemas/discovery.mjs +1 -2
- package/schemas/federation_pull.mjs +2 -2
- package/schemas/part_query.mjs +1 -1
- package/schemas/remote_event.mjs +1 -1
- package/transport/advert_ingest.mjs +17 -0
- package/transport/group_link_set.mjs +165 -134
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +283 -124
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +48 -47
- package/transport/peer_pool.mjs +169 -78
- package/transport/remote_user_room.mjs +18 -23
- package/transport/rtc_connection_budget.mjs +18 -4
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +172 -104
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +93 -161
- 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 +1 -4
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +11 -0
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/utils/shuffle.mjs +13 -0
- package/utils/ttl_map.mjs +29 -4
- package/wire/ingress.mjs +1 -1
- package/wire/part_ingress.mjs +38 -36
- package/wire/part_invoke.mjs +4 -4
- package/wire/part_query.mjs +28 -23
- package/wire/part_query.tunables.json +6 -1
- package/wire/part_query_cache.mjs +1 -1
- package/wire/volatile_signature.mjs +1 -1
- package/discovery/mdns.mjs +0 -191
- package/transport/signal_crypto.mjs +0 -82
package/transport/user_room.mjs
CHANGED
|
@@ -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 {
|
|
7
|
-
import { attachPartQueryWire } from '../wire/part_query.mjs'
|
|
5
|
+
import { shuffleInPlace } from '../utils/shuffle.mjs'
|
|
8
6
|
|
|
9
|
-
import { listLinks, sendToNodeLink
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @param {string}
|
|
16
|
-
* @param {
|
|
17
|
-
* @returns {
|
|
15
|
+
* 经 node scope 向对端发 action。
|
|
16
|
+
* @param {string} peerId 对端 nodeHash
|
|
17
|
+
* @param {string} action 动作名
|
|
18
|
+
* @param {unknown} payload 载荷
|
|
19
|
+
* @returns {Promise<boolean>} 是否发出
|
|
18
20
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
24
|
+
/** @type {Promise<UserRoomSlot> | null} */
|
|
26
25
|
let userRoomInflight = null
|
|
27
26
|
|
|
28
27
|
/** @type {UserRoomSlot | null} */
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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 }>}
|
|
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
|
-
|
|
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
|
-
* @
|
|
137
|
-
* @returns {string | null} 对端 id
|
|
65
|
+
* @returns {Array<{ peerId: string, remoteNodeHash: string | undefined }>} 当前 roster
|
|
138
66
|
*/
|
|
139
|
-
|
|
67
|
+
getRoster: () => slot.getRoster(),
|
|
140
68
|
/**
|
|
141
|
-
* @param {string}
|
|
142
|
-
* @
|
|
143
|
-
|
|
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) =>
|
|
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
|
-
*
|
|
166
|
-
* @
|
|
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(
|
|
169
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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>}
|
|
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 =
|
|
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
|
|
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'
|
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,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:
|
|
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()
|
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
|
}
|
|
@@ -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 (
|
|
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
|
|
|
@@ -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
|
-
* @
|
|
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
|
-
|
|
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
|
|
22
|
+
if (!raw) return null
|
|
23
23
|
const text = typeof raw === 'string'
|
|
24
24
|
? raw
|
|
25
25
|
: Buffer.isBuffer(raw)
|