@steve02081504/fount-p2p 0.0.5 → 0.0.7
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/tcp_port.mjs +11 -0
- package/dag/canonicalize_row.mjs +5 -5
- package/discovery/advert_peer_hints.mjs +21 -0
- package/discovery/{bt.mjs → bt/index.mjs} +151 -55
- package/discovery/bt/peer_hints.mjs +41 -0
- package/discovery/bt/runtime.mjs +46 -0
- package/discovery/lan_peer_hints.mjs +43 -0
- package/discovery/mdns.mjs +10 -6
- 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/topo_order_memo.mjs +3 -3
- package/files/chunk_responder.mjs +1 -1
- package/files/evfs.mjs +26 -26
- package/files/transfer_key.mjs +9 -9
- package/files/transfer_key_registry.mjs +9 -9
- package/governance/join_pow.mjs +17 -17
- package/index.mjs +3 -2
- package/link/channel_mux.mjs +7 -7
- package/link/frame.mjs +5 -5
- package/link/handshake.mjs +63 -36
- package/link/pipe.mjs +404 -0
- package/link/providers/ble_gatt.mjs +339 -0
- package/link/providers/index.mjs +90 -0
- package/link/providers/lan_tcp.mjs +342 -0
- package/link/providers/levels.mjs +9 -0
- package/link/providers/webrtc.mjs +385 -0
- package/mailbox/deliver_or_store.mjs +13 -13
- package/mailbox/wire.mjs +4 -4
- package/node/reputation_store.mjs +5 -5
- package/node/retention_policy.mjs +8 -8
- package/overlay/index.mjs +6 -6
- package/package.json +3 -2
- package/registries/inbound.mjs +8 -8
- package/rooms/scoped_link.mjs +23 -21
- package/timeline/append_core.mjs +3 -3
- package/transport/group_link_set.mjs +35 -33
- package/transport/link_registry.mjs +250 -63
- package/transport/peer_pool.mjs +4 -4
- package/transport/user_room.mjs +12 -14
- package/utils/ttl_map.mjs +41 -0
- package/wire/group_part.mjs +3 -3
- package/wire/part_ingress.mjs +14 -14
- package/wire/part_query.mjs +81 -82
- package/wire/part_query_cache.mjs +7 -0
- package/link/link.mjs +0 -617
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { getSignalingRuntimeConfig } from '../../node/instance.mjs'
|
|
2
|
+
import { ms } from '../../utils/duration.mjs'
|
|
3
|
+
import { createLruMap } from '../../utils/lru.mjs'
|
|
4
|
+
import {
|
|
5
|
+
CHANNEL_BULK,
|
|
6
|
+
CHANNEL_CONTROL,
|
|
7
|
+
CHANNEL_LOW_THRESHOLD_BYTES,
|
|
8
|
+
configureBufferedAmountLowThreshold,
|
|
9
|
+
createChannelSendQueues,
|
|
10
|
+
onBufferedAmountLow,
|
|
11
|
+
} from '../channel_mux.mjs'
|
|
12
|
+
import { asLinkHandle, createLinkPipe } from '../pipe.mjs'
|
|
13
|
+
import {
|
|
14
|
+
attachDataChannelListener,
|
|
15
|
+
attachIceCandidateListener,
|
|
16
|
+
loadNodeRtcPolyfill,
|
|
17
|
+
signalDataToBytes,
|
|
18
|
+
waitForChannelState,
|
|
19
|
+
} from '../rtc.mjs'
|
|
20
|
+
import { extractDtlsFingerprint } from '../sdp_fingerprint.mjs'
|
|
21
|
+
|
|
22
|
+
import { LINK_LEVEL_WEBRTC } from './levels.mjs'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 将错误对象格式化为短字符串。
|
|
26
|
+
* @param {unknown} error 原始错误
|
|
27
|
+
* @returns {string} 短 reason
|
|
28
|
+
*/
|
|
29
|
+
function formatErrorReason(error) {
|
|
30
|
+
return String(error?.message ?? error ?? 'unknown-error').replace(/\s+/g, ' ').slice(0, 240)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 绑定 data channel message 回调。
|
|
35
|
+
* @param {RTCDataChannel} channel RTC 数据通道
|
|
36
|
+
* @param {(data: unknown) => void} handler 消息处理器
|
|
37
|
+
* @returns {void}
|
|
38
|
+
*/
|
|
39
|
+
function attachChannelMessageListener(channel, handler) {
|
|
40
|
+
channel.addEventListener?.('message', event => handler(event?.data))
|
|
41
|
+
/**
|
|
42
|
+
* @param {MessageEvent} event 消息事件
|
|
43
|
+
* @returns {void}
|
|
44
|
+
*/
|
|
45
|
+
channel.onmessage = event => handler(event?.data)
|
|
46
|
+
channel.onMessage?.subscribe(message => handler(message))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let cachedAvailable = null
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 探测 WebRTC(node-datachannel)是否可用。
|
|
53
|
+
* @returns {Promise<boolean>} 可用为 true
|
|
54
|
+
*/
|
|
55
|
+
export async function canUseWebRtcLink() {
|
|
56
|
+
if (cachedAvailable !== null) return cachedAvailable
|
|
57
|
+
try {
|
|
58
|
+
await loadNodeRtcPolyfill()
|
|
59
|
+
cachedAvailable = true
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
cachedAvailable = false
|
|
63
|
+
}
|
|
64
|
+
return cachedAvailable
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 建立 WebRTC link(双 DataChannel + discovery 信令)。
|
|
69
|
+
* @param {object} options link 配置
|
|
70
|
+
* @param {string | null} [options.nodeHash] 期望的对端 nodeHash
|
|
71
|
+
* @param {boolean} options.initiator 是否为连接发起方
|
|
72
|
+
* @param {{ send: (message: unknown) => void | Promise<void>, onRemote: (handler: (message: unknown) => void) => (() => void) | void }} options.signal 信令收发接口
|
|
73
|
+
* @param {RTCConfiguration['iceServers']} [options.iceServers] ICE 服务器列表
|
|
74
|
+
* @param {number} [options.heartbeatMs] 心跳间隔
|
|
75
|
+
* @param {number} [options.idleTimeoutMs] 无入站流量超时
|
|
76
|
+
* @param {number} [options.handshakeTimeoutMs] 握手超时
|
|
77
|
+
* @param {{ RTCPeerConnection: typeof RTCPeerConnection } | null} [options.rtc] RTC 构造器
|
|
78
|
+
* @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array, nonce?: string } | null} [options.localIdentity] 本地握手身份
|
|
79
|
+
* @returns {Promise<import('./index.mjs').LinkHandle>} link 句柄
|
|
80
|
+
*/
|
|
81
|
+
export async function createWebRtcLink(options) {
|
|
82
|
+
const handshakeTimeoutMs = Number(options.handshakeTimeoutMs) || ms('10s')
|
|
83
|
+
const channelOpenTimeoutMs = Math.max(handshakeTimeoutMs, ms('30s'))
|
|
84
|
+
const trickleIceOff = getSignalingRuntimeConfig().trickleIceOff === true
|
|
85
|
+
const rtc = options.rtc ?? await loadNodeRtcPolyfill()
|
|
86
|
+
const pc = new rtc.RTCPeerConnection(options.iceServers?.length ? { iceServers: options.iceServers } : undefined)
|
|
87
|
+
const remoteSignalQueue = []
|
|
88
|
+
const seenRemoteSignals = createLruMap(1024)
|
|
89
|
+
let remoteDescriptionSet = false
|
|
90
|
+
let controlChannel = null
|
|
91
|
+
let bulkChannel = null
|
|
92
|
+
let unlistenRemote = null
|
|
93
|
+
let sendQueues = null
|
|
94
|
+
let controlLowEvents = 0
|
|
95
|
+
let bulkLowEvents = 0
|
|
96
|
+
let reconnectCount = 0
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {unknown} message 信令载荷
|
|
100
|
+
* @returns {Promise<void>}
|
|
101
|
+
*/
|
|
102
|
+
async function sendSignal(message) {
|
|
103
|
+
await Promise.resolve(options.signal.send(message))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const pipe = createLinkPipe({
|
|
107
|
+
providerId: 'webrtc',
|
|
108
|
+
level: LINK_LEVEL_WEBRTC,
|
|
109
|
+
initiator: !!options.initiator,
|
|
110
|
+
nodeHash: options.nodeHash,
|
|
111
|
+
localIdentity: options.localIdentity,
|
|
112
|
+
heartbeatMs: options.heartbeatMs,
|
|
113
|
+
idleTimeoutMs: options.idleTimeoutMs,
|
|
114
|
+
handshakeTimeoutMs,
|
|
115
|
+
/** @returns {string} 本端 DTLS fingerprint */
|
|
116
|
+
getLocalBinding: () => extractDtlsFingerprint(pc.localDescription?.sdp || ''),
|
|
117
|
+
/** @returns {string} 对端 DTLS fingerprint */
|
|
118
|
+
getRemoteBinding: () => extractDtlsFingerprint(pc.remoteDescription?.sdp || ''),
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} text control JSON
|
|
121
|
+
* @returns {void}
|
|
122
|
+
*/
|
|
123
|
+
sendControlText(text) {
|
|
124
|
+
if (!controlChannel) throw new Error('p2p: control channel unavailable')
|
|
125
|
+
controlChannel.send(text)
|
|
126
|
+
},
|
|
127
|
+
/**
|
|
128
|
+
* @param {string} action envelope action
|
|
129
|
+
* @param {Uint8Array} frame 帧字节
|
|
130
|
+
* @returns {void}
|
|
131
|
+
*/
|
|
132
|
+
sendFrame(action, frame) {
|
|
133
|
+
if (!sendQueues) throw new Error('p2p: send queues unavailable')
|
|
134
|
+
sendQueues.enqueue(action, frame)
|
|
135
|
+
},
|
|
136
|
+
/**
|
|
137
|
+
* @returns {Promise<void>}
|
|
138
|
+
*/
|
|
139
|
+
async closeTransport() {
|
|
140
|
+
unlistenRemote?.()
|
|
141
|
+
sendQueues?.clear()
|
|
142
|
+
try { controlChannel?.close() } catch { /* ignore */ }
|
|
143
|
+
try { bulkChannel?.close() } catch { /* ignore */ }
|
|
144
|
+
try { await pc.close() } catch { /* ignore */ }
|
|
145
|
+
},
|
|
146
|
+
/**
|
|
147
|
+
* @returns {object} WebRTC 附加 stats
|
|
148
|
+
*/
|
|
149
|
+
extraStats() {
|
|
150
|
+
return {
|
|
151
|
+
connectionState: pc.connectionState,
|
|
152
|
+
iceConnectionState: pc.iceConnectionState,
|
|
153
|
+
reconnectCount,
|
|
154
|
+
pending: sendQueues?.pending() ?? { control: 0, bulk: 0 },
|
|
155
|
+
controlBufferedAmount: controlChannel?.bufferedAmount ?? 0,
|
|
156
|
+
bulkBufferedAmount: bulkChannel?.bufferedAmount ?? 0,
|
|
157
|
+
controlLowEvents,
|
|
158
|
+
bulkLowEvents,
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {RTCDataChannel} channel RTC 数据通道
|
|
165
|
+
* @returns {void}
|
|
166
|
+
*/
|
|
167
|
+
function attachBackpressurePump(channel) {
|
|
168
|
+
configureBufferedAmountLowThreshold(channel, CHANNEL_LOW_THRESHOLD_BYTES)
|
|
169
|
+
onBufferedAmountLow(channel, () => {
|
|
170
|
+
if (channel.label === CHANNEL_CONTROL) controlLowEvents++
|
|
171
|
+
if (channel.label === CHANNEL_BULK) bulkLowEvents++
|
|
172
|
+
if (channel.label === CHANNEL_CONTROL) sendQueues?.flush(CHANNEL_CONTROL)
|
|
173
|
+
if (channel.label === CHANNEL_BULK) sendQueues?.flush(CHANNEL_BULK)
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {RTCSessionDescriptionInit | RTCSessionDescription} description 远端会话描述
|
|
179
|
+
* @returns {Promise<void>}
|
|
180
|
+
*/
|
|
181
|
+
async function applyRemoteDescription(description) {
|
|
182
|
+
await pc.setRemoteDescription(description)
|
|
183
|
+
remoteDescriptionSet = true
|
|
184
|
+
await flushQueuedIceCandidates()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {unknown} error addIceCandidate 错误
|
|
189
|
+
* @returns {boolean} 是否应暂存后重试
|
|
190
|
+
*/
|
|
191
|
+
function shouldRetryQueuedIce(error) {
|
|
192
|
+
return /without ice transport/i.test(String(error?.message ?? error ?? ''))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @returns {Promise<void>}
|
|
197
|
+
*/
|
|
198
|
+
async function waitForIceGatheringComplete() {
|
|
199
|
+
if (!trickleIceOff || pc.iceGatheringState === 'complete') return
|
|
200
|
+
const deadline = Date.now() + handshakeTimeoutMs
|
|
201
|
+
while (pc.iceGatheringState !== 'complete' && Date.now() < deadline)
|
|
202
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @returns {Promise<void>}
|
|
207
|
+
*/
|
|
208
|
+
async function flushQueuedIceCandidates() {
|
|
209
|
+
if (!remoteDescriptionSet || !pc.localDescription) return
|
|
210
|
+
while (remoteSignalQueue.length) {
|
|
211
|
+
const candidate = remoteSignalQueue.shift()
|
|
212
|
+
try {
|
|
213
|
+
await pc.addIceCandidate(candidate)
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (shouldRetryQueuedIce(error)) {
|
|
217
|
+
remoteSignalQueue.unshift(candidate)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
throw error
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @param {unknown} message 信令消息
|
|
227
|
+
* @returns {Promise<void>}
|
|
228
|
+
*/
|
|
229
|
+
async function handleRemoteSignal(message) {
|
|
230
|
+
if (!message || typeof message !== 'object') return
|
|
231
|
+
const signalKey = JSON.stringify(message)
|
|
232
|
+
if (seenRemoteSignals.has(signalKey)) return
|
|
233
|
+
seenRemoteSignals.touch(signalKey, true)
|
|
234
|
+
if (message.type === 'description' && message.description) {
|
|
235
|
+
if (message.description.type === 'answer' && pc.signalingState === 'stable') return
|
|
236
|
+
await applyRemoteDescription(message.description)
|
|
237
|
+
if (message.description.type === 'offer') {
|
|
238
|
+
const answer = await pc.createAnswer()
|
|
239
|
+
await pc.setLocalDescription(answer)
|
|
240
|
+
await flushQueuedIceCandidates()
|
|
241
|
+
await waitForIceGatheringComplete()
|
|
242
|
+
await sendSignal({
|
|
243
|
+
type: 'description',
|
|
244
|
+
description: pc.localDescription?.toJSON?.() ?? pc.localDescription ?? answer,
|
|
245
|
+
})
|
|
246
|
+
await pipe.maybeSendAuth()
|
|
247
|
+
}
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
if (message.type === 'ice' && message.candidate) {
|
|
251
|
+
if (!remoteDescriptionSet || !pc.localDescription || pc.signalingState !== 'stable') {
|
|
252
|
+
remoteSignalQueue.push(message.candidate)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
await pc.addIceCandidate(message.candidate)
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (shouldRetryQueuedIce(error)) {
|
|
260
|
+
remoteSignalQueue.push(message.candidate)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
throw error
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @param {RTCDataChannel} channel RTC 数据通道
|
|
270
|
+
* @returns {Promise<void>}
|
|
271
|
+
*/
|
|
272
|
+
async function attachChannel(channel) {
|
|
273
|
+
if (channel.label === CHANNEL_CONTROL) controlChannel = channel
|
|
274
|
+
else if (channel.label === CHANNEL_BULK) bulkChannel = channel
|
|
275
|
+
else return
|
|
276
|
+
attachChannelMessageListener(channel, data => {
|
|
277
|
+
try {
|
|
278
|
+
if (typeof data === 'string') pipe.handleInbound(data)
|
|
279
|
+
else pipe.handleInbound(signalDataToBytes(data))
|
|
280
|
+
}
|
|
281
|
+
catch { /* drop */ }
|
|
282
|
+
})
|
|
283
|
+
attachBackpressurePump(channel)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @returns {Promise<void>}
|
|
288
|
+
*/
|
|
289
|
+
async function maybeStartPostOpenFlow() {
|
|
290
|
+
if (!controlChannel || !bulkChannel) return
|
|
291
|
+
await Promise.all([
|
|
292
|
+
waitForChannelState(controlChannel, 'open', channelOpenTimeoutMs),
|
|
293
|
+
waitForChannelState(bulkChannel, 'open', channelOpenTimeoutMs),
|
|
294
|
+
])
|
|
295
|
+
if (!sendQueues)
|
|
296
|
+
sendQueues = createChannelSendQueues({
|
|
297
|
+
/**
|
|
298
|
+
* @param {'control' | 'bulk'} name 通道名
|
|
299
|
+
* @returns {RTCDataChannel | null | undefined} 对应通道
|
|
300
|
+
*/
|
|
301
|
+
getChannel: name => name === CHANNEL_CONTROL ? controlChannel : bulkChannel,
|
|
302
|
+
})
|
|
303
|
+
await pipe.startHandshake()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
unlistenRemote = options.signal.onRemote(message => {
|
|
307
|
+
void handleRemoteSignal(message).catch(error => pipe.close(`signal-error:${formatErrorReason(error)}`))
|
|
308
|
+
}) ?? null
|
|
309
|
+
|
|
310
|
+
attachIceCandidateListener(pc, event => {
|
|
311
|
+
if (trickleIceOff || !event.candidate) return
|
|
312
|
+
void sendSignal({
|
|
313
|
+
type: 'ice',
|
|
314
|
+
candidate: typeof event.candidate.toJSON === 'function' ? event.candidate.toJSON() : event.candidate,
|
|
315
|
+
}).catch(error => pipe.close(`signal-send-failed:${formatErrorReason(error)}`))
|
|
316
|
+
})
|
|
317
|
+
attachDataChannelListener(pc, event => {
|
|
318
|
+
void attachChannel(event.channel)
|
|
319
|
+
.then(() => maybeStartPostOpenFlow())
|
|
320
|
+
.catch(error => pipe.close(`channel-attach-failed:${error?.message ?? error}`))
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
/** 连接失败/关闭时关掉 pipe。 */
|
|
324
|
+
pc.onconnectionstatechange = () => {
|
|
325
|
+
if (['failed', 'closed', 'disconnected'].includes(pc.connectionState)) {
|
|
326
|
+
reconnectCount++
|
|
327
|
+
void pipe.close(`connection-${pc.connectionState}`)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (options.initiator) {
|
|
332
|
+
await attachChannel(pc.createDataChannel(CHANNEL_CONTROL))
|
|
333
|
+
await attachChannel(pc.createDataChannel(CHANNEL_BULK))
|
|
334
|
+
const offer = await pc.createOffer()
|
|
335
|
+
await pc.setLocalDescription(offer)
|
|
336
|
+
await waitForIceGatheringComplete()
|
|
337
|
+
await sendSignal({
|
|
338
|
+
type: 'description',
|
|
339
|
+
description: pc.localDescription?.toJSON?.() ?? pc.localDescription ?? offer,
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
void maybeStartPostOpenFlow().catch(error => pipe.close(`open-flow-failed:${error?.message ?? error}`))
|
|
344
|
+
|
|
345
|
+
return asLinkHandle(pipe, {
|
|
346
|
+
/**
|
|
347
|
+
* @internal 仅 live 背压测试用,不属公开 LinkHandle
|
|
348
|
+
* @param {'control' | 'bulk'} name 通道名
|
|
349
|
+
* @returns {RTCDataChannel | null} 测试用通道
|
|
350
|
+
*/
|
|
351
|
+
_channelForTest(name) {
|
|
352
|
+
return name === CHANNEL_CONTROL ? controlChannel : bulkChannel
|
|
353
|
+
},
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 创建 WebRTC LinkProvider。
|
|
359
|
+
* @param {object} [options] 可选覆盖
|
|
360
|
+
* @param {typeof createWebRtcLink} [options.createWebRtcLink] 链路工厂(测试注入)
|
|
361
|
+
* @returns {import('./index.mjs').LinkProvider} WebRTC provider
|
|
362
|
+
*/
|
|
363
|
+
export function createWebRtcLinkProvider(options = {}) {
|
|
364
|
+
const createImpl = options.createWebRtcLink ?? createWebRtcLink
|
|
365
|
+
return {
|
|
366
|
+
id: 'webrtc',
|
|
367
|
+
level: LINK_LEVEL_WEBRTC,
|
|
368
|
+
caps: { needsOfferAnswer: true, needsDiscoverySignal: true },
|
|
369
|
+
isAvailable: canUseWebRtcLink,
|
|
370
|
+
/**
|
|
371
|
+
* @param {object} dialOptions dial 参数(含 signal / iceServers 等)
|
|
372
|
+
* @returns {Promise<import('./index.mjs').LinkHandle>} 已建链句柄
|
|
373
|
+
*/
|
|
374
|
+
async dial(dialOptions) {
|
|
375
|
+
return createImpl({ ...dialOptions, initiator: true })
|
|
376
|
+
},
|
|
377
|
+
/**
|
|
378
|
+
* @param {object} acceptOptions accept 参数
|
|
379
|
+
* @returns {Promise<import('./index.mjs').LinkHandle>} 已建链句柄
|
|
380
|
+
*/
|
|
381
|
+
async accept(acceptOptions) {
|
|
382
|
+
return createImpl({ ...acceptOptions, initiator: false })
|
|
383
|
+
},
|
|
384
|
+
}
|
|
385
|
+
}
|
|
@@ -59,26 +59,26 @@ async function resolveRouting(username) {
|
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* @param {string} username 副本用户名(trust graph 投递上下文)
|
|
62
|
-
* @param {object}
|
|
62
|
+
* @param {object} options 投递选项
|
|
63
63
|
* @returns {Promise<{ stored: boolean, delivered: boolean, relayed: number }>} 存转结果
|
|
64
64
|
*/
|
|
65
|
-
export async function deliverOrStoreMailboxPut(username,
|
|
65
|
+
export async function deliverOrStoreMailboxPut(username, options) {
|
|
66
66
|
const routing = await resolveRouting(username)
|
|
67
|
-
const toPubKeyHash = normalizeHex64(
|
|
67
|
+
const toPubKeyHash = normalizeHex64(options.toPubKeyHash)
|
|
68
68
|
if (!toPubKeyHash) return { stored: false, delivered: false, relayed: 0 }
|
|
69
|
-
const hop = normalizeMailboxHop(
|
|
69
|
+
const hop = normalizeMailboxHop(options.hop)
|
|
70
70
|
if (hop >= routing.maxHop) return { stored: false, delivered: false, relayed: 0 }
|
|
71
71
|
const tier = mailboxTierFromHop(hop)
|
|
72
72
|
const nodeHash = getNodeHash()
|
|
73
73
|
const record = {
|
|
74
|
-
...
|
|
74
|
+
...options.record,
|
|
75
75
|
toPubKeyHash,
|
|
76
76
|
hop,
|
|
77
77
|
tier,
|
|
78
|
-
fromNodeHash:
|
|
78
|
+
fromNodeHash: options.record?.fromNodeHash || nodeHash,
|
|
79
79
|
}
|
|
80
80
|
const stored = await storeMailboxRecord(record)
|
|
81
|
-
const toNodeHash =
|
|
81
|
+
const toNodeHash = options.toNodeHash?.trim().toLowerCase()
|
|
82
82
|
const delivered = toNodeHash && isMailboxRecordWithinSizeLimit(record)
|
|
83
83
|
? await requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).sendToNode(username, toNodeHash, 'mailbox_put', { nodeHash, record })
|
|
84
84
|
: false
|
|
@@ -108,17 +108,17 @@ export async function publishMailboxRecord(username, toPubKeyHash, record, toNod
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
* @param {{ replicaUsername?: string }}
|
|
111
|
+
* @param {{ replicaUsername?: string }} wireContext 入站上下文
|
|
112
112
|
* @param {object} put 入站 mailbox_put
|
|
113
113
|
* @param {string} [peerId] Trystero 对端 id(有则校验 nodeHash 绑定)
|
|
114
114
|
* @returns {Promise<void>}
|
|
115
115
|
*/
|
|
116
|
-
export async function ingestMailboxPut(
|
|
116
|
+
export async function ingestMailboxPut(wireContext, put, peerId = '') {
|
|
117
117
|
const { record } = put
|
|
118
118
|
if (!record?.envelope || !record?.toPubKeyHash) return
|
|
119
119
|
const fromNode = normalizeHex64(put.nodeHash)
|
|
120
120
|
if (!fromNode || !takeIncomingMailboxPutSlot(fromNode)) return
|
|
121
|
-
const username = String(
|
|
121
|
+
const username = String(wireContext.replicaUsername || '').trim()
|
|
122
122
|
if (!username) return
|
|
123
123
|
if (peerId) {
|
|
124
124
|
const remote = await resolveRemoteNodeHashForPeer(username, peerId)
|
|
@@ -157,14 +157,14 @@ export async function respondMailboxWant(want, sendGive, peerId) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
|
-
* @param {{ replicaUsername?: string }}
|
|
160
|
+
* @param {{ replicaUsername?: string }} wireContext 入站上下文
|
|
161
161
|
* @param {object} give mailbox_give 载荷
|
|
162
162
|
* @returns {Promise<number>} 投递给消费者的记录数
|
|
163
163
|
*/
|
|
164
|
-
export async function ingestMailboxGive(
|
|
164
|
+
export async function ingestMailboxGive(wireContext, give) {
|
|
165
165
|
const records = (give.records || []).filter(isDeliverableMailboxRecord)
|
|
166
166
|
if (!records.length) return 0
|
|
167
|
-
const username = String(
|
|
167
|
+
const username = String(wireContext.replicaUsername || '').trim()
|
|
168
168
|
if (!username) return 0
|
|
169
169
|
const { dispatchMailboxRecordsToConsumers } = await import('./consumer_registry.mjs')
|
|
170
170
|
const { deleteMailboxRecords } = await import('./store.mjs')
|
package/mailbox/wire.mjs
CHANGED
|
@@ -10,15 +10,15 @@ import { parseMailboxGive, parseMailboxPut, parseMailboxWant } from './parse.mjs
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* @param {MailboxWireContext}
|
|
13
|
+
* @param {MailboxWireContext} wireContext 入站上下文
|
|
14
14
|
* @param {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire Trystero 适配器
|
|
15
15
|
* @returns {void}
|
|
16
16
|
*/
|
|
17
|
-
export function attachMailboxWire(
|
|
17
|
+
export function attachMailboxWire(wireContext, wire) {
|
|
18
18
|
wire.on('mailbox_put', (payload, peerId) => {
|
|
19
19
|
const put = parseMailboxPut(payload)
|
|
20
20
|
if (!put.ok) return
|
|
21
|
-
void ingestMailboxPut(
|
|
21
|
+
void ingestMailboxPut(wireContext, put.value, peerId).catch(err => console.error('mailbox: put ingest failed', err))
|
|
22
22
|
})
|
|
23
23
|
|
|
24
24
|
wire.on('mailbox_want', (payload, peerId) => {
|
|
@@ -35,6 +35,6 @@ export function attachMailboxWire(ctx, wire) {
|
|
|
35
35
|
wire.on('mailbox_give', payload => {
|
|
36
36
|
const give = parseMailboxGive(payload)
|
|
37
37
|
if (!give.ok) return
|
|
38
|
-
void ingestMailboxGive(
|
|
38
|
+
void ingestMailboxGive(wireContext, give.value).catch(err => console.error('mailbox: give ingest failed', err))
|
|
39
39
|
})
|
|
40
40
|
}
|
|
@@ -26,12 +26,12 @@ import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
|
|
|
26
26
|
|
|
27
27
|
const DATA_NAME = 'reputation'
|
|
28
28
|
|
|
29
|
-
/** @type {((
|
|
29
|
+
/** @type {((options: object) => Promise<boolean>) | null} */
|
|
30
30
|
let blockReputationHandler = null
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Shell Load 时注册:公开 block/unblock → 信誉传导。
|
|
34
|
-
* @param {(
|
|
34
|
+
* @param {(options: object) => Promise<boolean>} handler block/unblock 信誉传导回调
|
|
35
35
|
* @returns {void}
|
|
36
36
|
*/
|
|
37
37
|
export function registerBlockReputationHandler(handler) {
|
|
@@ -308,10 +308,10 @@ export function pickNodeScore(nodeId) {
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
/**
|
|
311
|
-
* @param {object}
|
|
311
|
+
* @param {object} options applyFollowedBlockSignal 参数
|
|
312
312
|
* @returns {Promise<boolean>} 是否已应用
|
|
313
313
|
*/
|
|
314
|
-
export async function applyBlockReputationSignal(
|
|
314
|
+
export async function applyBlockReputationSignal(options) {
|
|
315
315
|
if (!blockReputationHandler) return false
|
|
316
|
-
return blockReputationHandler(
|
|
316
|
+
return blockReputationHandler(options)
|
|
317
317
|
}
|
|
@@ -8,16 +8,16 @@ import {
|
|
|
8
8
|
* 在共识分支上计算须保留的事件 id(连通子图,不用拓扑下标切片)。
|
|
9
9
|
* @param {string[]} order 规范拓扑序
|
|
10
10
|
* @param {Map<string, object>} byId id → 事件
|
|
11
|
-
* @param {object}
|
|
12
|
-
* @param {number}
|
|
13
|
-
* @param {number}
|
|
14
|
-
* @param {Set<string>}
|
|
15
|
-
* @param {string | null} [
|
|
16
|
-
* @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] 共识分支尖
|
|
17
17
|
* @returns {Set<string>} 保留 id
|
|
18
18
|
*/
|
|
19
|
-
export function computeRetentionKeepIds(order, byId,
|
|
20
|
-
const { maxDepth, cutoffWall, anchorTypes, checkpointTipId, branchTipId } =
|
|
19
|
+
export function computeRetentionKeepIds(order, byId, options) {
|
|
20
|
+
const { maxDepth, cutoffWall, anchorTypes, checkpointTipId, branchTipId } = options
|
|
21
21
|
const branchOrder = authzFoldOrderIds(order, byId, branchTipId)
|
|
22
22
|
const branchSet = new Set(branchOrder)
|
|
23
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steve02081504/fount-p2p",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"network",
|
|
@@ -39,8 +39,9 @@
|
|
|
39
39
|
"./schemas/*": "./schemas/*.mjs",
|
|
40
40
|
"./node/*": "./node/*.mjs",
|
|
41
41
|
"./discovery": "./discovery/index.mjs",
|
|
42
|
+
"./discovery/bt": "./discovery/bt/index.mjs",
|
|
43
|
+
"./discovery/bt/*": "./discovery/bt/*.mjs",
|
|
42
44
|
"./discovery/*": "./discovery/*.mjs",
|
|
43
|
-
"./link/*": "./link/*.mjs",
|
|
44
45
|
"./transport/*": "./transport/*.mjs",
|
|
45
46
|
"./rooms/*": "./rooms/*.mjs",
|
|
46
47
|
"./overlay": "./overlay/index.mjs",
|
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,27 +42,27 @@ export function registerDeliveryInboundHandler(type, handler) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* @param {InboundContext}
|
|
45
|
+
* @param {InboundContext} inboundContext 入站上下文
|
|
46
46
|
* @param {object} message 已校验的线载荷(含 type)
|
|
47
47
|
* @returns {Promise<PartInvokeResponse | null>} 处理器返回值
|
|
48
48
|
*/
|
|
49
|
-
export async function dispatchRpcInbound(
|
|
49
|
+
export async function dispatchRpcInbound(inboundContext, message) {
|
|
50
50
|
const type = String(message?.type || '').trim()
|
|
51
51
|
if (!type) return null
|
|
52
52
|
const handler = rpcHandlers.get(type)
|
|
53
53
|
if (!handler) return null
|
|
54
|
-
return handler(
|
|
54
|
+
return handler(inboundContext, message)
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
* @param {InboundContext}
|
|
58
|
+
* @param {InboundContext} inboundContext 入站上下文
|
|
59
59
|
* @param {object} message 已校验的线载荷(含 type)
|
|
60
60
|
* @returns {Promise<void>}
|
|
61
61
|
*/
|
|
62
|
-
export async function dispatchDeliveryInbound(
|
|
62
|
+
export async function dispatchDeliveryInbound(inboundContext, message) {
|
|
63
63
|
const type = String(message?.type || '').trim()
|
|
64
64
|
if (!type) return
|
|
65
65
|
const handler = deliveryHandlers.get(type)
|
|
66
66
|
if (!handler) return
|
|
67
|
-
await handler(
|
|
67
|
+
await handler(inboundContext, message)
|
|
68
68
|
}
|