@steve02081504/fount-p2p 0.0.4 → 0.0.6
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/core/bytes_codec.mjs +0 -27
- package/core/constants.mjs +0 -31
- package/core/hexIds.mjs +0 -13
- package/crypto/key.mjs +14 -15
- package/dag/canonicalize_row.mjs +5 -5
- package/dag/index.mjs +0 -73
- package/dag/storage.mjs +0 -22
- package/discovery/mdns.mjs +4 -4
- package/discovery/nostr.mjs +3 -3
- package/federation/chunk_fetch_pending.mjs +3 -3
- package/federation/chunk_fetch_scheduler.mjs +3 -3
- package/federation/dag_order_cache.mjs +3 -3
- package/federation/entity_key_chain.mjs +0 -9
- package/federation/topo_order_memo.mjs +3 -18
- package/files/assemble.mjs +55 -39
- package/files/assemble_stream.mjs +96 -44
- package/files/chunk_provider_registry.mjs +0 -6
- package/files/chunk_responder.mjs +1 -1
- package/files/chunk_store.mjs +1 -11
- package/files/evfs.mjs +74 -47
- package/files/manifest.mjs +11 -2
- package/files/manifest_acl_registry.mjs +0 -6
- package/files/transfer_key.mjs +21 -15
- package/files/transfer_key_registry.mjs +9 -16
- package/governance/branch.mjs +0 -11
- package/governance/join_pow.mjs +17 -17
- package/link/channel_mux.mjs +9 -7
- package/link/frame.mjs +5 -5
- package/link/handshake.mjs +17 -17
- package/link/link.mjs +33 -33
- package/mailbox/deliver_or_store.mjs +13 -13
- package/mailbox/importance.mjs +0 -33
- package/mailbox/settings.mjs +0 -9
- package/mailbox/wire.mjs +4 -4
- package/node/denylist.mjs +0 -24
- package/node/network.mjs +0 -51
- package/node/personal_block.mjs +0 -35
- package/node/reputation_store.mjs +5 -14
- package/node/retention_policy.mjs +8 -23
- package/overlay/index.mjs +6 -6
- package/package.json +1 -1
- package/registries/event_type.mjs +0 -12
- package/registries/inbound.mjs +8 -30
- package/reputation/engine.mjs +0 -11
- package/rooms/scoped_link.mjs +18 -18
- package/schemas/part_query.mjs +156 -0
- package/timeline/append_core.mjs +3 -3
- package/transport/group_link_set.mjs +30 -30
- package/transport/ice_servers.mjs +0 -8
- package/transport/link_registry.mjs +13 -13
- package/transport/peer_identity_maps.mjs +0 -58
- package/transport/peer_pool.mjs +4 -4
- package/transport/remote_user_room.mjs +0 -12
- package/transport/rtc_connection_budget.mjs +2 -0
- package/transport/user_room.mjs +13 -24
- package/trust_graph/cache.mjs +0 -13
- package/trust_graph/engine.mjs +0 -24
- package/utils/async_mutex.mjs +0 -9
- package/wire/group_part.mjs +3 -3
- package/wire/part_fanout.mjs +0 -19
- package/wire/part_ingress.mjs +14 -14
- package/wire/part_query.mjs +486 -0
- package/wire/part_query.tunables.json +14 -0
- package/wire/part_query_cache.mjs +101 -0
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
bumpChunkStorageReputationPure,
|
|
8
8
|
bumpReputationOnRelayPure,
|
|
9
9
|
ensureReputationShape,
|
|
10
|
-
isQuarantinedPure,
|
|
11
10
|
observeBehaviorSamplePure,
|
|
12
11
|
penalizeArchiveServeMismatchPure,
|
|
13
12
|
penalizeChunkStorageFailurePure,
|
|
@@ -27,12 +26,12 @@ import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
|
|
|
27
26
|
|
|
28
27
|
const DATA_NAME = 'reputation'
|
|
29
28
|
|
|
30
|
-
/** @type {((
|
|
29
|
+
/** @type {((options: object) => Promise<boolean>) | null} */
|
|
31
30
|
let blockReputationHandler = null
|
|
32
31
|
|
|
33
32
|
/**
|
|
34
33
|
* Shell Load 时注册:公开 block/unblock → 信誉传导。
|
|
35
|
-
* @param {(
|
|
34
|
+
* @param {(options: object) => Promise<boolean>} handler block/unblock 信誉传导回调
|
|
36
35
|
* @returns {void}
|
|
37
36
|
*/
|
|
38
37
|
export function registerBlockReputationHandler(handler) {
|
|
@@ -145,14 +144,6 @@ export async function observePeerBehavior(peerNodeHash, sample) {
|
|
|
145
144
|
return anomaly
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
/**
|
|
149
|
-
* @param {string} peerNodeHash 对端
|
|
150
|
-
* @returns {boolean} 是否处于本地隔离
|
|
151
|
-
*/
|
|
152
|
-
export function isPeerQuarantined(peerNodeHash) {
|
|
153
|
-
return isQuarantinedPure(loadReputation(), peerNodeHash)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
147
|
/**
|
|
157
148
|
* @param {string} storagePeerKey 责任方
|
|
158
149
|
* @returns {void}
|
|
@@ -317,10 +308,10 @@ export function pickNodeScore(nodeId) {
|
|
|
317
308
|
}
|
|
318
309
|
|
|
319
310
|
/**
|
|
320
|
-
* @param {object}
|
|
311
|
+
* @param {object} options applyFollowedBlockSignal 参数
|
|
321
312
|
* @returns {Promise<boolean>} 是否已应用
|
|
322
313
|
*/
|
|
323
|
-
export async function applyBlockReputationSignal(
|
|
314
|
+
export async function applyBlockReputationSignal(options) {
|
|
324
315
|
if (!blockReputationHandler) return false
|
|
325
|
-
return blockReputationHandler(
|
|
316
|
+
return blockReputationHandler(options)
|
|
326
317
|
}
|
|
@@ -3,36 +3,21 @@ import {
|
|
|
3
3
|
authzFoldOrderIds,
|
|
4
4
|
descendantClosureFromTip,
|
|
5
5
|
} from '../governance/branch.mjs'
|
|
6
|
-
import { getPermissionAnchorTypes } from '../registries/event_type.mjs'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @param {string[]} order 拓扑序 id 列表
|
|
10
|
-
* @param {Map<string, object>} byId id → 事件
|
|
11
|
-
* @returns {number} 最早须保留的事件下标
|
|
12
|
-
*/
|
|
13
|
-
export function permissionAnchorStartIndex(order, byId) {
|
|
14
|
-
let anchor = order.length
|
|
15
|
-
for (let index = order.length - 1; index >= 0; index--) {
|
|
16
|
-
const ev = byId.get(order[index])
|
|
17
|
-
if (ev && getPermissionAnchorTypes().has(ev.type)) anchor = index
|
|
18
|
-
}
|
|
19
|
-
return anchor
|
|
20
|
-
}
|
|
21
6
|
|
|
22
7
|
/**
|
|
23
8
|
* 在共识分支上计算须保留的事件 id(连通子图,不用拓扑下标切片)。
|
|
24
9
|
* @param {string[]} order 规范拓扑序
|
|
25
10
|
* @param {Map<string, object>} byId id → 事件
|
|
26
|
-
* @param {object}
|
|
27
|
-
* @param {number}
|
|
28
|
-
* @param {number}
|
|
29
|
-
* @param {Set<string>}
|
|
30
|
-
* @param {string | null} [
|
|
31
|
-
* @param {string | null} [
|
|
11
|
+
* @param {object} options 保留策略
|
|
12
|
+
* @param {number} options.maxDepth 分支上最大事件深度
|
|
13
|
+
* @param {number} options.cutoffWall 最早保留的 HLC wall
|
|
14
|
+
* @param {Set<string>} options.anchorTypes 权限锚点事件类型
|
|
15
|
+
* @param {string | null} [options.checkpointTipId] checkpoint 尖
|
|
16
|
+
* @param {string | null} [options.branchTipId] 共识分支尖
|
|
32
17
|
* @returns {Set<string>} 保留 id
|
|
33
18
|
*/
|
|
34
|
-
export function computeRetentionKeepIds(order, byId,
|
|
35
|
-
const { maxDepth, cutoffWall, anchorTypes, checkpointTipId, branchTipId } =
|
|
19
|
+
export function computeRetentionKeepIds(order, byId, options) {
|
|
20
|
+
const { maxDepth, cutoffWall, anchorTypes, checkpointTipId, branchTipId } = options
|
|
36
21
|
const branchOrder = authzFoldOrderIds(order, byId, branchTipId)
|
|
37
22
|
const branchSet = new Set(branchOrder)
|
|
38
23
|
if (!branchSet.size) return new Set()
|
package/overlay/index.mjs
CHANGED
|
@@ -131,15 +131,15 @@ export function createOverlayRouter(registry, ttl = 3) {
|
|
|
131
131
|
/**
|
|
132
132
|
* 发现到目标节点的签名路由路径。
|
|
133
133
|
* @param {string} targetNodeHash 目标节点 64 hex
|
|
134
|
-
* @param {object} [
|
|
135
|
-
* @param {number} [
|
|
136
|
-
* @param {number} [
|
|
134
|
+
* @param {object} [options] 选项
|
|
135
|
+
* @param {number} [options.ttl] 路由 TTL
|
|
136
|
+
* @param {number} [options.timeoutMs] 超时毫秒
|
|
137
137
|
* @returns {Promise<string[]>} 从本节点到目标的 nodeHash 路径
|
|
138
138
|
*/
|
|
139
|
-
async discoverRoute(targetNodeHash,
|
|
139
|
+
async discoverRoute(targetNodeHash, options = {}) {
|
|
140
140
|
const reqId = randomMsgIdHex()
|
|
141
|
-
const maxTtl = Number(
|
|
142
|
-
const timeoutMs = Number(
|
|
141
|
+
const maxTtl = Number(options.ttl) || ttl
|
|
142
|
+
const timeoutMs = Number(options.timeoutMs) || 10_000
|
|
143
143
|
const promise = new Promise((resolve, reject) => {
|
|
144
144
|
const timer = setTimeout(() => {
|
|
145
145
|
pendingRoutes.delete(reqId)
|
package/package.json
CHANGED
|
@@ -56,18 +56,6 @@ export function getGovernanceAuthzTypes() {
|
|
|
56
56
|
return typesWithFlag('governance')
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/** 联邦入站/中继前须物化 ACL 门控的类型。 */
|
|
60
|
-
/** @returns {Set<string>} ACL 门控事件 type 集合 */
|
|
61
|
-
export function getFederationAclGatedEventTypes() {
|
|
62
|
-
return typesWithFlag('aclGated')
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** §6.2 频道 GC 沉寂计时排除的类型。 */
|
|
66
|
-
/** @returns {Set<string>} GC 排除事件 type 集合 */
|
|
67
|
-
export function getChannelGcExcludedEventTypes() {
|
|
68
|
-
return typesWithFlag('gcExclude')
|
|
69
|
-
}
|
|
70
|
-
|
|
71
59
|
/** 裁剪时不得早于最早一条权限锚点事件(§7.1)。 */
|
|
72
60
|
/** @returns {Set<string>} 权限锚点事件 type 集合 */
|
|
73
61
|
export function getPermissionAnchorTypes() {
|
package/registries/inbound.mjs
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* @typedef {(
|
|
13
|
+
* @typedef {(inboundContext: InboundContext, message: object) => Promise<PartInvokeResponse | null>} RpcInboundHandler
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* @typedef {(
|
|
17
|
+
* @typedef {(inboundContext: InboundContext, message: object) => Promise<void>} DeliveryInboundHandler
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
/** @type {Map<string, RpcInboundHandler>} */
|
|
@@ -42,49 +42,27 @@ export function registerDeliveryInboundHandler(type, handler) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* @param {
|
|
46
|
-
* @returns {void}
|
|
47
|
-
*/
|
|
48
|
-
export function unregisterRpcInboundHandler(type) {
|
|
49
|
-
rpcHandlers.delete(String(type || '').trim())
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @param {string} type 入站类型
|
|
54
|
-
* @returns {void}
|
|
55
|
-
*/
|
|
56
|
-
export function unregisterDeliveryInboundHandler(type) {
|
|
57
|
-
deliveryHandlers.delete(String(type || '').trim())
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @param {InboundContext} ctx 入站上下文
|
|
45
|
+
* @param {InboundContext} inboundContext 入站上下文
|
|
62
46
|
* @param {object} message 已校验的线载荷(含 type)
|
|
63
47
|
* @returns {Promise<PartInvokeResponse | null>} 处理器返回值
|
|
64
48
|
*/
|
|
65
|
-
export async function dispatchRpcInbound(
|
|
49
|
+
export async function dispatchRpcInbound(inboundContext, message) {
|
|
66
50
|
const type = String(message?.type || '').trim()
|
|
67
51
|
if (!type) return null
|
|
68
52
|
const handler = rpcHandlers.get(type)
|
|
69
53
|
if (!handler) return null
|
|
70
|
-
return handler(
|
|
54
|
+
return handler(inboundContext, message)
|
|
71
55
|
}
|
|
72
56
|
|
|
73
57
|
/**
|
|
74
|
-
* @param {InboundContext}
|
|
58
|
+
* @param {InboundContext} inboundContext 入站上下文
|
|
75
59
|
* @param {object} message 已校验的线载荷(含 type)
|
|
76
60
|
* @returns {Promise<void>}
|
|
77
61
|
*/
|
|
78
|
-
export async function dispatchDeliveryInbound(
|
|
62
|
+
export async function dispatchDeliveryInbound(inboundContext, message) {
|
|
79
63
|
const type = String(message?.type || '').trim()
|
|
80
64
|
if (!type) return
|
|
81
65
|
const handler = deliveryHandlers.get(type)
|
|
82
66
|
if (!handler) return
|
|
83
|
-
await handler(
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** @returns {void} 测试用 */
|
|
87
|
-
export function clearInboundHandlers() {
|
|
88
|
-
rpcHandlers.clear()
|
|
89
|
-
deliveryHandlers.clear()
|
|
67
|
+
await handler(inboundContext, message)
|
|
90
68
|
}
|
package/reputation/engine.mjs
CHANGED
|
@@ -445,14 +445,3 @@ export function isQuarantinedPure(data, peerNodeHash, now = Date.now()) {
|
|
|
445
445
|
const until = Number(data.byNodeHash?.[id]?.quarantinedUntil ?? 0)
|
|
446
446
|
return Number.isFinite(until) && until > now
|
|
447
447
|
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* @param {ReputationFile} data 信誉表
|
|
451
|
-
* @param {string} peerNodeHash 对端
|
|
452
|
-
* @param {number} [now] 当前时间
|
|
453
|
-
* @param {typeof reputationTunables} [tunables] tunables
|
|
454
|
-
* @returns {boolean} 是否检测到异常并触发隔离
|
|
455
|
-
*/
|
|
456
|
-
export function detectAnomalyPure(data, peerNodeHash, now = Date.now(), tunables = reputationTunables) {
|
|
457
|
-
return isQuarantinedPure(data, peerNodeHash, now)
|
|
458
|
-
}
|
package/rooms/scoped_link.mjs
CHANGED
|
@@ -9,17 +9,17 @@ import {
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* 在指定 scope 与 roomSecret 下创建 link 层房间(discovery + registry 转发)。
|
|
12
|
-
* @param {object}
|
|
13
|
-
* @param {string}
|
|
14
|
-
* @param {string}
|
|
15
|
-
* @param {(nodeHash: string) => boolean} [
|
|
16
|
-
* @returns {{ start: () => Promise<void>, leave: () => Promise<void>, makeAction: (name: string) => [(payload: unknown, peerId?: string | string[] | null) => Promise<void>, (handler: (payload: unknown, peerId: string) => void) => void], onPeerJoin: (
|
|
12
|
+
* @param {object} options 房间选项
|
|
13
|
+
* @param {string} options.scope link registry scope(如 `group:{id}`)
|
|
14
|
+
* @param {string} options.roomSecret 房间 rendezvous 密钥
|
|
15
|
+
* @param {(nodeHash: string) => boolean} [options.allowNode] 是否允许与某 nodeHash 通信
|
|
16
|
+
* @returns {{ start: () => Promise<void>, leave: () => Promise<void>, makeAction: (name: string) => [(payload: unknown, peerId?: string | string[] | null) => Promise<void>, (handler: (payload: unknown, peerId: string) => void) => void], onPeerJoin: (callback: (peerId: string) => void) => () => void, onPeerLeave: (callback: (peerId: string) => void) => () => void, getPeers: () => Record<string, true> }} 房间句柄
|
|
17
17
|
*/
|
|
18
|
-
export function createScopedLinkRoom(
|
|
18
|
+
export function createScopedLinkRoom(options) {
|
|
19
19
|
const registry = getLinkRegistry()
|
|
20
|
-
const scope = String(
|
|
21
|
-
const topic = groupRendezvousTopic(
|
|
22
|
-
const allowNode = typeof
|
|
20
|
+
const scope = String(options.scope)
|
|
21
|
+
const topic = groupRendezvousTopic(options.roomSecret)
|
|
22
|
+
const allowNode = typeof options.allowNode === 'function' ? options.allowNode : () => true
|
|
23
23
|
/** @type {Set<string>} */
|
|
24
24
|
const discoveredPeers = new Set()
|
|
25
25
|
/** @type {Set<string>} */
|
|
@@ -156,24 +156,24 @@ export function createScopedLinkRoom(opts) {
|
|
|
156
156
|
]
|
|
157
157
|
},
|
|
158
158
|
/**
|
|
159
|
-
* @param {(peerId: string) => void}
|
|
159
|
+
* @param {(peerId: string) => void} callback 新 peer 上线回调
|
|
160
160
|
* @returns {() => void} 取消订阅
|
|
161
161
|
*/
|
|
162
|
-
onPeerJoin(
|
|
163
|
-
joinListeners.add(
|
|
162
|
+
onPeerJoin(callback) {
|
|
163
|
+
joinListeners.add(callback)
|
|
164
164
|
for (const peerId of activePeerIds())
|
|
165
165
|
announcedPeers.add(peerId)
|
|
166
166
|
for (const peerId of announcedPeers)
|
|
167
|
-
try {
|
|
168
|
-
return () => joinListeners.delete(
|
|
167
|
+
try { callback(peerId) } catch { /* ignore */ }
|
|
168
|
+
return () => joinListeners.delete(callback)
|
|
169
169
|
},
|
|
170
170
|
/**
|
|
171
|
-
* @param {(peerId: string) => void}
|
|
171
|
+
* @param {(peerId: string) => void} callback peer 离线回调
|
|
172
172
|
* @returns {() => void} 取消订阅
|
|
173
173
|
*/
|
|
174
|
-
onPeerLeave(
|
|
175
|
-
leaveListeners.add(
|
|
176
|
-
return () => leaveListeners.delete(
|
|
174
|
+
onPeerLeave(callback) {
|
|
175
|
+
leaveListeners.add(callback)
|
|
176
|
+
return () => leaveListeners.delete(callback)
|
|
177
177
|
},
|
|
178
178
|
/**
|
|
179
179
|
* @returns {Record<string, true>} 当前活跃 peer 的 nodeHash 集合
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
|
|
3
|
+
import { canonicalStringify } from '../core/canonical_json.mjs'
|
|
4
|
+
import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
5
|
+
import { isPlainObject } from '../wire/ingress.mjs'
|
|
6
|
+
import { normalizePartpath } from '../wire/part_invoke.mjs'
|
|
7
|
+
import partQueryTunables from '../wire/part_query.tunables.json' with { type: 'json' }
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{
|
|
11
|
+
* requestId: string
|
|
12
|
+
* originNodeHash: string
|
|
13
|
+
* partpath: string
|
|
14
|
+
* kind: string
|
|
15
|
+
* query: unknown
|
|
16
|
+
* ttl: number
|
|
17
|
+
* budget: { maxHits: number }
|
|
18
|
+
* }} PartQueryReq
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {{
|
|
23
|
+
* requestId: string
|
|
24
|
+
* fromNodeHash: string
|
|
25
|
+
* rows: unknown[]
|
|
26
|
+
* }} PartQueryRes
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {unknown} value 任意 JSON
|
|
31
|
+
* @returns {number} UTF-8 序列化字节数;不可序列化时 Infinity
|
|
32
|
+
*/
|
|
33
|
+
export function measureJsonBytes(value) {
|
|
34
|
+
try {
|
|
35
|
+
return Buffer.byteLength(JSON.stringify(value), 'utf8')
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return Number.POSITIVE_INFINITY
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {unknown} budget 请求预算
|
|
44
|
+
* @param {number} [maxHits=partQueryTunables.maxHits] 上限
|
|
45
|
+
* @returns {{ maxHits: number }} 钳制后的预算
|
|
46
|
+
*/
|
|
47
|
+
export function clampPartQueryBudget(budget, maxHits = partQueryTunables.maxHits) {
|
|
48
|
+
const cap = Math.max(1, Math.floor(Number(maxHits) || partQueryTunables.maxHits))
|
|
49
|
+
const raw = isPlainObject(budget) ? Number(budget.maxHits) : NaN
|
|
50
|
+
const hits = Number.isFinite(raw) ? Math.floor(raw) : cap
|
|
51
|
+
return { maxHits: Math.max(1, Math.min(cap, hits)) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {unknown} ttl 跳数预算
|
|
56
|
+
* @param {number} [maxTtl=partQueryTunables.maxTtl] 上限
|
|
57
|
+
* @returns {number | null} 钳制后的 ttl;非法为 null
|
|
58
|
+
*/
|
|
59
|
+
export function clampPartQueryTtl(ttl, maxTtl = partQueryTunables.maxTtl) {
|
|
60
|
+
const cap = Math.max(1, Math.floor(Number(maxTtl) || partQueryTunables.maxTtl))
|
|
61
|
+
const n = Math.floor(Number(ttl))
|
|
62
|
+
if (!Number.isFinite(n) || n < 1) return null
|
|
63
|
+
return Math.min(cap, n)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {unknown} rows 行数组
|
|
68
|
+
* @param {number} maxHits 条数上限
|
|
69
|
+
* @param {number} [maxRowsBytes=partQueryTunables.maxRowsBytes] 总尺寸上限
|
|
70
|
+
* @returns {unknown[] | null} 通过校验的 rows;失败 null
|
|
71
|
+
*/
|
|
72
|
+
export function clampPartQueryRows(rows, maxHits, maxRowsBytes = partQueryTunables.maxRowsBytes) {
|
|
73
|
+
if (!Array.isArray(rows)) return null
|
|
74
|
+
const limited = rows.slice(0, Math.max(0, Math.floor(Number(maxHits) || 0)))
|
|
75
|
+
if (measureJsonBytes(limited) > maxRowsBytes) return null
|
|
76
|
+
return limited
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** requestId 上限(randomUUID 36 字符;防超长键灌爆 dedupe/pending 表) */
|
|
80
|
+
const REQUEST_ID_MAX_LENGTH = 128
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {unknown} value 原始 requestId
|
|
84
|
+
* @returns {string | null} 修剪后的 requestId;非法 null
|
|
85
|
+
*/
|
|
86
|
+
function normalizeRequestId(value) {
|
|
87
|
+
const requestId = String(value || '').trim()
|
|
88
|
+
if (!requestId || requestId.length > REQUEST_ID_MAX_LENGTH) return null
|
|
89
|
+
return requestId
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {unknown} value 入站 req
|
|
94
|
+
* @param {typeof partQueryTunables} [tunables] 可调参数
|
|
95
|
+
* @returns {PartQueryReq | null} 校验通过的 req
|
|
96
|
+
*/
|
|
97
|
+
export function parsePartQueryReq(value, tunables = partQueryTunables) {
|
|
98
|
+
if (!isPlainObject(value)) return null
|
|
99
|
+
const requestId = normalizeRequestId(value.requestId)
|
|
100
|
+
if (!requestId) return null
|
|
101
|
+
const originNodeHash = normalizeHex64(value.originNodeHash)
|
|
102
|
+
if (!isHex64(originNodeHash)) return null
|
|
103
|
+
const partpath = normalizePartpath(value.partpath)
|
|
104
|
+
if (!partpath) return null
|
|
105
|
+
const kind = String(value.kind || '').trim()
|
|
106
|
+
if (!kind) return null
|
|
107
|
+
if (!Object.prototype.hasOwnProperty.call(value, 'query')) return null
|
|
108
|
+
if (measureJsonBytes(value.query) > (tunables.maxQueryBytes ?? 2048)) return null
|
|
109
|
+
const ttl = clampPartQueryTtl(value.ttl, tunables.maxTtl)
|
|
110
|
+
if (ttl == null) return null
|
|
111
|
+
const budget = clampPartQueryBudget(value.budget, tunables.maxHits)
|
|
112
|
+
return {
|
|
113
|
+
requestId,
|
|
114
|
+
originNodeHash,
|
|
115
|
+
partpath,
|
|
116
|
+
kind,
|
|
117
|
+
query: value.query,
|
|
118
|
+
ttl,
|
|
119
|
+
budget,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {unknown} value 入站 res
|
|
125
|
+
* @param {typeof partQueryTunables} [tunables] 可调参数
|
|
126
|
+
* @returns {PartQueryRes | null} 校验通过的 res
|
|
127
|
+
*/
|
|
128
|
+
export function parsePartQueryRes(value, tunables = partQueryTunables) {
|
|
129
|
+
if (!isPlainObject(value)) return null
|
|
130
|
+
const requestId = normalizeRequestId(value.requestId)
|
|
131
|
+
if (!requestId) return null
|
|
132
|
+
const fromNodeHash = normalizeHex64(value.fromNodeHash)
|
|
133
|
+
if (!isHex64(fromNodeHash)) return null
|
|
134
|
+
const rows = clampPartQueryRows(value.rows, tunables.maxHits, tunables.maxRowsBytes)
|
|
135
|
+
if (!rows) return null
|
|
136
|
+
return { requestId, fromNodeHash, rows }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {unknown} partpath part 路径
|
|
141
|
+
* @param {unknown} kind 查询标签
|
|
142
|
+
* @param {unknown} query 不透明查询
|
|
143
|
+
* @returns {string | null} 规范化三元组的缓存材料;非法 null
|
|
144
|
+
*/
|
|
145
|
+
export function normalizePartQueryCacheMaterial(partpath, kind, query) {
|
|
146
|
+
const path = normalizePartpath(partpath)
|
|
147
|
+
const k = String(kind || '').trim()
|
|
148
|
+
if (!path || !k) return null
|
|
149
|
+
if (measureJsonBytes(query) > partQueryTunables.maxQueryBytes) return null
|
|
150
|
+
try {
|
|
151
|
+
return canonicalStringify({ partpath: path, kind: k, query })
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
}
|
package/timeline/append_core.mjs
CHANGED
|
@@ -14,17 +14,17 @@ import { computeDagTipIdsFromEvents } from '../governance/branch.mjs'
|
|
|
14
14
|
* 为追加事件计算 HLC 与 DAG 前驱(chat 群与 social 时间线共用)。
|
|
15
15
|
* @param {object[]} previous 已有事件
|
|
16
16
|
* @param {object} event 待追加事件
|
|
17
|
-
* @param {{ multiTip?: boolean }} [
|
|
17
|
+
* @param {{ multiTip?: boolean }} [options] chat 在多 tip 时连接全部 tip
|
|
18
18
|
* @returns {{ hlc: object, prev_event_ids: string[], last: object | undefined }} HLC、前驱与末条事件
|
|
19
19
|
*/
|
|
20
|
-
export function computeAppendHlcAndPrev(previous, event,
|
|
20
|
+
export function computeAppendHlcAndPrev(previous, event, options = {}) {
|
|
21
21
|
const last = previous[previous.length - 1]
|
|
22
22
|
const hlc = nextHlc(last?.hlc, event.timestamp ?? Date.now())
|
|
23
23
|
const tips = computeDagTipIdsFromEvents(previous)
|
|
24
24
|
let prev_event_ids
|
|
25
25
|
if (event.prev_event_ids?.length)
|
|
26
26
|
prev_event_ids = sortedPrevEventIds(event.prev_event_ids)
|
|
27
|
-
else if (
|
|
27
|
+
else if (options.multiTip && tips.length > 1)
|
|
28
28
|
prev_event_ids = sortedPrevEventIds(tips)
|
|
29
29
|
else if (tips.length)
|
|
30
30
|
prev_event_ids = sortedPrevEventIds(tips)
|
|
@@ -14,22 +14,22 @@ import { resolveFederationPoolLimits, selectLinkTargetsFromMembers } from './pee
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* 创建基于 link registry 的群组联邦房间。
|
|
17
|
-
* @param {object}
|
|
18
|
-
* @param {string}
|
|
19
|
-
* @param {string}
|
|
20
|
-
* @param {string[]}
|
|
21
|
-
* @param {object} [
|
|
22
|
-
* @returns {{ groupId: string, scope: string, start: () => Promise<void>, leave: () => Promise<void>, getRoster: () => Array<{ peerId: string, remoteNodeHash: string }>, getPeerIdByNodeHash: (nodeHash: string) => string | null, sendToPeer: (peerId: string, actionName: string, payload: unknown) => Promise<boolean>, send: (actionName: string, payload: unknown, peerId?: string | null) => Promise<number>, onEnvelope: (
|
|
17
|
+
* @param {object} options 选项
|
|
18
|
+
* @param {string} options.groupId 群组 id
|
|
19
|
+
* @param {string} options.roomSecret 房间密钥(用于 rendezvous topic)
|
|
20
|
+
* @param {string[]} options.members 初始成员 nodeHash 列表
|
|
21
|
+
* @param {object} [options.groupSettings] 群设置(连接预算:trustedPeerSlots/explorePeerSlots/maxPeers 等)
|
|
22
|
+
* @returns {{ groupId: string, scope: string, start: () => Promise<void>, leave: () => Promise<void>, getRoster: () => Array<{ peerId: string, remoteNodeHash: string }>, getPeerIdByNodeHash: (nodeHash: string) => string | null, sendToPeer: (peerId: string, actionName: string, payload: unknown) => Promise<boolean>, send: (actionName: string, payload: unknown, peerId?: string | null) => Promise<number>, onEnvelope: (callback: (senderNodeHash: string, envelope: object) => void) => () => void, onPeerJoin: (callback: (peerId: string) => void) => () => void, onPeerLeave: (callback: (peerId: string) => void) => () => void, getPeers: () => Record<string, true>, makeAction: (name: string) => [(payload: unknown, peerId?: string | string[] | null) => Promise<void>, (handler: (payload: unknown, peerId: string) => void) => void], registerCleanup: (cleanup: () => void) => void, isActive: () => boolean }} 群组 link set 接口
|
|
23
23
|
*/
|
|
24
|
-
export function createGroupLinkSet(
|
|
25
|
-
const registry =
|
|
26
|
-
const autoconnect =
|
|
27
|
-
const groupId = String(
|
|
24
|
+
export function createGroupLinkSet(options) {
|
|
25
|
+
const registry = options.registry ?? getLinkRegistry()
|
|
26
|
+
const autoconnect = options.autoconnect !== false
|
|
27
|
+
const groupId = String(options.groupId)
|
|
28
28
|
const scope = `group:${groupId}`
|
|
29
|
-
const topic = groupRendezvousTopic(
|
|
30
|
-
const members = new Set((Array.isArray(
|
|
29
|
+
const topic = groupRendezvousTopic(options.roomSecret)
|
|
30
|
+
const members = new Set((Array.isArray(options.members) ? options.members : []).map(String))
|
|
31
31
|
const selfNodeHash = registry.localIdentity.nodeHash
|
|
32
|
-
const groupSettings =
|
|
32
|
+
const groupSettings = options.groupSettings ?? {}
|
|
33
33
|
// 初始成员是调用方明确知道的引导集合(如 introducer/creator/seed),作为必连锚点保证引导期连通。
|
|
34
34
|
const initialAnchors = new Set(members)
|
|
35
35
|
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
@@ -50,12 +50,12 @@ export function createGroupLinkSet(opts) {
|
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* 注册 leave 时执行的清理回调。
|
|
53
|
-
* @param {() => void}
|
|
53
|
+
* @param {() => void} cleanup 清理函数
|
|
54
54
|
* @returns {void}
|
|
55
55
|
*/
|
|
56
|
-
function registerCleanup(
|
|
57
|
-
if (typeof
|
|
58
|
-
cleanups.add(
|
|
56
|
+
function registerCleanup(cleanup) {
|
|
57
|
+
if (typeof cleanup !== 'function') return
|
|
58
|
+
cleanups.add(cleanup)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
@@ -255,34 +255,34 @@ export function createGroupLinkSet(opts) {
|
|
|
255
255
|
},
|
|
256
256
|
/**
|
|
257
257
|
* 订阅入站 envelope。
|
|
258
|
-
* @param {(senderNodeHash: string, envelope: object) => void}
|
|
258
|
+
* @param {(senderNodeHash: string, envelope: object) => void} callback 回调
|
|
259
259
|
* @returns {() => void} 取消订阅函数
|
|
260
260
|
*/
|
|
261
|
-
onEnvelope(
|
|
262
|
-
envelopeListeners.add(
|
|
263
|
-
return () => envelopeListeners.delete(
|
|
261
|
+
onEnvelope(callback) {
|
|
262
|
+
envelopeListeners.add(callback)
|
|
263
|
+
return () => envelopeListeners.delete(callback)
|
|
264
264
|
},
|
|
265
265
|
/**
|
|
266
266
|
* 订阅 peer 加入事件(含当前已在线 peer 的即时回调)。
|
|
267
|
-
* @param {(peerId: string) => void}
|
|
267
|
+
* @param {(peerId: string) => void} callback 回调
|
|
268
268
|
* @returns {() => void} 取消订阅函数
|
|
269
269
|
*/
|
|
270
|
-
onPeerJoin(
|
|
271
|
-
peerJoinListeners.add(
|
|
270
|
+
onPeerJoin(callback) {
|
|
271
|
+
peerJoinListeners.add(callback)
|
|
272
272
|
for (const { peerId } of activeRoster())
|
|
273
273
|
if (peerId) announcedPeers.add(peerId)
|
|
274
274
|
for (const peerId of announcedPeers)
|
|
275
|
-
try {
|
|
276
|
-
return () => peerJoinListeners.delete(
|
|
275
|
+
try { callback(peerId) } catch { /* ignore */ }
|
|
276
|
+
return () => peerJoinListeners.delete(callback)
|
|
277
277
|
},
|
|
278
278
|
/**
|
|
279
279
|
* 订阅 peer 离开事件。
|
|
280
|
-
* @param {(peerId: string) => void}
|
|
280
|
+
* @param {(peerId: string) => void} callback 回调
|
|
281
281
|
* @returns {() => void} 取消订阅函数
|
|
282
282
|
*/
|
|
283
|
-
onPeerLeave(
|
|
284
|
-
peerLeaveListeners.add(
|
|
285
|
-
return () => peerLeaveListeners.delete(
|
|
283
|
+
onPeerLeave(callback) {
|
|
284
|
+
peerLeaveListeners.add(callback)
|
|
285
|
+
return () => peerLeaveListeners.delete(callback)
|
|
286
286
|
},
|
|
287
287
|
/**
|
|
288
288
|
* 返回当前在线 peer 的 Record 映射。
|
|
@@ -47,14 +47,6 @@ export function resolveIceServers(groupSettings) {
|
|
|
47
47
|
return out.length ? out : [...DEFAULT_ICE_SERVERS]
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/**
|
|
51
|
-
* @param {unknown} groupSettings 物化群设置
|
|
52
|
-
* @returns {{ iceServers: ReturnType<typeof resolveIceServers> }} Trystero rtcConfig 片段
|
|
53
|
-
*/
|
|
54
|
-
export function resolveIceServersForTrystero(groupSettings) {
|
|
55
|
-
return { iceServers: resolveIceServers(groupSettings) }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
50
|
/**
|
|
59
51
|
* 校验并规范化待写入 DAG 的 iceServers 数组。
|
|
60
52
|
* @param {unknown} raw 请求体字段
|
|
@@ -187,20 +187,20 @@ function subscribeBucket(buckets, key, listener) {
|
|
|
187
187
|
|
|
188
188
|
/**
|
|
189
189
|
* 创建 P2P 链路注册表(discovery、信令、直连与 overlay relay)。
|
|
190
|
-
* @param {object} [
|
|
191
|
-
* @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array }} [
|
|
192
|
-
* @param {typeof createLink} [
|
|
193
|
-
* @param {RTCConfiguration['iceServers']} [
|
|
194
|
-
* @param {number} [
|
|
195
|
-
* @param {boolean} [
|
|
190
|
+
* @param {object} [options] 选项
|
|
191
|
+
* @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array }} [options.localIdentity] 本地身份
|
|
192
|
+
* @param {typeof createLink} [options.createLink] 链路工厂(可注入 mock)
|
|
193
|
+
* @param {RTCConfiguration['iceServers']} [options.iceServers] ICE 服务器列表
|
|
194
|
+
* @param {number} [options.maxActive] 最大并发活跃链路数
|
|
195
|
+
* @param {boolean} [options.autoRegisterDiscoveryProviders] 是否自动注册 discovery provider
|
|
196
196
|
* @returns {object} link registry 接口
|
|
197
197
|
*/
|
|
198
|
-
export function createLinkRegistry(
|
|
199
|
-
const localIdentity = resolveLocalIdentity(
|
|
200
|
-
const createLinkImpl =
|
|
201
|
-
const iceServers =
|
|
202
|
-
const maxActive = Math.max(4, Number(
|
|
203
|
-
const autoRegisterDiscoveryProviders =
|
|
198
|
+
export function createLinkRegistry(options = {}) {
|
|
199
|
+
const localIdentity = resolveLocalIdentity(options.localIdentity)
|
|
200
|
+
const createLinkImpl = options.createLink ?? createLink
|
|
201
|
+
const iceServers = options.iceServers?.length ? options.iceServers : DEFAULT_ICE_SERVERS
|
|
202
|
+
const maxActive = Math.max(4, Number(options.maxActive) || 32)
|
|
203
|
+
const autoRegisterDiscoveryProviders = options.autoRegisterDiscoveryProviders !== false
|
|
204
204
|
const selfTopic = nodeRendezvousTopic(localIdentity.nodeHash)
|
|
205
205
|
/** @type {Map<string, Awaited<ReturnType<typeof createLinkImpl>>>} */
|
|
206
206
|
const links = new Map()
|
|
@@ -348,7 +348,7 @@ export function createLinkRegistry(opts = {}) {
|
|
|
348
348
|
/**
|
|
349
349
|
* 为一条 connId 会话建 PC 并走 glare 择一:建 link → 绑 onDown 清 session → ready → registerResolvedLink。
|
|
350
350
|
* initiator 侧建链前先 trimToBudget 腾预算。出错则清理该 connId 会话。
|
|
351
|
-
* @param {{ remoteNodeHash: string, connId: string, session: ReturnType<typeof createBufferedSignalSession>, initiator: boolean }}
|
|
351
|
+
* @param {{ remoteNodeHash: string, connId: string, session: ReturnType<typeof createBufferedSignalSession>, initiator: boolean }} options 建链参数
|
|
352
352
|
* @returns {Promise<Awaited<ReturnType<typeof createLinkImpl>> | null>} 当前规范链(本条可能因 glare 被关,取 links 现值);失败 null
|
|
353
353
|
*/
|
|
354
354
|
async function buildConnLink({ remoteNodeHash, connId, session, initiator }) {
|