@steve02081504/fount-p2p 0.0.23 → 0.0.24
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 +8 -6
- package/core/object.mjs +9 -0
- package/core/partpath.mjs +10 -0
- package/dag/storage.mjs +0 -5
- package/dag/strip_extensions.mjs +1 -3
- package/{wire/part_query_cache.mjs → federation/part_query/cache.mjs} +7 -7
- package/{wire/part_query.mjs → federation/part_query/runtime.mjs} +36 -77
- package/files/assemble.mjs +5 -5
- package/files/assemble_stream.mjs +2 -2
- package/files/chunk/fetch.mjs +101 -0
- package/files/chunk/pending.mjs +59 -0
- package/files/{chunk_provider_registry.mjs → chunk/provider_registry.mjs} +4 -5
- package/files/chunk/responder.mjs +104 -0
- package/files/{chunk_store.mjs → chunk/store.mjs} +2 -2
- package/files/{chunk_fetch_verify.mjs → chunk/verify.mjs} +2 -2
- package/files/evfs.mjs +18 -19
- package/files/fed/fetch_shared.mjs +39 -0
- package/files/fed/responder.mjs +47 -0
- package/files/{acl.mjs → manifest/acl.mjs} +4 -4
- package/files/{manifest_acl_registry.mjs → manifest/acl_registry.mjs} +14 -17
- package/files/{manifest_fetch.mjs → manifest/fetch.mjs} +47 -52
- package/files/{manifest.mjs → manifest/normalize.mjs} +2 -2
- package/files/manifest/pending.mjs +53 -0
- package/files/{public_manifest.mjs → manifest/public.mjs} +12 -12
- package/files/transfer_key.mjs +2 -2
- package/files/transfer_key_registry.mjs +7 -7
- package/index.mjs +4 -4
- package/infra/service.mjs +1 -1
- package/link/handshake.mjs +0 -3
- package/mailbox/deliver_or_store.mjs +0 -2
- package/mailbox/parse.mjs +1 -1
- package/mailbox/store.mjs +0 -9
- package/mailbox/wire.mjs +24 -16
- package/node/reputation_sync.mjs +33 -29
- package/package.json +8 -8
- package/registries/inbound.mjs +19 -9
- package/registries/part_path.mjs +3 -8
- package/schemas/discovery.mjs +1 -1
- package/schemas/federation_pull.mjs +1 -1
- package/schemas/part_query.mjs +6 -5
- package/transport/group_link_set.mjs +2 -2
- package/transport/link_registry.mjs +2 -2
- package/transport/node_scope/features.mjs +166 -0
- package/transport/node_scope/wire.mjs +124 -0
- package/transport/remote_user_room.mjs +0 -1
- package/{rooms → transport}/scoped_link.mjs +1 -1
- package/transport/user_room.mjs +15 -28
- package/utils/fetch_wait.mjs +98 -0
- package/wire/adapter.mjs +13 -0
- package/wire/ingress.mjs +1 -8
- package/wire/part/fanout.mjs +99 -0
- package/wire/part/group.mjs +45 -0
- package/wire/{part_ingress.mjs → part/ingress.mjs} +58 -67
- package/wire/{part_invoke.mjs → part/invoke.mjs} +1 -22
- package/wire/part/pending.mjs +32 -0
- package/wire/part/query.mjs +91 -0
- package/wire/subscribe.mjs +13 -0
- package/federation/chunk_fetch_pending.mjs +0 -124
- package/federation/manifest_fetch_pending.mjs +0 -88
- package/files/chunk_fetch.mjs +0 -99
- package/files/chunk_responder.mjs +0 -142
- package/transport/advert_ingest.mjs +0 -17
- package/transport/node_scope.mjs +0 -289
- package/wire/group_part.mjs +0 -43
- package/wire/part_common.mjs +0 -55
- package/wire/part_fanout.mjs +0 -52
- /package/{wire → schemas}/part_query.tunables.json +0 -0
package/files/chunk_fetch.mjs
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
|
|
3
|
-
import { bytesToBase64 } from '../core/bytes_codec.mjs'
|
|
4
|
-
import {
|
|
5
|
-
MAX_PENDING_CHUNK_FETCHES,
|
|
6
|
-
registerChunkFetchWait,
|
|
7
|
-
} from '../federation/chunk_fetch_pending.mjs'
|
|
8
|
-
import { ms } from '../utils/duration.mjs'
|
|
9
|
-
import { createInflightTable } from '../utils/inflight_table.mjs'
|
|
10
|
-
|
|
11
|
-
import { verifiedChunkBytes } from './chunk_fetch_verify.mjs'
|
|
12
|
-
import { fetchFederationChunk, resolveNodeHash } from './chunk_provider_registry.mjs'
|
|
13
|
-
import { getChunk, hasChunk, putChunk } from './chunk_store.mjs'
|
|
14
|
-
import { fanoutFedFetch } from './fetch_fanout.mjs'
|
|
15
|
-
|
|
16
|
-
const DEFAULT_CHUNK_FETCH_TIMEOUT_MS = ms('8s')
|
|
17
|
-
|
|
18
|
-
/** 同 username+hash 共享一次 fanout;队满只丢已超基础超时的队首。 */
|
|
19
|
-
const chunkInflight = createInflightTable({
|
|
20
|
-
maxSize: MAX_PENDING_CHUNK_FETCHES,
|
|
21
|
-
baseTimeoutMs: DEFAULT_CHUNK_FETCH_TIMEOUT_MS,
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @typedef {{
|
|
26
|
-
* username: string,
|
|
27
|
-
* ciphertextHash: string,
|
|
28
|
-
* ownerEntityHash?: string,
|
|
29
|
-
* groupId?: string,
|
|
30
|
-
* }} FetchChunkContext
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @param {FetchChunkContext} context 上下文
|
|
35
|
-
* @returns {Promise<Uint8Array | null>} 密文块
|
|
36
|
-
*/
|
|
37
|
-
export async function fetchChunk(context) {
|
|
38
|
-
const hash = String(context.ciphertextHash || '').trim().toLowerCase()
|
|
39
|
-
const { username } = context
|
|
40
|
-
if (!hash || !username) return null
|
|
41
|
-
|
|
42
|
-
if (await hasChunk(hash))
|
|
43
|
-
return new Uint8Array(await getChunk(hash))
|
|
44
|
-
|
|
45
|
-
if (context.groupId) {
|
|
46
|
-
const u8 = await fetchFederationChunk(username, context.groupId, hash)
|
|
47
|
-
const verified = verifiedChunkBytes(hash, u8)
|
|
48
|
-
if (verified) {
|
|
49
|
-
await putChunk(hash, verified)
|
|
50
|
-
return verified
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const inflightKey = `${username}\0${hash}`
|
|
55
|
-
const shared = chunkInflight.acquire(inflightKey, () => {
|
|
56
|
-
const requestId = randomUUID()
|
|
57
|
-
const wait = registerChunkFetchWait(requestId, hash, DEFAULT_CHUNK_FETCH_TIMEOUT_MS)
|
|
58
|
-
void (async () => {
|
|
59
|
-
try {
|
|
60
|
-
const { nodeHash } = await resolveNodeHash(username)
|
|
61
|
-
await fanoutFedFetch(username, 'fed_chunk_get', {
|
|
62
|
-
requestId,
|
|
63
|
-
nodeHash,
|
|
64
|
-
chunkHash: hash,
|
|
65
|
-
ownerEntityHash: context.ownerEntityHash,
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
catch { /* pending wait 超时/cancel 负责 settle */ }
|
|
69
|
-
})()
|
|
70
|
-
return { done: wait.done, cancel: wait.cancel }
|
|
71
|
-
})
|
|
72
|
-
if (!shared) return null
|
|
73
|
-
|
|
74
|
-
const result = await shared
|
|
75
|
-
const verified = verifiedChunkBytes(hash, result)
|
|
76
|
-
if (verified) {
|
|
77
|
-
await putChunk(hash, verified)
|
|
78
|
-
return verified
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return null
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 若本机有 chunk 则响应 fed_chunk_get。
|
|
86
|
-
* @param {string} username 用户
|
|
87
|
-
* @param {object} payload 请求
|
|
88
|
-
* @param {(response: object, peerId: string) => void} sendResponse 发送
|
|
89
|
-
* @param {string} peerId 对端
|
|
90
|
-
* @returns {Promise<void>}
|
|
91
|
-
*/
|
|
92
|
-
export async function handleIncomingChunkGet(username, payload, sendResponse, peerId) {
|
|
93
|
-
const hash = String(payload?.chunkHash || '').trim().toLowerCase()
|
|
94
|
-
if (!hash) return
|
|
95
|
-
if (!await hasChunk(hash)) return
|
|
96
|
-
const chunkBytes = await getChunk(hash)
|
|
97
|
-
if (!chunkBytes?.length) return
|
|
98
|
-
sendResponse({ requestId: payload.requestId, dataBase64: bytesToBase64(chunkBytes) }, peerId)
|
|
99
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { resolvePendingChunkFetch } from '../federation/chunk_fetch_pending.mjs'
|
|
2
|
-
import { resolvePendingManifestFetch } from '../federation/manifest_fetch_pending.mjs'
|
|
3
|
-
|
|
4
|
-
import { handleIncomingChunkGet } from './chunk_fetch.mjs'
|
|
5
|
-
import { handleIncomingManifestGet } from './manifest_fetch.mjs'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @param {string} username 副本用户名 用户名
|
|
9
|
-
* @param {object} data 入站 fed_chunk_get
|
|
10
|
-
* @param {string} peerId 对端 id
|
|
11
|
-
* @param {(resp: object, peerId: string) => void} sendChunkData 发送 fed_chunk_data
|
|
12
|
-
* @returns {Promise<void>}
|
|
13
|
-
*/
|
|
14
|
-
export async function handleFedChunkGetIngress(username, data, peerId, sendChunkData) {
|
|
15
|
-
await handleIncomingChunkGet(username, data, sendChunkData, peerId)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param {object} data 入站 fed_chunk_data(含 requestId 时 resolve pending fetch)
|
|
20
|
-
* @returns {void}
|
|
21
|
-
*/
|
|
22
|
-
export function handleFedChunkDataIngress(data) {
|
|
23
|
-
resolvePendingChunkFetch(data)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @param {string} username 副本用户名
|
|
28
|
-
* @param {object} data 入站 fed_manifest_get
|
|
29
|
-
* @param {string} peerId 对端 id
|
|
30
|
-
* @param {(resp: object, peerId: string) => void} sendManifestData 发送 fed_manifest_data
|
|
31
|
-
* @returns {Promise<void>}
|
|
32
|
-
*/
|
|
33
|
-
export async function handleFedManifestGetIngress(username, data, peerId, sendManifestData) {
|
|
34
|
-
await handleIncomingManifestGet(username, data, sendManifestData, peerId)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* @param {object} data 入站 fed_manifest_data
|
|
39
|
-
* @returns {Promise<void>}
|
|
40
|
-
*/
|
|
41
|
-
export async function handleFedManifestDataIngress(data) {
|
|
42
|
-
await resolvePendingManifestFetch(data)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* node scope user-room wire:注册 fed_chunk_* + fed_manifest_*。
|
|
47
|
-
* @param {string | (() => string)} usernameOrGetter 副本用户名或 live getter
|
|
48
|
-
* @param {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => (() => void) | void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire action 表
|
|
49
|
-
* @returns {() => void} 取消挂载的 dispose
|
|
50
|
-
*/
|
|
51
|
-
export function attachNodeScopeFedChunkResponder(usernameOrGetter, wire) {
|
|
52
|
-
const resolveUsername = typeof usernameOrGetter === 'function'
|
|
53
|
-
? usernameOrGetter
|
|
54
|
-
: () => usernameOrGetter
|
|
55
|
-
const offs = [
|
|
56
|
-
wire.on('fed_chunk_get', (data, peerId) => {
|
|
57
|
-
void handleFedChunkGetIngress(resolveUsername(), data, peerId, (resp, pid) => {
|
|
58
|
-
try { wire.send('fed_chunk_data', resp, pid) }
|
|
59
|
-
catch { /* disconnected */ }
|
|
60
|
-
})
|
|
61
|
-
}),
|
|
62
|
-
wire.on('fed_chunk_data', handleFedChunkDataIngress),
|
|
63
|
-
wire.on('fed_manifest_get', (data, peerId) => {
|
|
64
|
-
void handleFedManifestGetIngress(resolveUsername(), data, peerId, (resp, pid) => {
|
|
65
|
-
try { wire.send('fed_manifest_data', resp, pid) }
|
|
66
|
-
catch { /* disconnected */ }
|
|
67
|
-
})
|
|
68
|
-
}),
|
|
69
|
-
wire.on('fed_manifest_data', data => {
|
|
70
|
-
void handleFedManifestDataIngress(data)
|
|
71
|
-
}),
|
|
72
|
-
]
|
|
73
|
-
return () => {
|
|
74
|
-
for (const off of offs)
|
|
75
|
-
try { off?.() } catch { /* ignore */ }
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Trystero room:注册带 requestId 的 fed_chunk_* + fed_manifest_*(TrustGraph 全局 miss)。
|
|
81
|
-
* @param {string} username 用户
|
|
82
|
-
* @param {object} room Trystero room
|
|
83
|
-
* @param {{ enqueue: (prio: number, cleanup: () => void) => void }} [fedOut] 出站队列
|
|
84
|
-
* @param {(roomKey: string, action: string, rtcLimits: object) => boolean} [guardGet] RTC 负载守卫
|
|
85
|
-
* @param {object} [rtcLimits] RTC 限额
|
|
86
|
-
* @param {string} [roomKey] 房间键
|
|
87
|
-
* @returns {void}
|
|
88
|
-
*/
|
|
89
|
-
export function attachTrustGraphFedChunkResponder(username, room, fedOut, guardGet, rtcLimits = {}, roomKey = '') {
|
|
90
|
-
const [sendChunkData, getChunkData] = room.makeAction('fed_chunk_data')
|
|
91
|
-
const [, getChunkGet] = room.makeAction('fed_chunk_get')
|
|
92
|
-
const [sendManifestData, getManifestData] = room.makeAction('fed_manifest_data')
|
|
93
|
-
const [, getManifestGet] = room.makeAction('fed_manifest_get')
|
|
94
|
-
|
|
95
|
-
getChunkGet((data, peerId) => {
|
|
96
|
-
if (guardGet && !guardGet(roomKey, 'fed_chunk_get', rtcLimits)) return
|
|
97
|
-
void (async () => {
|
|
98
|
-
if (!data || typeof data !== 'object') return
|
|
99
|
-
if (!String(data.requestId || '')) return
|
|
100
|
-
await handleFedChunkGetIngress(username, data, peerId, (resp, pid) => {
|
|
101
|
-
/**
|
|
102
|
-
* 发送 fed_chunk_data 响应(可经 fedOut 限速队列)。
|
|
103
|
-
*/
|
|
104
|
-
const send = () => {
|
|
105
|
-
try { sendChunkData(resp, pid) }
|
|
106
|
-
catch { /* peer gone — normal */ }
|
|
107
|
-
}
|
|
108
|
-
if (fedOut) fedOut.enqueue(6, send)
|
|
109
|
-
else send()
|
|
110
|
-
})
|
|
111
|
-
})().catch(() => { /* peer/handler race — normal degrade */ })
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
getChunkData(data => {
|
|
115
|
-
if (!data || typeof data !== 'object' || !data.requestId) return
|
|
116
|
-
handleFedChunkDataIngress(data)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
getManifestGet((data, peerId) => {
|
|
120
|
-
if (guardGet && !guardGet(roomKey, 'fed_manifest_get', rtcLimits)) return
|
|
121
|
-
void (async () => {
|
|
122
|
-
if (!data || typeof data !== 'object') return
|
|
123
|
-
if (!String(data.requestId || '')) return
|
|
124
|
-
await handleFedManifestGetIngress(username, data, peerId, (resp, pid) => {
|
|
125
|
-
/**
|
|
126
|
-
* 发送 fed_manifest_data 响应(可经 fedOut 限速队列)。
|
|
127
|
-
*/
|
|
128
|
-
const send = () => {
|
|
129
|
-
try { sendManifestData(resp, pid) }
|
|
130
|
-
catch { /* peer gone — normal */ }
|
|
131
|
-
}
|
|
132
|
-
if (fedOut) fedOut.enqueue(6, send)
|
|
133
|
-
else send()
|
|
134
|
-
})
|
|
135
|
-
})().catch(() => { /* peer/handler race — normal degrade */ })
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
getManifestData(data => {
|
|
139
|
-
if (!data || typeof data !== 'object' || !data.requestId) return
|
|
140
|
-
void handleFedManifestDataIngress(data)
|
|
141
|
-
})
|
|
142
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 解密并验签 advert(`ingestEncryptedAdvert` 别名)。不写 peer hints;写入需 `applyAdvertPeerHints`。Untrusted ingress。
|
|
5
|
-
*/
|
|
6
|
-
export { ingestEncryptedAdvert as ingestSignedAdvert } from '../discovery/adverts.mjs'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 将已验签 advert 的 peer hints 写入本地(apply 侧;与 `ingestSignedAdvert` 分离)。
|
|
10
|
-
* @param {string} verifiedNodeHash 已验签 nodeHash
|
|
11
|
-
* @param {object} body advert body
|
|
12
|
-
* @param {object} [meta] 元数据
|
|
13
|
-
* @returns {void}
|
|
14
|
-
*/
|
|
15
|
-
export function applyAdvertPeerHints(verifiedNodeHash, body, meta) {
|
|
16
|
-
noteAdvertPeerHints(verifiedNodeHash, body, meta)
|
|
17
|
-
}
|
package/transport/node_scope.mjs
DELETED
|
@@ -1,289 +0,0 @@
|
|
|
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
|
-
}
|
package/wire/group_part.mjs
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { isPlainObject } from './ingress.mjs'
|
|
2
|
-
import { attachPartWire } from './part_ingress.mjs'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @param {unknown} data part_invoke 载荷
|
|
6
|
-
* @param {string} groupId 群 ID
|
|
7
|
-
* @returns {object | null} 校验通过后的载荷
|
|
8
|
-
*/
|
|
9
|
-
function assertGroupContext(data, groupId) {
|
|
10
|
-
if (!isPlainObject(data)) return null
|
|
11
|
-
if (data.groupId !== groupId) return null
|
|
12
|
-
return data
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @param {import('./part_ingress.mjs').PartWireAdapter} wire 底层适配器
|
|
17
|
-
* @param {string} groupId 群 ID
|
|
18
|
-
* @returns {import('./part_ingress.mjs').PartWireAdapter['on']} 注入 groupId 的 on 包装
|
|
19
|
-
*/
|
|
20
|
-
function wrapWireOn(wire, groupId) {
|
|
21
|
-
return (name, handler) => {
|
|
22
|
-
wire.on(name, (data, peerId) => {
|
|
23
|
-
const payload = assertGroupContext(data, groupId)
|
|
24
|
-
if (!payload) return
|
|
25
|
-
handler(payload, peerId)
|
|
26
|
-
})
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 群联邦房间挂载 part_wire(要求线载荷带 `groupId`)。
|
|
32
|
-
* @param {{ replicaUsername?: string }} wireContext 入站上下文
|
|
33
|
-
* @param {string} groupId 群 ID
|
|
34
|
-
* @param {import('./part_ingress.mjs').PartWireAdapter} wire Trystero 适配器
|
|
35
|
-
* @param {{ allowPartInvoke?: (payload: object) => boolean }} [options] 入站过滤
|
|
36
|
-
* @returns {void}
|
|
37
|
-
*/
|
|
38
|
-
export function attachGroupPartWire(wireContext, groupId, wire, options = {}) {
|
|
39
|
-
attachPartWire(wireContext, {
|
|
40
|
-
send: wire.send.bind(wire),
|
|
41
|
-
on: wrapWireOn(wire, groupId),
|
|
42
|
-
}, options)
|
|
43
|
-
}
|
package/wire/part_common.mjs
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { unwrapPartInvokeResult } from './part_invoke.mjs'
|
|
2
|
-
|
|
3
|
-
/** 时间线 part_timeline_put fanout 上限 */
|
|
4
|
-
export const TIMELINE_FANOUT_LIMIT = 8
|
|
5
|
-
/** part_invoke RPC collect 默认响应数 */
|
|
6
|
-
export const PART_INVOKE_FANOUT_DEFAULT = 6
|
|
7
|
-
/** User Room 随机 peer 转发默认上限 */
|
|
8
|
-
export const USER_ROOM_PEER_FANOUT_DEFAULT = 6
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @param {object} fields 载荷字段
|
|
12
|
-
* @param {string} fields.partpath part 路径
|
|
13
|
-
* @param {import('./part_invoke.mjs').PartInvoke} fields.invoke 调用体
|
|
14
|
-
* @param {string} [fields.nodeHash] 来源节点
|
|
15
|
-
* @param {string} [fields.requestId] RPC 请求 id
|
|
16
|
-
* @param {string} [fields.groupId] 群上下文(mailbox give ingest)
|
|
17
|
-
* @returns {object} part_invoke 线载荷
|
|
18
|
-
*/
|
|
19
|
-
export function buildPartInvokePayload({ partpath, invoke, nodeHash, requestId, groupId }) {
|
|
20
|
-
return {
|
|
21
|
-
partpath,
|
|
22
|
-
invoke,
|
|
23
|
-
...nodeHash ? { nodeHash } : {},
|
|
24
|
-
...requestId ? { requestId } : {},
|
|
25
|
-
...groupId ? { groupId } : {},
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @param {import('./part_invoke.mjs').PartInvokeResponse[]} results collect 原始结果
|
|
31
|
-
* @returns {object[]} 仅含成功 result 的载荷
|
|
32
|
-
*/
|
|
33
|
-
export function partInvokeDataRows(results) {
|
|
34
|
-
/** @type {object[]} */
|
|
35
|
-
const rows = []
|
|
36
|
-
for (const row of results) {
|
|
37
|
-
const data = unwrapPartInvokeResult(row)
|
|
38
|
-
if (data != null) rows.push(/** @type {object} */ data)
|
|
39
|
-
}
|
|
40
|
-
return rows
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @param {import('./part_invoke.mjs').PartInvokeResponse[]} results collect 原始结果
|
|
45
|
-
* @returns {string[]} 邻居返回的错误信息
|
|
46
|
-
*/
|
|
47
|
-
export function partInvokeErrorMessages(results) {
|
|
48
|
-
/** @type {string[]} */
|
|
49
|
-
const errors = []
|
|
50
|
-
for (const row of results) {
|
|
51
|
-
const message = row?.error?.message
|
|
52
|
-
if (message) errors.push(message)
|
|
53
|
-
}
|
|
54
|
-
return errors
|
|
55
|
-
}
|
package/wire/part_fanout.mjs
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
|
|
3
|
-
import { getNodeHash } from '../node/identity.mjs'
|
|
4
|
-
import { DEFAULT_TRUST_GRAPH_OWNER, requireTrustGraphProvider } from '../trust_graph/registry.mjs'
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
buildPartInvokePayload,
|
|
8
|
-
PART_INVOKE_FANOUT_DEFAULT,
|
|
9
|
-
} from './part_common.mjs'
|
|
10
|
-
import { pendingPartInvoke } from './part_ingress.mjs'
|
|
11
|
-
|
|
12
|
-
/** @typedef {import('./part_invoke.mjs').PartInvokeResponse} PartInvokeResponse */
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @param {string} username 用户(trust graph fanout 上下文)
|
|
16
|
-
* @param {string} partpath part 路径
|
|
17
|
-
* @param {import('./part_invoke.mjs').PartInvoke} invoke 调用体
|
|
18
|
-
* @param {number} [timeoutMs=2500] 超时
|
|
19
|
-
* @param {number} [maxResponses=6] 最多响应数
|
|
20
|
-
* @returns {Promise<PartInvokeResponse[]>} 邻居 PartInvokeResponse(含 error)
|
|
21
|
-
*/
|
|
22
|
-
export async function collectPartInvokeResponses(username, partpath, invoke, timeoutMs = 2500, maxResponses = PART_INVOKE_FANOUT_DEFAULT) {
|
|
23
|
-
const requestId = randomUUID()
|
|
24
|
-
const nodeHash = getNodeHash()
|
|
25
|
-
/** @type {PartInvokeResponse[]} */
|
|
26
|
-
const responses = []
|
|
27
|
-
|
|
28
|
-
const waitForResponses = new Promise(resolve => {
|
|
29
|
-
/**
|
|
30
|
-
* 超时或收齐响应后结束等待并清理 pending part_invoke。
|
|
31
|
-
*/
|
|
32
|
-
const finish = () => {
|
|
33
|
-
clearTimeout(timer)
|
|
34
|
-
pendingPartInvoke.delete(requestId)
|
|
35
|
-
resolve(responses)
|
|
36
|
-
}
|
|
37
|
-
const timer = setTimeout(finish, timeoutMs)
|
|
38
|
-
pendingPartInvoke.set(requestId, { responses, finish, maxResponses, respondedPeers: new Set() })
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const sent = await requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).fanoutToTopNodes(username, 'part_invoke', buildPartInvokePayload({
|
|
42
|
-
partpath,
|
|
43
|
-
invoke,
|
|
44
|
-
nodeHash,
|
|
45
|
-
requestId,
|
|
46
|
-
}), maxResponses)
|
|
47
|
-
|
|
48
|
-
const pending = pendingPartInvoke.get(requestId)
|
|
49
|
-
if (pending && sent === 0) pending.finish()
|
|
50
|
-
|
|
51
|
-
return waitForResponses
|
|
52
|
-
}
|
|
File without changes
|