@steve02081504/fount-p2p 0.0.3 → 0.0.5

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 (57) hide show
  1. package/README.md +1 -1
  2. package/core/bytes_codec.mjs +0 -27
  3. package/core/constants.mjs +0 -31
  4. package/core/hexIds.mjs +0 -13
  5. package/{entity → core}/logical_entity.mjs +2 -1
  6. package/crypto/key.mjs +14 -15
  7. package/dag/index.mjs +0 -73
  8. package/dag/storage.mjs +0 -22
  9. package/federation/entity_key_chain.mjs +0 -9
  10. package/federation/topo_order_memo.mjs +0 -15
  11. package/{entity/files → files}/acl.mjs +3 -3
  12. package/files/assemble.mjs +55 -39
  13. package/files/assemble_stream.mjs +96 -44
  14. package/files/chunk_provider_registry.mjs +0 -6
  15. package/files/chunk_store.mjs +1 -11
  16. package/{entity/files → files}/evfs.mjs +81 -55
  17. package/{entity/files → files}/evfs_ref.mjs +3 -5
  18. package/files/manifest.mjs +11 -2
  19. package/{entity/files → files}/manifest_acl_registry.mjs +4 -10
  20. package/files/manifest_fetch.mjs +2 -2
  21. package/files/public_manifest.mjs +1 -1
  22. package/files/transfer_key.mjs +12 -6
  23. package/files/transfer_key_registry.mjs +0 -7
  24. package/governance/branch.mjs +0 -11
  25. package/link/channel_mux.mjs +2 -0
  26. package/mailbox/importance.mjs +0 -33
  27. package/mailbox/settings.mjs +0 -9
  28. package/node/denylist.mjs +0 -24
  29. package/node/identity.mjs +33 -2
  30. package/node/network.mjs +0 -51
  31. package/node/personal_block.mjs +1 -36
  32. package/node/reputation_store.mjs +0 -9
  33. package/node/retention_policy.mjs +0 -15
  34. package/package.json +1 -3
  35. package/registries/event_type.mjs +0 -12
  36. package/registries/inbound.mjs +0 -22
  37. package/reputation/engine.mjs +0 -11
  38. package/schemas/part_query.mjs +156 -0
  39. package/transport/ice_servers.mjs +0 -8
  40. package/transport/peer_identity_maps.mjs +0 -58
  41. package/transport/remote_user_room.mjs +0 -12
  42. package/transport/rtc_connection_budget.mjs +2 -0
  43. package/transport/user_room.mjs +2 -11
  44. package/trust_graph/cache.mjs +0 -13
  45. package/trust_graph/engine.mjs +0 -24
  46. package/utils/async_mutex.mjs +0 -9
  47. package/wire/part_fanout.mjs +0 -19
  48. package/wire/part_query.mjs +487 -0
  49. package/wire/part_query.tunables.json +14 -0
  50. package/wire/part_query_cache.mjs +94 -0
  51. package/entity/files/replica_host_cache.mjs +0 -46
  52. package/entity/hosting_registry.mjs +0 -47
  53. package/entity/logical_entity_id_registry.mjs +0 -40
  54. package/entity/node_hash.mjs +0 -15
  55. package/entity/replica.mjs +0 -20
  56. package/entity/session_snapshot_registry.mjs +0 -38
  57. package/registries/p2p_viewer.mjs +0 -35
@@ -4,6 +4,7 @@ import { attachMailboxWire } from '../mailbox/wire.mjs'
4
4
  import { ensureNodeDefaults, getNodeHash } from '../node/identity.mjs'
5
5
  import { registerFederationRoomProvider } from '../registries/room_provider.mjs'
6
6
  import { attachPartWire } from '../wire/part_ingress.mjs'
7
+ import { attachPartQueryWire } from '../wire/part_query.mjs'
7
8
 
8
9
  import { listLinks, sendToNodeLink, subscribeScope, getLinkRegistry } from './link_registry.mjs'
9
10
  import { USER_ROOM_SCOPE } from './room_scopes.mjs'
@@ -105,6 +106,7 @@ async function ensureNodeScopeRuntime(ctx) {
105
106
  const wire = createNodeScopeWire()
106
107
  nodeScopeWire = wire
107
108
  attachPartWire({ replicaUsername: ctx.replicaUsername }, wire)
109
+ attachPartQueryWire({ replicaUsername: ctx.replicaUsername }, wire)
108
110
  attachMailboxWire({ replicaUsername: ctx.replicaUsername }, wire)
109
111
  attachUserRoomChunkHandlers(ctx.replicaUsername || '', wire)
110
112
  for (const hook of nodeScopeWireHooks)
@@ -217,17 +219,6 @@ export async function ensureUserRoom(ctx = {}) {
217
219
  return await userRoomInflight
218
220
  }
219
221
 
220
- /**
221
- * @returns {void}
222
- */
223
- export function invalidateUserRoom() {
224
- userRoomSlot = null
225
- userRoomInflight = null
226
- nodeScopeWire = null
227
- nodeScopeReplicaUsername = ''
228
- nodeScopeCleanup = null
229
- }
230
-
231
222
  /**
232
223
  * @param {string} username 副本用户名
233
224
  * @param {string} actionName 节点 scope action
@@ -4,13 +4,6 @@ let revision = 0
4
4
 
5
5
  const DEFAULT_TTL_MS = 30_000
6
6
 
7
- /**
8
- * @returns {number} 当前 revision
9
- */
10
- export function getTrustGraphRevision() {
11
- return revision
12
- }
13
-
14
7
  /**
15
8
  * @returns {void}
16
9
  */
@@ -36,9 +29,3 @@ export async function getCachedTrustGraph(username, build, ttlMs = DEFAULT_TTL_M
36
29
  cacheByUsername.set(key, { graph, builtAt: now, revision })
37
30
  return graph
38
31
  }
39
-
40
- /** @returns {void} 测试用 */
41
- export function clearTrustGraphCache() {
42
- cacheByUsername.clear()
43
- revision = 0
44
- }
@@ -17,30 +17,6 @@ import trustGraphTunables from './tunables.json' with { type: 'json' }
17
17
  * }} TrustGraphInputs
18
18
  */
19
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
20
  /**
45
21
  * @param {TrustGraphInputs} inputs 图输入
46
22
  * @param {typeof trustGraphTunables} [tunables] tunables
@@ -58,12 +58,3 @@ export async function withAsyncMutex(key, fn) {
58
58
  }
59
59
  })
60
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
- }
@@ -50,22 +50,3 @@ export async function collectPartInvokeResponses(username, partpath, invoke, tim
50
50
 
51
51
  return waitForResponses
52
52
  }
53
-
54
- /**
55
- * 单向 part_invoke fanout(mailbox put 等)。
56
- * @param {string} username 用户
57
- * @param {string} partpath part 路径
58
- * @param {import('./part_invoke.mjs').PartInvoke} invoke 调用体
59
- * @param {number} limit fanout 上限
60
- * @param {string} [nodeHash] 来源节点
61
- * @param {string} [groupId] 群上下文
62
- * @returns {Promise<number>} 发送次数
63
- */
64
- export async function fanoutPartInvoke(username, partpath, invoke, limit, nodeHash, groupId) {
65
- return requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).fanoutToTopNodes(username, 'part_invoke', buildPartInvokePayload({
66
- partpath,
67
- invoke,
68
- nodeHash,
69
- groupId,
70
- }), limit)
71
- }
@@ -0,0 +1,487 @@
1
+ import { randomUUID } from 'node:crypto'
2
+
3
+ import { createDedupeSlot } from '../federation/dedupe_slot.mjs'
4
+ import { getNodeHash } from '../node/identity.mjs'
5
+ import { loadReputation } from '../node/reputation_store.mjs'
6
+ import { isQuarantinedPure } from '../reputation/engine.mjs'
7
+ import {
8
+ clampPartQueryRows,
9
+ parsePartQueryReq,
10
+ parsePartQueryRes,
11
+ } from '../schemas/part_query.mjs'
12
+ import { buildMergedGraph } from '../trust_graph/build.mjs'
13
+ import { pickTopFromGraph } from '../trust_graph/engine.mjs'
14
+ import { DEFAULT_TRUST_GRAPH_OWNER, requireTrustGraphProvider } from '../trust_graph/registry.mjs'
15
+ import { resolveFederationFanoutTopK } from '../trust_graph/resolve.mjs'
16
+ import trustGraphTunables from '../trust_graph/tunables.json' with { type: 'json' }
17
+
18
+ import { isPlainObject } from './ingress.mjs'
19
+ import { createPartQueryCache, partQueryCache } from './part_query_cache.mjs'
20
+ import partQueryTunables from './part_query.tunables.json' with { type: 'json' }
21
+ import { consumeWireRateBucket } from './rate_bucket.mjs'
22
+ import { finishMultiWireWaiters, registerMultiWireWait } from './wait.mjs'
23
+
24
+ /** @typedef {import('../schemas/part_query.mjs').PartQueryReq} PartQueryReq */
25
+ /** @typedef {import('../schemas/part_query.mjs').PartQueryRes} PartQueryRes */
26
+ /** @typedef {import('./part_ingress.mjs').PartWireAdapter} PartWireAdapter */
27
+
28
+ /**
29
+ * @typedef {{
30
+ * replicaUsername?: string
31
+ * peerId?: string
32
+ * requesterNodeHash?: string | null
33
+ * }} QueryInboundContext
34
+ */
35
+
36
+ /**
37
+ * @typedef {(ctx: QueryInboundContext, query: unknown) => Promise<unknown[] | null | undefined> | unknown[] | null | undefined} QueryInboundHandler
38
+ */
39
+
40
+ /**
41
+ * @typedef {{
42
+ * takeDedupe: (key: string) => boolean
43
+ * relayPending: Map<string, RelayPending>
44
+ * originWaits: Map<string, Map<string, import('./wait.mjs').WireWaiter[]>>
45
+ * originBags: Map<string, { rows: unknown[], maxHits: number, expected: number, received: number, respondedPeers: Set<string>, rowKey?: (row: unknown) => string }>
46
+ * cache: ReturnType<typeof createPartQueryCache>
47
+ * handlers: Map<string, QueryInboundHandler>
48
+ * }} PartQueryNodeState
49
+ */
50
+
51
+ /**
52
+ * @typedef {{
53
+ * selectNeighbors?: (exclude: Set<string>) => Promise<string[]>
54
+ * deliver?: (nodeHash: string, action: string, payload: unknown) => Promise<boolean> | boolean
55
+ * getNodeHash?: () => string
56
+ * now?: () => number
57
+ * state?: PartQueryNodeState
58
+ * }} PartQueryDeps
59
+ */
60
+
61
+ /**
62
+ * @typedef {{
63
+ * upstreamPeerId: string
64
+ * wire: PartWireAdapter
65
+ * req: PartQueryReq
66
+ * localRows: unknown[]
67
+ * remoteRows: unknown[]
68
+ * expected: number
69
+ * received: number
70
+ * respondedPeers: Set<string>
71
+ * flushed: boolean
72
+ * timer: ReturnType<typeof setTimeout> | null
73
+ * deps: PartQueryDeps
74
+ * state: PartQueryNodeState
75
+ * }} RelayPending
76
+ */
77
+
78
+ /**
79
+ * @param {{ cache?: ReturnType<typeof createPartQueryCache> }} [options] 选项
80
+ * @returns {PartQueryNodeState} 单节点运行时状态
81
+ */
82
+ export function createPartQueryNodeState(options = {}) {
83
+ return {
84
+ takeDedupe: createDedupeSlot({
85
+ maxSize: partQueryTunables.dedupeMaxSize,
86
+ ttlMs: partQueryTunables.dedupeTtlMs,
87
+ }),
88
+ relayPending: new Map(),
89
+ originWaits: new Map(),
90
+ originBags: new Map(),
91
+ cache: options.cache || createPartQueryCache(),
92
+ handlers: new Map(),
93
+ }
94
+ }
95
+
96
+ /** 本进程默认单节点状态 */
97
+ const defaultState = createPartQueryNodeState({ cache: partQueryCache })
98
+
99
+ /**
100
+ * @param {PartQueryDeps} [deps] 依赖
101
+ * @returns {PartQueryNodeState} 节点状态
102
+ */
103
+ function resolveState(deps = {}) {
104
+ return deps.state || defaultState
105
+ }
106
+
107
+ /**
108
+ * @param {string} partpath part 路径
109
+ * @param {string} kind 查询标签
110
+ * @returns {string} handler 键
111
+ */
112
+ function handlerKey(partpath, kind) {
113
+ return `${partpath}\0${kind}`
114
+ }
115
+
116
+ /**
117
+ * Shell 注册 kind 语义处理器(怎么匹配、返回什么)。
118
+ * @param {string} partpath part 路径
119
+ * @param {string} kind 查询标签(如 entity_search)
120
+ * @param {QueryInboundHandler} handler 本地匹配器
121
+ * @param {PartQueryNodeState} [state] 节点状态(默认本机)
122
+ * @returns {void}
123
+ */
124
+ export function registerQueryInboundHandler(partpath, kind, handler, state = defaultState) {
125
+ state.handlers.set(handlerKey(String(partpath || '').trim(), String(kind || '').trim()), handler)
126
+ }
127
+
128
+ /**
129
+ * ttl 越大等得越久;发起端用 `ttl + 1` 取值,保证严格长于第一跳中继的 flush 超时。
130
+ * @param {number} ttl 当前节点剩余 ttl
131
+ * @param {typeof partQueryTunables} [tunables] 可调
132
+ * @returns {number} 等待下游超时
133
+ */
134
+ export function resolvePartQueryHopTimeoutMs(ttl, tunables = partQueryTunables) {
135
+ const table = Array.isArray(tunables.hopTimeoutMs) ? tunables.hopTimeoutMs : [1000, 2500, 4000, 6000]
136
+ const index = Math.max(0, Math.min(table.length - 1, Math.floor(ttl) - 1))
137
+ return Math.max(1, Math.floor(Number(table[index]) || tunables.defaultTimeoutMs || 4000))
138
+ }
139
+
140
+ /**
141
+ * @param {unknown[]} lists 多路 rows
142
+ * @param {number} maxHits 上限
143
+ * @param {(row: unknown) => string} [rowKey] 去重键
144
+ * @returns {unknown[]} 合并去重后的 rows
145
+ */
146
+ export function mergeQueryRows(lists, maxHits, rowKey) {
147
+ const out = []
148
+ const seen = new Set()
149
+ const keyOf = typeof rowKey === 'function'
150
+ ? rowKey
151
+ : row => {
152
+ try { return JSON.stringify(row) }
153
+ catch { return `\0${out.length}` }
154
+ }
155
+ for (const list of lists) {
156
+ if (!Array.isArray(list)) continue
157
+ for (const row of list) {
158
+ const key = keyOf(row)
159
+ if (seen.has(key)) continue
160
+ seen.add(key)
161
+ out.push(row)
162
+ if (out.length >= maxHits) return out
163
+ }
164
+ }
165
+ return out
166
+ }
167
+
168
+ /**
169
+ * @param {PartQueryNodeState} state 节点状态
170
+ * @param {QueryInboundContext} ctx 入站上下文
171
+ * @param {string} partpath part 路径
172
+ * @param {string} kind 查询标签
173
+ * @param {unknown} query 查询体
174
+ * @returns {Promise<unknown[]>} 本地 rows
175
+ */
176
+ async function runLocalHandler(state, ctx, partpath, kind, query) {
177
+ const handler = state.handlers.get(handlerKey(partpath, kind))
178
+ if (!handler) return []
179
+ const rows = await handler(ctx, query)
180
+ return Array.isArray(rows) ? rows : []
181
+ }
182
+
183
+ /**
184
+ * @param {string} username trust graph 上下文
185
+ * @param {Set<string>} exclude 排除节点
186
+ * @param {PartQueryDeps} deps 可注入依赖
187
+ * @returns {Promise<string[]>} 邻居 nodeHash
188
+ */
189
+ async function selectQueryNeighbors(username, exclude, deps) {
190
+ if (deps.selectNeighbors) return deps.selectNeighbors(exclude)
191
+ const graph = await buildMergedGraph(username)
192
+ const fanoutCap = Math.max(1, Math.floor(Number(partQueryTunables.fanoutCap) || 4))
193
+ const k = Math.min(fanoutCap, resolveFederationFanoutTopK(graph.size, trustGraphTunables))
194
+ const rep = loadReputation()
195
+ const quarantined = new Set(
196
+ Object.keys(rep.byNodeHash || {}).filter(id => isQuarantinedPure(rep, id)),
197
+ )
198
+ const oversample = Math.min(graph.size, k + exclude.size + 2)
199
+ return pickTopFromGraph(graph, oversample, trustGraphTunables, quarantined)
200
+ .map(node => node.nodeHash)
201
+ .filter(hash => !exclude.has(hash))
202
+ .slice(0, k)
203
+ }
204
+
205
+ /**
206
+ * @param {string} username 用户
207
+ * @param {string} nodeHash 目标
208
+ * @param {string} action action 名
209
+ * @param {unknown} payload 载荷
210
+ * @param {PartQueryDeps} deps 依赖
211
+ * @returns {Promise<boolean>} 是否发出
212
+ */
213
+ async function deliverQuery(username, nodeHash, action, payload, deps) {
214
+ if (deps.deliver) return Boolean(await deps.deliver(nodeHash, action, payload))
215
+ return requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).sendToNode(username, nodeHash, action, payload)
216
+ }
217
+
218
+ /**
219
+ * @param {PartQueryReq} req 请求
220
+ * @param {unknown[]} rows 行
221
+ * @param {() => string} nodeHashOf 本机 hash
222
+ * @returns {PartQueryRes} 响应载荷
223
+ */
224
+ function buildRes(req, rows, nodeHashOf) {
225
+ const capped = clampPartQueryRows(rows, req.budget.maxHits) || []
226
+ return {
227
+ requestId: req.requestId,
228
+ fromNodeHash: nodeHashOf(),
229
+ rows: capped,
230
+ }
231
+ }
232
+
233
+ /**
234
+ * @param {RelayPending} pending 中继槽
235
+ * @returns {void}
236
+ */
237
+ function flushRelayPending(pending) {
238
+ if (pending.flushed) return
239
+ pending.flushed = true
240
+ if (pending.timer) {
241
+ clearTimeout(pending.timer)
242
+ pending.timer = null
243
+ }
244
+ pending.state.relayPending.delete(pending.req.requestId)
245
+ const merged = mergeQueryRows([pending.localRows, pending.remoteRows], pending.req.budget.maxHits)
246
+ const now = pending.deps.now || Date.now
247
+ pending.state.cache.set(pending.req.partpath, pending.req.kind, pending.req.query, merged, now())
248
+ const nodeHashOf = pending.deps.getNodeHash || getNodeHash
249
+ try {
250
+ pending.wire.send('part_query_res', buildRes(pending.req, merged, nodeHashOf), pending.upstreamPeerId)
251
+ }
252
+ catch { /* disconnected */ }
253
+ }
254
+
255
+ /**
256
+ * @param {object} ctx attach 上下文
257
+ * @param {PartWireAdapter} wire wire
258
+ * @param {PartQueryReq} req 已校验请求
259
+ * @param {string} peerId 来路
260
+ * @param {PartQueryDeps} deps 依赖
261
+ * @returns {Promise<void>}
262
+ */
263
+ async function processIncomingReq(ctx, wire, req, peerId, deps) {
264
+ const state = resolveState(deps)
265
+ const nodeHashOf = deps.getNodeHash || getNodeHash
266
+ const now = deps.now || Date.now
267
+ const username = String(ctx.replicaUsername || '')
268
+
269
+ const cached = state.cache.get(req.partpath, req.kind, req.query, now())
270
+ if (cached) {
271
+ try { wire.send('part_query_res', buildRes(req, cached, nodeHashOf), peerId) }
272
+ catch { /* disconnected */ }
273
+ return
274
+ }
275
+
276
+ const localRows = await runLocalHandler(state, {
277
+ replicaUsername: ctx.replicaUsername,
278
+ requesterNodeHash: req.originNodeHash,
279
+ peerId,
280
+ }, req.partpath, req.kind, req.query)
281
+
282
+ const nextTtl = req.ttl - 1
283
+ if (nextTtl <= 0) {
284
+ state.cache.set(req.partpath, req.kind, req.query, localRows, now())
285
+ try { wire.send('part_query_res', buildRes(req, localRows, nodeHashOf), peerId) }
286
+ catch { /* disconnected */ }
287
+ return
288
+ }
289
+
290
+ const selfHash = nodeHashOf()
291
+ const exclude = new Set([selfHash, req.originNodeHash, String(peerId || '').trim().toLowerCase()].filter(Boolean))
292
+ const neighbors = username ? await selectQueryNeighbors(username, exclude, deps) : []
293
+ const forwardPayload = { ...req, ttl: nextTtl }
294
+
295
+ /** @type {RelayPending} */
296
+ const pending = {
297
+ upstreamPeerId: peerId,
298
+ wire,
299
+ req,
300
+ localRows,
301
+ remoteRows: [],
302
+ expected: 0,
303
+ received: 0,
304
+ respondedPeers: new Set(),
305
+ flushed: false,
306
+ timer: null,
307
+ deps,
308
+ state,
309
+ }
310
+ state.relayPending.set(req.requestId, pending)
311
+
312
+ let sent = 0
313
+ for (const target of neighbors)
314
+ if (await deliverQuery(username, target, 'part_query_req', forwardPayload, deps)) sent++
315
+ pending.expected = sent
316
+
317
+ if (sent === 0 || pending.received >= pending.expected) {
318
+ flushRelayPending(pending)
319
+ return
320
+ }
321
+
322
+ pending.timer = setTimeout(() => flushRelayPending(pending), resolvePartQueryHopTimeoutMs(req.ttl))
323
+ }
324
+
325
+ /**
326
+ * 挂载 part_query_req / part_query_res。
327
+ * @param {{ replicaUsername?: string }} ctx 入站上下文
328
+ * @param {PartWireAdapter} wire wire
329
+ * @param {PartQueryDeps} [deps] 可注入依赖(含 per-node state)
330
+ * @returns {void}
331
+ */
332
+ export function attachPartQueryWire(ctx, wire, deps = {}) {
333
+ const state = resolveState(deps)
334
+ wire.on('part_query_req', (data, peerId) => {
335
+ if (!isPlainObject(data)) return
336
+ const req = parsePartQueryReq(data)
337
+ if (!req) return
338
+ if (!state.takeDedupe(req.requestId)) return
339
+ const source = String(peerId || req.originNodeHash || '').trim().toLowerCase()
340
+ if (source && !consumeWireRateBucket(`part_query:${source}`, {
341
+ maxCount: partQueryTunables.ratePerSourcePerMin,
342
+ })) return
343
+ void processIncomingReq(ctx, wire, req, String(peerId || ''), deps)
344
+ })
345
+
346
+ wire.on('part_query_res', (data, peerId) => {
347
+ const res = parsePartQueryRes(data)
348
+ if (!res) return
349
+ handleIncomingPartQueryRes(res, String(peerId || ''), deps)
350
+ })
351
+ }
352
+
353
+ /**
354
+ * @param {PartQueryRes} res 响应
355
+ * @param {string} peerId 来路
356
+ * @param {PartQueryDeps} [deps] 依赖
357
+ * @returns {void}
358
+ */
359
+ export function handleIncomingPartQueryRes(res, peerId = '', deps = {}) {
360
+ const state = resolveState(deps)
361
+ // 同一 peer 只计一次,防重复回包灌水/提早凑齐 expected
362
+ const responderKey = String(peerId || res.fromNodeHash || '').trim().toLowerCase()
363
+ const relay = state.relayPending.get(res.requestId)
364
+ if (relay) {
365
+ if (responderKey) {
366
+ if (relay.respondedPeers.has(responderKey)) return
367
+ relay.respondedPeers.add(responderKey)
368
+ }
369
+ relay.remoteRows.push(...res.rows)
370
+ relay.received += 1
371
+ if (relay.expected > 0 && relay.received >= relay.expected) flushRelayPending(relay)
372
+ return
373
+ }
374
+
375
+ const bag = state.originBags.get(res.requestId)
376
+ if (!bag) return
377
+ if (responderKey) {
378
+ if (bag.respondedPeers.has(responderKey)) return
379
+ bag.respondedPeers.add(responderKey)
380
+ }
381
+ bag.rows = mergeQueryRows([bag.rows, res.rows], bag.maxHits, bag.rowKey)
382
+ bag.received += 1
383
+ if (bag.expected > 0 && bag.received >= bag.expected)
384
+ finishMultiWireWaiters(state.originWaits, res.requestId, '')
385
+ }
386
+
387
+ /**
388
+ * 多跳查询:本地 handler + 网络回流(反向路径聚合);本地缓存命中则不广播。
389
+ * @param {string} username trust graph 上下文
390
+ * @param {string} partpath part 路径
391
+ * @param {string} kind 查询标签
392
+ * @param {unknown} query 不透明查询
393
+ * @param {{
394
+ * ttl?: number
395
+ * timeoutMs?: number
396
+ * maxHits?: number
397
+ * rowKey?: (row: unknown) => string
398
+ * budget?: { maxHits?: number }
399
+ * } & PartQueryDeps} [options] 选项
400
+ * @returns {Promise<unknown[]>} 合并后的 rows
401
+ */
402
+ export async function queryNetwork(username, partpath, kind, query, options = {}) {
403
+ const deps = options
404
+ const state = resolveState(deps)
405
+ const now = deps.now || Date.now
406
+ const nodeHashOf = deps.getNodeHash || getNodeHash
407
+
408
+ const cached = state.cache.get(partpath, kind, query, now())
409
+ if (cached) return cached
410
+
411
+ const ttl = Math.min(
412
+ Math.max(1, Math.floor(Number(options.ttl) || partQueryTunables.maxTtl)),
413
+ partQueryTunables.maxTtl,
414
+ )
415
+ const maxHits = Math.min(
416
+ partQueryTunables.maxHits,
417
+ Math.max(1, Math.floor(Number(options.maxHits ?? options.budget?.maxHits) || partQueryTunables.maxHits)),
418
+ )
419
+ // 第一跳中继等待 hopTimeout(ttl) 才 flush,发起端默认取 ttl+1 档以免先行超时
420
+ const timeoutMs = Math.max(
421
+ 1,
422
+ Math.floor(Number(options.timeoutMs) || resolvePartQueryHopTimeoutMs(ttl + 1)),
423
+ )
424
+
425
+ const localRows = await runLocalHandler(state, {
426
+ replicaUsername: username,
427
+ requesterNodeHash: nodeHashOf(),
428
+ }, partpath, kind, query)
429
+
430
+ /** @type {PartQueryReq} */
431
+ const req = {
432
+ requestId: randomUUID(),
433
+ originNodeHash: nodeHashOf(),
434
+ partpath: String(partpath || '').trim(),
435
+ kind: String(kind || '').trim(),
436
+ query,
437
+ ttl,
438
+ budget: { maxHits },
439
+ }
440
+ const parsed = parsePartQueryReq(req)
441
+ if (!parsed) return mergeQueryRows([localRows], maxHits, options.rowKey)
442
+
443
+ // 预占 dedupe:若查询绕环回流到本机,入站侧直接丢弃
444
+ state.takeDedupe(parsed.requestId)
445
+
446
+ const bag = {
447
+ rows: [],
448
+ maxHits,
449
+ expected: 0,
450
+ received: 0,
451
+ respondedPeers: new Set(),
452
+ rowKey: options.rowKey,
453
+ }
454
+ state.originBags.set(parsed.requestId, bag)
455
+ const waitPromise = registerMultiWireWait(state.originWaits, parsed.requestId, '', timeoutMs, () => undefined)
456
+
457
+ const selfHash = nodeHashOf()
458
+ const exclude = new Set([selfHash, parsed.originNodeHash])
459
+ const neighbors = await selectQueryNeighbors(username, exclude, deps)
460
+ let sent = 0
461
+ for (const target of neighbors)
462
+ if (await deliverQuery(username, target, 'part_query_req', parsed, deps)) sent++
463
+ bag.expected = sent
464
+
465
+ // deliver 可能同步回流;在赋值 expected 后再检查是否已齐
466
+ if (sent === 0 || bag.received >= bag.expected)
467
+ finishMultiWireWaiters(state.originWaits, parsed.requestId, '')
468
+ await waitPromise
469
+ state.originBags.delete(parsed.requestId)
470
+
471
+ const merged = mergeQueryRows([localRows, bag.rows], maxHits, options.rowKey)
472
+ state.cache.set(parsed.partpath, parsed.kind, parsed.query, merged, now())
473
+ return merged
474
+ }
475
+
476
+ /** @returns {void} 测试用重置默认状态 */
477
+ export function resetPartQueryStateForTests() {
478
+ defaultState.handlers.clear()
479
+ defaultState.relayPending.clear()
480
+ defaultState.originWaits.clear()
481
+ defaultState.originBags.clear()
482
+ defaultState.cache.clear()
483
+ defaultState.takeDedupe = createDedupeSlot({
484
+ maxSize: partQueryTunables.dedupeMaxSize,
485
+ ttlMs: partQueryTunables.dedupeTtlMs,
486
+ })
487
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "maxTtl": 3,
3
+ "maxHits": 32,
4
+ "maxQueryBytes": 2048,
5
+ "maxRowsBytes": 16384,
6
+ "fanoutCap": 4,
7
+ "ratePerSourcePerMin": 30,
8
+ "dedupeTtlMs": 300000,
9
+ "dedupeMaxSize": 4000,
10
+ "cacheTtlMs": 300000,
11
+ "cacheMaxKeys": 256,
12
+ "hopTimeoutMs": [1000, 2500, 4000, 6000],
13
+ "defaultTimeoutMs": 4000
14
+ }