@steve02081504/fount-p2p 0.0.11 → 0.0.13
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 +42 -11
- package/core/bytes_codec.mjs +65 -10
- package/core/composite_key.mjs +5 -5
- package/core/tcp_port.mjs +1 -1
- package/crypto/checkpoint_sign.mjs +2 -2
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/canonicalize_row.mjs +3 -3
- package/dag/index.mjs +2 -3
- package/dag/storage.mjs +36 -36
- package/discovery/advert_peer_hints.mjs +1 -1
- package/discovery/adverts.mjs +109 -0
- package/discovery/bt/index.mjs +154 -113
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +5 -12
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +430 -141
- package/federation/chunk_fetch_pending.mjs +14 -18
- package/federation/dag_order_cache.mjs +1 -1
- package/federation/entity_key_chain.mjs +3 -4
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/federation/topo_order_memo.mjs +11 -4
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_fetch.mjs +2 -2
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +31 -30
- package/files/manifest_fetch.mjs +16 -3
- package/files/public_manifest.mjs +11 -11
- package/files/transfer_key_registry.mjs +2 -2
- package/index.mjs +86 -11
- package/infra/cli.mjs +62 -0
- package/infra/debug_log.mjs +56 -0
- package/infra/default_node_dir.mjs +22 -0
- package/infra/priority.mjs +56 -0
- package/infra/service.mjs +140 -0
- package/infra/tunables.json +5 -0
- package/link/channel_mux.mjs +10 -10
- package/link/frame.mjs +76 -131
- package/link/handshake.mjs +19 -20
- package/link/pipe.mjs +53 -79
- package/link/providers/ble_gatt.mjs +19 -31
- 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 +24 -12
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +6 -6
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/identity.mjs +4 -4
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +46 -14
- package/node/reputation_store.mjs +4 -4
- package/node/reputation_sync.mjs +318 -0
- package/node/routing_profile.mjs +21 -0
- package/node/signaling_config.mjs +30 -11
- package/overlay/index.mjs +35 -14
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -182
- 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 +17 -0
- package/transport/group_link_set.mjs +165 -134
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +283 -124
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +48 -47
- package/transport/peer_pool.mjs +169 -78
- package/transport/remote_user_room.mjs +18 -23
- package/transport/rtc_connection_budget.mjs +18 -4
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +172 -104
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +93 -161
- package/trust_graph/build.mjs +0 -2
- package/trust_graph/cache.mjs +12 -3
- package/trust_graph/registry.mjs +6 -6
- package/trust_graph/send.mjs +1 -4
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +11 -0
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/utils/shuffle.mjs +13 -0
- package/utils/ttl_map.mjs +29 -4
- package/wire/ingress.mjs +1 -1
- package/wire/part_ingress.mjs +38 -36
- package/wire/part_invoke.mjs +4 -4
- package/wire/part_query.mjs +28 -23
- package/wire/part_query.tunables.json +6 -1
- package/wire/part_query_cache.mjs +1 -1
- package/wire/volatile_signature.mjs +1 -1
- package/discovery/mdns.mjs +0 -191
- package/transport/signal_crypto.mjs +0 -82
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
|
+
import { listVisibleNodeHashes as discoveryListVisible } from '../discovery/index.mjs'
|
|
3
|
+
import { nodeDebug, shortHash } from '../node/log.mjs'
|
|
4
|
+
import { applyNetworkHint, loadPeerPoolView, promoteExplorePeer } from '../node/network.mjs'
|
|
5
|
+
import { loadReputation } from '../node/reputation_store.mjs'
|
|
6
|
+
import { getRoutingProfile } from '../node/routing_profile.mjs'
|
|
7
|
+
|
|
8
|
+
import { pickMeshEvictionVictim, resolveMeshPoolLimits, selectMeshLinkTargets } from './peer_pool.mjs'
|
|
9
|
+
import { loadTransportTunables } from './tunables.mjs'
|
|
10
|
+
|
|
11
|
+
/** 本机主动关链:不清槽重拨,由下次 tick / 调用方决定。 */
|
|
12
|
+
const INTENTIONAL_CLOSE = new Set([
|
|
13
|
+
'budget-evict',
|
|
14
|
+
'manual-close',
|
|
15
|
+
'registry-shutdown',
|
|
16
|
+
'inbound-no-nodehash',
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string | undefined} reason 关链原因
|
|
21
|
+
* @returns {boolean} 是否为本机主动关链(不重拨)
|
|
22
|
+
*/
|
|
23
|
+
export function isMeshIntentionalClose(reason) {
|
|
24
|
+
return INTENTIONAL_CLOSE.has(String(reason || ''))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 创建 mesh 保活控制器:扫描可见节点、按 N/K 拨号、稳定探索晋升熟人。
|
|
29
|
+
* @param {object} deps 依赖
|
|
30
|
+
* @param {object} deps.registry link registry(ensureLinkToNode / listLinks / onLinkUp / onLinkDown)
|
|
31
|
+
* @param {boolean} [deps.enabled=true] 是否启用
|
|
32
|
+
* @returns {{ exploreLinkHashes: Set<string>, start: () => void, stop: () => Promise<void> }} mesh 保活控制器
|
|
33
|
+
*/
|
|
34
|
+
export function createMeshKeepalive(deps) {
|
|
35
|
+
const { registry, enabled = true } = deps
|
|
36
|
+
const tunables = loadTransportTunables()
|
|
37
|
+
/** @type {ReturnType<typeof setInterval> | null} */
|
|
38
|
+
let timer = null
|
|
39
|
+
/** @type {Set<string>} */
|
|
40
|
+
const exploreLinks = new Set()
|
|
41
|
+
/** @type {Map<string, number>} */
|
|
42
|
+
const exploreStableSince = new Map()
|
|
43
|
+
/** @type {(() => void) | null} */
|
|
44
|
+
let stopLinkDown = null
|
|
45
|
+
/** @type {(() => void) | null} */
|
|
46
|
+
let stopLinkUp = null
|
|
47
|
+
/** @type {Promise<void> | null} */
|
|
48
|
+
let tickInflight = null
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 非熟人活跃链记入探索槽(含入站);熟人则清掉探索标记。
|
|
52
|
+
* @param {string} nodeHash 对端
|
|
53
|
+
* @param {string[]} [trustedPeers] 熟人表;省略则读盘
|
|
54
|
+
* @returns {void}
|
|
55
|
+
*/
|
|
56
|
+
function syncExploreMark(nodeHash, trustedPeers) {
|
|
57
|
+
const hash = normalizeHex64(nodeHash)
|
|
58
|
+
if (!hash) return
|
|
59
|
+
const trusted = trustedPeers ?? loadPeerPoolView().trustedPeers
|
|
60
|
+
if (trusted.includes(hash)) {
|
|
61
|
+
exploreLinks.delete(hash)
|
|
62
|
+
exploreStableSince.delete(hash)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
exploreLinks.add(hash)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 为拨号腾出空位:优先踢探索。
|
|
70
|
+
* @param {number} needSlots 需要空位
|
|
71
|
+
* @param {string[]} trustedPeers 熟人表
|
|
72
|
+
* @returns {Promise<void>}
|
|
73
|
+
*/
|
|
74
|
+
async function evictExploreForRoom(needSlots, trustedPeers) {
|
|
75
|
+
for (let i = 0; i < needSlots; i++) {
|
|
76
|
+
const connected = registry.listLinks().map(entry => entry.nodeHash)
|
|
77
|
+
const victimHash = pickMeshEvictionVictim(connected, exploreLinks, trustedPeers, () => 0)
|
|
78
|
+
if (!victimHash || trustedPeers.includes(victimHash)) break
|
|
79
|
+
const entry = registry.listLinks().find(item => item.nodeHash === victimHash)
|
|
80
|
+
if (!entry?.link?.close) break
|
|
81
|
+
await entry.link.close('budget-evict')
|
|
82
|
+
exploreLinks.delete(victimHash)
|
|
83
|
+
exploreStableSince.delete(victimHash)
|
|
84
|
+
nodeDebug('p2p:mesh evict', { peer: shortHash(victimHash), reason: 'budget-evict' })
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @returns {Promise<void>}
|
|
90
|
+
*/
|
|
91
|
+
async function tick() {
|
|
92
|
+
if (!enabled) return
|
|
93
|
+
const limits = resolveMeshPoolLimits(getRoutingProfile(), tunables)
|
|
94
|
+
const promoteMs = Math.max(60_000, Number(tunables.meshPromoteStableMs) || 30 * 60_000)
|
|
95
|
+
const scanLimit = Math.max(8, Number(tunables.meshScanLimit) || 64)
|
|
96
|
+
const visible = await discoveryListVisible({ limit: scanLimit })
|
|
97
|
+
for (const hash of visible)
|
|
98
|
+
applyNetworkHint({ nodeHash: hash, source: 'mesh:scan', kind: 'visible', weight: 0.15 })
|
|
99
|
+
|
|
100
|
+
const peers = loadPeerPoolView()
|
|
101
|
+
const rep = loadReputation()
|
|
102
|
+
const now = Date.now()
|
|
103
|
+
const connected = new Set(registry.listLinks().map(entry => entry.nodeHash))
|
|
104
|
+
for (const nodeHash of connected)
|
|
105
|
+
syncExploreMark(nodeHash, peers.trustedPeers)
|
|
106
|
+
|
|
107
|
+
const targets = selectMeshLinkTargets({
|
|
108
|
+
selfNodeHash: registry.localIdentity.nodeHash,
|
|
109
|
+
trustedPeers: peers.trustedPeers,
|
|
110
|
+
exploreCandidates: [...new Set([...peers.explorePeers, ...visible])],
|
|
111
|
+
hintSources: peers.hintSources,
|
|
112
|
+
limits,
|
|
113
|
+
connectedHashes: connected,
|
|
114
|
+
rep,
|
|
115
|
+
blockedPeers: peers.blockedPeers,
|
|
116
|
+
now,
|
|
117
|
+
})
|
|
118
|
+
const toDial = targets.filter(nodeHash => !connected.has(nodeHash))
|
|
119
|
+
nodeDebug('p2p:mesh tick', {
|
|
120
|
+
N: limits.N,
|
|
121
|
+
K_max: limits.K_max,
|
|
122
|
+
visible: visible.map(hash => shortHash(hash)),
|
|
123
|
+
connected: [...connected].map(hash => shortHash(hash)),
|
|
124
|
+
dial: toDial.map(hash => shortHash(hash)),
|
|
125
|
+
})
|
|
126
|
+
const overflow = connected.size + toDial.length - limits.N
|
|
127
|
+
if (overflow > 0)
|
|
128
|
+
await evictExploreForRoom(overflow, peers.trustedPeers)
|
|
129
|
+
|
|
130
|
+
for (const nodeHash of toDial)
|
|
131
|
+
void registry.ensureLinkToNode(nodeHash).then(link => {
|
|
132
|
+
if (link) {
|
|
133
|
+
syncExploreMark(nodeHash, peers.trustedPeers)
|
|
134
|
+
nodeDebug('p2p:mesh dial ok', { peer: shortHash(nodeHash), provider: link.providerId })
|
|
135
|
+
}
|
|
136
|
+
else
|
|
137
|
+
nodeDebug('p2p:mesh dial miss', { peer: shortHash(nodeHash) })
|
|
138
|
+
}).catch(error => {
|
|
139
|
+
nodeDebug('p2p:mesh dial fail', {
|
|
140
|
+
peer: shortHash(nodeHash),
|
|
141
|
+
err: String(error?.message || error),
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const connectedAfter = new Set(registry.listLinks().map(entry => entry.nodeHash))
|
|
146
|
+
for (const nodeHash of connectedAfter) {
|
|
147
|
+
if (peers.trustedPeers.includes(nodeHash)) {
|
|
148
|
+
exploreStableSince.delete(nodeHash)
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
if (!exploreLinks.has(nodeHash)) continue
|
|
152
|
+
const since = exploreStableSince.get(nodeHash) ?? now
|
|
153
|
+
if (!exploreStableSince.has(nodeHash)) exploreStableSince.set(nodeHash, since)
|
|
154
|
+
else if (now - since >= promoteMs) {
|
|
155
|
+
promoteExplorePeer(nodeHash)
|
|
156
|
+
exploreLinks.delete(nodeHash)
|
|
157
|
+
exploreStableSince.delete(nodeHash)
|
|
158
|
+
nodeDebug('p2p:mesh promote', { peer: shortHash(nodeHash) })
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const hash of [...exploreStableSince.keys()])
|
|
162
|
+
if (!connectedAfter.has(hash)) exploreStableSince.delete(hash)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @returns {Promise<void>}
|
|
167
|
+
*/
|
|
168
|
+
function runTick() {
|
|
169
|
+
if (tickInflight) return tickInflight
|
|
170
|
+
tickInflight = tick().catch(error => {
|
|
171
|
+
nodeDebug('p2p:mesh tick fail', { err: String(error?.message || error) })
|
|
172
|
+
}).finally(() => { tickInflight = null })
|
|
173
|
+
return tickInflight
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
/** @returns {Set<string>} 探索链路集合(trim 时优先驱逐) */
|
|
178
|
+
exploreLinkHashes: exploreLinks,
|
|
179
|
+
/**
|
|
180
|
+
* 启动 mesh 扫描 / 拨号 / 晋升循环。
|
|
181
|
+
* @returns {void}
|
|
182
|
+
*/
|
|
183
|
+
start() {
|
|
184
|
+
if (!enabled || timer) return
|
|
185
|
+
nodeDebug('p2p:mesh start', { self: shortHash(registry.localIdentity?.nodeHash) })
|
|
186
|
+
stopLinkUp = registry.onLinkUp?.(nodeHash => {
|
|
187
|
+
syncExploreMark(nodeHash)
|
|
188
|
+
}) ?? null
|
|
189
|
+
stopLinkDown = registry.onLinkDown((nodeHash, reason) => {
|
|
190
|
+
const hash = normalizeHex64(nodeHash)
|
|
191
|
+
exploreLinks.delete(hash)
|
|
192
|
+
exploreStableSince.delete(hash)
|
|
193
|
+
nodeDebug('p2p:mesh link down', { peer: shortHash(hash), reason })
|
|
194
|
+
// 主动关链不立刻补洞(避免 budget-evict 刚踢又连);意外断链用 tick 按 N/K 重选,而非粘住原对端。
|
|
195
|
+
if (isMeshIntentionalClose(reason)) return
|
|
196
|
+
void runTick()
|
|
197
|
+
})
|
|
198
|
+
void runTick()
|
|
199
|
+
timer = setInterval(() => { void runTick() }, Math.max(15_000, Number(tunables.meshKeepaliveIntervalMs) || 60_000))
|
|
200
|
+
timer.unref?.()
|
|
201
|
+
},
|
|
202
|
+
/**
|
|
203
|
+
* 停止保活并清空探索标记。
|
|
204
|
+
* @returns {Promise<void>}
|
|
205
|
+
*/
|
|
206
|
+
async stop() {
|
|
207
|
+
if (timer) { clearInterval(timer); timer = null }
|
|
208
|
+
stopLinkUp?.()
|
|
209
|
+
stopLinkUp = null
|
|
210
|
+
stopLinkDown?.()
|
|
211
|
+
stopLinkDown = null
|
|
212
|
+
exploreLinks.clear()
|
|
213
|
+
exploreStableSince.clear()
|
|
214
|
+
if (tickInflight) await tickInflight.catch(() => { })
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { attachNodeScopeFedChunkResponder } from '../files/chunk_responder.mjs'
|
|
2
|
+
import { attachMailboxWire } from '../mailbox/wire.mjs'
|
|
3
|
+
import { attachPartWire } from '../wire/part_ingress.mjs'
|
|
4
|
+
import { attachPartQueryWire } from '../wire/part_query.mjs'
|
|
5
|
+
|
|
6
|
+
import { sendToNodeLink, subscribeScope } from './link_registry.mjs'
|
|
7
|
+
|
|
8
|
+
/** @type {Map<string, Set<(payload: unknown, peerId: string) => void>>} */
|
|
9
|
+
const nodeActionHandlers = new Map()
|
|
10
|
+
|
|
11
|
+
/** @type {Set<(context: NodeScopeContext, wire: NodeScopeWire) => void>} */
|
|
12
|
+
const nodeScopeWireHooks = new Set()
|
|
13
|
+
|
|
14
|
+
/** @type {NodeScopeContext} */
|
|
15
|
+
const nodeScopeContext = { replicaUsername: '' }
|
|
16
|
+
|
|
17
|
+
/** @type {NodeScopeWire | null} */
|
|
18
|
+
let nodeScopeWire = null
|
|
19
|
+
|
|
20
|
+
/** @type {(() => void) | null} */
|
|
21
|
+
let nodeScopeSubscribeCleanup = null
|
|
22
|
+
|
|
23
|
+
/** @type {Set<() => void>} */
|
|
24
|
+
const nodeScopeFeatureDisposers = new Set()
|
|
25
|
+
|
|
26
|
+
/** @type {Map<string, { count: number, disposeCore: () => void }>} */
|
|
27
|
+
const featureAttachRefs = new Map()
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {{ replicaUsername: string }} NodeScopeContext
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {{
|
|
35
|
+
* on: (name: string, handler: (payload: unknown, peerId: string) => void) => () => void
|
|
36
|
+
* send: (name: string, payload: unknown, peerId: string | null) => void
|
|
37
|
+
* }} NodeScopeWire
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} peerId 对端 nodeHash
|
|
42
|
+
* @param {string} action 动作名
|
|
43
|
+
* @param {unknown} payload 载荷
|
|
44
|
+
* @returns {Promise<boolean>} 是否成功发出
|
|
45
|
+
*/
|
|
46
|
+
const sendNodeAction = (peerId, action, payload) =>
|
|
47
|
+
sendToNodeLink(peerId, { scope: 'node', action, payload })
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @returns {NodeScopeWire} node scope 的 wire 适配器
|
|
51
|
+
*/
|
|
52
|
+
function createNodeScopeWire() {
|
|
53
|
+
return {
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} name - action 名
|
|
56
|
+
* @param {(payload: unknown, peerId: string) => void} handler - 入站处理器
|
|
57
|
+
* @returns {() => void} 取消注册的 dispose
|
|
58
|
+
*/
|
|
59
|
+
on(name, handler) {
|
|
60
|
+
if (!nodeActionHandlers.has(name)) nodeActionHandlers.set(name, new Set())
|
|
61
|
+
nodeActionHandlers.get(name).add(handler)
|
|
62
|
+
return () => {
|
|
63
|
+
const set = nodeActionHandlers.get(name)
|
|
64
|
+
if (!set) return
|
|
65
|
+
set.delete(handler)
|
|
66
|
+
if (!set.size) nodeActionHandlers.delete(name)
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} name - action 名
|
|
71
|
+
* @param {unknown} payload - 出站载荷
|
|
72
|
+
* @param {string | null} peerId - 目标 peer,null 时忽略
|
|
73
|
+
* @returns {void}
|
|
74
|
+
*/
|
|
75
|
+
send(name, payload, peerId) {
|
|
76
|
+
if (!peerId) return
|
|
77
|
+
void sendNodeAction(peerId, name, payload).catch(() => { })
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @returns {boolean} 是否已订阅 node scope
|
|
84
|
+
*/
|
|
85
|
+
export function isNodeScopeSubscribed() {
|
|
86
|
+
return nodeScopeSubscribeCleanup != null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} action - action 名
|
|
91
|
+
* @returns {boolean} 是否已挂载处理器
|
|
92
|
+
*/
|
|
93
|
+
export function hasNodeScopeAction(action) {
|
|
94
|
+
return (nodeActionHandlers.get(action)?.size ?? 0) > 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} action - action 名
|
|
99
|
+
* @returns {number} 已注册的处理器数量
|
|
100
|
+
*/
|
|
101
|
+
export function countNodeScopeActionHandlers(action) {
|
|
102
|
+
return nodeActionHandlers.get(action)?.size ?? 0
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 测试/调试:直接派发已挂载的 node action。
|
|
107
|
+
* @param {string} action - action 名
|
|
108
|
+
* @param {unknown} payload - 载荷
|
|
109
|
+
* @param {string} peerId - 发送方 nodeHash
|
|
110
|
+
* @returns {boolean} 是否有处理器被调用
|
|
111
|
+
*/
|
|
112
|
+
export function dispatchNodeScopeAction(action, payload, peerId) {
|
|
113
|
+
const handlers = nodeActionHandlers.get(action)
|
|
114
|
+
if (!handlers?.size) return false
|
|
115
|
+
for (const handler of handlers)
|
|
116
|
+
try { handler(payload, peerId) } catch { /* ignore */ }
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @returns {NodeScopeWire | null} 当前 wire,未 ensure 时为 null
|
|
122
|
+
*/
|
|
123
|
+
export function getNodeScopeWire() {
|
|
124
|
+
return nodeScopeWire
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @returns {NodeScopeContext} 可变 node scope 上下文
|
|
129
|
+
*/
|
|
130
|
+
export function getNodeScopeContext() {
|
|
131
|
+
return nodeScopeContext
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 只订阅 node scope 派发,不挂任何 feature。
|
|
136
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
137
|
+
* @returns {() => void} 取消订阅的 dispose
|
|
138
|
+
*/
|
|
139
|
+
export function ensureNodeScope(options = {}) {
|
|
140
|
+
if (options.replicaUsername != null)
|
|
141
|
+
nodeScopeContext.replicaUsername = String(options.replicaUsername)
|
|
142
|
+
if (nodeScopeSubscribeCleanup) return nodeScopeSubscribeCleanup
|
|
143
|
+
nodeScopeSubscribeCleanup = subscribeScope('node', (senderNodeHash, envelope) => {
|
|
144
|
+
const handlers = nodeActionHandlers.get(envelope.action)
|
|
145
|
+
if (!handlers?.size) return
|
|
146
|
+
for (const handler of handlers)
|
|
147
|
+
try { handler(envelope.payload, senderNodeHash) } catch { /* ignore */ }
|
|
148
|
+
})
|
|
149
|
+
if (!nodeScopeWire) {
|
|
150
|
+
nodeScopeWire = createNodeScopeWire()
|
|
151
|
+
for (const hook of nodeScopeWireHooks)
|
|
152
|
+
try { hook(nodeScopeContext, nodeScopeWire) } catch { /* ignore */ }
|
|
153
|
+
}
|
|
154
|
+
return nodeScopeSubscribeCleanup
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {(context: NodeScopeContext, wire: NodeScopeWire) => void} hook - wire 创建时回调
|
|
159
|
+
* @returns {() => void} 取消注册的 dispose
|
|
160
|
+
*/
|
|
161
|
+
export function registerNodeScopeWireHook(hook) {
|
|
162
|
+
nodeScopeWireHooks.add(hook)
|
|
163
|
+
if (nodeScopeWire)
|
|
164
|
+
try { hook(nodeScopeContext, nodeScopeWire) } catch { /* ignore */ }
|
|
165
|
+
return () => nodeScopeWireHooks.delete(hook)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {() => void} dispose - feature 卸载函数
|
|
170
|
+
* @returns {() => void} 包装后的 dispose(同时从跟踪集移除)
|
|
171
|
+
*/
|
|
172
|
+
function trackFeatureDisposer(dispose) {
|
|
173
|
+
nodeScopeFeatureDisposers.add(dispose)
|
|
174
|
+
return () => {
|
|
175
|
+
dispose()
|
|
176
|
+
nodeScopeFeatureDisposers.delete(dispose)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 同一 feature 多次 attach 共享一份 wire;dispose 引用计数归零才卸。
|
|
182
|
+
* @param {string} key - feature 去重键
|
|
183
|
+
* @param {() => () => void} attachCore - 首次 attach 时执行,返回核心 dispose
|
|
184
|
+
* @returns {() => void} 引用计数包装的 dispose
|
|
185
|
+
*/
|
|
186
|
+
function attachFeatureRefCounted(key, attachCore) {
|
|
187
|
+
let entry = featureAttachRefs.get(key)
|
|
188
|
+
if (!entry) {
|
|
189
|
+
entry = { count: 0, disposeCore: attachCore() }
|
|
190
|
+
featureAttachRefs.set(key, entry)
|
|
191
|
+
}
|
|
192
|
+
entry.count++
|
|
193
|
+
return trackFeatureDisposer(() => {
|
|
194
|
+
const cur = featureAttachRefs.get(key)
|
|
195
|
+
if (!cur) return
|
|
196
|
+
cur.count--
|
|
197
|
+
if (cur.count > 0) return
|
|
198
|
+
cur.disposeCore()
|
|
199
|
+
featureAttachRefs.delete(key)
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 自定义 node scope feature(与 mailbox/part 相同 refcount 语义)。
|
|
205
|
+
* @param {string} key - feature 去重键
|
|
206
|
+
* @param {(wire: NodeScopeWire, context: NodeScopeContext) => () => void} attachCore - 首次 attach 注册 handler,返回核心 dispose
|
|
207
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
208
|
+
* @returns {() => void} 引用计数包装的 dispose
|
|
209
|
+
*/
|
|
210
|
+
export function attachNodeScopeFeature(key, attachCore, options = {}) {
|
|
211
|
+
ensureNodeScope(options)
|
|
212
|
+
return attachFeatureRefCounted(key, () => attachCore(nodeScopeWire, nodeScopeContext))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* refcount 挂载 mailbox wire。
|
|
217
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
218
|
+
* @returns {() => void} 取消挂载的 dispose
|
|
219
|
+
*/
|
|
220
|
+
export function attachNodeScopeMailbox(options = {}) {
|
|
221
|
+
ensureNodeScope(options)
|
|
222
|
+
return attachFeatureRefCounted('mailbox', () => attachMailboxWire(nodeScopeContext, nodeScopeWire))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* refcount 挂载 part ingress wire。
|
|
227
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
228
|
+
* @returns {() => void} 取消挂载的 dispose
|
|
229
|
+
*/
|
|
230
|
+
export function attachNodeScopePart(options = {}) {
|
|
231
|
+
ensureNodeScope(options)
|
|
232
|
+
return attachFeatureRefCounted('part', () => attachPartWire(nodeScopeContext, nodeScopeWire))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* refcount 挂载 part_query wire。
|
|
237
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
238
|
+
* @returns {() => void} 取消挂载的 dispose
|
|
239
|
+
*/
|
|
240
|
+
export function attachNodeScopePartQuery(options = {}) {
|
|
241
|
+
ensureNodeScope(options)
|
|
242
|
+
return attachFeatureRefCounted('partQuery', () => attachPartQueryWire(nodeScopeContext, nodeScopeWire))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* refcount 挂载 fed chunk responder。
|
|
247
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
248
|
+
* @returns {() => void} 取消挂载的 dispose
|
|
249
|
+
*/
|
|
250
|
+
export function attachNodeScopeChunks(options = {}) {
|
|
251
|
+
ensureNodeScope(options)
|
|
252
|
+
return attachFeatureRefCounted('chunks', () =>
|
|
253
|
+
attachNodeScopeFedChunkResponder(() => nodeScopeContext.replicaUsername, nodeScopeWire))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 全业务 preset(等价旧 ensureNodeScopeRuntime 默认行为)。
|
|
258
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
259
|
+
* @returns {() => void} 取消全部 preset 挂载的 dispose
|
|
260
|
+
*/
|
|
261
|
+
export function attachUserRoomDefaultWires(options = {}) {
|
|
262
|
+
const disposers = [
|
|
263
|
+
attachNodeScopePart(options),
|
|
264
|
+
attachNodeScopePartQuery(options),
|
|
265
|
+
attachNodeScopeMailbox(options),
|
|
266
|
+
attachNodeScopeChunks(options),
|
|
267
|
+
]
|
|
268
|
+
return () => {
|
|
269
|
+
for (const dispose of disposers) dispose()
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 卸掉 feature 挂载;可选保留 node scope 订阅。
|
|
275
|
+
* @param {{ keepSubscribe?: boolean }} [options] - true 时保留 node scope 订阅
|
|
276
|
+
* @returns {void}
|
|
277
|
+
*/
|
|
278
|
+
export function stopNodeScopeRuntime(options = {}) {
|
|
279
|
+
for (const dispose of [...nodeScopeFeatureDisposers]) dispose()
|
|
280
|
+
nodeScopeFeatureDisposers.clear()
|
|
281
|
+
for (const entry of featureAttachRefs.values())
|
|
282
|
+
try { entry.disposeCore() } catch { /* ignore */ }
|
|
283
|
+
featureAttachRefs.clear()
|
|
284
|
+
if (options.keepSubscribe) return
|
|
285
|
+
nodeScopeSubscribeCleanup?.()
|
|
286
|
+
nodeScopeSubscribeCleanup = null
|
|
287
|
+
nodeScopeWire = null
|
|
288
|
+
nodeActionHandlers.clear()
|
|
289
|
+
}
|