@steve02081504/fount-p2p 0.0.11 → 0.0.12

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 +65 -10
  3. package/core/tcp_port.mjs +1 -1
  4. package/crypto/checkpoint_sign.mjs +2 -2
  5. package/dag/canonicalize_row.mjs +3 -3
  6. package/dag/index.mjs +2 -3
  7. package/discovery/bt/index.mjs +5 -5
  8. package/discovery/bt/runtime.mjs +3 -3
  9. package/discovery/mdns.mjs +7 -1
  10. package/discovery/nostr.mjs +53 -99
  11. package/federation/chunk_fetch_pending.mjs +4 -5
  12. package/federation/dag_order_cache.mjs +1 -1
  13. package/federation/entity_key_chain.mjs +3 -4
  14. package/federation/topo_order_memo.mjs +11 -4
  15. package/files/chunk_fetch.mjs +2 -2
  16. package/files/evfs.mjs +2 -2
  17. package/link/channel_mux.mjs +10 -10
  18. package/link/frame.mjs +76 -124
  19. package/link/handshake.mjs +5 -5
  20. package/link/pipe.mjs +49 -75
  21. package/link/providers/ble_gatt.mjs +14 -26
  22. package/link/providers/lan_tcp.mjs +17 -32
  23. package/link/providers/link_id_pipe.mjs +46 -0
  24. package/link/providers/webrtc.mjs +42 -51
  25. package/link/rtc.mjs +21 -9
  26. package/mailbox/deliver_or_store.mjs +1 -1
  27. package/node/identity.mjs +4 -4
  28. package/node/network.mjs +9 -9
  29. package/overlay/index.mjs +13 -13
  30. package/package.json +1 -1
  31. package/rooms/scoped_link.mjs +19 -35
  32. package/schemas/discovery.mjs +1 -2
  33. package/schemas/federation_pull.mjs +2 -2
  34. package/schemas/part_query.mjs +1 -1
  35. package/schemas/remote_event.mjs +1 -1
  36. package/transport/advert_ingest.mjs +20 -0
  37. package/transport/group_link_set.mjs +22 -41
  38. package/transport/ice_servers.mjs +2 -2
  39. package/transport/link_registry.mjs +113 -105
  40. package/transport/offer_answer.mjs +6 -2
  41. package/transport/peer_pool.mjs +53 -64
  42. package/transport/remote_user_room.mjs +13 -15
  43. package/transport/rtc_connection_budget.mjs +18 -4
  44. package/transport/runtime_bootstrap.mjs +8 -2
  45. package/transport/signal_crypto.mjs +25 -3
  46. package/transport/user_room.mjs +22 -15
  47. package/trust_graph/send.mjs +1 -1
  48. package/utils/emit_safe.mjs +11 -0
  49. package/utils/shuffle.mjs +13 -0
  50. package/utils/ttl_map.mjs +29 -4
  51. package/wire/ingress.mjs +1 -1
  52. package/wire/part_ingress.mjs +6 -8
  53. package/wire/part_invoke.mjs +4 -4
  54. package/wire/part_query.mjs +2 -3
  55. package/wire/part_query.tunables.json +6 -1
  56. package/wire/part_query_cache.mjs +1 -1
  57. package/wire/volatile_signature.mjs +1 -1
@@ -23,7 +23,7 @@ export function resolveRtcBudgetLimits(limits = {}) {
23
23
  maxActive: Math.max(4, Math.min(128, Number(limits.maxActive) || 32)),
24
24
  maxJoinsPerMin: Math.max(1, Math.min(120, Number(limits.maxJoinsPerMin) || 12)),
25
25
  overloadCooldownMs: Math.max(1000, Number(limits.overloadCooldownMs) || 15_000),
26
- trustedPeers: Array.isArray(limits.trustedPeers) ? limits.trustedPeers.map(String) : [],
26
+ trustedPeers: limits.trustedPeers || [],
27
27
  trustedReserveFraction: Math.max(0.1, Math.min(0.5, Number(limits.trustedReserveFraction) || DEFAULT_TRUSTED_RESERVE_FRACTION)),
28
28
  minTrustedReserved: Math.max(1, Math.floor(Number(limits.minTrustedReserved) || DEFAULT_MIN_TRUSTED_RESERVED)),
29
29
  }
@@ -85,10 +85,14 @@ export function takeRtcJoinSlot(roomKey, peerId, limits = {}, sourceId = 'peer')
85
85
  const isTrusted = peerId && bucket.trustedPeers.has(peerId)
86
86
  const trustedReserved = Math.max(minTrustedReserved, Math.floor(maxActive * trustedReserveFraction))
87
87
  const maxNonTrusted = Math.max(1, maxActive - trustedReserved)
88
- const nonTrustedCount = [...bucket.active].filter(id => !bucket.trustedPeers.has(id)).length
88
+ let nonTrustedCount = 0
89
+ for (const id of bucket.active)
90
+ if (!bucket.trustedPeers.has(id)) nonTrustedCount++
89
91
  if (!isTrusted) {
90
92
  const source = String(sourceId || 'peer')
91
- const sameSource = [...bucket.sourceByPeer.entries()].filter(([, s]) => s === source).length
93
+ let sameSource = 0
94
+ for (const peerSource of bucket.sourceByPeer.values())
95
+ if (peerSource === source) sameSource++
92
96
  const sourceCap = Math.max(1, Math.floor(maxActive * MAX_SOURCE_SLOT_FRACTION))
93
97
  if (sameSource >= sourceCap) return false
94
98
  if (nonTrustedCount >= maxNonTrusted && bucket.active.size >= maxActive) {
@@ -149,6 +153,16 @@ export function releaseRtcPeer(roomKey, peerId) {
149
153
  bucket.active.delete(peerId)
150
154
  bucket.sourceByPeer.delete(peerId)
151
155
  bucket.peerNodeHash.delete(peerId)
156
+ // 房间空闲后丢掉桶,避免 leave 过的 roomKey 永久占内存
157
+ if (!bucket.active.size) budgets.delete(roomKey)
158
+ }
159
+
160
+ /**
161
+ * 当前预算桶数量(测试用)。
162
+ * @returns {number} 房间桶数
163
+ */
164
+ export function rtcBudgetRoomCount() {
165
+ return budgets.size
152
166
  }
153
167
 
154
168
  /** RTC 过载时跳过的非关键联邦 action */
@@ -180,5 +194,5 @@ const NON_CRITICAL_FED_ACTIONS = new Set([
180
194
  */
181
195
  export function isFederationActionAllowedUnderLoad(roomKey, actionName, limits = {}) {
182
196
  if (!isRtcRoomOverloaded(roomKey, limits)) return true
183
- return !NON_CRITICAL_FED_ACTIONS.has(String(actionName || '').trim())
197
+ return !NON_CRITICAL_FED_ACTIONS.has(actionName)
184
198
  }
@@ -40,7 +40,7 @@ export function collectFastListenProviders(ownedLanTcp) {
40
40
  if (id.startsWith('lan_tcp') || id.startsWith('ble_gatt')) continue
41
41
  if (typeof provider.ensureListening !== 'function') continue
42
42
  if (providerHasNativeProbe(provider)) continue
43
- if (typeof provider.isAvailable === 'function') {
43
+ if (typeof provider.isAvailable === 'function')
44
44
  try {
45
45
  const available = provider.isAvailable()
46
46
  // 未标 probe:native 却返回 thenable:跳过,勿 await。
@@ -48,7 +48,7 @@ export function collectFastListenProviders(ownedLanTcp) {
48
48
  if (!available) continue
49
49
  }
50
50
  catch { continue }
51
- }
51
+
52
52
  listenProviders.push(provider)
53
53
  }
54
54
  return listenProviders
@@ -200,6 +200,9 @@ export function createRuntimeBootstrap(deps) {
200
200
  function chainStop(kind, stop) {
201
201
  if (kind === 'signal') {
202
202
  const prev = stopSignalListener
203
+ /**
204
+ *
205
+ */
203
206
  stopSignalListener = () => {
204
207
  try { stop() } catch { /* ignore */ }
205
208
  prev?.()
@@ -207,6 +210,9 @@ export function createRuntimeBootstrap(deps) {
207
210
  return
208
211
  }
209
212
  const prev = stopAdvert
213
+ /**
214
+ *
215
+ */
210
216
  stopAdvert = () => {
211
217
  try { stop() } catch { /* ignore */ }
212
218
  prev?.()
@@ -3,11 +3,15 @@ import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:
3
3
 
4
4
  import { normalizeHex64 } from '../core/hexIds.mjs'
5
5
  import { sha256Hex } from '../crypto/crypto.mjs'
6
+ import { createLruMap } from '../utils/lru.mjs'
6
7
 
7
8
  const SIGNAL_DOMAIN = 'fount-signal'
8
9
  const NODE_TOPIC_DOMAIN = 'fount-rdv-node:'
9
10
  const GROUP_TOPIC_DOMAIN = 'fount-rdv-group:'
10
11
 
12
+ /** topic → AES-256 密钥缓存上限(群/节点 topic 会随时间增长,须有界) */
13
+ export const SIGNAL_KEY_CACHE_MAX = 512
14
+
11
15
  /**
12
16
  * 由 nodeHash 派生节点 rendezvous topic。
13
17
  * @param {string} nodeHash 节点 64 hex
@@ -23,16 +27,34 @@ export function nodeRendezvousTopic(nodeHash) {
23
27
  * @returns {string} rendezvous topic 哈希
24
28
  */
25
29
  export function groupRendezvousTopic(roomSecret) {
26
- return sha256Hex(`${GROUP_TOPIC_DOMAIN}${String(roomSecret || '')}`)
30
+ return sha256Hex(`${GROUP_TOPIC_DOMAIN}${roomSecret}`)
27
31
  }
28
32
 
33
+ /** topic → AES-256 密钥 LRU */
34
+ const signalKeysByTopic = createLruMap(SIGNAL_KEY_CACHE_MAX)
35
+
29
36
  /**
30
- * 由 topic 派生信令 AES 密钥。
37
+ * 由 topic 派生信令 AES 密钥(同 topic 复用,避免每包 SHA-256)。
31
38
  * @param {string} topic rendezvous 主题
32
39
  * @returns {Buffer} AES-256 密钥
33
40
  */
34
41
  function signalKeyForTopic(topic) {
35
- return createHash('sha256').update(`${SIGNAL_DOMAIN}:${String(topic)}`).digest()
42
+ let cached = signalKeysByTopic.get(topic)
43
+ if (cached) {
44
+ signalKeysByTopic.touch(topic, cached)
45
+ return cached
46
+ }
47
+ cached = createHash('sha256').update(`${SIGNAL_DOMAIN}:${topic}`).digest()
48
+ signalKeysByTopic.touch(topic, cached)
49
+ return cached
50
+ }
51
+
52
+ /**
53
+ * 当前密钥缓存条目数(测试用)。
54
+ * @returns {number} 缓存大小
55
+ */
56
+ export function signalKeyCacheSize() {
57
+ return signalKeysByTopic.size
36
58
  }
37
59
 
38
60
  /**
@@ -3,12 +3,24 @@ import { createHash } from 'node:crypto'
3
3
  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
+ import { shuffleInPlace } from '../utils/shuffle.mjs'
6
7
  import { attachPartWire } from '../wire/part_ingress.mjs'
7
8
  import { attachPartQueryWire } from '../wire/part_query.mjs'
8
9
 
9
10
  import { listLinks, sendToNodeLink, subscribeScope, getLinkRegistry } from './link_registry.mjs'
10
11
  import { USER_ROOM_SCOPE } from './room_scopes.mjs'
11
12
 
13
+ /**
14
+ * 经 node scope 向对端发 action。
15
+ * @param {string} peerId 对端 nodeHash
16
+ * @param {string} action 动作名
17
+ * @param {unknown} payload 载荷
18
+ * @returns {Promise<boolean>} 是否发出
19
+ */
20
+ const sendNodeAction = (peerId, action, payload) =>
21
+ sendToNodeLink(peerId, { scope: 'node', action, payload })
22
+
23
+
12
24
  /**
13
25
  * 在 node scope action 表上注册 fed_chunk_get / fed_chunk_data handler,
14
26
  * 供用户房间(本地 + 远端)双向 chunk 传输使用。
@@ -63,9 +75,8 @@ function createNodeScopeWire() {
63
75
  * @returns {void}
64
76
  */
65
77
  on(name, handler) {
66
- const key = String(name)
67
- if (!nodeActionHandlers.has(key)) nodeActionHandlers.set(key, new Set())
68
- nodeActionHandlers.get(key).add(handler)
78
+ if (!nodeActionHandlers.has(name)) nodeActionHandlers.set(name, new Set())
79
+ nodeActionHandlers.get(name).add(handler)
69
80
  },
70
81
  /**
71
82
  * 向指定 peer 发送 node scope action。
@@ -76,7 +87,7 @@ function createNodeScopeWire() {
76
87
  */
77
88
  send(name, payload, peerId) {
78
89
  if (!peerId) return
79
- void sendToNodeLink(peerId, { scope: 'node', action: String(name), payload }).catch(() => { })
90
+ void sendNodeAction(peerId, name, payload).catch(() => { })
80
91
  },
81
92
  }
82
93
  }
@@ -96,9 +107,9 @@ function activeLinkRoster() {
96
107
  */
97
108
  async function ensureNodeScopeRuntime(wireContext) {
98
109
  if (nodeScopeCleanup) return
99
- nodeScopeReplicaUsername = String(wireContext.replicaUsername || nodeScopeReplicaUsername || '')
110
+ nodeScopeReplicaUsername = wireContext.replicaUsername || nodeScopeReplicaUsername || ''
100
111
  nodeScopeCleanup = subscribeScope('node', (senderNodeHash, envelope) => {
101
- const handlers = nodeActionHandlers.get(String(envelope?.action || ''))
112
+ const handlers = nodeActionHandlers.get(envelope.action)
102
113
  if (!handlers?.size) return
103
114
  for (const handler of handlers)
104
115
  try { handler(envelope.payload, senderNodeHash) } catch { /* ignore */ }
@@ -188,7 +199,7 @@ export async function ensureUserRoom(wireContext = {}) {
188
199
  * @returns {void}
189
200
  */
190
201
  sendToPeer(peerId, actionName, payload) {
191
- void sendToNodeLink(peerId, { scope: 'node', action: String(actionName), payload }).catch(() => { })
202
+ void sendNodeAction(peerId, actionName, payload).catch(() => { })
192
203
  },
193
204
  /**
194
205
  * 返回当前活跃链路 roster。
@@ -201,7 +212,7 @@ export async function ensureUserRoom(wireContext = {}) {
201
212
  * @returns {string | null} 对端 id;无链路时为 null
202
213
  */
203
214
  getPeerIdByNodeHash(nodeHash) {
204
- return getLinkRegistry().getLink(nodeHash) ? String(nodeHash) : null
215
+ return getLinkRegistry().getLink(nodeHash) ? nodeHash : null
205
216
  },
206
217
  }
207
218
  return userRoomSlot
@@ -234,15 +245,11 @@ export async function deliverToUserRoomPeers(username, actionName, payload, exce
234
245
  if (!slot) return 0
235
246
  const body = { ...payload, nodeHash: getNodeHash() }
236
247
  let sent = 0
237
- const peers = [...slot.getRoster()
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
- }
248
+ const peers = shuffleInPlace(slot.getRoster()
249
+ .filter(({ peerId }) => peerId && peerId !== exceptPeerId))
243
250
  for (const { peerId } of peers)
244
251
  try {
245
- if (await sendToNodeLink(peerId, { scope: 'node', action: String(actionName), payload: body }))
252
+ if (await sendNodeAction(peerId, actionName, body))
246
253
  sent++
247
254
  if (sent >= fanoutLimit) break
248
255
  }
@@ -26,7 +26,7 @@ export async function sendToNode(username, targetNodeHash, actionName, payload,
26
26
  await ensureUserRoom({ replicaUsername: username })
27
27
 
28
28
  // 已直连 peer 不经 trust-graph scope 也应能收发 node scope action(非成员 CAS chunk / follow hint 等)
29
- if (await sendToNodeLink(target, { scope: 'node', action: String(actionName), payload }))
29
+ if (await sendToNodeLink(target, { scope: 'node', action: actionName, payload }))
30
30
  return true
31
31
 
32
32
  const merged = graph ?? await buildMergedGraph(username)
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 调用一组 listener,单次抛错不影响其余。
3
+ * @param {Iterable<Function>} listeners 回调集合
4
+ * @param {...unknown} args 传给每个 listener 的参数
5
+ * @returns {void}
6
+ */
7
+ export function emitSafe(listeners, ...args) {
8
+ for (const listener of listeners)
9
+ try { listener(...args) }
10
+ catch { /* ignore */ }
11
+ }
@@ -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
- * @returns {{ ttlMs: number, set: (key: string, value: T) => void, get: (key: string, now?: number) => T | null, clear: () => void }} TTL Map 句柄
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
- map.set(key, { value, seenAt: Date.now() })
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 == null) return null
22
+ if (!raw) return null
23
23
  const text = typeof raw === 'string'
24
24
  ? raw
25
25
  : Buffer.isBuffer(raw)
@@ -116,7 +116,7 @@ export async function handleIncomingPartInvokeRequest(wireContext, payload, wire
116
116
  if (!partpath || !payload.requestId) return
117
117
 
118
118
  const response = await dispatchPartInvoke(wireContext, { ...payload, peerId })
119
- if (response == null || !isPartInvokeResponse(response)) return
119
+ if (!isPartInvokeResponse(response)) return
120
120
 
121
121
  try {
122
122
  wire.send('part_invoke_response', {
@@ -159,14 +159,12 @@ export async function handleIncomingPartInvokeFireAndForget(wireContext, payload
159
159
  * @returns {void}
160
160
  */
161
161
  export function handleIncomingPartInvokeResponse(payload, peerId = '') {
162
- const pending = pendingPartInvoke.get(String(payload?.requestId || ''))
163
- if (!pending || payload?.response == null) return
164
- const peerKey = String(peerId || payload?.nodeHash || '').trim()
165
- if (peerKey) {
166
- if (pending.respondedPeers.has(peerKey)) return
167
- pending.respondedPeers.add(peerKey)
162
+ const pending = pendingPartInvoke.get(payload.requestId)
163
+ if (!pending || !isPartInvokeResponse(payload.response)) return
164
+ if (peerId) {
165
+ if (pending.respondedPeers.has(peerId)) return
166
+ pending.respondedPeers.add(peerId)
168
167
  }
169
- if (!isPartInvokeResponse(payload.response)) return
170
168
  pending.responses.push(payload.response)
171
169
  if (pending.responses.length >= pending.maxResponses) pending.finish()
172
170
  }
@@ -45,9 +45,9 @@ export function isPartInvokeResponse(value) {
45
45
  const err = value.error
46
46
  return isPlainObject(err)
47
47
  && typeof err.message === 'string'
48
- && err.message.length > 0
49
48
  && typeof err.code === 'string'
50
- && err.code.length > 0
49
+ && err.message.length
50
+ && err.code.length
51
51
  }
52
52
  return true
53
53
  }
@@ -57,7 +57,7 @@ export function isPartInvokeResponse(value) {
57
57
  * @returns {unknown | null} 成功 `result`;失败或空为 null
58
58
  */
59
59
  export function unwrapPartInvokeResult(response) {
60
- if (!response || !isPartInvokeResponse(response) || 'error' in response) return null
60
+ if (!isPartInvokeResponse(response) || 'error' in response) return null
61
61
  return response.result ?? null
62
62
  }
63
63
 
@@ -77,5 +77,5 @@ export function normalizePartpath(value) {
77
77
  export function isPartInvoke(value) {
78
78
  if (!isPlainObject(value)) return false
79
79
  const { kind } = value
80
- return typeof kind === 'string' && kind.length > 0
80
+ return typeof kind === 'string' && kind.length
81
81
  }
@@ -132,7 +132,7 @@ export function registerQueryInboundHandler(partpath, kind, handler, state = def
132
132
  * @returns {number} 等待下游超时
133
133
  */
134
134
  export function resolvePartQueryHopTimeoutMs(ttl, tunables = partQueryTunables) {
135
- const table = Array.isArray(tunables.hopTimeoutMs) ? tunables.hopTimeoutMs : [1000, 2500, 4000, 6000]
135
+ const table = tunables.hopTimeoutMs || [1000, 2500, 4000, 6000]
136
136
  const index = Math.max(0, Math.min(table.length - 1, Math.floor(ttl) - 1))
137
137
  return Math.max(1, Math.floor(Number(table[index]) || tunables.defaultTimeoutMs || 4000))
138
138
  }
@@ -176,8 +176,7 @@ export function mergeQueryRows(lists, maxHits, rowKey) {
176
176
  async function runLocalHandler(state, queryContext, partpath, kind, query) {
177
177
  const handler = state.handlers.get(handlerKey(partpath, kind))
178
178
  if (!handler) return []
179
- const rows = await handler(queryContext, query)
180
- return Array.isArray(rows) ? rows : []
179
+ return await handler(queryContext, query) || []
181
180
  }
182
181
 
183
182
  /**
@@ -9,6 +9,11 @@
9
9
  "dedupeMaxSize": 4000,
10
10
  "cacheTtlMs": 300000,
11
11
  "cacheMaxKeys": 256,
12
- "hopTimeoutMs": [1000, 2500, 4000, 6000],
12
+ "hopTimeoutMs": [
13
+ 1000,
14
+ 2500,
15
+ 4000,
16
+ 6000
17
+ ],
13
18
  "defaultTimeoutMs": 4000
14
19
  }
@@ -77,7 +77,7 @@ export function createPartQueryCache(options = {}) {
77
77
  */
78
78
  set(partpath, kind, query, rows, now = Date.now()) {
79
79
  const key = partQueryCacheKey(partpath, kind, query)
80
- if (!key || !Array.isArray(rows)) return
80
+ if (!key || !rows) return
81
81
  sweep(now)
82
82
  map.touch(key, {
83
83
  rows: rows.slice(0, maxHits),
@@ -17,7 +17,7 @@ export const MAX_STREAM_VOLATILE_SLICES = 256
17
17
  * @returns {object[] | null} 合法切片或 null
18
18
  */
19
19
  export function boundStreamSlices(slices) {
20
- if (!Array.isArray(slices) || slices.length > MAX_STREAM_VOLATILE_SLICES) return null
20
+ if (!slices?.length || slices.length > MAX_STREAM_VOLATILE_SLICES) return null
21
21
  return slices
22
22
  }
23
23