@steve02081504/fount-p2p 0.0.0
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 +62 -0
- package/core/bytes_codec.mjs +51 -0
- package/core/canonical_json.mjs +21 -0
- package/core/composite_key.mjs +91 -0
- package/core/constants.mjs +53 -0
- package/core/entity_id.mjs +53 -0
- package/core/entity_id_parse.mjs +40 -0
- package/core/evfs_logical_path.mjs +15 -0
- package/core/hexIds.mjs +73 -0
- package/core/hlc.mjs +114 -0
- package/core/random_id.mjs +9 -0
- package/crypto/channel.mjs +101 -0
- package/crypto/checkpoint_sign.mjs +67 -0
- package/crypto/crypto.mjs +158 -0
- package/crypto/key.mjs +431 -0
- package/dag/canonicalize_row.mjs +65 -0
- package/dag/event_query.mjs +15 -0
- package/dag/index.mjs +335 -0
- package/dag/storage.mjs +333 -0
- package/dag/strip_extensions.mjs +18 -0
- package/dag/tunables.json +6 -0
- package/discovery/bt.mjs +260 -0
- package/discovery/index.mjs +134 -0
- package/discovery/mdns.mjs +138 -0
- package/discovery/nostr.mjs +273 -0
- package/entity/files/acl.mjs +38 -0
- package/entity/files/evfs.mjs +189 -0
- package/entity/files/evfs_ref.mjs +37 -0
- package/entity/files/manifest_acl_registry.mjs +93 -0
- package/entity/files/replica_host_cache.mjs +46 -0
- package/entity/files/url.mjs +19 -0
- package/entity/hosting_registry.mjs +47 -0
- package/entity/localized_core.mjs +141 -0
- package/entity/logical_entity.mjs +25 -0
- package/entity/logical_entity_id_registry.mjs +40 -0
- package/entity/node_hash.mjs +15 -0
- package/entity/presentation_registry.mjs +44 -0
- package/entity/profile.mjs +256 -0
- package/entity/replica.mjs +20 -0
- package/entity/session_snapshot_registry.mjs +38 -0
- package/federation/chunk_fetch_pending.mjs +128 -0
- package/federation/chunk_fetch_scheduler.mjs +97 -0
- package/federation/dag_order_cache.mjs +124 -0
- package/federation/dedupe_slot.mjs +33 -0
- package/federation/message_rate_limit.mjs +23 -0
- package/federation/operator_key_chain.mjs +200 -0
- package/federation/topo_order_memo.mjs +44 -0
- package/federation/volatile_streams.mjs +52 -0
- package/federation/want_ids.mjs +158 -0
- package/files/assemble.mjs +220 -0
- package/files/assemble_stream.mjs +129 -0
- package/files/chunk_fetch.mjs +113 -0
- package/files/chunk_fetch_verify.mjs +23 -0
- package/files/chunk_provider_registry.mjs +70 -0
- package/files/chunk_responder.mjs +79 -0
- package/files/chunk_store.mjs +87 -0
- package/files/manifest.mjs +99 -0
- package/files/transfer_key.mjs +87 -0
- package/files/transfer_key_registry.mjs +108 -0
- package/governance/branch.mjs +230 -0
- package/governance/join_pow.mjs +165 -0
- package/governance/owner_succession_ballot.mjs +57 -0
- package/governance/tunables.json +7 -0
- package/index.mjs +39 -0
- package/link/channel_mux.mjs +222 -0
- package/link/frame.mjs +246 -0
- package/link/handshake.mjs +175 -0
- package/link/link.mjs +617 -0
- package/link/rtc.mjs +109 -0
- package/link/sdp_fingerprint.mjs +25 -0
- package/mailbox/consumer_registry.mjs +71 -0
- package/mailbox/deliver_or_store.mjs +190 -0
- package/mailbox/importance.mjs +72 -0
- package/mailbox/parse.mjs +73 -0
- package/mailbox/prune.mjs +106 -0
- package/mailbox/rate.mjs +86 -0
- package/mailbox/settings.mjs +53 -0
- package/mailbox/store.mjs +256 -0
- package/mailbox/tunables.json +12 -0
- package/mailbox/wire.mjs +40 -0
- package/node/denylist.mjs +309 -0
- package/node/entity_store.mjs +214 -0
- package/node/identity.mjs +99 -0
- package/node/instance.mjs +115 -0
- package/node/network.mjs +306 -0
- package/node/personal_block.mjs +286 -0
- package/node/reputation_store.mjs +326 -0
- package/node/retention_policy.mjs +82 -0
- package/node/signaling_config.mjs +32 -0
- package/node/storage.mjs +30 -0
- package/node/storage_plugins.mjs +71 -0
- package/node/user_paths.mjs +10 -0
- package/overlay/index.mjs +191 -0
- package/package.json +73 -0
- package/permissions/bitmask.mjs +35 -0
- package/permissions/evaluator.mjs +90 -0
- package/permissions/index.mjs +12 -0
- package/permissions/layered.mjs +39 -0
- package/registries/action.mjs +94 -0
- package/registries/event_type.mjs +75 -0
- package/registries/inbound.mjs +90 -0
- package/registries/p2p_viewer.mjs +35 -0
- package/registries/part_path.mjs +35 -0
- package/registries/room_provider.mjs +47 -0
- package/reputation/engine.mjs +458 -0
- package/reputation/math.mjs +73 -0
- package/reputation/pick_score.mjs +11 -0
- package/reputation/relay_dedupe.mjs +22 -0
- package/reputation/tunables.json +36 -0
- package/rooms/scoped_link.mjs +185 -0
- package/schemas/discovery.mjs +110 -0
- package/schemas/federation_pull.mjs +102 -0
- package/schemas/mailbox.mjs +37 -0
- package/schemas/remote_event.mjs +17 -0
- package/timeline/append_core.mjs +54 -0
- package/timeline/materialize_runner.mjs +26 -0
- package/timeline/prune.mjs +39 -0
- package/timeline/retention.mjs +54 -0
- package/timeline/retention_runner.mjs +35 -0
- package/timeline/verify_remote.mjs +25 -0
- package/transport/group_link_set.mjs +324 -0
- package/transport/ice_servers.mjs +72 -0
- package/transport/link_registry.mjs +749 -0
- package/transport/peer_identity_maps.mjs +94 -0
- package/transport/peer_pool.mjs +355 -0
- package/transport/remote_user_room.mjs +109 -0
- package/transport/room_scopes.mjs +2 -0
- package/transport/room_wire_action.mjs +19 -0
- package/transport/rtc_connection_budget.mjs +180 -0
- package/transport/rtc_mdns_filter.mjs +118 -0
- package/transport/stale_peer_log.mjs +50 -0
- package/transport/user_room.mjs +263 -0
- package/trust_graph/build.mjs +99 -0
- package/trust_graph/cache.mjs +44 -0
- package/trust_graph/engine.mjs +184 -0
- package/trust_graph/registry.mjs +38 -0
- package/trust_graph/resolve.mjs +105 -0
- package/trust_graph/send.mjs +88 -0
- package/trust_graph/tunables.json +13 -0
- package/utils/async_mutex.mjs +69 -0
- package/utils/debug_log.mjs +16 -0
- package/utils/duration.mjs +28 -0
- package/utils/json_io.mjs +57 -0
- package/utils/lru.mjs +22 -0
- package/utils/map_pool.mjs +27 -0
- package/wire/group_part.mjs +43 -0
- package/wire/ingress.mjs +58 -0
- package/wire/part_common.mjs +55 -0
- package/wire/part_fanout.mjs +71 -0
- package/wire/part_ingress.mjs +172 -0
- package/wire/part_invoke.mjs +81 -0
- package/wire/rate_bucket.mjs +49 -0
- package/wire/volatile_signature.mjs +76 -0
- package/wire/wait.mjs +127 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import fsp from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} filePath 绝对路径
|
|
7
|
+
* @returns {Promise<object | null>} JSON 或 null
|
|
8
|
+
*/
|
|
9
|
+
export async function readJsonFile(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
const raw = await fsp.readFile(filePath, 'utf8')
|
|
12
|
+
return JSON.parse(raw)
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
if (/** @type {NodeJS.ErrnoException} */ err.code === 'ENOENT') return null
|
|
16
|
+
throw err
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} filePath 绝对路径
|
|
22
|
+
* @param {unknown} data 可序列化对象
|
|
23
|
+
* @returns {Promise<void>}
|
|
24
|
+
*/
|
|
25
|
+
export async function writeJsonFile(filePath, data) {
|
|
26
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true })
|
|
27
|
+
const tmp = `${filePath}.tmp`
|
|
28
|
+
await fsp.writeFile(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
|
|
29
|
+
await fsp.rename(tmp, filePath)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} filePath 绝对路径
|
|
34
|
+
* @returns {object | null} JSON 或 null
|
|
35
|
+
*/
|
|
36
|
+
export function readJsonFileSync(filePath) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = fs.readFileSync(filePath, 'utf8')
|
|
39
|
+
return JSON.parse(raw)
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
if (/** @type {NodeJS.ErrnoException} */ err.code === 'ENOENT') return null
|
|
43
|
+
throw err
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} filePath 绝对路径
|
|
49
|
+
* @param {unknown} data 可序列化对象
|
|
50
|
+
* @returns {void}
|
|
51
|
+
*/
|
|
52
|
+
export function writeJsonFileSync(filePath, data) {
|
|
53
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
54
|
+
const tmp = `${filePath}.tmp`
|
|
55
|
+
fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
|
|
56
|
+
fs.renameSync(tmp, filePath)
|
|
57
|
+
}
|
package/utils/lru.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template K, V
|
|
3
|
+
* @param {number} max 最大条目
|
|
4
|
+
* @returns {Map<K, V> & { touch: (key: K, value: V) => void }} 带 touch 方法的 LRU Map
|
|
5
|
+
*/
|
|
6
|
+
export function createLruMap(max) {
|
|
7
|
+
/** @type {Map<K, V>} */
|
|
8
|
+
const map = new Map()
|
|
9
|
+
/**
|
|
10
|
+
* @param {K} key 键
|
|
11
|
+
* @param {V} value 值
|
|
12
|
+
*/
|
|
13
|
+
const touch = (key, value) => {
|
|
14
|
+
if (map.has(key)) map.delete(key)
|
|
15
|
+
map.set(key, value)
|
|
16
|
+
while (map.size > max) {
|
|
17
|
+
const oldest = map.keys().next().value
|
|
18
|
+
map.delete(oldest)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return Object.assign(map, { touch })
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 有限并发 map(进程内,无外部依赖)。
|
|
3
|
+
* @template T, R
|
|
4
|
+
* @param {T[]} items 待处理项
|
|
5
|
+
* @param {(item: T, index: number) => Promise<R>} fn 异步映射
|
|
6
|
+
* @param {number} concurrency 并发上限
|
|
7
|
+
* @returns {Promise<R[]>} 与 items 同序的结果
|
|
8
|
+
*/
|
|
9
|
+
export async function mapPool(items, fn, concurrency) {
|
|
10
|
+
if (!items.length) return []
|
|
11
|
+
const limit = Math.max(1, Math.min(concurrency, items.length))
|
|
12
|
+
/** @type {R[]} */
|
|
13
|
+
const results = new Array(items.length)
|
|
14
|
+
let nextIndex = 0
|
|
15
|
+
|
|
16
|
+
/** @returns {Promise<void>} */
|
|
17
|
+
const worker = async () => {
|
|
18
|
+
for (; ;) {
|
|
19
|
+
const i = nextIndex++
|
|
20
|
+
if (i >= items.length) return
|
|
21
|
+
results[i] = await fn(items[i], i)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await Promise.all(Array.from({ length: limit }, () => worker()))
|
|
26
|
+
return results
|
|
27
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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 }} ctx 入站上下文
|
|
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(ctx, groupId, wire, options = {}) {
|
|
39
|
+
attachPartWire(ctx, {
|
|
40
|
+
send: wire.send.bind(wire),
|
|
41
|
+
on: wrapWireOn(wire, groupId),
|
|
42
|
+
}, options)
|
|
43
|
+
}
|
package/wire/ingress.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P2P / Trystero 入站 wire 公共工具(外来网络边界)。
|
|
3
|
+
*/
|
|
4
|
+
import { Buffer } from 'node:buffer'
|
|
5
|
+
|
|
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
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 解析 WebSocket / Trystero 入站 JSON 帧。
|
|
18
|
+
* @param {unknown} raw `ws` message 或字符串
|
|
19
|
+
* @returns {Record<string, unknown> | null} 解析失败或非对象时为 null
|
|
20
|
+
*/
|
|
21
|
+
export function parseInboundJson(raw) {
|
|
22
|
+
if (raw == null) return null
|
|
23
|
+
const text = typeof raw === 'string'
|
|
24
|
+
? raw
|
|
25
|
+
: Buffer.isBuffer(raw)
|
|
26
|
+
? raw.toString('utf8')
|
|
27
|
+
: String(raw)
|
|
28
|
+
let parsed
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(text)
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
return isPlainObject(parsed) ? parsed : null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {unknown} event 单条 DAG 行
|
|
40
|
+
* @returns {boolean} 是否具备完整远程签名形态
|
|
41
|
+
*/
|
|
42
|
+
export function isSignedDagEventRow(event) {
|
|
43
|
+
return isPlainObject(event)
|
|
44
|
+
&& isHex64(event.id)
|
|
45
|
+
&& isSignatureHex128(event.signature)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 从联邦 `dag_event` 载荷取出已签名事件行。
|
|
50
|
+
* @param {unknown} payload Trystero 载荷(完整签名事件)
|
|
51
|
+
* @param {string} groupId 本群 ID
|
|
52
|
+
* @returns {object | null} 验形通过的事件;否则 null
|
|
53
|
+
*/
|
|
54
|
+
export function extractInboundSignedEvent(payload, groupId) {
|
|
55
|
+
if (!isSignedDagEventRow(payload)) return null
|
|
56
|
+
if (payload.groupId && String(payload.groupId) !== groupId) return null
|
|
57
|
+
return payload
|
|
58
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
*
|
|
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
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 单向 part_invoke fanout(mailbox put 等)。
|
|
56
|
+
* @param {string} username 用户
|
|
57
|
+
* @param {string} partpath part 路径
|
|
58
|
+
* @param {import('./part_invoke.mjs').PartInvoke} invoke 调用体
|
|
59
|
+
* @param {number} limit fanout 上限
|
|
60
|
+
* @param {string} [nodeHash] 来源节点
|
|
61
|
+
* @param {string} [groupId] 群上下文
|
|
62
|
+
* @returns {Promise<number>} 发送次数
|
|
63
|
+
*/
|
|
64
|
+
export async function fanoutPartInvoke(username, partpath, invoke, limit, nodeHash, groupId) {
|
|
65
|
+
return requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).fanoutToTopNodes(username, 'part_invoke', buildPartInvokePayload({
|
|
66
|
+
partpath,
|
|
67
|
+
invoke,
|
|
68
|
+
nodeHash,
|
|
69
|
+
groupId,
|
|
70
|
+
}), limit)
|
|
71
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
|
|
2
|
+
import { dispatchDeliveryInbound, dispatchRpcInbound } from '../registries/inbound.mjs'
|
|
3
|
+
|
|
4
|
+
import { isPlainObject } from './ingress.mjs'
|
|
5
|
+
import { buildPartInvokePayload } from './part_common.mjs'
|
|
6
|
+
import {
|
|
7
|
+
isPartInvoke,
|
|
8
|
+
isPartInvokeResponse,
|
|
9
|
+
normalizePartpath,
|
|
10
|
+
unwrapPartInvokeResult,
|
|
11
|
+
} from './part_invoke.mjs'
|
|
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
|
|
22
|
+
* }} PartWireAdapter
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{ replicaUsername?: string }} PartWireContext
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {object} data 入站 part_timeline_put 载荷
|
|
31
|
+
* @param {string} partpath 已规范化 part 路径
|
|
32
|
+
* @returns {object | null} 白名单字段
|
|
33
|
+
*/
|
|
34
|
+
function parsePartTimelinePut(data, partpath) {
|
|
35
|
+
const timelineEntityHash = String(data.timelineEntityHash || '').trim().toLowerCase()
|
|
36
|
+
if (!timelineEntityHash || !isPlainObject(data.event)) return null
|
|
37
|
+
return {
|
|
38
|
+
type: 'part_timeline_put',
|
|
39
|
+
partpath,
|
|
40
|
+
timelineEntityHash,
|
|
41
|
+
event: data.event,
|
|
42
|
+
...data.nodeHash ? { nodeHash: String(data.nodeHash).trim() } : {},
|
|
43
|
+
...data.groupId ? { groupId: String(data.groupId).trim() } : {},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {PartWireContext} ctx 入站上下文
|
|
49
|
+
* @param {object} payload part_invoke 请求
|
|
50
|
+
* @returns {Promise<PartInvokeResponse | null>} RPC 处理器返回值
|
|
51
|
+
*/
|
|
52
|
+
async function dispatchPartInvoke(ctx, payload) {
|
|
53
|
+
const partpath = normalizePartpath(payload?.partpath)
|
|
54
|
+
const invoke = payload?.invoke
|
|
55
|
+
if (!partpath || !isPlainObject(invoke)) return null
|
|
56
|
+
return dispatchRpcInbound({
|
|
57
|
+
replicaUsername: ctx.replicaUsername,
|
|
58
|
+
requesterNodeHash: payload.nodeHash ? String(payload.nodeHash).trim() : null,
|
|
59
|
+
groupId: payload.groupId ? String(payload.groupId).trim() : undefined,
|
|
60
|
+
peerId: payload.peerId,
|
|
61
|
+
}, {
|
|
62
|
+
type: 'part_invoke',
|
|
63
|
+
partpath,
|
|
64
|
+
invoke,
|
|
65
|
+
nodeHash: payload.nodeHash,
|
|
66
|
+
groupId: payload.groupId,
|
|
67
|
+
requestId: payload.requestId,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 挂载 part_timeline_put / part_invoke / part_invoke_response。
|
|
73
|
+
* @param {PartWireContext} ctx 入站上下文
|
|
74
|
+
* @param {PartWireAdapter} wire Trystero 适配器
|
|
75
|
+
* @param {{ allowPartInvoke?: (payload: object) => boolean }} [options] 入站过滤
|
|
76
|
+
* @returns {void}
|
|
77
|
+
*/
|
|
78
|
+
export function attachPartWire(ctx, wire, options = {}) {
|
|
79
|
+
wire.on('part_timeline_put', data => {
|
|
80
|
+
if (!isPlainObject(data)) return
|
|
81
|
+
const partpath = normalizePartpath(data.partpath)
|
|
82
|
+
if (!partpath) return
|
|
83
|
+
const message = parsePartTimelinePut(data, partpath)
|
|
84
|
+
if (!message) return
|
|
85
|
+
void dispatchDeliveryInbound({
|
|
86
|
+
replicaUsername: ctx.replicaUsername,
|
|
87
|
+
requesterNodeHash: data.nodeHash ? String(data.nodeHash).trim() : null,
|
|
88
|
+
}, message)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
wire.on('part_invoke', (data, peerId) => {
|
|
92
|
+
if (!isPlainObject(data)) return
|
|
93
|
+
if (options.allowPartInvoke?.(data) === false) return
|
|
94
|
+
const payload = { ...data, peerId }
|
|
95
|
+
if (payload.requestId)
|
|
96
|
+
void handleIncomingPartInvokeRequest(ctx, payload, wire, peerId)
|
|
97
|
+
else
|
|
98
|
+
void handleIncomingPartInvokeFireAndForget(ctx, payload, wire, peerId)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
wire.on('part_invoke_response', (data, peerId) => {
|
|
102
|
+
if (!isPlainObject(data)) return
|
|
103
|
+
handleIncomingPartInvokeResponse(data, peerId)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {PartWireContext} ctx 入站上下文
|
|
109
|
+
* @param {object} payload part_invoke 请求(含 requestId)
|
|
110
|
+
* @param {PartWireAdapter} wire 发送适配器
|
|
111
|
+
* @param {string} peerId 对端
|
|
112
|
+
* @returns {Promise<void>}
|
|
113
|
+
*/
|
|
114
|
+
export async function handleIncomingPartInvokeRequest(ctx, payload, wire, peerId) {
|
|
115
|
+
const partpath = normalizePartpath(payload?.partpath)
|
|
116
|
+
if (!partpath || !payload.requestId) return
|
|
117
|
+
|
|
118
|
+
const response = await dispatchPartInvoke(ctx, { ...payload, peerId })
|
|
119
|
+
if (response == null || !isPartInvokeResponse(response)) return
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
wire.send('part_invoke_response', {
|
|
123
|
+
requestId: payload.requestId,
|
|
124
|
+
partpath,
|
|
125
|
+
response,
|
|
126
|
+
}, peerId)
|
|
127
|
+
}
|
|
128
|
+
catch { /* disconnected */ }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {PartWireContext} ctx 入站上下文
|
|
133
|
+
* @param {object} payload part_invoke 请求(无 requestId)
|
|
134
|
+
* @param {PartWireAdapter} wire 发送适配器
|
|
135
|
+
* @param {string} peerId 对端
|
|
136
|
+
* @returns {Promise<void>}
|
|
137
|
+
*/
|
|
138
|
+
export async function handleIncomingPartInvokeFireAndForget(ctx, payload, wire, peerId) {
|
|
139
|
+
const partpath = normalizePartpath(payload?.partpath)
|
|
140
|
+
if (!partpath) return
|
|
141
|
+
|
|
142
|
+
const response = await dispatchPartInvoke(ctx, { ...payload, peerId })
|
|
143
|
+
const followUp = unwrapPartInvokeResult(response)
|
|
144
|
+
if (!isPartInvoke(followUp)) return
|
|
145
|
+
try {
|
|
146
|
+
wire.send('part_invoke', buildPartInvokePayload({
|
|
147
|
+
partpath,
|
|
148
|
+
invoke: followUp,
|
|
149
|
+
nodeHash: payload.nodeHash,
|
|
150
|
+
groupId: payload.groupId,
|
|
151
|
+
}), peerId)
|
|
152
|
+
}
|
|
153
|
+
catch { /* disconnected */ }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {object} payload 响应
|
|
158
|
+
* @param {string} [peerId] Trystero 对端 id,用于同 peer 去重
|
|
159
|
+
* @returns {void}
|
|
160
|
+
*/
|
|
161
|
+
export function handleIncomingPartInvokeResponse(payload, peerId = '') {
|
|
162
|
+
const pending = pendingPartInvoke.get(String(payload?.requestId || ''))
|
|
163
|
+
if (!pending || payload?.response == null) return
|
|
164
|
+
const peerKey = String(peerId || payload?.nodeHash || '').trim()
|
|
165
|
+
if (peerKey) {
|
|
166
|
+
if (pending.respondedPeers.has(peerKey)) return
|
|
167
|
+
pending.respondedPeers.add(peerKey)
|
|
168
|
+
}
|
|
169
|
+
if (!isPartInvokeResponse(payload.response)) return
|
|
170
|
+
pending.responses.push(payload.response)
|
|
171
|
+
if (pending.responses.length >= pending.maxResponses) pending.finish()
|
|
172
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { isPlainObject } from './ingress.mjs'
|
|
2
|
+
|
|
3
|
+
const PARTPATH_RE = /^[\w-]+(?:\/[\w-]+)*$/
|
|
4
|
+
|
|
5
|
+
/** @typedef {string} PartInvokeKind */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object & { kind: PartInvokeKind }} PartInvoke
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** @typedef {{ message: string, code: string }} PartInvokeError */
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} PartInvokeResultResponse
|
|
15
|
+
* @property {unknown} result
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} PartInvokeErrorResponse
|
|
20
|
+
* @property {PartInvokeError} error
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** @typedef {PartInvokeResultResponse | PartInvokeErrorResponse} PartInvokeResponse */
|
|
24
|
+
|
|
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
|
+
/**
|
|
36
|
+
* @param {unknown} value 候选响应
|
|
37
|
+
* @returns {value is PartInvokeResponse} 是否为 part_invoke 响应体
|
|
38
|
+
*/
|
|
39
|
+
export function isPartInvokeResponse(value) {
|
|
40
|
+
if (!isPlainObject(value)) return false
|
|
41
|
+
const hasResult = Object.prototype.hasOwnProperty.call(value, 'result')
|
|
42
|
+
const hasError = Object.prototype.hasOwnProperty.call(value, 'error')
|
|
43
|
+
if (hasResult === hasError) return false
|
|
44
|
+
if (hasError) {
|
|
45
|
+
const err = value.error
|
|
46
|
+
return isPlainObject(err)
|
|
47
|
+
&& typeof err.message === 'string'
|
|
48
|
+
&& err.message.length > 0
|
|
49
|
+
&& typeof err.code === 'string'
|
|
50
|
+
&& err.code.length > 0
|
|
51
|
+
}
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {PartInvokeResponse | null | undefined} response RPC 响应
|
|
57
|
+
* @returns {unknown | null} 成功 `result`;失败或空为 null
|
|
58
|
+
*/
|
|
59
|
+
export function unwrapPartInvokeResult(response) {
|
|
60
|
+
if (!response || !isPartInvokeResponse(response) || 'error' in response) return null
|
|
61
|
+
return response.result ?? null
|
|
62
|
+
}
|
|
63
|
+
|
|
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
|
+
/**
|
|
74
|
+
* @param {unknown} value invoke 体
|
|
75
|
+
* @returns {value is PartInvoke} 是否含已知 kind
|
|
76
|
+
*/
|
|
77
|
+
export function isPartInvoke(value) {
|
|
78
|
+
if (!isPlainObject(value)) return false
|
|
79
|
+
const kind = value.kind
|
|
80
|
+
return typeof kind === 'string' && kind.length > 0
|
|
81
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 60 秒令牌桶限速(fed_chunk / fed_emoji 等 want-data 线共用)。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createLruMap } from '../utils/lru.mjs'
|
|
6
|
+
|
|
7
|
+
const WINDOW_MS = 60_000
|
|
8
|
+
const BUCKET_MAP_MAX = 5000
|
|
9
|
+
|
|
10
|
+
/** @type {ReturnType<typeof createLruMap<string, { tokens: number, bytes: number, lastRefill: number }>>} */
|
|
11
|
+
const buckets = createLruMap(BUCKET_MAP_MAX)
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} bucketKey 房间或资源键
|
|
15
|
+
* @param {{ maxCount: number, byteCount?: number, maxBytesPerWindow?: number }} limits 限额
|
|
16
|
+
* @returns {boolean} 是否允许本次消费
|
|
17
|
+
*/
|
|
18
|
+
export function consumeWireRateBucket(bucketKey, limits) {
|
|
19
|
+
const byteCount = Math.max(0, Number(limits.byteCount) || 0)
|
|
20
|
+
const maxBytes = Number(limits.maxBytesPerWindow) || 0
|
|
21
|
+
const maxCount = limits.maxCount
|
|
22
|
+
const now = Date.now()
|
|
23
|
+
let bucket = buckets.get(bucketKey)
|
|
24
|
+
if (!bucket) {
|
|
25
|
+
bucket = {
|
|
26
|
+
tokens: maxCount,
|
|
27
|
+
bytes: maxBytes > 0 ? maxBytes : Number.POSITIVE_INFINITY,
|
|
28
|
+
lastRefill: now,
|
|
29
|
+
}
|
|
30
|
+
buckets.touch(bucketKey, bucket)
|
|
31
|
+
}
|
|
32
|
+
else buckets.touch(bucketKey, bucket)
|
|
33
|
+
const elapsed = now - bucket.lastRefill
|
|
34
|
+
if (elapsed > 0) {
|
|
35
|
+
const rate = maxCount / WINDOW_MS
|
|
36
|
+
bucket.tokens = Math.min(maxCount, bucket.tokens + elapsed * rate)
|
|
37
|
+
if (maxBytes > 0) {
|
|
38
|
+
const byteRate = maxBytes / WINDOW_MS
|
|
39
|
+
bucket.bytes = Math.min(maxBytes, bucket.bytes + elapsed * byteRate)
|
|
40
|
+
}
|
|
41
|
+
bucket.lastRefill = now
|
|
42
|
+
}
|
|
43
|
+
if (bucket.tokens < 1) return false
|
|
44
|
+
if (maxBytes > 0 && bucket.bytes < byteCount) return false
|
|
45
|
+
bucket.tokens -= 1
|
|
46
|
+
if (maxBytes > 0) bucket.bytes -= byteCount
|
|
47
|
+
buckets.touch(bucketKey, bucket)
|
|
48
|
+
return true
|
|
49
|
+
}
|