@steve02081504/fount-p2p 0.0.22 → 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/entity_id.mjs +5 -5
- package/core/logical_entity.mjs +1 -1
- package/core/object.mjs +9 -0
- package/core/partpath.mjs +10 -0
- package/core/random_id.mjs +4 -2
- package/crypto/crypto.mjs +26 -93
- 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 +9 -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
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 有界 pending fetch 等待表(chunk / manifest 等入站响应槽)。
|
|
3
|
+
* @template T
|
|
4
|
+
* @param {{ maxSize: number }} options 容量
|
|
5
|
+
* @returns {{
|
|
6
|
+
* pending: Map<string, { expectedKey: string, timer: ReturnType<typeof setTimeout>, finish: (value: T | null | Error) => void, handle: { done: Promise<T | null>, cancel: () => void } }>,
|
|
7
|
+
* register: (key: string, expectedKey: string, timeoutMs: number, options?: { rejectOnTimeout?: boolean }) => { done: Promise<T | null>, cancel: () => void },
|
|
8
|
+
* peek: (key: string) => { expectedKey: string, timer: ReturnType<typeof setTimeout>, finish: (value: T | null | Error) => void, handle: { done: Promise<T | null>, cancel: () => void } } | undefined,
|
|
9
|
+
* settle: (key: string, value: T | null | Error) => boolean,
|
|
10
|
+
* }} 等待表 API
|
|
11
|
+
*/
|
|
12
|
+
export function createFetchWaitTable({ maxSize }) {
|
|
13
|
+
/** @type {Map<string, { expectedKey: string, timer: ReturnType<typeof setTimeout>, finish: (value: T | null | Error) => void, handle: { done: Promise<T | null>, cancel: () => void } }>} */
|
|
14
|
+
const pending = new Map()
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} key 等待键
|
|
18
|
+
* @param {T | null | Error} value 结果或错误
|
|
19
|
+
* @returns {boolean} 是否命中并完成
|
|
20
|
+
*/
|
|
21
|
+
function settle(key, value) {
|
|
22
|
+
const entry = pending.get(key)
|
|
23
|
+
if (!entry) return false
|
|
24
|
+
clearTimeout(entry.timer)
|
|
25
|
+
pending.delete(key)
|
|
26
|
+
entry.finish(value)
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
pending,
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} key 唯一等待键
|
|
35
|
+
* @param {string} expectedKey 期望匹配键
|
|
36
|
+
* @param {number} timeoutMs 超时
|
|
37
|
+
* @param {{ rejectOnTimeout?: boolean }} [options] 超时是否 reject
|
|
38
|
+
* @returns {{ done: Promise<T | null>, cancel: () => void }} 等待句柄
|
|
39
|
+
*/
|
|
40
|
+
register(key, expectedKey, timeoutMs, options = {}) {
|
|
41
|
+
if (!key)
|
|
42
|
+
return {
|
|
43
|
+
done: Promise.resolve(null),
|
|
44
|
+
/** @returns {void} */
|
|
45
|
+
cancel: () => { },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const existing = pending.get(key)
|
|
49
|
+
if (existing) return existing.handle
|
|
50
|
+
|
|
51
|
+
if (pending.size >= maxSize)
|
|
52
|
+
return {
|
|
53
|
+
done: Promise.resolve(null),
|
|
54
|
+
/** @returns {void} */
|
|
55
|
+
cancel: () => { },
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @type {(value: T | null | Error) => void} */
|
|
59
|
+
let finish
|
|
60
|
+
const done = new Promise((resolve, reject) => {
|
|
61
|
+
/**
|
|
62
|
+
* @param {T | null | Error} value 完成值或错误
|
|
63
|
+
* @returns {void}
|
|
64
|
+
*/
|
|
65
|
+
finish = value => {
|
|
66
|
+
if (value instanceof Error) reject(value)
|
|
67
|
+
else resolve(value)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
pending.delete(key)
|
|
73
|
+
if (options.rejectOnTimeout) finish(new Error('fetch timeout'))
|
|
74
|
+
else finish(null)
|
|
75
|
+
}, timeoutMs)
|
|
76
|
+
|
|
77
|
+
/** @type {{ done: Promise<T | null>, cancel: () => void }} */
|
|
78
|
+
const handle = {
|
|
79
|
+
done,
|
|
80
|
+
/** @returns {void} 取消等待 */
|
|
81
|
+
cancel: () => settle(key, null),
|
|
82
|
+
}
|
|
83
|
+
pending.set(key, { expectedKey, timer, finish, handle })
|
|
84
|
+
return handle
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 只读查看,不移除。
|
|
89
|
+
* @param {string} key 等待键
|
|
90
|
+
* @returns {{ expectedKey: string, timer: ReturnType<typeof setTimeout>, finish: (value: T | null | Error) => void, handle: { done: Promise<T | null>, cancel: () => void } } | undefined} 等待条目
|
|
91
|
+
*/
|
|
92
|
+
peek(key) {
|
|
93
|
+
return pending.get(key)
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
settle,
|
|
97
|
+
}
|
|
98
|
+
}
|
package/wire/adapter.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{
|
|
3
|
+
* on: (name: string, handler: (payload: unknown, peerId: string) => void) => (() => void) | void
|
|
4
|
+
* send: (name: string, payload: unknown, peerId: string | null) => void
|
|
5
|
+
* }} WireAdapter
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {{ replicaUsername?: string }} WireContext
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Wire 适配器与上下文的类型导出(运行时代码见各 wire 模块)。 */
|
|
13
|
+
export {}
|
package/wire/ingress.mjs
CHANGED
|
@@ -4,14 +4,7 @@
|
|
|
4
4
|
import { Buffer } from 'node:buffer'
|
|
5
5
|
|
|
6
6
|
import { isHex64, isSignatureHex128 } from '../core/hexIds.mjs'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @param {unknown} value 待判定值
|
|
10
|
-
* @returns {value is Record<string, unknown>} 是否为非 null 的普通对象(非数组)
|
|
11
|
-
*/
|
|
12
|
-
export function isPlainObject(value) {
|
|
13
|
-
return value != null && !Array.isArray(value) && typeof value === 'object'
|
|
14
|
-
}
|
|
7
|
+
import { isPlainObject } from '../core/object.mjs'
|
|
15
8
|
|
|
16
9
|
/**
|
|
17
10
|
* 解析 WebSocket / Trystero 入站 JSON 帧。
|
|
@@ -0,0 +1,99 @@
|
|
|
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 { unwrapPartInvokeResult } from './invoke.mjs'
|
|
7
|
+
import { pendingPartInvoke } from './pending.mjs'
|
|
8
|
+
|
|
9
|
+
/** @typedef {import('./invoke.mjs').PartInvokeResponse} PartInvokeResponse */
|
|
10
|
+
/** @typedef {import('./invoke.mjs').PartInvoke} PartInvoke */
|
|
11
|
+
|
|
12
|
+
/** 时间线 part_timeline_put fanout 上限 */
|
|
13
|
+
export const TIMELINE_FANOUT_LIMIT = 8
|
|
14
|
+
/** part_invoke RPC collect 默认响应数 */
|
|
15
|
+
export const PART_INVOKE_FANOUT_DEFAULT = 6
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} partpath part 路径
|
|
19
|
+
* @param {PartInvoke} invoke 调用体
|
|
20
|
+
* @param {string} [nodeHash] 来源节点
|
|
21
|
+
* @param {string} [requestId] RPC 请求 id
|
|
22
|
+
* @returns {object} part_invoke 线载荷
|
|
23
|
+
*/
|
|
24
|
+
function buildPartInvokePayload(partpath, invoke, nodeHash, requestId) {
|
|
25
|
+
return {
|
|
26
|
+
partpath,
|
|
27
|
+
invoke,
|
|
28
|
+
...nodeHash ? { nodeHash } : {},
|
|
29
|
+
...requestId ? { requestId } : {},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {PartInvokeResponse[]} results collect 原始结果
|
|
35
|
+
* @returns {object[]} 仅含成功 result 的载荷
|
|
36
|
+
*/
|
|
37
|
+
export function partInvokeDataRows(results) {
|
|
38
|
+
/** @type {object[]} */
|
|
39
|
+
const rows = []
|
|
40
|
+
for (const row of results) {
|
|
41
|
+
const data = unwrapPartInvokeResult(row)
|
|
42
|
+
if (data != null) rows.push(/** @type {object} */ data)
|
|
43
|
+
}
|
|
44
|
+
return rows
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {PartInvokeResponse[]} results collect 原始结果
|
|
49
|
+
* @returns {string[]} 邻居返回的错误信息
|
|
50
|
+
*/
|
|
51
|
+
export function partInvokeErrorMessages(results) {
|
|
52
|
+
/** @type {string[]} */
|
|
53
|
+
const errors = []
|
|
54
|
+
for (const row of results) {
|
|
55
|
+
const message = row?.error?.message
|
|
56
|
+
if (message) errors.push(message)
|
|
57
|
+
}
|
|
58
|
+
return errors
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 向 trust-graph top-K 扇出 part_invoke,收集 part_invoke_response。
|
|
63
|
+
* 调用方须已挂载 `attachPartWire`(否则收不到 response)。
|
|
64
|
+
* @param {string} username trust graph 上下文
|
|
65
|
+
* @param {string} partpath part 路径
|
|
66
|
+
* @param {PartInvoke} invoke 调用体
|
|
67
|
+
* @param {number} [timeoutMs=2500] 超时
|
|
68
|
+
* @param {number} [maxResponses=6] 最多响应数
|
|
69
|
+
* @returns {Promise<PartInvokeResponse[]>} 邻居 PartInvokeResponse(含 error)
|
|
70
|
+
*/
|
|
71
|
+
export async function collectPartInvokeResponses(username, partpath, invoke, timeoutMs = 2500, maxResponses = PART_INVOKE_FANOUT_DEFAULT) {
|
|
72
|
+
const requestId = randomUUID()
|
|
73
|
+
const nodeHash = getNodeHash()
|
|
74
|
+
/** @type {PartInvokeResponse[]} */
|
|
75
|
+
const responses = []
|
|
76
|
+
|
|
77
|
+
const waitForResponses = new Promise(resolve => {
|
|
78
|
+
/** 超时或收满响应后清理 pending 并完成 Promise。 */
|
|
79
|
+
const finish = () => {
|
|
80
|
+
clearTimeout(timer)
|
|
81
|
+
pendingPartInvoke.delete(requestId)
|
|
82
|
+
resolve(responses)
|
|
83
|
+
}
|
|
84
|
+
const timer = setTimeout(finish, timeoutMs)
|
|
85
|
+
pendingPartInvoke.set(requestId, { responses, finish, maxResponses, respondedPeers: new Set() })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const sent = await requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).fanoutToTopNodes(
|
|
89
|
+
username,
|
|
90
|
+
'part_invoke',
|
|
91
|
+
buildPartInvokePayload(partpath, invoke, nodeHash, requestId),
|
|
92
|
+
maxResponses,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const pending = pendingPartInvoke.get(requestId)
|
|
96
|
+
if (pending && sent === 0) pending.finish()
|
|
97
|
+
|
|
98
|
+
return waitForResponses
|
|
99
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isPlainObject } from '../../core/object.mjs'
|
|
2
|
+
|
|
3
|
+
import { attachPartWire } from './ingress.mjs'
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('../../wire/adapter.mjs').WireAdapter} WireAdapter */
|
|
6
|
+
/** @typedef {import('../../wire/adapter.mjs').WireContext} WireContext */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {unknown} data part 载荷
|
|
10
|
+
* @param {string} groupId 群 ID
|
|
11
|
+
* @returns {object | null} 校验通过后的载荷
|
|
12
|
+
*/
|
|
13
|
+
function parseGroupContext(data, groupId) {
|
|
14
|
+
if (!isPlainObject(data)) return null
|
|
15
|
+
if (data.groupId !== groupId) return null
|
|
16
|
+
return data
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {WireAdapter} wire 底层适配器
|
|
21
|
+
* @param {string} groupId 群 ID
|
|
22
|
+
* @returns {WireAdapter['on']} 注入 groupId 的 on 包装
|
|
23
|
+
*/
|
|
24
|
+
function wrapWireOn(wire, groupId) {
|
|
25
|
+
return (name, handler) => wire.on(name, (data, peerId) => {
|
|
26
|
+
const payload = parseGroupContext(data, groupId)
|
|
27
|
+
if (!payload) return
|
|
28
|
+
handler(payload, peerId)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 群联邦房间挂载 part_wire(要求线载荷带 `groupId`)。
|
|
34
|
+
* @param {WireContext} wireContext 入站上下文
|
|
35
|
+
* @param {string} groupId 群 ID
|
|
36
|
+
* @param {WireAdapter} wire action 表
|
|
37
|
+
* @param {{ allowPartInvoke?: (payload: object) => boolean }} [options] 入站过滤
|
|
38
|
+
* @returns {() => void} 取消挂载的 dispose
|
|
39
|
+
*/
|
|
40
|
+
export function attachGroupPartWire(wireContext, groupId, wire, options = {}) {
|
|
41
|
+
return attachPartWire(wireContext, {
|
|
42
|
+
send: wire.send.bind(wire),
|
|
43
|
+
on: wrapWireOn(wire, groupId),
|
|
44
|
+
}, options)
|
|
45
|
+
}
|
|
@@ -1,34 +1,23 @@
|
|
|
1
|
+
import { isPlainObject } from '../../core/object.mjs'
|
|
2
|
+
import { parsePartpath } from '../../core/partpath.mjs'
|
|
3
|
+
import { dispatchDeliveryInbound, dispatchRpcInbound } from '../../registries/inbound.mjs'
|
|
4
|
+
import { subscribeWire } from '../subscribe.mjs'
|
|
1
5
|
|
|
2
|
-
import { dispatchDeliveryInbound, dispatchRpcInbound } from '../registries/inbound.mjs'
|
|
3
|
-
|
|
4
|
-
import { isPlainObject } from './ingress.mjs'
|
|
5
|
-
import { buildPartInvokePayload } from './part_common.mjs'
|
|
6
6
|
import {
|
|
7
7
|
isPartInvoke,
|
|
8
8
|
isPartInvokeResponse,
|
|
9
|
-
normalizePartpath,
|
|
10
9
|
unwrapPartInvokeResult,
|
|
11
|
-
} from './
|
|
12
|
-
|
|
13
|
-
/** @typedef {import('./part_invoke.mjs').PartInvokeResponse} PartInvokeResponse */
|
|
14
|
-
|
|
15
|
-
/** @type {Map<string, { responses: PartInvokeResponse[], finish: () => void, maxResponses: number, respondedPeers: Set<string> }>} */
|
|
16
|
-
export const pendingPartInvoke = new Map()
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @typedef {{
|
|
20
|
-
* send: (name: string, payload: unknown, peerId: string | null) => void
|
|
21
|
-
* on: (name: string, handler: (payload: unknown, peerId: string) => void) => (() => void) | void
|
|
22
|
-
* }} PartWireAdapter
|
|
23
|
-
*/
|
|
10
|
+
} from './invoke.mjs'
|
|
11
|
+
import { handleIncomingPartInvokeResponse } from './pending.mjs'
|
|
24
12
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
*/
|
|
13
|
+
/** @typedef {import('../../wire/adapter.mjs').WireAdapter} WireAdapter */
|
|
14
|
+
/** @typedef {import('../../wire/adapter.mjs').WireContext} PartWireContext */
|
|
15
|
+
/** @typedef {import('./invoke.mjs').PartInvokeResponse} PartInvokeResponse */
|
|
16
|
+
/** @typedef {import('./invoke.mjs').PartInvoke} PartInvoke */
|
|
28
17
|
|
|
29
18
|
/**
|
|
30
19
|
* @param {object} data 入站 part_timeline_put 载荷
|
|
31
|
-
* @param {string} partpath
|
|
20
|
+
* @param {string} partpath 已校验 part 路径
|
|
32
21
|
* @returns {object | null} 白名单字段
|
|
33
22
|
*/
|
|
34
23
|
function parsePartTimelinePut(data, partpath) {
|
|
@@ -44,25 +33,40 @@ function parsePartTimelinePut(data, partpath) {
|
|
|
44
33
|
}
|
|
45
34
|
}
|
|
46
35
|
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} partpath part 路径
|
|
38
|
+
* @param {PartInvoke} invoke 调用体
|
|
39
|
+
* @param {string} [nodeHash] 来源节点
|
|
40
|
+
* @param {string} [groupId] 群上下文
|
|
41
|
+
* @returns {object} part_invoke 线载荷
|
|
42
|
+
*/
|
|
43
|
+
function buildPartInvokePayload(partpath, invoke, nodeHash, groupId) {
|
|
44
|
+
return {
|
|
45
|
+
partpath,
|
|
46
|
+
invoke,
|
|
47
|
+
...nodeHash ? { nodeHash } : {},
|
|
48
|
+
...groupId ? { groupId } : {},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
/**
|
|
48
53
|
* @param {PartWireContext} wireContext 入站上下文
|
|
49
|
-
* @param {object} payload part_invoke
|
|
54
|
+
* @param {object} payload part_invoke 请求(含已验证 peerId)
|
|
50
55
|
* @returns {Promise<PartInvokeResponse | null>} RPC 处理器返回值
|
|
51
56
|
*/
|
|
52
57
|
async function dispatchPartInvoke(wireContext, payload) {
|
|
53
|
-
const partpath =
|
|
58
|
+
const partpath = parsePartpath(payload?.partpath)
|
|
54
59
|
const invoke = payload?.invoke
|
|
55
60
|
if (!partpath || !isPlainObject(invoke)) return null
|
|
56
61
|
return dispatchRpcInbound({
|
|
57
62
|
replicaUsername: wireContext.replicaUsername,
|
|
58
|
-
requesterNodeHash: payload.
|
|
63
|
+
requesterNodeHash: payload.peerId ? String(payload.peerId).trim() : null,
|
|
59
64
|
groupId: payload.groupId ? String(payload.groupId).trim() : undefined,
|
|
60
65
|
peerId: payload.peerId,
|
|
61
66
|
}, {
|
|
62
67
|
type: 'part_invoke',
|
|
63
68
|
partpath,
|
|
64
69
|
invoke,
|
|
65
|
-
nodeHash: payload.nodeHash,
|
|
66
70
|
groupId: payload.groupId,
|
|
67
71
|
requestId: payload.requestId,
|
|
68
72
|
})
|
|
@@ -71,24 +75,32 @@ async function dispatchPartInvoke(wireContext, payload) {
|
|
|
71
75
|
/**
|
|
72
76
|
* 挂载 part_timeline_put / part_invoke / part_invoke_response。
|
|
73
77
|
* @param {PartWireContext} wireContext 入站上下文
|
|
74
|
-
* @param {
|
|
78
|
+
* @param {WireAdapter} wire action 表
|
|
75
79
|
* @param {{ allowPartInvoke?: (payload: object) => boolean }} [options] 入站过滤
|
|
76
80
|
* @returns {() => void} 取消挂载的 dispose
|
|
77
81
|
*/
|
|
78
82
|
export function attachPartWire(wireContext, wire, options = {}) {
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
return subscribeWire(wire, {
|
|
84
|
+
/**
|
|
85
|
+
* @param {unknown} data part_timeline_put 载荷
|
|
86
|
+
* @param {string} peerId 对端 nodeHash
|
|
87
|
+
*/
|
|
88
|
+
part_timeline_put(data, peerId) {
|
|
81
89
|
if (!isPlainObject(data)) return
|
|
82
|
-
const partpath =
|
|
90
|
+
const partpath = parsePartpath(data.partpath)
|
|
83
91
|
if (!partpath) return
|
|
84
92
|
const message = parsePartTimelinePut(data, partpath)
|
|
85
93
|
if (!message) return
|
|
86
94
|
void dispatchDeliveryInbound({
|
|
87
95
|
replicaUsername: wireContext.replicaUsername,
|
|
88
|
-
requesterNodeHash:
|
|
96
|
+
requesterNodeHash: peerId ? String(peerId).trim() : null,
|
|
89
97
|
}, message)
|
|
90
|
-
}
|
|
91
|
-
|
|
98
|
+
},
|
|
99
|
+
/**
|
|
100
|
+
* @param {unknown} data part_invoke 载荷
|
|
101
|
+
* @param {string} peerId 对端 nodeHash
|
|
102
|
+
*/
|
|
103
|
+
part_invoke(data, peerId) {
|
|
92
104
|
if (!isPlainObject(data)) return
|
|
93
105
|
if (options.allowPartInvoke?.(data) === false) return
|
|
94
106
|
const payload = { ...data, peerId }
|
|
@@ -96,27 +108,27 @@ export function attachPartWire(wireContext, wire, options = {}) {
|
|
|
96
108
|
void handleIncomingPartInvokeRequest(wireContext, payload, wire, peerId)
|
|
97
109
|
else
|
|
98
110
|
void handleIncomingPartInvokeFireAndForget(wireContext, payload, wire, peerId)
|
|
99
|
-
}
|
|
100
|
-
|
|
111
|
+
},
|
|
112
|
+
/**
|
|
113
|
+
* @param {unknown} data part_invoke_response 载荷
|
|
114
|
+
* @param {string} peerId 对端 nodeHash
|
|
115
|
+
*/
|
|
116
|
+
part_invoke_response(data, peerId) {
|
|
101
117
|
if (!isPlainObject(data)) return
|
|
102
118
|
handleIncomingPartInvokeResponse(data, peerId)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return () => {
|
|
106
|
-
for (const off of offs)
|
|
107
|
-
try { off?.() } catch { /* ignore */ }
|
|
108
|
-
}
|
|
119
|
+
},
|
|
120
|
+
})
|
|
109
121
|
}
|
|
110
122
|
|
|
111
123
|
/**
|
|
112
124
|
* @param {PartWireContext} wireContext 入站上下文
|
|
113
125
|
* @param {object} payload part_invoke 请求(含 requestId)
|
|
114
|
-
* @param {
|
|
126
|
+
* @param {WireAdapter} wire 发送适配器
|
|
115
127
|
* @param {string} peerId 对端
|
|
116
128
|
* @returns {Promise<void>}
|
|
117
129
|
*/
|
|
118
130
|
export async function handleIncomingPartInvokeRequest(wireContext, payload, wire, peerId) {
|
|
119
|
-
const partpath =
|
|
131
|
+
const partpath = parsePartpath(payload?.partpath)
|
|
120
132
|
if (!partpath || !payload.requestId) return
|
|
121
133
|
|
|
122
134
|
const response = await dispatchPartInvoke(wireContext, { ...payload, peerId })
|
|
@@ -135,40 +147,19 @@ export async function handleIncomingPartInvokeRequest(wireContext, payload, wire
|
|
|
135
147
|
/**
|
|
136
148
|
* @param {PartWireContext} wireContext 入站上下文
|
|
137
149
|
* @param {object} payload part_invoke 请求(无 requestId)
|
|
138
|
-
* @param {
|
|
150
|
+
* @param {WireAdapter} wire 发送适配器
|
|
139
151
|
* @param {string} peerId 对端
|
|
140
152
|
* @returns {Promise<void>}
|
|
141
153
|
*/
|
|
142
154
|
export async function handleIncomingPartInvokeFireAndForget(wireContext, payload, wire, peerId) {
|
|
143
|
-
const partpath =
|
|
155
|
+
const partpath = parsePartpath(payload?.partpath)
|
|
144
156
|
if (!partpath) return
|
|
145
157
|
|
|
146
158
|
const response = await dispatchPartInvoke(wireContext, { ...payload, peerId })
|
|
147
159
|
const followUp = unwrapPartInvokeResult(response)
|
|
148
160
|
if (!isPartInvoke(followUp)) return
|
|
149
161
|
try {
|
|
150
|
-
wire.send('part_invoke', buildPartInvokePayload(
|
|
151
|
-
partpath,
|
|
152
|
-
invoke: followUp,
|
|
153
|
-
nodeHash: payload.nodeHash,
|
|
154
|
-
groupId: payload.groupId,
|
|
155
|
-
}), peerId)
|
|
162
|
+
wire.send('part_invoke', buildPartInvokePayload(partpath, followUp, peerId, payload.groupId), peerId)
|
|
156
163
|
}
|
|
157
164
|
catch { /* disconnected */ }
|
|
158
165
|
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* @param {object} payload 响应
|
|
162
|
-
* @param {string} [peerId] Trystero 对端 id,用于同 peer 去重
|
|
163
|
-
* @returns {void}
|
|
164
|
-
*/
|
|
165
|
-
export function handleIncomingPartInvokeResponse(payload, peerId = '') {
|
|
166
|
-
const pending = pendingPartInvoke.get(payload.requestId)
|
|
167
|
-
if (!pending || !isPartInvokeResponse(payload.response)) return
|
|
168
|
-
if (peerId) {
|
|
169
|
-
if (pending.respondedPeers.has(peerId)) return
|
|
170
|
-
pending.respondedPeers.add(peerId)
|
|
171
|
-
}
|
|
172
|
-
pending.responses.push(payload.response)
|
|
173
|
-
if (pending.responses.length >= pending.maxResponses) pending.finish()
|
|
174
|
-
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { isPlainObject } from '
|
|
2
|
-
|
|
3
|
-
const PARTPATH_RE = /^[\w-]+(?:\/[\w-]+)*$/
|
|
1
|
+
import { isPlainObject } from '../../core/object.mjs'
|
|
4
2
|
|
|
5
3
|
/** @typedef {string} PartInvokeKind */
|
|
6
4
|
|
|
@@ -22,16 +20,6 @@ const PARTPATH_RE = /^[\w-]+(?:\/[\w-]+)*$/
|
|
|
22
20
|
|
|
23
21
|
/** @typedef {PartInvokeResultResponse | PartInvokeErrorResponse} PartInvokeResponse */
|
|
24
22
|
|
|
25
|
-
/**
|
|
26
|
-
* @typedef {object} SocialRpcPartInvokeResponse
|
|
27
|
-
* @property {object} result RPC 响应体
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* @typedef {object} TimelinePutPartInvokeResponse
|
|
32
|
-
* @property {{ ok: boolean }} result timeline ingest 结果
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
23
|
/**
|
|
36
24
|
* @param {unknown} value 候选响应
|
|
37
25
|
* @returns {value is PartInvokeResponse} 是否为 part_invoke 响应体
|
|
@@ -61,15 +49,6 @@ export function unwrapPartInvokeResult(response) {
|
|
|
61
49
|
return response.result ?? null
|
|
62
50
|
}
|
|
63
51
|
|
|
64
|
-
/**
|
|
65
|
-
* @param {unknown} value partpath 字符串
|
|
66
|
-
* @returns {string | null} 规范化 partpath
|
|
67
|
-
*/
|
|
68
|
-
export function normalizePartpath(value) {
|
|
69
|
-
const path = String(value || '').trim().replace(/^\/+|\/+$/g, '')
|
|
70
|
-
return PARTPATH_RE.test(path) ? path : null
|
|
71
|
-
}
|
|
72
|
-
|
|
73
52
|
/**
|
|
74
53
|
* @param {unknown} value invoke 体
|
|
75
54
|
* @returns {value is PartInvoke} 是否含已知 kind
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { isPartInvokeResponse } from './invoke.mjs'
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('./invoke.mjs').PartInvokeResponse} PartInvokeResponse */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{
|
|
7
|
+
* responses: PartInvokeResponse[]
|
|
8
|
+
* finish: () => void
|
|
9
|
+
* maxResponses: number
|
|
10
|
+
* respondedPeers: Set<string>
|
|
11
|
+
* }} PendingPartInvoke
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** @type {Map<string, PendingPartInvoke>} */
|
|
15
|
+
export const pendingPartInvoke = new Map()
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 入站 part_invoke_response:写入对应 pending 槽。
|
|
19
|
+
* @param {object} payload 响应
|
|
20
|
+
* @param {string} [peerId] 对端 id,用于同 peer 去重
|
|
21
|
+
* @returns {void}
|
|
22
|
+
*/
|
|
23
|
+
export function handleIncomingPartInvokeResponse(payload, peerId = '') {
|
|
24
|
+
const pending = pendingPartInvoke.get(payload.requestId)
|
|
25
|
+
if (!pending || !isPartInvokeResponse(payload.response)) return
|
|
26
|
+
if (peerId) {
|
|
27
|
+
if (pending.respondedPeers.has(peerId)) return
|
|
28
|
+
pending.respondedPeers.add(peerId)
|
|
29
|
+
}
|
|
30
|
+
pending.responses.push(payload.response)
|
|
31
|
+
if (pending.responses.length >= pending.maxResponses) pending.finish()
|
|
32
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
handleIncomingPartQueryResponse,
|
|
3
|
+
processIncomingPartQueryRequest,
|
|
4
|
+
queryNetwork as queryNetworkCore,
|
|
5
|
+
resolvePartQueryState,
|
|
6
|
+
} from '../../federation/part_query/runtime.mjs'
|
|
7
|
+
import {
|
|
8
|
+
parsePartQueryReq,
|
|
9
|
+
parsePartQueryRes,
|
|
10
|
+
} from '../../schemas/part_query.mjs'
|
|
11
|
+
import partQueryTunables from '../../schemas/part_query.tunables.json' with { type: 'json' }
|
|
12
|
+
import { sendToNodeLink } from '../../transport/link_registry.mjs'
|
|
13
|
+
import { consumeWireRateBucket } from '../rate_bucket.mjs'
|
|
14
|
+
import { subscribeWire } from '../subscribe.mjs'
|
|
15
|
+
|
|
16
|
+
/** @typedef {import('../adapter.mjs').WireAdapter} WireAdapter */
|
|
17
|
+
/** @typedef {import('../adapter.mjs').WireContext} WireContext */
|
|
18
|
+
/** @typedef {import('../../federation/part_query/runtime.mjs').PartQueryDependencies} PartQueryDependencies */
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {PartQueryDependencies} [dependencies] 可注入依赖
|
|
22
|
+
* @returns {PartQueryDependencies} 补上默认 deliver 后的依赖
|
|
23
|
+
*/
|
|
24
|
+
function withDefaultDeliver(dependencies = {}) {
|
|
25
|
+
return {
|
|
26
|
+
...dependencies,
|
|
27
|
+
deliver: dependencies.deliver || ((nodeHash, action, payload) =>
|
|
28
|
+
sendToNodeLink(nodeHash, { scope: 'node', action, payload })),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 挂载 part_query_req / part_query_res(入站 parse / dedupe / rate;运行时在 federation)。
|
|
34
|
+
* @param {WireContext} wireContext 入站上下文
|
|
35
|
+
* @param {WireAdapter} wire wire
|
|
36
|
+
* @param {PartQueryDependencies} [dependencies] 可注入依赖(含 per-node state)
|
|
37
|
+
* @returns {() => void} 取消挂载的 dispose
|
|
38
|
+
*/
|
|
39
|
+
export function attachPartQueryWire(wireContext, wire, dependencies = {}) {
|
|
40
|
+
const deps = withDefaultDeliver(dependencies)
|
|
41
|
+
const state = resolvePartQueryState(deps)
|
|
42
|
+
return subscribeWire(wire, {
|
|
43
|
+
/**
|
|
44
|
+
* @param {unknown} data part_query_req 载荷
|
|
45
|
+
* @param {string} peerId 对端 nodeHash
|
|
46
|
+
*/
|
|
47
|
+
part_query_req(data, peerId) {
|
|
48
|
+
const request = parsePartQueryReq(data)
|
|
49
|
+
if (!request) return
|
|
50
|
+
if (!state.takeDedupe(request.requestId)) return
|
|
51
|
+
const source = String(peerId || request.originNodeHash || '').trim().toLowerCase()
|
|
52
|
+
if (source && !consumeWireRateBucket(`part_query:${source}`, {
|
|
53
|
+
maxCount: partQueryTunables.ratePerSourcePerMin,
|
|
54
|
+
})) return
|
|
55
|
+
void processIncomingPartQueryRequest(wireContext, wire, request, String(peerId || ''), deps)
|
|
56
|
+
},
|
|
57
|
+
/**
|
|
58
|
+
* @param {unknown} data part_query_res 载荷
|
|
59
|
+
* @param {string} peerId 对端 nodeHash
|
|
60
|
+
*/
|
|
61
|
+
part_query_res(data, peerId) {
|
|
62
|
+
const response = parsePartQueryRes(data)
|
|
63
|
+
if (!response) return
|
|
64
|
+
handleIncomingPartQueryResponse(response, String(peerId || ''), deps)
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 多跳查询(默认经 link_registry 投递;可注入 deliver)。
|
|
71
|
+
* @param {string} username trust graph 上下文
|
|
72
|
+
* @param {string} partpath part 路径
|
|
73
|
+
* @param {string} kind 查询标签
|
|
74
|
+
* @param {unknown} query 不透明查询
|
|
75
|
+
* @param {Parameters<typeof queryNetworkCore>[4]} [options] 选项
|
|
76
|
+
* @returns {Promise<unknown[]>} 合并后的 rows
|
|
77
|
+
*/
|
|
78
|
+
export function queryNetwork(username, partpath, kind, query, options = {}) {
|
|
79
|
+
return queryNetworkCore(username, partpath, kind, query, withDefaultDeliver(options))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Part query 运行时(state、入站 handler、hop timeout、测试重置)。
|
|
84
|
+
*/
|
|
85
|
+
export {
|
|
86
|
+
createPartQueryNodeState,
|
|
87
|
+
registerQueryInboundHandler,
|
|
88
|
+
resetPartQueryStateForTests,
|
|
89
|
+
resolvePartQueryHopTimeoutMs,
|
|
90
|
+
resolvePartQueryState,
|
|
91
|
+
} from '../../federation/part_query/runtime.mjs'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 批量注册 wire action,返回统一 dispose。
|
|
3
|
+
* @param {import('./adapter.mjs').WireAdapter} wire action 表
|
|
4
|
+
* @param {Record<string, (payload: unknown, peerId: string) => void>} handlers action → handler
|
|
5
|
+
* @returns {() => void} 取消全部注册
|
|
6
|
+
*/
|
|
7
|
+
export function subscribeWire(wire, handlers) {
|
|
8
|
+
const disposers = Object.entries(handlers).map(([name, handler]) => wire.on(name, handler))
|
|
9
|
+
return () => {
|
|
10
|
+
for (const dispose of disposers)
|
|
11
|
+
dispose()
|
|
12
|
+
}
|
|
13
|
+
}
|