@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.
- package/README.md +1 -1
- package/core/bytes_codec.mjs +65 -10
- package/core/tcp_port.mjs +1 -1
- package/crypto/checkpoint_sign.mjs +2 -2
- package/dag/canonicalize_row.mjs +3 -3
- package/dag/index.mjs +2 -3
- package/discovery/bt/index.mjs +5 -5
- package/discovery/bt/runtime.mjs +3 -3
- package/discovery/mdns.mjs +7 -1
- package/discovery/nostr.mjs +53 -99
- package/federation/chunk_fetch_pending.mjs +4 -5
- package/federation/dag_order_cache.mjs +1 -1
- package/federation/entity_key_chain.mjs +3 -4
- package/federation/topo_order_memo.mjs +11 -4
- package/files/chunk_fetch.mjs +2 -2
- package/files/evfs.mjs +2 -2
- package/link/channel_mux.mjs +10 -10
- package/link/frame.mjs +76 -124
- package/link/handshake.mjs +5 -5
- package/link/pipe.mjs +49 -75
- package/link/providers/ble_gatt.mjs +14 -26
- package/link/providers/lan_tcp.mjs +17 -32
- package/link/providers/link_id_pipe.mjs +46 -0
- package/link/providers/webrtc.mjs +42 -51
- package/link/rtc.mjs +21 -9
- package/mailbox/deliver_or_store.mjs +1 -1
- package/node/identity.mjs +4 -4
- package/node/network.mjs +9 -9
- package/overlay/index.mjs +13 -13
- package/package.json +1 -1
- package/rooms/scoped_link.mjs +19 -35
- package/schemas/discovery.mjs +1 -2
- package/schemas/federation_pull.mjs +2 -2
- package/schemas/part_query.mjs +1 -1
- package/schemas/remote_event.mjs +1 -1
- package/transport/advert_ingest.mjs +20 -0
- package/transport/group_link_set.mjs +22 -41
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +113 -105
- package/transport/offer_answer.mjs +6 -2
- package/transport/peer_pool.mjs +53 -64
- package/transport/remote_user_room.mjs +13 -15
- package/transport/rtc_connection_budget.mjs +18 -4
- package/transport/runtime_bootstrap.mjs +8 -2
- package/transport/signal_crypto.mjs +25 -3
- package/transport/user_room.mjs +22 -15
- package/trust_graph/send.mjs +1 -1
- package/utils/emit_safe.mjs +11 -0
- package/utils/shuffle.mjs +13 -0
- package/utils/ttl_map.mjs +29 -4
- package/wire/ingress.mjs +1 -1
- package/wire/part_ingress.mjs +6 -8
- package/wire/part_invoke.mjs +4 -4
- package/wire/part_query.mjs +2 -3
- package/wire/part_query.tunables.json +6 -1
- package/wire/part_query_cache.mjs +1 -1
- 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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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}${
|
|
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
|
-
|
|
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
|
/**
|
package/transport/user_room.mjs
CHANGED
|
@@ -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
|
-
|
|
67
|
-
|
|
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
|
|
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 =
|
|
110
|
+
nodeScopeReplicaUsername = wireContext.replicaUsername || nodeScopeReplicaUsername || ''
|
|
100
111
|
nodeScopeCleanup = subscribeScope('node', (senderNodeHash, envelope) => {
|
|
101
|
-
const handlers = nodeActionHandlers.get(
|
|
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
|
|
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) ?
|
|
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 =
|
|
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
|
|
252
|
+
if (await sendNodeAction(peerId, actionName, body))
|
|
246
253
|
sent++
|
|
247
254
|
if (sent >= fanoutLimit) break
|
|
248
255
|
}
|
package/trust_graph/send.mjs
CHANGED
|
@@ -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:
|
|
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
|
-
* @
|
|
5
|
+
* @param {number} [maxSize=4096] 最大条目(防只写不读时过期项堆积)
|
|
6
|
+
* @returns {{ ttlMs: number, maxSize: number, size: () => number, set: (key: string, value: T) => void, get: (key: string, now?: number) => T | null, clear: () => void }} TTL Map 句柄
|
|
6
7
|
*/
|
|
7
|
-
export function createTtlMap(ttlMs) {
|
|
8
|
+
export function createTtlMap(ttlMs, maxSize = 4096) {
|
|
9
|
+
const cap = Math.max(1, Math.floor(Number(maxSize) || 4096))
|
|
8
10
|
/** @type {Map<string, { value: T, seenAt: number }>} */
|
|
9
11
|
const map = new Map()
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 删除过期项;若仍满则按插入序驱逐最旧。
|
|
15
|
+
* @param {number} now 当前时间
|
|
16
|
+
* @returns {void}
|
|
17
|
+
*/
|
|
18
|
+
function prune(now) {
|
|
19
|
+
for (const [key, entry] of map)
|
|
20
|
+
if (now - entry.seenAt > ttlMs) map.delete(key)
|
|
21
|
+
while (map.size >= cap) {
|
|
22
|
+
const oldest = map.keys().next().value
|
|
23
|
+
map.delete(oldest)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
return {
|
|
11
28
|
ttlMs,
|
|
29
|
+
maxSize: cap,
|
|
30
|
+
/**
|
|
31
|
+
* @returns {number} 当前条目数(含未 get 的过期项)
|
|
32
|
+
*/
|
|
33
|
+
size: () => map.size,
|
|
12
34
|
/**
|
|
13
35
|
* @param {string} key 键
|
|
14
36
|
* @param {T} value 值
|
|
15
37
|
* @returns {void}
|
|
16
38
|
*/
|
|
17
39
|
set(key, value) {
|
|
18
|
-
|
|
40
|
+
const now = Date.now()
|
|
41
|
+
if (map.has(key)) map.delete(key)
|
|
42
|
+
if (map.size >= cap) prune(now)
|
|
43
|
+
map.set(key, { value, seenAt: now })
|
|
19
44
|
},
|
|
20
45
|
/**
|
|
21
46
|
* @param {string} key 键
|
package/wire/ingress.mjs
CHANGED
|
@@ -19,7 +19,7 @@ export function isPlainObject(value) {
|
|
|
19
19
|
* @returns {Record<string, unknown> | null} 解析失败或非对象时为 null
|
|
20
20
|
*/
|
|
21
21
|
export function parseInboundJson(raw) {
|
|
22
|
-
if (raw
|
|
22
|
+
if (!raw) return null
|
|
23
23
|
const text = typeof raw === 'string'
|
|
24
24
|
? raw
|
|
25
25
|
: Buffer.isBuffer(raw)
|
package/wire/part_ingress.mjs
CHANGED
|
@@ -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 (
|
|
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(
|
|
163
|
-
if (!pending || payload
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
}
|
package/wire/part_invoke.mjs
CHANGED
|
@@ -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.
|
|
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 (!
|
|
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
|
|
80
|
+
return typeof kind === 'string' && kind.length
|
|
81
81
|
}
|
package/wire/part_query.mjs
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
180
|
-
return Array.isArray(rows) ? rows : []
|
|
179
|
+
return await handler(queryContext, query) || []
|
|
181
180
|
}
|
|
182
181
|
|
|
183
182
|
/**
|
|
@@ -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 || !
|
|
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 (!
|
|
20
|
+
if (!slices?.length || slices.length > MAX_STREAM_VOLATILE_SLICES) return null
|
|
21
21
|
return slices
|
|
22
22
|
}
|
|
23
23
|
|