@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.
Files changed (154) hide show
  1. package/README.md +62 -0
  2. package/core/bytes_codec.mjs +51 -0
  3. package/core/canonical_json.mjs +21 -0
  4. package/core/composite_key.mjs +91 -0
  5. package/core/constants.mjs +53 -0
  6. package/core/entity_id.mjs +53 -0
  7. package/core/entity_id_parse.mjs +40 -0
  8. package/core/evfs_logical_path.mjs +15 -0
  9. package/core/hexIds.mjs +73 -0
  10. package/core/hlc.mjs +114 -0
  11. package/core/random_id.mjs +9 -0
  12. package/crypto/channel.mjs +101 -0
  13. package/crypto/checkpoint_sign.mjs +67 -0
  14. package/crypto/crypto.mjs +158 -0
  15. package/crypto/key.mjs +431 -0
  16. package/dag/canonicalize_row.mjs +65 -0
  17. package/dag/event_query.mjs +15 -0
  18. package/dag/index.mjs +335 -0
  19. package/dag/storage.mjs +333 -0
  20. package/dag/strip_extensions.mjs +18 -0
  21. package/dag/tunables.json +6 -0
  22. package/discovery/bt.mjs +260 -0
  23. package/discovery/index.mjs +134 -0
  24. package/discovery/mdns.mjs +138 -0
  25. package/discovery/nostr.mjs +273 -0
  26. package/entity/files/acl.mjs +38 -0
  27. package/entity/files/evfs.mjs +189 -0
  28. package/entity/files/evfs_ref.mjs +37 -0
  29. package/entity/files/manifest_acl_registry.mjs +93 -0
  30. package/entity/files/replica_host_cache.mjs +46 -0
  31. package/entity/files/url.mjs +19 -0
  32. package/entity/hosting_registry.mjs +47 -0
  33. package/entity/localized_core.mjs +141 -0
  34. package/entity/logical_entity.mjs +25 -0
  35. package/entity/logical_entity_id_registry.mjs +40 -0
  36. package/entity/node_hash.mjs +15 -0
  37. package/entity/presentation_registry.mjs +44 -0
  38. package/entity/profile.mjs +256 -0
  39. package/entity/replica.mjs +20 -0
  40. package/entity/session_snapshot_registry.mjs +38 -0
  41. package/federation/chunk_fetch_pending.mjs +128 -0
  42. package/federation/chunk_fetch_scheduler.mjs +97 -0
  43. package/federation/dag_order_cache.mjs +124 -0
  44. package/federation/dedupe_slot.mjs +33 -0
  45. package/federation/message_rate_limit.mjs +23 -0
  46. package/federation/operator_key_chain.mjs +200 -0
  47. package/federation/topo_order_memo.mjs +44 -0
  48. package/federation/volatile_streams.mjs +52 -0
  49. package/federation/want_ids.mjs +158 -0
  50. package/files/assemble.mjs +220 -0
  51. package/files/assemble_stream.mjs +129 -0
  52. package/files/chunk_fetch.mjs +113 -0
  53. package/files/chunk_fetch_verify.mjs +23 -0
  54. package/files/chunk_provider_registry.mjs +70 -0
  55. package/files/chunk_responder.mjs +79 -0
  56. package/files/chunk_store.mjs +87 -0
  57. package/files/manifest.mjs +99 -0
  58. package/files/transfer_key.mjs +87 -0
  59. package/files/transfer_key_registry.mjs +108 -0
  60. package/governance/branch.mjs +230 -0
  61. package/governance/join_pow.mjs +165 -0
  62. package/governance/owner_succession_ballot.mjs +57 -0
  63. package/governance/tunables.json +7 -0
  64. package/index.mjs +39 -0
  65. package/link/channel_mux.mjs +222 -0
  66. package/link/frame.mjs +246 -0
  67. package/link/handshake.mjs +175 -0
  68. package/link/link.mjs +617 -0
  69. package/link/rtc.mjs +109 -0
  70. package/link/sdp_fingerprint.mjs +25 -0
  71. package/mailbox/consumer_registry.mjs +71 -0
  72. package/mailbox/deliver_or_store.mjs +190 -0
  73. package/mailbox/importance.mjs +72 -0
  74. package/mailbox/parse.mjs +73 -0
  75. package/mailbox/prune.mjs +106 -0
  76. package/mailbox/rate.mjs +86 -0
  77. package/mailbox/settings.mjs +53 -0
  78. package/mailbox/store.mjs +256 -0
  79. package/mailbox/tunables.json +12 -0
  80. package/mailbox/wire.mjs +40 -0
  81. package/node/denylist.mjs +309 -0
  82. package/node/entity_store.mjs +214 -0
  83. package/node/identity.mjs +99 -0
  84. package/node/instance.mjs +115 -0
  85. package/node/network.mjs +306 -0
  86. package/node/personal_block.mjs +286 -0
  87. package/node/reputation_store.mjs +326 -0
  88. package/node/retention_policy.mjs +82 -0
  89. package/node/signaling_config.mjs +32 -0
  90. package/node/storage.mjs +30 -0
  91. package/node/storage_plugins.mjs +71 -0
  92. package/node/user_paths.mjs +10 -0
  93. package/overlay/index.mjs +191 -0
  94. package/package.json +73 -0
  95. package/permissions/bitmask.mjs +35 -0
  96. package/permissions/evaluator.mjs +90 -0
  97. package/permissions/index.mjs +12 -0
  98. package/permissions/layered.mjs +39 -0
  99. package/registries/action.mjs +94 -0
  100. package/registries/event_type.mjs +75 -0
  101. package/registries/inbound.mjs +90 -0
  102. package/registries/p2p_viewer.mjs +35 -0
  103. package/registries/part_path.mjs +35 -0
  104. package/registries/room_provider.mjs +47 -0
  105. package/reputation/engine.mjs +458 -0
  106. package/reputation/math.mjs +73 -0
  107. package/reputation/pick_score.mjs +11 -0
  108. package/reputation/relay_dedupe.mjs +22 -0
  109. package/reputation/tunables.json +36 -0
  110. package/rooms/scoped_link.mjs +185 -0
  111. package/schemas/discovery.mjs +110 -0
  112. package/schemas/federation_pull.mjs +102 -0
  113. package/schemas/mailbox.mjs +37 -0
  114. package/schemas/remote_event.mjs +17 -0
  115. package/timeline/append_core.mjs +54 -0
  116. package/timeline/materialize_runner.mjs +26 -0
  117. package/timeline/prune.mjs +39 -0
  118. package/timeline/retention.mjs +54 -0
  119. package/timeline/retention_runner.mjs +35 -0
  120. package/timeline/verify_remote.mjs +25 -0
  121. package/transport/group_link_set.mjs +324 -0
  122. package/transport/ice_servers.mjs +72 -0
  123. package/transport/link_registry.mjs +749 -0
  124. package/transport/peer_identity_maps.mjs +94 -0
  125. package/transport/peer_pool.mjs +355 -0
  126. package/transport/remote_user_room.mjs +109 -0
  127. package/transport/room_scopes.mjs +2 -0
  128. package/transport/room_wire_action.mjs +19 -0
  129. package/transport/rtc_connection_budget.mjs +180 -0
  130. package/transport/rtc_mdns_filter.mjs +118 -0
  131. package/transport/stale_peer_log.mjs +50 -0
  132. package/transport/user_room.mjs +263 -0
  133. package/trust_graph/build.mjs +99 -0
  134. package/trust_graph/cache.mjs +44 -0
  135. package/trust_graph/engine.mjs +184 -0
  136. package/trust_graph/registry.mjs +38 -0
  137. package/trust_graph/resolve.mjs +105 -0
  138. package/trust_graph/send.mjs +88 -0
  139. package/trust_graph/tunables.json +13 -0
  140. package/utils/async_mutex.mjs +69 -0
  141. package/utils/debug_log.mjs +16 -0
  142. package/utils/duration.mjs +28 -0
  143. package/utils/json_io.mjs +57 -0
  144. package/utils/lru.mjs +22 -0
  145. package/utils/map_pool.mjs +27 -0
  146. package/wire/group_part.mjs +43 -0
  147. package/wire/ingress.mjs +58 -0
  148. package/wire/part_common.mjs +55 -0
  149. package/wire/part_fanout.mjs +71 -0
  150. package/wire/part_ingress.mjs +172 -0
  151. package/wire/part_invoke.mjs +81 -0
  152. package/wire/rate_bucket.mjs +49 -0
  153. package/wire/volatile_signature.mjs +76 -0
  154. 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
+ }
@@ -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
+ }