@steve02081504/fount-p2p 0.0.4 → 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.
- package/core/bytes_codec.mjs +0 -27
- package/core/constants.mjs +0 -31
- package/core/hexIds.mjs +0 -13
- package/crypto/key.mjs +14 -15
- package/dag/index.mjs +0 -73
- package/dag/storage.mjs +0 -22
- package/federation/entity_key_chain.mjs +0 -9
- package/federation/topo_order_memo.mjs +0 -15
- package/files/assemble.mjs +55 -39
- package/files/assemble_stream.mjs +96 -44
- package/files/chunk_provider_registry.mjs +0 -6
- package/files/chunk_store.mjs +1 -11
- package/files/evfs.mjs +64 -37
- package/files/manifest.mjs +11 -2
- package/files/manifest_acl_registry.mjs +0 -6
- package/files/transfer_key.mjs +12 -6
- package/files/transfer_key_registry.mjs +0 -7
- package/governance/branch.mjs +0 -11
- package/link/channel_mux.mjs +2 -0
- package/mailbox/importance.mjs +0 -33
- package/mailbox/settings.mjs +0 -9
- package/node/denylist.mjs +0 -24
- package/node/network.mjs +0 -51
- package/node/personal_block.mjs +0 -35
- package/node/reputation_store.mjs +0 -9
- package/node/retention_policy.mjs +0 -15
- package/package.json +1 -1
- package/registries/event_type.mjs +0 -12
- package/registries/inbound.mjs +0 -22
- package/reputation/engine.mjs +0 -11
- package/schemas/part_query.mjs +156 -0
- package/transport/ice_servers.mjs +0 -8
- package/transport/peer_identity_maps.mjs +0 -58
- package/transport/remote_user_room.mjs +0 -12
- package/transport/rtc_connection_budget.mjs +2 -0
- package/transport/user_room.mjs +2 -11
- package/trust_graph/cache.mjs +0 -13
- package/trust_graph/engine.mjs +0 -24
- package/utils/async_mutex.mjs +0 -9
- package/wire/part_fanout.mjs +0 -19
- package/wire/part_query.mjs +487 -0
- package/wire/part_query.tunables.json +14 -0
- package/wire/part_query_cache.mjs +94 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
*/
|
|
26
|
+
export function createPartQueryCache(options = {}) {
|
|
27
|
+
const maxKeys = Math.max(1, Math.floor(Number(options.maxKeys) || partQueryTunables.cacheMaxKeys))
|
|
28
|
+
const ttlMs = Math.max(1, Math.floor(Number(options.ttlMs) || partQueryTunables.cacheTtlMs))
|
|
29
|
+
const maxHits = Math.max(1, Math.floor(Number(options.maxHits) || partQueryTunables.maxHits))
|
|
30
|
+
/** @type {ReturnType<typeof createLruMap<string, PartQueryCacheEntry>>} */
|
|
31
|
+
const map = createLruMap(maxKeys)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {number} [now=Date.now()] 当前时间
|
|
35
|
+
* @returns {void}
|
|
36
|
+
*/
|
|
37
|
+
const sweep = (now = Date.now()) => {
|
|
38
|
+
for (const [key, entry] of map)
|
|
39
|
+
if (now - entry.storedAt >= ttlMs) map.delete(key)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} partpath part 路径
|
|
45
|
+
* @param {string} kind 查询标签
|
|
46
|
+
* @param {unknown} query 查询体
|
|
47
|
+
* @param {number} [now=Date.now()] 当前时间
|
|
48
|
+
* @returns {unknown[] | null} 未过期 rows
|
|
49
|
+
*/
|
|
50
|
+
get(partpath, kind, query, now = Date.now()) {
|
|
51
|
+
const key = partQueryCacheKey(partpath, kind, query)
|
|
52
|
+
if (!key) return null
|
|
53
|
+
const entry = map.get(key)
|
|
54
|
+
if (!entry) return null
|
|
55
|
+
if (now - entry.storedAt >= ttlMs) {
|
|
56
|
+
map.delete(key)
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
map.touch(key, entry)
|
|
60
|
+
return entry.rows.slice()
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} partpath part 路径
|
|
65
|
+
* @param {string} kind 查询标签
|
|
66
|
+
* @param {unknown} query 查询体
|
|
67
|
+
* @param {unknown[]} rows 聚合 rows
|
|
68
|
+
* @param {number} [now=Date.now()] 当前时间
|
|
69
|
+
* @returns {void}
|
|
70
|
+
*/
|
|
71
|
+
set(partpath, kind, query, rows, now = Date.now()) {
|
|
72
|
+
const key = partQueryCacheKey(partpath, kind, query)
|
|
73
|
+
if (!key || !Array.isArray(rows)) return
|
|
74
|
+
sweep(now)
|
|
75
|
+
map.touch(key, {
|
|
76
|
+
rows: rows.slice(0, maxHits),
|
|
77
|
+
storedAt: now,
|
|
78
|
+
})
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/** @returns {void} */
|
|
82
|
+
clear() {
|
|
83
|
+
map.clear()
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/** @returns {number} 当前条目数 */
|
|
87
|
+
get size() {
|
|
88
|
+
return map.size
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 进程内默认中继缓存 */
|
|
94
|
+
export const partQueryCache = createPartQueryCache()
|