@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,165 @@
1
+ /**
2
+ * 无状态入群 PoW:绑定群近期 DAG tip/checkpoint root,任意 replica 可独立验证。
3
+ */
4
+ import { createHash } from 'node:crypto'
5
+
6
+ import admissionTunables from './tunables.json' with { type: 'json' }
7
+
8
+ /** 默认 epoch 窗口(1 小时) */
9
+ export const JOIN_POW_DEFAULT_EPOCH_MS = 3_600_000
10
+
11
+ /** 默认 epoch 偏移容忍(±1 个 epoch) */
12
+ export const JOIN_POW_DEFAULT_EPOCH_SKEW = 1
13
+
14
+ /**
15
+ * @param {object} fields preimage 字段
16
+ * @param {string} fields.groupId 群 ID
17
+ * @param {string} fields.anchorRef 近期 tip 或 checkpoint root
18
+ * @param {string} fields.joinerNodeHash 入群者 nodeHash
19
+ * @param {number|string} fields.epoch epoch 桶
20
+ * @param {string|number} fields.nonce 随机 nonce
21
+ * @returns {string} SHA-256 十六进制
22
+ */
23
+ export function computeJoinPowHash({ groupId, anchorRef, joinerNodeHash, epoch, nonce }) {
24
+ const preimage = `${String(groupId)}:${String(anchorRef)}:${String(joinerNodeHash)}:${String(epoch)}:${String(nonce)}`
25
+ return createHash('sha256').update(preimage, 'utf8').digest('hex')
26
+ }
27
+
28
+ /**
29
+ * @param {string} hexHash SHA-256 十六进制
30
+ * @param {number} difficultyBits 前导零 bit 数
31
+ * @returns {boolean} 是否满足难度
32
+ */
33
+ export function joinPowHashMeetsDifficulty(hexHash, difficultyBits) {
34
+ const bits = Math.max(0, Math.min(256, Math.floor(Number(difficultyBits) || 0)))
35
+ if (bits <= 0) return true
36
+ const neededHexChars = Math.ceil(bits / 4)
37
+ const prefix = hexHash.slice(0, neededHexChars)
38
+ if (prefix.length < neededHexChars) return false
39
+ for (let i = 0; i < prefix.length; i++) {
40
+ const nibble = Number.parseInt(prefix[i], 16)
41
+ if (!Number.isFinite(nibble)) return false
42
+ const nibbleBits = i < neededHexChars - 1 ? 4 : bits - i * 4
43
+ const mask = (0xF << (4 - nibbleBits)) & 0xF
44
+ if ((nibble & mask) !== 0) return false
45
+ }
46
+ return true
47
+ }
48
+
49
+ /**
50
+ * @param {string} hexHash SHA-256 十六进制
51
+ * @returns {number} 实际达成的前导零 bit 数 0..256
52
+ */
53
+ export function countAchievedLeadingZeroBits(hexHash) {
54
+ let bits = 0
55
+ for (let i = 0; i < hexHash.length; i++) {
56
+ const nibble = Number.parseInt(hexHash[i], 16)
57
+ if (!Number.isFinite(nibble)) break
58
+ if (nibble === 0) {
59
+ bits += 4
60
+ continue
61
+ }
62
+ for (let b = 3; b >= 0; b--)
63
+ if ((nibble & (1 << b)) === 0) bits++
64
+ else return bits
65
+
66
+ }
67
+ return bits
68
+ }
69
+
70
+ /**
71
+ * floor 以上自愿多做 bit 的 log 递减封顶信誉加成。
72
+ * @param {number} achievedBits 解实际达成 bit
73
+ * @param {number} floorBits 准入 floor
74
+ * @param {typeof admissionTunables} [tunables] admission tunables
75
+ * @returns {number} 加成 0..cap
76
+ */
77
+ export function powVoluntaryBonus(achievedBits, floorBits, tunables = admissionTunables) {
78
+ const cap = Number(tunables.powVoluntaryBonusCap ?? 0)
79
+ const scale = Math.max(1, Number(tunables.powVoluntaryBonusScaleBits ?? 6))
80
+ if (!Number.isFinite(cap) || cap <= 0) return 0
81
+ const extra = Math.max(0, Math.floor(Number(achievedBits) || 0) - Math.floor(Number(floorBits) || 0))
82
+ if (extra <= 0) return 0
83
+ return cap * (1 - 2 ** (-extra / scale))
84
+ }
85
+
86
+ /**
87
+ * @param {object} powSolution 客户端 solution
88
+ * @param {object} opts 校验上下文
89
+ * @param {string} opts.groupId 群 ID
90
+ * @param {string} opts.senderNodeHash 签名者 nodeHash(须与 joiner 绑定)
91
+ * @param {string[]} opts.knownAnchors 近期 tip / checkpoint root 列表
92
+ * @param {number} [opts.now=Date.now()] 当前时间
93
+ * @param {number} [opts.difficultyBits=0] 准入 floor(前导零 bit)
94
+ * @param {number} [opts.epochMs=JOIN_POW_DEFAULT_EPOCH_MS] epoch 长度
95
+ * @param {number} [opts.epochSkew=JOIN_POW_DEFAULT_EPOCH_SKEW] 允许 epoch 偏移
96
+ * @returns {{ ok: boolean, achievedBits: number }} 校验结果与实际达成 bit
97
+ */
98
+ export function verifyJoinPow(powSolution, opts) {
99
+ const floorBits = Math.max(0, Math.floor(Number(opts.difficultyBits) || 0))
100
+ if (floorBits <= 0) return { ok: true, achievedBits: 0 }
101
+ if (!powSolution || typeof powSolution !== 'object') return { ok: false, achievedBits: 0 }
102
+
103
+ const anchorRef = String(powSolution.anchorRef ?? '').trim()
104
+ const nonce = powSolution.nonce
105
+ const epoch = Number(powSolution.epoch)
106
+ const joinerNodeHash = String(powSolution.joinerNodeHash ?? opts.senderNodeHash ?? '').trim().toLowerCase()
107
+ const senderNodeHash = String(opts.senderNodeHash ?? '').trim().toLowerCase()
108
+
109
+ if (!anchorRef || nonce == null || !Number.isFinite(epoch)) return { ok: false, achievedBits: 0 }
110
+ if (!joinerNodeHash || joinerNodeHash !== senderNodeHash) return { ok: false, achievedBits: 0 }
111
+
112
+ const anchors = (opts.knownAnchors ?? []).map(a => String(a).trim()).filter(Boolean)
113
+ if (!anchors.length || !anchors.includes(anchorRef)) return { ok: false, achievedBits: 0 }
114
+
115
+ const epochMs = Math.max(60_000, Number(opts.epochMs) || JOIN_POW_DEFAULT_EPOCH_MS)
116
+ const skew = Math.max(0, Math.floor(Number(opts.epochSkew) ?? JOIN_POW_DEFAULT_EPOCH_SKEW))
117
+ const nowEpoch = Math.floor((opts.now ?? Date.now()) / epochMs)
118
+ if (Math.abs(epoch - nowEpoch) > skew) return { ok: false, achievedBits: 0 }
119
+
120
+ const hash = computeJoinPowHash({
121
+ groupId: opts.groupId,
122
+ anchorRef,
123
+ joinerNodeHash,
124
+ epoch,
125
+ nonce: String(nonce),
126
+ })
127
+ const achievedBits = countAchievedLeadingZeroBits(hash)
128
+ return {
129
+ ok: joinPowHashMeetsDifficulty(hash, floorBits),
130
+ achievedBits,
131
+ }
132
+ }
133
+
134
+ /**
135
+ * 浏览器/Node 通用求解(同步 brute-force,适合低难度)。
136
+ * @param {object} fields preimage 字段(不含 nonce)
137
+ * @param {number} floorBits 准入 floor(前导零 bit)
138
+ * @param {number} [maxAttempts=5_000_000] 最大尝试次数
139
+ * @param {number} [targetBits] 目标 bit(省略则等于 floor)
140
+ * @returns {{ anchorRef: string, joinerNodeHash: string, epoch: number, nonce: string, achievedBits: number } | null} solution
141
+ */
142
+ export function solveJoinPow(fields, floorBits, maxAttempts = 5_000_000, targetBits) {
143
+ const floor = Math.max(1, Math.floor(Number(floorBits) || 1))
144
+ const target = Math.max(floor, Math.floor(Number(targetBits) || floor))
145
+ let best = null
146
+ for (let nonce = 0; nonce < maxAttempts; nonce++) {
147
+ const hash = computeJoinPowHash({ ...fields, nonce: String(nonce) })
148
+ const achieved = countAchievedLeadingZeroBits(hash)
149
+ if (achieved >= floor) {
150
+ const solution = { ...fields, nonce: String(nonce), achievedBits: achieved }
151
+ if (achieved >= target) return solution
152
+ if (!best || achieved > best.achievedBits) best = solution
153
+ }
154
+ }
155
+ return best
156
+ }
157
+
158
+ /**
159
+ * @param {number} difficultyBits 难度 bit
160
+ * @returns {number} 期望哈希次数(2^bits)
161
+ */
162
+ export function expectedJoinPowHashes(difficultyBits) {
163
+ const bits = Math.max(0, Math.floor(Number(difficultyBits) || 0))
164
+ return bits <= 52 ? 2 ** bits : Number.MAX_SAFE_INTEGER
165
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 群主继任联署选票验签(§8):管理员对固定 canonical 载荷签名,达到阈值即通过。
3
+ */
4
+ import { Buffer } from 'node:buffer'
5
+
6
+ import { canonicalStringify } from '../core/canonical_json.mjs'
7
+ import { isHex64, isSignatureHex128 } from '../core/hexIds.mjs'
8
+ import { pubKeyHash, verify } from '../crypto/crypto.mjs'
9
+
10
+ const BALLOT_DOMAIN = 'fount-owner-succession'
11
+
12
+ /**
13
+ * @param {{ proposedOwnerPubKeyHash: string, groupId: string, ballotId: string }} ballot 选票正文
14
+ * @returns {Buffer} 验签消息
15
+ */
16
+ export function ownerSuccessionBallotSignBytes(ballot) {
17
+ const body = {
18
+ proposedOwnerPubKeyHash: String(ballot.proposedOwnerPubKeyHash ?? '').trim().toLowerCase(),
19
+ groupId: String(ballot.groupId ?? '').trim(),
20
+ ballotId: String(ballot.ballotId ?? '').trim(),
21
+ }
22
+ return Buffer.from(`${BALLOT_DOMAIN}\0${canonicalStringify(body)}`, 'utf8')
23
+ }
24
+
25
+ /**
26
+ * 校验管理员联署是否达到 `thresholdRatio`(默认半数以上)。
27
+ * @param {{ proposedOwnerPubKeyHash: string, groupId: string, ballotId: string, adminSignatures: Array<{ pubKeyHex: string, signature: string }> }} ballot 选票正文与签名列表
28
+ * @param {Set<string>} adminPubKeyHashes 合法管理员公钥指纹
29
+ * @param {number} [thresholdRatio] 通过比例 (0,1]
30
+ * @returns {Promise<boolean>} 达到阈值时为 true
31
+ */
32
+ export async function verifyOwnerSuccessionThreshold(ballot, adminPubKeyHashes, thresholdRatio = 0.5) {
33
+ const admins = adminPubKeyHashes instanceof Set ? adminPubKeyHashes : new Set(adminPubKeyHashes)
34
+ if (!admins.size) return false
35
+
36
+ const ratio = Number(thresholdRatio)
37
+ if (!Number.isFinite(ratio) || ratio <= 0 || ratio > 1) return false
38
+
39
+ const signBytes = ownerSuccessionBallotSignBytes(ballot)
40
+ const needed = Math.ceil(admins.size * ratio)
41
+ const seen = new Set()
42
+ let valid = 0
43
+
44
+ for (const entry of ballot.adminSignatures || []) {
45
+ const pubKeyHex = String(entry?.pubKeyHex ?? '').trim().toLowerCase()
46
+ const signatureHex = String(entry?.signature ?? '').trim().toLowerCase()
47
+ if (!isHex64(pubKeyHex) || !isSignatureHex128(signatureHex)) continue
48
+
49
+ const hash = pubKeyHash(Buffer.from(pubKeyHex, 'hex'))
50
+ if (!admins.has(hash) || seen.has(hash)) continue
51
+ if (!await verify(Buffer.from(signatureHex, 'hex'), signBytes, Buffer.from(pubKeyHex, 'hex'))) continue
52
+
53
+ seen.add(hash)
54
+ if (++valid >= needed) return true
55
+ }
56
+ return false
57
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "powFloorBits": 18,
3
+ "powVoluntaryBonusCap": 0.12,
4
+ "powVoluntaryBonusScaleBits": 6,
5
+ "powEpochMs": 3600000,
6
+ "powEpochSkew": 1
7
+ }
package/index.mjs ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Federation P2P 门面:通用引导与房间/发现入口。
3
+ * 重型子系统请直接从子路径导入(如 `./dag/index.mjs`)。
4
+ */
5
+ import { registerDiscoveryProvider } from './discovery/index.mjs'
6
+ import { ensureNodeDefaults, getNodeHash } from './node/identity.mjs'
7
+ import { getNodeDir, initNode, isNodeInitialized } from './node/instance.mjs'
8
+ import { createScopedLinkRoom } from './rooms/scoped_link.mjs'
9
+ import { createGroupLinkSet } from './transport/group_link_set.mjs'
10
+ import { getLinkRegistry } from './transport/link_registry.mjs'
11
+ import { ensureUserRoom } from './transport/user_room.mjs'
12
+
13
+ /**
14
+ *
15
+ */
16
+ export {
17
+ createGroupLinkSet,
18
+ createScopedLinkRoom,
19
+ ensureNodeDefaults,
20
+ ensureUserRoom,
21
+ getLinkRegistry,
22
+ getNodeDir,
23
+ getNodeHash,
24
+ initNode,
25
+ isNodeInitialized,
26
+ registerDiscoveryProvider,
27
+ }
28
+
29
+ /**
30
+ * 初始化并启动 P2P 节点运行时(身份、链路网、link registry)。
31
+ * @param {{ nodeDir: string, entityStore?: import('./node/entity_store.mjs').EntityStore, logger?: object, signaling?: import('./node/signaling_config.mjs').SignalingRuntimeConfig }} options 节点配置
32
+ * @returns {Promise<void>}
33
+ */
34
+ export async function startNode(options) {
35
+ if (!isNodeInitialized())
36
+ initNode(options)
37
+ ensureNodeDefaults()
38
+ await getLinkRegistry().ensureRuntime()
39
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * control 通道名:低延迟、小载荷与路由类消息。
3
+ */
4
+ export const CHANNEL_CONTROL = 'control'
5
+ /**
6
+ * bulk 通道名:大载荷与低优先级消息。
7
+ */
8
+ export const CHANNEL_BULK = 'bulk'
9
+ /**
10
+ * bufferedAmount 低水位默认阈值(256 KiB)。
11
+ */
12
+ export const CHANNEL_LOW_THRESHOLD_BYTES = 256 * 1024
13
+ /**
14
+ * 发送队列高水位默认阈值(1 MiB)。
15
+ */
16
+ export const CHANNEL_HIGH_WATERMARK_BYTES = 1024 * 1024
17
+ /**
18
+ * 超过此字节数优先走 bulk 通道(64 KiB)。
19
+ */
20
+ export const BULK_CHANNEL_MIN_BYTES = 64 * 1024
21
+
22
+ const DEFAULT_ACTION_PRIORITIES = Object.freeze({
23
+ dag_event: 0,
24
+ gossip_request: 1,
25
+ gossip_response: 2,
26
+ channel_history_want: 2,
27
+ fed_bootstrap_request: 2,
28
+ fed_bootstrap_response: 2,
29
+ fed_join_snapshot_request: 2,
30
+ fed_archive_month_want: 2,
31
+ fed_archive_month_response: 2,
32
+ mailbox_put: 2,
33
+ mailbox_want: 2,
34
+ mailbox_give: 2,
35
+ fed_tip_ping: 3,
36
+ discovery_announce: 3,
37
+ discovery_query: 3,
38
+ discovery_query_response: 3,
39
+ part_invoke: 3,
40
+ part_invoke_response: 3,
41
+ char_rpc: 3,
42
+ fed_chunk_put: 5,
43
+ fed_chunk_get: 5,
44
+ fed_chunk_data: 5,
45
+ fed_chunk_ack: 5,
46
+ fed_partition_bridge: 5,
47
+ fed_volatile: 10,
48
+ })
49
+
50
+ /**
51
+ * 根据 action 名称解析发送优先级,数值越小越优先。
52
+ * @param {string} action 消息 action 名称
53
+ * @returns {number} 优先级(未知 action 默认 5)
54
+ */
55
+ export function resolveActionPriority(action) {
56
+ return DEFAULT_ACTION_PRIORITIES[String(action || '').trim()] ?? 5
57
+ }
58
+
59
+ /**
60
+ * 根据 action 与载荷大小选择 control 或 bulk 通道。
61
+ * @param {string} action 消息 action 名称
62
+ * @param {number} byteLength 载荷字节长度
63
+ * @returns {'control' | 'bulk'} 目标通道名
64
+ */
65
+ export function pickChannel(action, byteLength) {
66
+ const normalized = String(action || '').trim()
67
+ if (normalized === 'ping' || normalized === 'pong' || normalized.startsWith('route_'))
68
+ return CHANNEL_CONTROL
69
+ if (Number(byteLength) > BULK_CHANNEL_MIN_BYTES)
70
+ return CHANNEL_BULK
71
+ return resolveActionPriority(normalized) <= 3 ? CHANNEL_CONTROL : CHANNEL_BULK
72
+ }
73
+
74
+ /**
75
+ * 读取 RTC 数据通道当前 bufferedAmount,失败时返回 0。
76
+ * @param {RTCDataChannel} channel RTC 数据通道
77
+ * @returns {number} 缓冲区待发送字节数
78
+ */
79
+ export function readBufferedAmount(channel) {
80
+ try {
81
+ return Number.isFinite(channel?.bufferedAmount) ? Number(channel.bufferedAmount) : 0
82
+ }
83
+ catch {
84
+ return 0
85
+ }
86
+ }
87
+
88
+ /**
89
+ * 配置数据通道的 bufferedAmountLowThreshold。
90
+ * @param {RTCDataChannel} channel RTC 数据通道
91
+ * @param {number} [thresholdBytes=CHANNEL_LOW_THRESHOLD_BYTES] 低水位阈值(字节)
92
+ * @returns {number | null} 实际生效的阈值,不支持时返回 null
93
+ */
94
+ export function configureBufferedAmountLowThreshold(channel, thresholdBytes = CHANNEL_LOW_THRESHOLD_BYTES) {
95
+ try {
96
+ channel.bufferedAmountLowThreshold = thresholdBytes
97
+ return Number.isFinite(channel.bufferedAmountLowThreshold)
98
+ ? Number(channel.bufferedAmountLowThreshold)
99
+ : null
100
+ }
101
+ catch {
102
+ return null
103
+ }
104
+ }
105
+
106
+ /**
107
+ * 订阅 bufferedamountlow 事件,返回取消订阅函数。
108
+ * @param {RTCDataChannel} channel RTC 数据通道
109
+ * @param {() => void} cb 低水位回调
110
+ * @returns {() => void} 取消订阅函数
111
+ */
112
+ export function onBufferedAmountLow(channel, cb) {
113
+ /**
114
+ * bufferedamountlow 事件处理函数。
115
+ * @returns {void}
116
+ */
117
+ const handler = () => cb()
118
+ channel.addEventListener?.('bufferedamountlow', handler)
119
+ channel.onbufferedamountlow = handler
120
+ if (channel.bufferedAmountLow?.subscribe)
121
+ channel.bufferedAmountLow.subscribe(handler)
122
+ return () => {
123
+ channel.removeEventListener?.('bufferedamountlow', handler)
124
+ if (channel.onbufferedamountlow === handler) channel.onbufferedamountlow = null
125
+ }
126
+ }
127
+
128
+ /**
129
+ * 创建带优先级队列的双通道发送器(control/bulk)。
130
+ * @param {{ getChannel: (name: 'control' | 'bulk') => RTCDataChannel | null | undefined, highWatermarkBytes?: number }} opts 通道访问与高水位配置
131
+ * @returns {{ enqueue: (action: string, bytes: Uint8Array, preferredChannel?: 'control' | 'bulk') => void, flush: (channelName?: 'control' | 'bulk') => void, pending: () => { control: number, bulk: number }, clear: () => void }} 发送队列 API
132
+ */
133
+ export function createChannelSendQueues(opts) {
134
+ const getChannel = opts.getChannel
135
+ const highWatermarkBytes = Math.max(CHANNEL_LOW_THRESHOLD_BYTES, Number(opts.highWatermarkBytes) || CHANNEL_HIGH_WATERMARK_BYTES)
136
+ /** @type {{ control: Array<{ priority: number, seq: number, bytes: Uint8Array }>, bulk: Array<{ priority: number, seq: number, bytes: Uint8Array }> }} */
137
+ const queues = { control: [], bulk: [] }
138
+ /** @type {{ control: boolean, bulk: boolean }} */
139
+ const scheduled = { control: false, bulk: false }
140
+ let seq = 0
141
+
142
+ /**
143
+ * 在 microtask 中调度指定通道的 flush。
144
+ * @param {'control' | 'bulk'} channelName 通道名
145
+ * @returns {void}
146
+ */
147
+ const scheduleFlush = channelName => {
148
+ if (scheduled[channelName]) return
149
+ scheduled[channelName] = true
150
+ queueMicrotask(() => flush(channelName))
151
+ }
152
+
153
+ /**
154
+ * 将队列中的帧写入指定通道,受高水位限制。
155
+ * @param {'control' | 'bulk'} channelName 通道名
156
+ * @returns {void}
157
+ */
158
+ const flush = channelName => {
159
+ scheduled[channelName] = false
160
+ const channel = getChannel(channelName)
161
+ if (!channel || channel.readyState !== 'open') return
162
+ const queue = queues[channelName]
163
+ while (queue.length) {
164
+ if (readBufferedAmount(channel) > highWatermarkBytes) return
165
+ const item = queue.shift()
166
+ channel.send(item.bytes)
167
+ }
168
+ }
169
+
170
+ return {
171
+ /**
172
+ * 按优先级将字节帧入队并调度发送。
173
+ * @param {string} action 消息 action(用于选通道与优先级)
174
+ * @param {Uint8Array} bytes 待发送帧字节
175
+ * @param {'control' | 'bulk'} [preferredChannel] 强制使用的通道,省略则自动选择
176
+ * @returns {void}
177
+ */
178
+ enqueue(action, bytes, preferredChannel) {
179
+ const channelName = preferredChannel ?? pickChannel(action, bytes.byteLength)
180
+ const queue = queues[channelName]
181
+ const priority = resolveActionPriority(action)
182
+ const item = { priority, seq: ++seq, bytes }
183
+ let lo = 0
184
+ let hi = queue.length
185
+ while (lo < hi) {
186
+ const mid = (lo + hi) >> 1
187
+ const compare = priority - queue[mid].priority || item.seq - queue[mid].seq
188
+ if (compare < 0) hi = mid
189
+ else lo = mid + 1
190
+ }
191
+ queue.splice(lo, 0, item)
192
+ scheduleFlush(channelName)
193
+ },
194
+ /**
195
+ * 调度 flush:指定通道或双通道。
196
+ * @param {'control' | 'bulk'} [channelName] 通道名,省略则 flush 全部
197
+ * @returns {void}
198
+ */
199
+ flush(channelName) {
200
+ if (channelName) scheduleFlush(channelName)
201
+ else {
202
+ scheduleFlush(CHANNEL_CONTROL)
203
+ scheduleFlush(CHANNEL_BULK)
204
+ }
205
+ },
206
+ /**
207
+ * 返回各通道待发送帧数量。
208
+ * @returns {{ control: number, bulk: number }} control 与 bulk 队列长度
209
+ */
210
+ pending() {
211
+ return { control: queues.control.length, bulk: queues.bulk.length }
212
+ },
213
+ /**
214
+ * 清空所有发送队列。
215
+ * @returns {void}
216
+ */
217
+ clear() {
218
+ queues.control.length = 0
219
+ queues.bulk.length = 0
220
+ },
221
+ }
222
+ }