@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,342 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { randomBytes } from 'node:crypto'
|
|
3
|
+
import net from 'node:net'
|
|
4
|
+
|
|
5
|
+
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
6
|
+
import { getLanPeerHint } from '../../discovery/lan_peer_hints.mjs'
|
|
7
|
+
import { asLinkHandle, coercePipeInbound, createLinkPipe } from '../pipe.mjs'
|
|
8
|
+
|
|
9
|
+
import { LINK_LEVEL_LAN_TCP } from './levels.mjs'
|
|
10
|
+
|
|
11
|
+
const MAX_FRAME_BYTES = 1 << 20
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 在 socket 上挂 length-prefix 编解码(u32be + payload)。
|
|
15
|
+
* @param {import('node:net').Socket} socket TCP socket
|
|
16
|
+
* @param {(payload: Buffer) => void} onPayload 完整帧回调
|
|
17
|
+
* @returns {{ write: (payload: Buffer | Uint8Array | string) => void, destroy: () => void }} 编解码句柄
|
|
18
|
+
*/
|
|
19
|
+
function attachLengthPrefix(socket, onPayload) {
|
|
20
|
+
/** @type {Buffer[]} */
|
|
21
|
+
const chunks = []
|
|
22
|
+
let buffered = 0
|
|
23
|
+
/**
|
|
24
|
+
* @param {number} n 需要的字节数
|
|
25
|
+
* @returns {Buffer | null} 凑齐则返回,否则 null
|
|
26
|
+
*/
|
|
27
|
+
function take(n) {
|
|
28
|
+
if (buffered < n) return null
|
|
29
|
+
const out = Buffer.allocUnsafe(n)
|
|
30
|
+
let offset = 0
|
|
31
|
+
while (offset < n) {
|
|
32
|
+
const head = chunks[0]
|
|
33
|
+
const copy = Math.min(head.length, n - offset)
|
|
34
|
+
head.copy(out, offset, 0, copy)
|
|
35
|
+
offset += copy
|
|
36
|
+
buffered -= copy
|
|
37
|
+
if (copy === head.length) chunks.shift()
|
|
38
|
+
else chunks[0] = head.subarray(copy)
|
|
39
|
+
}
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* @param {Buffer} chunk 入站数据
|
|
44
|
+
* @returns {void}
|
|
45
|
+
*/
|
|
46
|
+
function onData(chunk) {
|
|
47
|
+
chunks.push(chunk)
|
|
48
|
+
buffered += chunk.length
|
|
49
|
+
while (buffered >= 4) {
|
|
50
|
+
const header = take(4)
|
|
51
|
+
const len = header.readUInt32BE(0)
|
|
52
|
+
if (len > MAX_FRAME_BYTES) {
|
|
53
|
+
socket.destroy(new Error('p2p: lan_tcp frame too large'))
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
if (buffered < len) {
|
|
57
|
+
chunks.unshift(header)
|
|
58
|
+
buffered += 4
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
onPayload(take(len))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
socket.on('data', onData)
|
|
65
|
+
return {
|
|
66
|
+
/**
|
|
67
|
+
* @param {Buffer | Uint8Array | string} payload 出站载荷
|
|
68
|
+
* @returns {void}
|
|
69
|
+
*/
|
|
70
|
+
write(payload) {
|
|
71
|
+
const body = Buffer.isBuffer(payload) ? payload : Buffer.from(payload)
|
|
72
|
+
const header = Buffer.allocUnsafe(4)
|
|
73
|
+
header.writeUInt32BE(body.length)
|
|
74
|
+
socket.write(Buffer.concat([header, body]))
|
|
75
|
+
},
|
|
76
|
+
/**
|
|
77
|
+
* @returns {void}
|
|
78
|
+
*/
|
|
79
|
+
destroy() {
|
|
80
|
+
socket.off('data', onData)
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 在已挂 codec 的 socket 上创建 pipe。
|
|
87
|
+
* @param {object} options 配置
|
|
88
|
+
* @returns {ReturnType<typeof createLinkPipe>} pipe 句柄
|
|
89
|
+
*/
|
|
90
|
+
function createTcpPipe(options) {
|
|
91
|
+
const linkId = normalizeHex64(options.linkId)
|
|
92
|
+
if (!linkId) throw new Error('p2p: lan_tcp linkId required')
|
|
93
|
+
const { socket, codec } = options
|
|
94
|
+
const pipe = createLinkPipe({
|
|
95
|
+
providerId: 'lan_tcp',
|
|
96
|
+
level: LINK_LEVEL_LAN_TCP,
|
|
97
|
+
initiator: !!options.initiator,
|
|
98
|
+
nodeHash: options.nodeHash,
|
|
99
|
+
localIdentity: options.localIdentity,
|
|
100
|
+
/** @returns {string} 本端 binding(linkId) */
|
|
101
|
+
getLocalBinding: () => linkId,
|
|
102
|
+
/** @returns {string} 对端 binding(linkId) */
|
|
103
|
+
getRemoteBinding: () => linkId,
|
|
104
|
+
/**
|
|
105
|
+
* @param {string} text control JSON
|
|
106
|
+
* @returns {void}
|
|
107
|
+
*/
|
|
108
|
+
sendControlText(text) {
|
|
109
|
+
codec.write(text)
|
|
110
|
+
},
|
|
111
|
+
/**
|
|
112
|
+
* @param {string} _action action
|
|
113
|
+
* @param {Uint8Array} frame 帧
|
|
114
|
+
* @returns {void}
|
|
115
|
+
*/
|
|
116
|
+
sendFrame(_action, frame) {
|
|
117
|
+
codec.write(frame)
|
|
118
|
+
},
|
|
119
|
+
/**
|
|
120
|
+
* @returns {void}
|
|
121
|
+
*/
|
|
122
|
+
closeTransport() {
|
|
123
|
+
codec.destroy()
|
|
124
|
+
socket.destroy()
|
|
125
|
+
},
|
|
126
|
+
/**
|
|
127
|
+
* @returns {object} 附加 stats
|
|
128
|
+
*/
|
|
129
|
+
extraStats() {
|
|
130
|
+
return { host: options.host || null, port: options.port || null }
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
socket.on('close', () => { void pipe.close('socket-close') })
|
|
134
|
+
socket.on('error', () => { void pipe.close('socket-error') })
|
|
135
|
+
return pipe
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 在已连接 socket 上建立 pipe(入站帧经 pending 缓冲,避免 link-open/hello 竞态丢包)。
|
|
140
|
+
* @param {object} options 配置
|
|
141
|
+
* @returns {Promise<import('./index.mjs').LinkHandle>} 已启动握手的 link
|
|
142
|
+
*/
|
|
143
|
+
async function openTcpPipe(options) {
|
|
144
|
+
const socket = options.socket
|
|
145
|
+
/** @type {ReturnType<typeof createLinkPipe> | null} */
|
|
146
|
+
let pipe = null
|
|
147
|
+
/** @type {Buffer[]} */
|
|
148
|
+
const pending = []
|
|
149
|
+
const codec = attachLengthPrefix(socket, payload => {
|
|
150
|
+
if (!pipe) {
|
|
151
|
+
pending.push(payload)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
pipe.handleInbound(coercePipeInbound(payload))
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
pipe = createTcpPipe({ ...options, codec, socket })
|
|
158
|
+
for (const payload of pending.splice(0))
|
|
159
|
+
pipe.handleInbound(coercePipeInbound(payload))
|
|
160
|
+
|
|
161
|
+
if (options.initiator)
|
|
162
|
+
codec.write(JSON.stringify({
|
|
163
|
+
type: 'link-open',
|
|
164
|
+
linkId: normalizeHex64(options.linkId),
|
|
165
|
+
from: options.localIdentity?.nodeHash || '',
|
|
166
|
+
}))
|
|
167
|
+
|
|
168
|
+
await pipe.startHandshake()
|
|
169
|
+
return asLinkHandle(pipe)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 拨号到已有 LAN hint。
|
|
174
|
+
* @param {object} options dial 选项
|
|
175
|
+
* @returns {Promise<import('./index.mjs').LinkHandle>} 已就绪的 link
|
|
176
|
+
*/
|
|
177
|
+
async function dialLanTcp(options) {
|
|
178
|
+
const remoteNodeHash = normalizeHex64(options.nodeHash)
|
|
179
|
+
const hint = getLanPeerHint(remoteNodeHash)
|
|
180
|
+
if (!hint) throw new Error('p2p: lan_tcp no peer hint')
|
|
181
|
+
|
|
182
|
+
const socket = await new Promise((resolve, reject) => {
|
|
183
|
+
const conn = net.createConnection({ host: hint.host, port: hint.port })
|
|
184
|
+
/**
|
|
185
|
+
* @param {Error} error 连接错误
|
|
186
|
+
* @returns {void}
|
|
187
|
+
*/
|
|
188
|
+
const onError = error => {
|
|
189
|
+
conn.off('connect', onConnect)
|
|
190
|
+
reject(error)
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* @returns {void}
|
|
194
|
+
*/
|
|
195
|
+
function onConnect() {
|
|
196
|
+
conn.off('error', onError)
|
|
197
|
+
resolve(conn)
|
|
198
|
+
}
|
|
199
|
+
conn.once('error', onError)
|
|
200
|
+
conn.once('connect', onConnect)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
const link = await openTcpPipe({
|
|
204
|
+
initiator: true,
|
|
205
|
+
linkId: randomBytes(32).toString('hex'),
|
|
206
|
+
nodeHash: remoteNodeHash,
|
|
207
|
+
localIdentity: options.localIdentity,
|
|
208
|
+
socket,
|
|
209
|
+
host: hint.host,
|
|
210
|
+
port: hint.port,
|
|
211
|
+
})
|
|
212
|
+
await link.ready
|
|
213
|
+
return link
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 创建 lan_tcp LinkProvider。
|
|
218
|
+
* 每个实例独立 listen socket;注册 id 唯一,避免同进程多 registry 互相覆盖。
|
|
219
|
+
* 链路上的 `providerId` 仍为 `lan_tcp`(见 createTcpPipe)。
|
|
220
|
+
* @returns {import('./index.mjs').LinkProvider & { ensureListening?: Function, localEndpoint?: Function }} LAN TCP provider
|
|
221
|
+
*/
|
|
222
|
+
export function createLanTcpLinkProvider() {
|
|
223
|
+
const instanceId = `lan_tcp:${randomBytes(4).toString('hex')}`
|
|
224
|
+
/** @type {((link: import('./index.mjs').LinkHandle) => void) | null} */
|
|
225
|
+
let onInbound = null
|
|
226
|
+
/** @type {object | null} */
|
|
227
|
+
let localIdentity = null
|
|
228
|
+
/** @type {import('node:net').Server | null} */
|
|
229
|
+
let server = null
|
|
230
|
+
let listenPort = 0
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @param {import('node:net').Socket} socket 入站连接
|
|
234
|
+
* @returns {void}
|
|
235
|
+
*/
|
|
236
|
+
function acceptConnection(socket) {
|
|
237
|
+
if (!onInbound || !localIdentity) {
|
|
238
|
+
socket.destroy()
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
/** @type {ReturnType<typeof createLinkPipe> | null} */
|
|
242
|
+
let pipe = null
|
|
243
|
+
const codec = attachLengthPrefix(socket, payload => {
|
|
244
|
+
if (pipe) {
|
|
245
|
+
pipe.handleInbound(coercePipeInbound(payload))
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
let parsed
|
|
249
|
+
try {
|
|
250
|
+
parsed = JSON.parse(payload.toString('utf8'))
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
socket.destroy()
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
if (parsed?.type !== 'link-open' || !parsed.linkId) {
|
|
257
|
+
socket.destroy()
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
pipe = createTcpPipe({
|
|
262
|
+
initiator: false,
|
|
263
|
+
linkId: parsed.linkId,
|
|
264
|
+
nodeHash: normalizeHex64(parsed.from) || null,
|
|
265
|
+
localIdentity,
|
|
266
|
+
socket,
|
|
267
|
+
codec,
|
|
268
|
+
port: listenPort,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
socket.destroy()
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
onInbound?.(asLinkHandle(pipe))
|
|
276
|
+
void pipe.startHandshake().catch(() => {
|
|
277
|
+
socket.destroy()
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
socket.on('error', () => { /* ignore */ })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
id: instanceId,
|
|
285
|
+
level: LINK_LEVEL_LAN_TCP,
|
|
286
|
+
caps: { needsOfferAnswer: false, needsDiscoverySignal: false },
|
|
287
|
+
/**
|
|
288
|
+
* @returns {boolean} 始终可用
|
|
289
|
+
*/
|
|
290
|
+
isAvailable() {
|
|
291
|
+
return true
|
|
292
|
+
},
|
|
293
|
+
/**
|
|
294
|
+
* @param {{ nodeHash: string }} remote 远端
|
|
295
|
+
* @returns {boolean} 是否有 LAN peer hint
|
|
296
|
+
*/
|
|
297
|
+
canReach(remote) {
|
|
298
|
+
return !!getLanPeerHint(remote.nodeHash)
|
|
299
|
+
},
|
|
300
|
+
/**
|
|
301
|
+
* @returns {{ port: number } | null} 本机 listen 端点
|
|
302
|
+
*/
|
|
303
|
+
localEndpoint() {
|
|
304
|
+
return listenPort > 0 ? { port: listenPort } : null
|
|
305
|
+
},
|
|
306
|
+
/**
|
|
307
|
+
* @param {object} options dial 选项
|
|
308
|
+
* @returns {Promise<import('./index.mjs').LinkHandle>} 已就绪的 link
|
|
309
|
+
*/
|
|
310
|
+
async dial(options) {
|
|
311
|
+
return dialLanTcp(options)
|
|
312
|
+
},
|
|
313
|
+
/**
|
|
314
|
+
* @param {{ onInbound: (link: import('./index.mjs').LinkHandle) => void, localIdentity: object }} handlers 回调
|
|
315
|
+
* @returns {Promise<() => void>} 停止 listening
|
|
316
|
+
*/
|
|
317
|
+
async ensureListening(handlers) {
|
|
318
|
+
onInbound = handlers.onInbound
|
|
319
|
+
localIdentity = handlers.localIdentity
|
|
320
|
+
if (!server) {
|
|
321
|
+
server = net.createServer(acceptConnection)
|
|
322
|
+
await new Promise((resolve, reject) => {
|
|
323
|
+
server.once('error', reject)
|
|
324
|
+
server.listen(0, '0.0.0.0', () => {
|
|
325
|
+
server.off('error', reject)
|
|
326
|
+
const addr = server.address()
|
|
327
|
+
listenPort = typeof addr === 'object' && addr ? addr.port : 0
|
|
328
|
+
resolve()
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
return () => {
|
|
333
|
+
onInbound = null
|
|
334
|
+
if (server) {
|
|
335
|
+
server.close()
|
|
336
|
+
server = null
|
|
337
|
+
listenPort = 0
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
}
|
|
342
|
+
}
|