@steve02081504/fount-p2p 0.0.4 → 0.0.6

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 (64) hide show
  1. package/core/bytes_codec.mjs +0 -27
  2. package/core/constants.mjs +0 -31
  3. package/core/hexIds.mjs +0 -13
  4. package/crypto/key.mjs +14 -15
  5. package/dag/canonicalize_row.mjs +5 -5
  6. package/dag/index.mjs +0 -73
  7. package/dag/storage.mjs +0 -22
  8. package/discovery/mdns.mjs +4 -4
  9. package/discovery/nostr.mjs +3 -3
  10. package/federation/chunk_fetch_pending.mjs +3 -3
  11. package/federation/chunk_fetch_scheduler.mjs +3 -3
  12. package/federation/dag_order_cache.mjs +3 -3
  13. package/federation/entity_key_chain.mjs +0 -9
  14. package/federation/topo_order_memo.mjs +3 -18
  15. package/files/assemble.mjs +55 -39
  16. package/files/assemble_stream.mjs +96 -44
  17. package/files/chunk_provider_registry.mjs +0 -6
  18. package/files/chunk_responder.mjs +1 -1
  19. package/files/chunk_store.mjs +1 -11
  20. package/files/evfs.mjs +74 -47
  21. package/files/manifest.mjs +11 -2
  22. package/files/manifest_acl_registry.mjs +0 -6
  23. package/files/transfer_key.mjs +21 -15
  24. package/files/transfer_key_registry.mjs +9 -16
  25. package/governance/branch.mjs +0 -11
  26. package/governance/join_pow.mjs +17 -17
  27. package/link/channel_mux.mjs +9 -7
  28. package/link/frame.mjs +5 -5
  29. package/link/handshake.mjs +17 -17
  30. package/link/link.mjs +33 -33
  31. package/mailbox/deliver_or_store.mjs +13 -13
  32. package/mailbox/importance.mjs +0 -33
  33. package/mailbox/settings.mjs +0 -9
  34. package/mailbox/wire.mjs +4 -4
  35. package/node/denylist.mjs +0 -24
  36. package/node/network.mjs +0 -51
  37. package/node/personal_block.mjs +0 -35
  38. package/node/reputation_store.mjs +5 -14
  39. package/node/retention_policy.mjs +8 -23
  40. package/overlay/index.mjs +6 -6
  41. package/package.json +1 -1
  42. package/registries/event_type.mjs +0 -12
  43. package/registries/inbound.mjs +8 -30
  44. package/reputation/engine.mjs +0 -11
  45. package/rooms/scoped_link.mjs +18 -18
  46. package/schemas/part_query.mjs +156 -0
  47. package/timeline/append_core.mjs +3 -3
  48. package/transport/group_link_set.mjs +30 -30
  49. package/transport/ice_servers.mjs +0 -8
  50. package/transport/link_registry.mjs +13 -13
  51. package/transport/peer_identity_maps.mjs +0 -58
  52. package/transport/peer_pool.mjs +4 -4
  53. package/transport/remote_user_room.mjs +0 -12
  54. package/transport/rtc_connection_budget.mjs +2 -0
  55. package/transport/user_room.mjs +13 -24
  56. package/trust_graph/cache.mjs +0 -13
  57. package/trust_graph/engine.mjs +0 -24
  58. package/utils/async_mutex.mjs +0 -9
  59. package/wire/group_part.mjs +3 -3
  60. package/wire/part_fanout.mjs +0 -19
  61. package/wire/part_ingress.mjs +14 -14
  62. package/wire/part_query.mjs +486 -0
  63. package/wire/part_query.tunables.json +14 -0
  64. package/wire/part_query_cache.mjs +101 -0
@@ -0,0 +1,486 @@
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 partQueryTunables from './part_query.tunables.json' with { type: 'json' }
20
+ import { createPartQueryCache, partQueryCache } from './part_query_cache.mjs'
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 {(queryContext: 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
+ * }} PartQueryDependencies
59
+ */
60
+
61
+ /**
62
+ * @typedef {{
63
+ * upstreamPeerId: string
64
+ * wire: PartWireAdapter
65
+ * request: 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
+ * dependencies: PartQueryDependencies
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 {PartQueryDependencies} [dependencies] 依赖
101
+ * @returns {PartQueryNodeState} 节点状态
102
+ */
103
+ function resolveState(dependencies = {}) {
104
+ return dependencies.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} queryContext 入站上下文
171
+ * @param {string} partpath part 路径
172
+ * @param {string} kind 查询标签
173
+ * @param {unknown} query 查询体
174
+ * @returns {Promise<unknown[]>} 本地 rows
175
+ */
176
+ async function runLocalHandler(state, queryContext, partpath, kind, query) {
177
+ const handler = state.handlers.get(handlerKey(partpath, kind))
178
+ if (!handler) return []
179
+ const rows = await handler(queryContext, query)
180
+ return Array.isArray(rows) ? rows : []
181
+ }
182
+
183
+ /**
184
+ * @param {string} username trust graph 上下文
185
+ * @param {Set<string>} exclude 排除节点
186
+ * @param {PartQueryDependencies} dependencies 可注入依赖
187
+ * @returns {Promise<string[]>} 邻居 nodeHash
188
+ */
189
+ async function selectQueryNeighbors(username, exclude, dependencies) {
190
+ if (dependencies.selectNeighbors) return dependencies.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 {PartQueryDependencies} dependencies 依赖
211
+ * @returns {Promise<boolean>} 是否发出
212
+ */
213
+ async function deliverQuery(username, nodeHash, action, payload, dependencies) {
214
+ if (dependencies.deliver) return Boolean(await dependencies.deliver(nodeHash, action, payload))
215
+ return requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).sendToNode(username, nodeHash, action, payload)
216
+ }
217
+
218
+ /**
219
+ * @param {PartQueryReq} request 请求
220
+ * @param {unknown[]} rows 行
221
+ * @param {() => string} nodeHashOf 本机 hash
222
+ * @returns {PartQueryRes} 响应载荷
223
+ */
224
+ function buildResponse(request, rows, nodeHashOf) {
225
+ const capped = clampPartQueryRows(rows, request.budget.maxHits) || []
226
+ return {
227
+ requestId: request.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.request.requestId)
245
+ const merged = mergeQueryRows([pending.localRows, pending.remoteRows], pending.request.budget.maxHits)
246
+ const now = pending.dependencies.now || Date.now
247
+ pending.state.cache.set(pending.request.partpath, pending.request.kind, pending.request.query, merged, now())
248
+ const nodeHashOf = pending.dependencies.getNodeHash || getNodeHash
249
+ try {
250
+ pending.wire.send('part_query_res', buildResponse(pending.request, merged, nodeHashOf), pending.upstreamPeerId)
251
+ }
252
+ catch { /* disconnected */ }
253
+ }
254
+
255
+ /**
256
+ * @param {{ replicaUsername?: string }} wireContext attach 上下文
257
+ * @param {PartWireAdapter} wire wire
258
+ * @param {PartQueryReq} request 已校验请求
259
+ * @param {string} peerId 来路
260
+ * @param {PartQueryDependencies} dependencies 依赖
261
+ * @returns {Promise<void>}
262
+ */
263
+ async function processIncomingRequest(wireContext, wire, request, peerId, dependencies) {
264
+ const state = resolveState(dependencies)
265
+ const nodeHashOf = dependencies.getNodeHash || getNodeHash
266
+ const now = dependencies.now || Date.now
267
+ const username = String(wireContext.replicaUsername || '')
268
+
269
+ const cached = state.cache.get(request.partpath, request.kind, request.query, now())
270
+ if (cached) {
271
+ try { wire.send('part_query_res', buildResponse(request, cached, nodeHashOf), peerId) }
272
+ catch { /* disconnected */ }
273
+ return
274
+ }
275
+
276
+ const localRows = await runLocalHandler(state, {
277
+ replicaUsername: wireContext.replicaUsername,
278
+ requesterNodeHash: request.originNodeHash,
279
+ peerId,
280
+ }, request.partpath, request.kind, request.query)
281
+
282
+ const nextTtl = request.ttl - 1
283
+ if (nextTtl <= 0) {
284
+ state.cache.set(request.partpath, request.kind, request.query, localRows, now())
285
+ try { wire.send('part_query_res', buildResponse(request, localRows, nodeHashOf), peerId) }
286
+ catch { /* disconnected */ }
287
+ return
288
+ }
289
+
290
+ const selfHash = nodeHashOf()
291
+ const exclude = new Set([selfHash, request.originNodeHash, String(peerId || '').trim().toLowerCase()].filter(Boolean))
292
+ const neighbors = username ? await selectQueryNeighbors(username, exclude, dependencies) : []
293
+ const forwardPayload = { ...request, ttl: nextTtl }
294
+
295
+ /** @type {RelayPending} */
296
+ const pending = {
297
+ upstreamPeerId: peerId,
298
+ wire,
299
+ request,
300
+ localRows,
301
+ remoteRows: [],
302
+ expected: 0,
303
+ received: 0,
304
+ respondedPeers: new Set(),
305
+ flushed: false,
306
+ timer: null,
307
+ dependencies,
308
+ state,
309
+ }
310
+ state.relayPending.set(request.requestId, pending)
311
+
312
+ let sent = 0
313
+ for (const target of neighbors)
314
+ if (await deliverQuery(username, target, 'part_query_req', forwardPayload, dependencies)) 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(request.ttl))
323
+ }
324
+
325
+ /**
326
+ * 挂载 part_query_req / part_query_res。
327
+ * @param {{ replicaUsername?: string }} wireContext 入站上下文
328
+ * @param {PartWireAdapter} wire wire
329
+ * @param {PartQueryDependencies} [dependencies] 可注入依赖(含 per-node state)
330
+ * @returns {void}
331
+ */
332
+ export function attachPartQueryWire(wireContext, wire, dependencies = {}) {
333
+ const state = resolveState(dependencies)
334
+ wire.on('part_query_req', (data, peerId) => {
335
+ if (!isPlainObject(data)) return
336
+ const request = parsePartQueryReq(data)
337
+ if (!request) return
338
+ if (!state.takeDedupe(request.requestId)) return
339
+ const source = String(peerId || request.originNodeHash || '').trim().toLowerCase()
340
+ if (source && !consumeWireRateBucket(`part_query:${source}`, {
341
+ maxCount: partQueryTunables.ratePerSourcePerMin,
342
+ })) return
343
+ void processIncomingRequest(wireContext, wire, request, String(peerId || ''), dependencies)
344
+ })
345
+
346
+ wire.on('part_query_res', (data, peerId) => {
347
+ const response = parsePartQueryRes(data)
348
+ if (!response) return
349
+ handleIncomingPartQueryResponse(response, String(peerId || ''), dependencies)
350
+ })
351
+ }
352
+
353
+ /**
354
+ * @param {PartQueryRes} response 响应
355
+ * @param {string} peerId 来路
356
+ * @param {PartQueryDependencies} [dependencies] 依赖
357
+ * @returns {void}
358
+ */
359
+ export function handleIncomingPartQueryResponse(response, peerId = '', dependencies = {}) {
360
+ const state = resolveState(dependencies)
361
+ // 同一 peer 只计一次,防重复回包灌水/提早凑齐 expected
362
+ const responderKey = String(peerId || response.fromNodeHash || '').trim().toLowerCase()
363
+ const relay = state.relayPending.get(response.requestId)
364
+ if (relay) {
365
+ if (responderKey) {
366
+ if (relay.respondedPeers.has(responderKey)) return
367
+ relay.respondedPeers.add(responderKey)
368
+ }
369
+ relay.remoteRows.push(...response.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(response.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, response.rows], bag.maxHits, bag.rowKey)
382
+ bag.received += 1
383
+ if (bag.expected > 0 && bag.received >= bag.expected)
384
+ finishMultiWireWaiters(state.originWaits, response.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
+ * } & PartQueryDependencies} [options] 选项
400
+ * @returns {Promise<unknown[]>} 合并后的 rows
401
+ */
402
+ export async function queryNetwork(username, partpath, kind, query, options = {}) {
403
+ const state = resolveState(options)
404
+ const now = options.now || Date.now
405
+ const nodeHashOf = options.getNodeHash || getNodeHash
406
+
407
+ const cached = state.cache.get(partpath, kind, query, now())
408
+ if (cached) return cached
409
+
410
+ const ttl = Math.min(
411
+ Math.max(1, Math.floor(Number(options.ttl) || partQueryTunables.maxTtl)),
412
+ partQueryTunables.maxTtl,
413
+ )
414
+ const maxHits = Math.min(
415
+ partQueryTunables.maxHits,
416
+ Math.max(1, Math.floor(Number(options.maxHits ?? options.budget?.maxHits) || partQueryTunables.maxHits)),
417
+ )
418
+ // 第一跳中继等待 hopTimeout(ttl) 才 flush,发起端默认取 ttl+1 档以免先行超时
419
+ const timeoutMs = Math.max(
420
+ 1,
421
+ Math.floor(Number(options.timeoutMs) || resolvePartQueryHopTimeoutMs(ttl + 1)),
422
+ )
423
+
424
+ const localRows = await runLocalHandler(state, {
425
+ replicaUsername: username,
426
+ requesterNodeHash: nodeHashOf(),
427
+ }, partpath, kind, query)
428
+
429
+ /** @type {PartQueryReq} */
430
+ const request = {
431
+ requestId: randomUUID(),
432
+ originNodeHash: nodeHashOf(),
433
+ partpath: String(partpath || '').trim(),
434
+ kind: String(kind || '').trim(),
435
+ query,
436
+ ttl,
437
+ budget: { maxHits },
438
+ }
439
+ const parsed = parsePartQueryReq(request)
440
+ if (!parsed) return mergeQueryRows([localRows], maxHits, options.rowKey)
441
+
442
+ // 预占 dedupe:若查询绕环回流到本机,入站侧直接丢弃
443
+ state.takeDedupe(parsed.requestId)
444
+
445
+ const bag = {
446
+ rows: [],
447
+ maxHits,
448
+ expected: 0,
449
+ received: 0,
450
+ respondedPeers: new Set(),
451
+ rowKey: options.rowKey,
452
+ }
453
+ state.originBags.set(parsed.requestId, bag)
454
+ const waitPromise = registerMultiWireWait(state.originWaits, parsed.requestId, '', timeoutMs, () => undefined)
455
+
456
+ const selfHash = nodeHashOf()
457
+ const exclude = new Set([selfHash, parsed.originNodeHash])
458
+ const neighbors = await selectQueryNeighbors(username, exclude, options)
459
+ let sent = 0
460
+ for (const target of neighbors)
461
+ if (await deliverQuery(username, target, 'part_query_req', parsed, options)) sent++
462
+ bag.expected = sent
463
+
464
+ // deliver 可能同步回流;在赋值 expected 后再检查是否已齐
465
+ if (sent === 0 || bag.received >= bag.expected)
466
+ finishMultiWireWaiters(state.originWaits, parsed.requestId, '')
467
+ await waitPromise
468
+ state.originBags.delete(parsed.requestId)
469
+
470
+ const merged = mergeQueryRows([localRows, bag.rows], maxHits, options.rowKey)
471
+ state.cache.set(parsed.partpath, parsed.kind, parsed.query, merged, now())
472
+ return merged
473
+ }
474
+
475
+ /** @returns {void} 测试用重置默认状态 */
476
+ export function resetPartQueryStateForTests() {
477
+ defaultState.handlers.clear()
478
+ defaultState.relayPending.clear()
479
+ defaultState.originWaits.clear()
480
+ defaultState.originBags.clear()
481
+ defaultState.cache.clear()
482
+ defaultState.takeDedupe = createDedupeSlot({
483
+ maxSize: partQueryTunables.dedupeMaxSize,
484
+ ttlMs: partQueryTunables.dedupeTtlMs,
485
+ })
486
+ }
@@ -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
+ }
@@ -0,0 +1,101 @@
1
+ import { sha256Hex } from '../crypto/crypto.mjs'
2
+ import { normalizePartQueryCacheMaterial } from '../schemas/part_query.mjs'
3
+ import { createLruMap } from '../utils/lru.mjs'
4
+
5
+ import partQueryTunables from './part_query.tunables.json' with { type: 'json' }
6
+
7
+ /**
8
+ * @typedef {{ rows: unknown[], storedAt: number }} PartQueryCacheEntry
9
+ */
10
+
11
+ /**
12
+ * @param {string} partpath part 路径
13
+ * @param {string} kind 查询标签
14
+ * @param {unknown} query 不透明查询
15
+ * @returns {string | null} sha256 hex 缓存键
16
+ */
17
+ export function partQueryCacheKey(partpath, kind, query) {
18
+ const material = normalizePartQueryCacheMaterial(partpath, kind, query)
19
+ if (!material) return null
20
+ return sha256Hex(material)
21
+ }
22
+
23
+ /**
24
+ * LRU + TTL 中继缓存(未验证线索;发起端仍需验签复核)。
25
+ * @param {{ maxKeys?: number, ttlMs?: number, maxHits?: number }} [options] 容量 / TTL / 单键 rows 上限
26
+ * @returns {{
27
+ * get: (partpath: string, kind: string, query: unknown, now?: number) => unknown[] | null
28
+ * set: (partpath: string, kind: string, query: unknown, rows: unknown[], now?: number) => void
29
+ * clear: () => void
30
+ * readonly size: number
31
+ * }} 缓存 API
32
+ */
33
+ export function createPartQueryCache(options = {}) {
34
+ const maxKeys = Math.max(1, Math.floor(Number(options.maxKeys) || partQueryTunables.cacheMaxKeys))
35
+ const ttlMs = Math.max(1, Math.floor(Number(options.ttlMs) || partQueryTunables.cacheTtlMs))
36
+ const maxHits = Math.max(1, Math.floor(Number(options.maxHits) || partQueryTunables.maxHits))
37
+ /** @type {ReturnType<typeof createLruMap<string, PartQueryCacheEntry>>} */
38
+ const map = createLruMap(maxKeys)
39
+
40
+ /**
41
+ * @param {number} [now=Date.now()] 当前时间
42
+ * @returns {void}
43
+ */
44
+ const sweep = (now = Date.now()) => {
45
+ for (const [key, entry] of map)
46
+ if (now - entry.storedAt >= ttlMs) map.delete(key)
47
+ }
48
+
49
+ return {
50
+ /**
51
+ * @param {string} partpath part 路径
52
+ * @param {string} kind 查询标签
53
+ * @param {unknown} query 查询体
54
+ * @param {number} [now=Date.now()] 当前时间
55
+ * @returns {unknown[] | null} 未过期 rows
56
+ */
57
+ get(partpath, kind, query, now = Date.now()) {
58
+ const key = partQueryCacheKey(partpath, kind, query)
59
+ if (!key) return null
60
+ const entry = map.get(key)
61
+ if (!entry) return null
62
+ if (now - entry.storedAt >= ttlMs) {
63
+ map.delete(key)
64
+ return null
65
+ }
66
+ map.touch(key, entry)
67
+ return entry.rows.slice()
68
+ },
69
+
70
+ /**
71
+ * @param {string} partpath part 路径
72
+ * @param {string} kind 查询标签
73
+ * @param {unknown} query 查询体
74
+ * @param {unknown[]} rows 聚合 rows
75
+ * @param {number} [now=Date.now()] 当前时间
76
+ * @returns {void}
77
+ */
78
+ set(partpath, kind, query, rows, now = Date.now()) {
79
+ const key = partQueryCacheKey(partpath, kind, query)
80
+ if (!key || !Array.isArray(rows)) return
81
+ sweep(now)
82
+ map.touch(key, {
83
+ rows: rows.slice(0, maxHits),
84
+ storedAt: now,
85
+ })
86
+ },
87
+
88
+ /** @returns {void} */
89
+ clear() {
90
+ map.clear()
91
+ },
92
+
93
+ /** @returns {number} 当前条目数 */
94
+ get size() {
95
+ return map.size
96
+ },
97
+ }
98
+ }
99
+
100
+ /** 进程内默认中继缓存 */
101
+ export const partQueryCache = createPartQueryCache()