@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.
Files changed (47) hide show
  1. package/core/tcp_port.mjs +11 -0
  2. package/dag/canonicalize_row.mjs +5 -5
  3. package/discovery/advert_peer_hints.mjs +21 -0
  4. package/discovery/{bt.mjs → bt/index.mjs} +151 -55
  5. package/discovery/bt/peer_hints.mjs +41 -0
  6. package/discovery/bt/runtime.mjs +46 -0
  7. package/discovery/lan_peer_hints.mjs +43 -0
  8. package/discovery/mdns.mjs +10 -6
  9. package/discovery/nostr.mjs +3 -3
  10. package/federation/chunk_fetch_pending.mjs +3 -3
  11. package/federation/chunk_fetch_scheduler.mjs +3 -3
  12. package/federation/dag_order_cache.mjs +3 -3
  13. package/federation/topo_order_memo.mjs +3 -3
  14. package/files/chunk_responder.mjs +1 -1
  15. package/files/evfs.mjs +26 -26
  16. package/files/transfer_key.mjs +9 -9
  17. package/files/transfer_key_registry.mjs +9 -9
  18. package/governance/join_pow.mjs +17 -17
  19. package/index.mjs +3 -2
  20. package/link/channel_mux.mjs +7 -7
  21. package/link/frame.mjs +5 -5
  22. package/link/handshake.mjs +63 -36
  23. package/link/pipe.mjs +404 -0
  24. package/link/providers/ble_gatt.mjs +339 -0
  25. package/link/providers/index.mjs +90 -0
  26. package/link/providers/lan_tcp.mjs +342 -0
  27. package/link/providers/levels.mjs +9 -0
  28. package/link/providers/webrtc.mjs +385 -0
  29. package/mailbox/deliver_or_store.mjs +13 -13
  30. package/mailbox/wire.mjs +4 -4
  31. package/node/reputation_store.mjs +5 -5
  32. package/node/retention_policy.mjs +8 -8
  33. package/overlay/index.mjs +6 -6
  34. package/package.json +3 -2
  35. package/registries/inbound.mjs +8 -8
  36. package/rooms/scoped_link.mjs +23 -21
  37. package/timeline/append_core.mjs +3 -3
  38. package/transport/group_link_set.mjs +35 -33
  39. package/transport/link_registry.mjs +250 -63
  40. package/transport/peer_pool.mjs +4 -4
  41. package/transport/user_room.mjs +12 -14
  42. package/utils/ttl_map.mjs +41 -0
  43. package/wire/group_part.mjs +3 -3
  44. package/wire/part_ingress.mjs +14 -14
  45. package/wire/part_query.mjs +81 -82
  46. package/wire/part_query_cache.mjs +7 -0
  47. package/link/link.mjs +0 -617
package/link/pipe.mjs ADDED
@@ -0,0 +1,404 @@
1
+ import { normalizeHex64 } from '../core/hexIds.mjs'
2
+ import { ms } from '../utils/duration.mjs'
3
+ import { createLruMap } from '../utils/lru.mjs'
4
+
5
+ import { createReassembler, encodeFrames, randomMsgIdHex } from './frame.mjs'
6
+ import { buildAuth, buildHello, parseHello, verifyAuth } from './handshake.mjs'
7
+
8
+ const encoder = new TextEncoder()
9
+ const decoder = new TextDecoder()
10
+
11
+ /**
12
+ * 依次调用监听器集合,忽略单个 listener 抛错。
13
+ * @param {Set<Function>} listeners 监听器集合
14
+ * @param {...unknown} args 传递给 listener 的参数
15
+ * @returns {void}
16
+ */
17
+ function emitListeners(listeners, ...args) {
18
+ for (const listener of listeners)
19
+ try { listener(...args) }
20
+ catch { /* ignore */ }
21
+ }
22
+
23
+ /**
24
+ * 原始字节:以 `{` 开头的 UTF-8 当 control 文本,否则当二进制帧。
25
+ * @param {Buffer | Uint8Array | string} raw 原始数据
26
+ * @returns {string | Uint8Array} control 文本或二进制帧
27
+ */
28
+ export function coercePipeInbound(raw) {
29
+ if (typeof raw === 'string') return raw
30
+ const bytes = raw instanceof Uint8Array ? raw : Uint8Array.from(raw)
31
+ try {
32
+ const text = decoder.decode(bytes)
33
+ if (text.startsWith('{')) return text
34
+ }
35
+ catch { /* binary */ }
36
+ return bytes
37
+ }
38
+
39
+ /**
40
+ * 把 createLinkPipe 句柄收成上层 LinkHandle(可附带测试/内部字段)。
41
+ * @param {ReturnType<typeof createLinkPipe>} pipe pipe
42
+ * @param {object} [extras] 附加字段(如 handleInbound、_channelForTest)
43
+ * @returns {object} LinkHandle 形状
44
+ */
45
+ export function asLinkHandle(pipe, extras = {}) {
46
+ return {
47
+ ready: pipe.ready,
48
+ /** @returns {string | null} 对端 nodeHash */
49
+ get nodeHash() { return pipe.nodeHash },
50
+ /** @returns {boolean} 是否发起方 */
51
+ get initiator() { return pipe.initiator },
52
+ /** @returns {string} 提供者 id */
53
+ get providerId() { return pipe.providerId },
54
+ /** @returns {number} 提供者 level */
55
+ get level() { return pipe.level },
56
+ /**
57
+ * @param {...unknown} args send 参数
58
+ * @returns {Promise<boolean>} 是否发送成功
59
+ */
60
+ send: (...args) => pipe.send(...args),
61
+ /**
62
+ * @param {...unknown} args onEnvelope 参数
63
+ * @returns {() => void} 取消订阅
64
+ */
65
+ onEnvelope: (...args) => pipe.onEnvelope(...args),
66
+ /**
67
+ * @param {...unknown} args onDown 参数
68
+ * @returns {() => void} 取消订阅
69
+ */
70
+ onDown: (...args) => pipe.onDown(...args),
71
+ /**
72
+ * @param {...unknown} args close 参数
73
+ * @returns {Promise<void>}
74
+ */
75
+ close: (...args) => pipe.close(...args),
76
+ /** @returns {object} 运行时统计 */
77
+ stats: () => pipe.stats(),
78
+ ...extras,
79
+ }
80
+ }
81
+
82
+ /**
83
+ * 在已打开的字节/控制双工上跑 hello/auth、分帧 envelope 与心跳。
84
+ * @param {object} options pipe 配置
85
+ * @param {string} options.providerId 提供者 id
86
+ * @param {number} options.level 提供者 level
87
+ * @param {boolean} options.initiator 是否发起方
88
+ * @param {string | null} [options.nodeHash] 期望对端 nodeHash
89
+ * @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array, nonce?: string } | null} [options.localIdentity] 本地身份
90
+ * @param {() => string | null} options.getLocalBinding 本地 binding(未就绪返回 null)
91
+ * @param {() => string | null} options.getRemoteBinding 远端 binding(未就绪返回 null)
92
+ * @param {(text: string) => void | Promise<void>} options.sendControlText 发送 control JSON 文本
93
+ * @param {(action: string, frame: Uint8Array) => void | Promise<void>} options.sendFrame 发送分帧二进制
94
+ * @param {() => void | Promise<void>} [options.closeTransport] 关闭底层传输
95
+ * @param {() => object} [options.extraStats] 附加 stats 字段
96
+ * @param {number} [options.heartbeatMs] 心跳间隔
97
+ * @param {number} [options.idleTimeoutMs] 空闲超时
98
+ * @param {number} [options.handshakeTimeoutMs] 握手超时
99
+ * @returns {object} link 句柄 + 入站 API
100
+ */
101
+ export function createLinkPipe(options) {
102
+ const providerId = String(options.providerId || '')
103
+ const level = Number(options.level) || 0
104
+ const heartbeatMs = Number(options.heartbeatMs) || ms('15s')
105
+ const idleTimeoutMs = Number(options.idleTimeoutMs) || ms('45s')
106
+ const handshakeTimeoutMs = Number(options.handshakeTimeoutMs) || ms('10s')
107
+ const targetNodeHash = normalizeHex64(options.nodeHash || '')
108
+ let closed = false
109
+ let ready = false
110
+ let closeReason = 'closed'
111
+ let remoteNodeHash = targetNodeHash || null
112
+ let remoteHello = null
113
+ let localHello = null
114
+ let handshakeTimer = null
115
+ let idleTimer = null
116
+ let heartbeatTimer = null
117
+ let helloSent = false
118
+ let authSent = false
119
+ let remoteAuthVerified = false
120
+ /** @type {object | null} */
121
+ let pendingAuth = null
122
+ let lastInboundAt = Date.now()
123
+ let lastOutboundAt = 0
124
+ let sentFrames = 0
125
+ let recvFrames = 0
126
+ const envelopeListeners = new Set()
127
+ const downListeners = new Set()
128
+ const completedMsgIds = createLruMap(4096)
129
+ const reassembler = createReassembler()
130
+ /** @type {(value: void | PromiseLike<void>) => void} */
131
+ let resolveReady
132
+ /** @type {(reason?: unknown) => void} */
133
+ let rejectReady
134
+ const readyPromise = new Promise((resolve, reject) => {
135
+ resolveReady = resolve
136
+ rejectReady = reject
137
+ })
138
+ void readyPromise.catch(() => { })
139
+
140
+ /**
141
+ * 经 control 发送 hello/auth JSON。
142
+ * @param {object} body hello 或 auth 字段
143
+ * @returns {Promise<void>}
144
+ */
145
+ async function sendRawControl(body) {
146
+ const text = JSON.stringify({ type: body.sig ? 'auth' : 'hello', ...body })
147
+ await Promise.resolve(options.sendControlText(text))
148
+ lastOutboundAt = Date.now()
149
+ }
150
+
151
+ /**
152
+ * binding 就绪后向对端发送 auth。
153
+ * @returns {Promise<void>}
154
+ */
155
+ async function maybeSendAuth() {
156
+ if (authSent || !remoteHello) return
157
+ const binding = options.getLocalBinding()
158
+ if (!binding) return
159
+ authSent = true
160
+ await sendRawControl(await buildAuth(remoteHello.nonce, binding, options.localIdentity ?? {}))
161
+ }
162
+
163
+ /**
164
+ * 握手完成后启动心跳并 resolve ready。
165
+ * @returns {Promise<void>}
166
+ */
167
+ async function maybeFinishHandshake() {
168
+ if (ready || !remoteHello || !remoteAuthVerified) return
169
+ ready = true
170
+ clearTimeout(handshakeTimer)
171
+ heartbeatTimer = setInterval(() => {
172
+ void send({ scope: 'link', action: 'ping', payload: {} }).catch(() => { })
173
+ }, heartbeatMs)
174
+ idleTimer = setInterval(() => {
175
+ if (Date.now() - lastInboundAt > idleTimeoutMs)
176
+ void close('idle-timeout')
177
+ }, Math.max(1000, Math.floor(heartbeatMs / 3)))
178
+ resolveReady()
179
+ }
180
+
181
+ /**
182
+ * 校验远端 auth。
183
+ * @param {{ sig: string }} auth auth 载荷
184
+ * @returns {Promise<void>}
185
+ */
186
+ async function handleAuth(auth) {
187
+ if (!remoteHello) {
188
+ pendingAuth = auth
189
+ return
190
+ }
191
+ const binding = options.getRemoteBinding()
192
+ const verifiedNodeHash = await verifyAuth(remoteHello, auth, localHello?.nonce, binding)
193
+ if (!verifiedNodeHash) {
194
+ await close(`auth-failed:binding=${binding || 'missing'} localNonce=${localHello?.nonce || 'missing'}`)
195
+ return
196
+ }
197
+ if (targetNodeHash && verifiedNodeHash !== targetNodeHash) {
198
+ await close('nodehash-mismatch')
199
+ return
200
+ }
201
+ remoteNodeHash = verifiedNodeHash
202
+ remoteAuthVerified = true
203
+ await maybeFinishHandshake()
204
+ }
205
+
206
+ /**
207
+ * 处理 control JSON(hello/auth)。
208
+ * @param {unknown} message 解析后的对象
209
+ * @returns {Promise<void>}
210
+ */
211
+ async function handleControlMessage(message) {
212
+ if (!message || typeof message !== 'object') return
213
+ if (message.type === 'hello') {
214
+ const parsed = parseHello(message)
215
+ if (!parsed) {
216
+ await close('hello-invalid')
217
+ return
218
+ }
219
+ remoteHello = parsed
220
+ await maybeSendAuth()
221
+ if (pendingAuth) {
222
+ const bufferedAuth = pendingAuth
223
+ pendingAuth = null
224
+ await handleAuth(bufferedAuth)
225
+ }
226
+ return
227
+ }
228
+ if (message.type === 'auth')
229
+ await handleAuth(message)
230
+ }
231
+
232
+ /**
233
+ * 处理入站二进制帧。
234
+ * @param {Uint8Array} bytes 帧字节
235
+ * @returns {void}
236
+ */
237
+ function handleBinaryFrame(bytes) {
238
+ try {
239
+ recvFrames++
240
+ const merged = reassembler.push(bytes)
241
+ if (!merged) return
242
+ const envelope = JSON.parse(decoder.decode(merged))
243
+ const msgId = envelope?.msgId ? String(envelope.msgId) : null
244
+ if (msgId && completedMsgIds.has(msgId)) return
245
+ if (msgId) completedMsgIds.touch(msgId, true)
246
+ if (envelope?.scope === 'link') {
247
+ if (envelope.action === 'ping') {
248
+ void send({ scope: 'link', action: 'pong', payload: {} }).catch(() => { })
249
+ return
250
+ }
251
+ if (envelope.action === 'pong') return
252
+ }
253
+ if (ready && remoteNodeHash)
254
+ emitListeners(envelopeListeners, envelope, remoteNodeHash)
255
+ }
256
+ catch {
257
+ /* drop malformed network ingress */
258
+ }
259
+ }
260
+
261
+ /**
262
+ * 统一入站:JSON control 或二进制帧。
263
+ * @param {unknown} data 原始数据
264
+ * @returns {void}
265
+ */
266
+ function handleInbound(data) {
267
+ lastInboundAt = Date.now()
268
+ if (typeof data === 'string') {
269
+ if (data.startsWith('{'))
270
+ try {
271
+ void handleControlMessage(JSON.parse(data))
272
+ return
273
+ }
274
+ catch { /* fall through */ }
275
+ try {
276
+ handleBinaryFrame(encoder.encode(data))
277
+ }
278
+ catch { /* ignore */ }
279
+ return
280
+ }
281
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || data instanceof Uint8Array) {
282
+ const bytes = data instanceof Uint8Array
283
+ ? data
284
+ : data instanceof ArrayBuffer
285
+ ? new Uint8Array(data)
286
+ : new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
287
+ // 尝试 UTF-8 JSON control
288
+ try {
289
+ const text = decoder.decode(bytes)
290
+ if (text.startsWith('{')) {
291
+ void handleControlMessage(JSON.parse(text))
292
+ return
293
+ }
294
+ }
295
+ catch { /* binary path */ }
296
+ handleBinaryFrame(bytes)
297
+ }
298
+ }
299
+
300
+ /**
301
+ * 传输就绪后启动握手(发 hello)。
302
+ * @returns {Promise<void>}
303
+ */
304
+ async function startHandshake() {
305
+ if (helloSent || closed) return
306
+ handshakeTimer = setTimeout(() => { void close('handshake-timeout') }, handshakeTimeoutMs)
307
+ helloSent = true
308
+ localHello = buildHello(options.localIdentity ?? {})
309
+ await sendRawControl(localHello)
310
+ await maybeSendAuth()
311
+ }
312
+
313
+ /**
314
+ * 发送业务 envelope。
315
+ * @param {{ scope: string, action: string, payload: unknown }} envelope 信封
316
+ * @returns {Promise<boolean>} 是否发送成功
317
+ */
318
+ async function send(envelope) {
319
+ await readyPromise
320
+ if (closed) return false
321
+ const message = {
322
+ scope: String(envelope?.scope || ''),
323
+ action: String(envelope?.action || ''),
324
+ payload: envelope?.payload ?? null,
325
+ msgId: randomMsgIdHex(),
326
+ }
327
+ const bytes = encoder.encode(JSON.stringify(message))
328
+ for (const frame of encodeFrames(message.msgId, bytes)) {
329
+ await Promise.resolve(options.sendFrame(message.action, frame))
330
+ sentFrames++
331
+ }
332
+ lastOutboundAt = Date.now()
333
+ return true
334
+ }
335
+
336
+ /**
337
+ * 关闭 pipe 与底层传输。
338
+ * @param {string} [reason='closed'] 关闭原因
339
+ * @returns {Promise<void>}
340
+ */
341
+ async function close(reason = 'closed') {
342
+ if (closed) return
343
+ closed = true
344
+ closeReason = reason
345
+ clearTimeout(handshakeTimer)
346
+ if (heartbeatTimer) clearInterval(heartbeatTimer)
347
+ if (idleTimer) clearInterval(idleTimer)
348
+ try { await Promise.resolve(options.closeTransport?.()) } catch { /* ignore */ }
349
+ if (!ready) rejectReady(new Error(`p2p: link closed before ready (${reason})`))
350
+ emitListeners(downListeners, reason)
351
+ }
352
+
353
+ return {
354
+ ready: readyPromise,
355
+ /** @returns {string | null} 对端 nodeHash(握手后) */
356
+ get nodeHash() { return remoteNodeHash },
357
+ /** @returns {boolean} 是否发起方 */
358
+ get initiator() { return !!options.initiator },
359
+ /** @returns {string} 提供者 id */
360
+ get providerId() { return providerId },
361
+ /** @returns {number} 提供者 level */
362
+ get level() { return level },
363
+ send,
364
+ /**
365
+ * @param {(envelope: object, remoteNodeHash: string) => void} callback 回调
366
+ * @returns {() => void} 取消订阅
367
+ */
368
+ onEnvelope(callback) {
369
+ envelopeListeners.add(callback)
370
+ return () => envelopeListeners.delete(callback)
371
+ },
372
+ /**
373
+ * @param {(reason: string) => void} callback 回调
374
+ * @returns {() => void} 取消订阅
375
+ */
376
+ onDown(callback) {
377
+ downListeners.add(callback)
378
+ return () => downListeners.delete(callback)
379
+ },
380
+ close,
381
+ handleInbound,
382
+ startHandshake,
383
+ maybeSendAuth,
384
+ /**
385
+ * @returns {object} 运行时统计
386
+ */
387
+ stats() {
388
+ return {
389
+ ready,
390
+ providerId,
391
+ level,
392
+ nodeHash: remoteNodeHash,
393
+ targetNodeHash: targetNodeHash || null,
394
+ initiator: !!options.initiator,
395
+ lastInboundAt,
396
+ lastOutboundAt,
397
+ sentFrames,
398
+ recvFrames,
399
+ closeReason,
400
+ ...options.extraStats?.() ?? {},
401
+ }
402
+ },
403
+ }
404
+ }