@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,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
|
+
}
|
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
|
+
}
|