@steve02081504/fount-p2p 0.0.6 → 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.
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * LinkProvider level 约定:数值越大越优先(建链时降序尝试)。
3
+ * Discovery 的 priority 仍为升序,两套语义故意分名。
4
+ */
5
+ export const LINK_LEVEL_LAN_TCP = 80
6
+ /** WebRTC 链路 level。 */
7
+ export const LINK_LEVEL_WEBRTC = 70
8
+ /** BLE GATT 链路 level。 */
9
+ export const LINK_LEVEL_BLE_GATT = 40