@steve02081504/fount-p2p 0.0.14 → 0.0.16
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/README.md +14 -4
- package/crypto/channel.mjs +7 -7
- package/deno.json +4 -1
- package/discovery/adverts.mjs +1 -2
- package/discovery/bt/index.mjs +4 -1
- package/discovery/lan.mjs +12 -4
- package/discovery/nostr.mjs +307 -133
- package/discovery/peer_clue.mjs +23 -0
- package/link/handshake.mjs +3 -3
- package/link/providers/index.mjs +1 -0
- package/link/providers/levels.mjs +2 -0
- package/link/providers/nostr.mjs +280 -0
- package/link/providers/webrtc.mjs +13 -1
- package/link/rtc.mjs +14 -5
- package/package.json +1 -1
- package/transport/link_registry.mjs +37 -1
- package/transport/offer_answer.mjs +18 -4
- package/transport/runtime_bootstrap.mjs +7 -1
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { randomBytes } from 'node:crypto'
|
|
3
|
+
|
|
4
|
+
import { base64ToBytes, bytesToBase64 } from '../../core/bytes_codec.mjs'
|
|
5
|
+
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
6
|
+
import { getDiscoveryProvider, sendNodeSignalPacket } from '../../discovery/index.mjs'
|
|
7
|
+
import { mergeSignalingRelayUrls } from '../../discovery/nostr.mjs'
|
|
8
|
+
import { getNodeTransportSettings } from '../../node/identity.mjs'
|
|
9
|
+
import { getSignalingRuntimeConfig } from '../../node/instance.mjs'
|
|
10
|
+
import { ms } from '../../utils/duration.mjs'
|
|
11
|
+
import { createLruMap } from '../../utils/lru.mjs'
|
|
12
|
+
import { asLinkHandle } from '../pipe.mjs'
|
|
13
|
+
|
|
14
|
+
import { LINK_LEVEL_NOSTR } from './levels.mjs'
|
|
15
|
+
import { createLinkIdBoundPipe } from './link_id_pipe.mjs'
|
|
16
|
+
|
|
17
|
+
/** 单包 payload(UTF-8 / base64)上限,避免撞 relay content 限制。 */
|
|
18
|
+
const MAX_LINK_PAYLOAD_CHARS = 12 * 1024
|
|
19
|
+
/** open 到达前为同一 linkId 暂存的 c/b 包上限。 */
|
|
20
|
+
const PENDING_PACKETS_MAX = 32
|
|
21
|
+
/** Nostr 链握手超时(relay RTT 更慢)。 */
|
|
22
|
+
const NOSTR_HANDSHAKE_TIMEOUT_MS = ms('30s')
|
|
23
|
+
/** Nostr 链心跳间隔。 */
|
|
24
|
+
const NOSTR_HEARTBEAT_MS = ms('60s')
|
|
25
|
+
/** Nostr 链空闲超时。 */
|
|
26
|
+
const NOSTR_IDLE_TIMEOUT_MS = ms('3m')
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @returns {string[]} 当前可用中继 URL
|
|
30
|
+
*/
|
|
31
|
+
function resolveDefaultRelayUrls() {
|
|
32
|
+
return getSignalingRuntimeConfig().relayOverride
|
|
33
|
+
?? mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} remoteNodeHash 对端
|
|
38
|
+
* @param {object} packet link 包
|
|
39
|
+
* @returns {Promise<void>}
|
|
40
|
+
*/
|
|
41
|
+
async function publishLinkPacket(remoteNodeHash, packet) {
|
|
42
|
+
await sendNodeSignalPacket(remoteNodeHash, packet)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 创建 Nostr 末位数据链路 provider(level = -∞)。
|
|
47
|
+
* @param {{ getRelayUrls?: () => string[] }} [options] 中继解析(测试可注入)
|
|
48
|
+
* @returns {import('./index.mjs').LinkProvider & { deliverPacket: (packet: object) => void }} provider
|
|
49
|
+
*/
|
|
50
|
+
export function createNostrLinkProvider(options = {}) {
|
|
51
|
+
const resolveRelayUrls = typeof options.getRelayUrls === 'function'
|
|
52
|
+
? options.getRelayUrls
|
|
53
|
+
: resolveDefaultRelayUrls
|
|
54
|
+
|
|
55
|
+
/** @type {((link: import('./index.mjs').LinkHandle) => void) | null} */
|
|
56
|
+
let onInbound = null
|
|
57
|
+
/** @type {object | null} */
|
|
58
|
+
let localIdentity = null
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {{
|
|
61
|
+
* pipe: ReturnType<typeof createLinkIdBoundPipe>,
|
|
62
|
+
* remoteNodeHash: string,
|
|
63
|
+
* initiator: boolean,
|
|
64
|
+
* }} NostrLinkSession
|
|
65
|
+
*/
|
|
66
|
+
/** @type {Map<string, NostrLinkSession>} */
|
|
67
|
+
const sessions = new Map()
|
|
68
|
+
/** @type {Map<string, object[]> & { touch: (key: string, value: object[]) => void }} */
|
|
69
|
+
const pendingByLinkId = createLruMap(64)
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @returns {boolean} discovery nostr 已注册且有中继
|
|
73
|
+
*/
|
|
74
|
+
function isAvailable() {
|
|
75
|
+
if (!getDiscoveryProvider('nostr')) return false
|
|
76
|
+
return resolveRelayUrls().length > 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {NostrLinkSession} session 会话
|
|
81
|
+
* @param {object} packet link 包
|
|
82
|
+
* @returns {void}
|
|
83
|
+
*/
|
|
84
|
+
function applySessionPacket(session, packet) {
|
|
85
|
+
const op = String(packet.op || '')
|
|
86
|
+
if (op === 'close') {
|
|
87
|
+
void session.pipe.close('remote-close')
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
if (op === 'c') {
|
|
91
|
+
if (typeof packet.payload !== 'string') return
|
|
92
|
+
session.pipe.handleInbound(packet.payload)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
if (op === 'b') {
|
|
96
|
+
if (typeof packet.payload !== 'string') return
|
|
97
|
+
try {
|
|
98
|
+
session.pipe.handleInbound(Buffer.from(base64ToBytes(packet.payload)))
|
|
99
|
+
}
|
|
100
|
+
catch { /* drop malformed */ }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {string} linkId 链路 id
|
|
106
|
+
* @param {object} packet 暂存包
|
|
107
|
+
* @returns {void}
|
|
108
|
+
*/
|
|
109
|
+
function bufferPending(linkId, packet) {
|
|
110
|
+
let list = pendingByLinkId.get(linkId)
|
|
111
|
+
if (!list) {
|
|
112
|
+
list = []
|
|
113
|
+
pendingByLinkId.touch(linkId, list)
|
|
114
|
+
}
|
|
115
|
+
if (list.length >= PENDING_PACKETS_MAX) list.shift()
|
|
116
|
+
list.push(packet)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} remoteNodeHash 对端
|
|
121
|
+
* @param {string} linkId 链路 id
|
|
122
|
+
* @param {string} op open|c|b|close
|
|
123
|
+
* @param {string} [payload] 可选载荷
|
|
124
|
+
* @returns {Promise<void>}
|
|
125
|
+
*/
|
|
126
|
+
async function sendOp(remoteNodeHash, linkId, op, payload) {
|
|
127
|
+
if (payload != null && payload.length > MAX_LINK_PAYLOAD_CHARS)
|
|
128
|
+
throw new Error('p2p: nostr link payload too large')
|
|
129
|
+
const packet = {
|
|
130
|
+
type: 'link',
|
|
131
|
+
op,
|
|
132
|
+
from: localIdentity?.nodeHash || '',
|
|
133
|
+
linkId,
|
|
134
|
+
}
|
|
135
|
+
if (payload != null) packet.payload = payload
|
|
136
|
+
await publishLinkPacket(remoteNodeHash, packet)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {object} opts 会话选项
|
|
141
|
+
* @returns {ReturnType<typeof createLinkIdBoundPipe>} pipe
|
|
142
|
+
*/
|
|
143
|
+
function openPipe(opts) {
|
|
144
|
+
const { linkId, remoteNodeHash, initiator } = opts
|
|
145
|
+
const pipe = createLinkIdBoundPipe({
|
|
146
|
+
providerId: 'nostr',
|
|
147
|
+
level: LINK_LEVEL_NOSTR,
|
|
148
|
+
initiator: !!initiator,
|
|
149
|
+
linkId,
|
|
150
|
+
nodeHash: remoteNodeHash,
|
|
151
|
+
localIdentity: opts.localIdentity || localIdentity,
|
|
152
|
+
handshakeTimeoutMs: NOSTR_HANDSHAKE_TIMEOUT_MS,
|
|
153
|
+
heartbeatMs: NOSTR_HEARTBEAT_MS,
|
|
154
|
+
idleTimeoutMs: NOSTR_IDLE_TIMEOUT_MS,
|
|
155
|
+
/**
|
|
156
|
+
* @param {string} text control JSON
|
|
157
|
+
* @returns {Promise<void>}
|
|
158
|
+
*/
|
|
159
|
+
async sendControlText(text) {
|
|
160
|
+
await sendOp(remoteNodeHash, linkId, 'c', text)
|
|
161
|
+
},
|
|
162
|
+
/**
|
|
163
|
+
* @param {string} _action action
|
|
164
|
+
* @param {Uint8Array} frame 帧
|
|
165
|
+
* @returns {Promise<void>}
|
|
166
|
+
*/
|
|
167
|
+
async sendFrame(_action, frame) {
|
|
168
|
+
await sendOp(remoteNodeHash, linkId, 'b', bytesToBase64(frame))
|
|
169
|
+
},
|
|
170
|
+
/**
|
|
171
|
+
* @returns {Promise<void>}
|
|
172
|
+
*/
|
|
173
|
+
async closeTransport() {
|
|
174
|
+
sessions.delete(linkId)
|
|
175
|
+
try {
|
|
176
|
+
await sendOp(remoteNodeHash, linkId, 'close')
|
|
177
|
+
}
|
|
178
|
+
catch { /* ignore */ }
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
sessions.set(linkId, { pipe, remoteNodeHash, initiator: !!initiator })
|
|
182
|
+
pipe.onDown(() => { sessions.delete(linkId) })
|
|
183
|
+
return pipe
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 入站已解密的 link 包(由信令 demux 调用)。
|
|
188
|
+
* @param {object} packet link 包
|
|
189
|
+
* @returns {void}
|
|
190
|
+
*/
|
|
191
|
+
function deliverPacket(packet) {
|
|
192
|
+
if (packet?.type !== 'link') return
|
|
193
|
+
const linkId = normalizeHex64(packet.linkId)
|
|
194
|
+
const from = normalizeHex64(packet.from)
|
|
195
|
+
if (!linkId || !from) return
|
|
196
|
+
if (localIdentity?.nodeHash && from === localIdentity.nodeHash) return
|
|
197
|
+
|
|
198
|
+
const op = String(packet.op || '')
|
|
199
|
+
if (op === 'open') {
|
|
200
|
+
if (sessions.has(linkId)) return
|
|
201
|
+
if (!onInbound || !localIdentity) return
|
|
202
|
+
const pipe = openPipe({
|
|
203
|
+
linkId,
|
|
204
|
+
remoteNodeHash: from,
|
|
205
|
+
initiator: false,
|
|
206
|
+
localIdentity,
|
|
207
|
+
})
|
|
208
|
+
onInbound(asLinkHandle(pipe))
|
|
209
|
+
void pipe.startHandshake().catch(() => {
|
|
210
|
+
sessions.delete(linkId)
|
|
211
|
+
void pipe.close('accept-failed')
|
|
212
|
+
})
|
|
213
|
+
const pending = pendingByLinkId.get(linkId) || []
|
|
214
|
+
pendingByLinkId.delete(linkId)
|
|
215
|
+
const session = sessions.get(linkId)
|
|
216
|
+
if (session)
|
|
217
|
+
for (const queued of pending)
|
|
218
|
+
applySessionPacket(session, queued)
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const session = sessions.get(linkId)
|
|
223
|
+
if (!session) {
|
|
224
|
+
if (op === 'c' || op === 'b' || op === 'close')
|
|
225
|
+
bufferPending(linkId, packet)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
applySessionPacket(session, packet)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
id: 'nostr',
|
|
233
|
+
level: LINK_LEVEL_NOSTR,
|
|
234
|
+
caps: { needsOfferAnswer: false, needsDiscoverySignal: false, probe: 'sync' },
|
|
235
|
+
isAvailable,
|
|
236
|
+
/**
|
|
237
|
+
* @returns {boolean} 有中继即可
|
|
238
|
+
*/
|
|
239
|
+
canReach() {
|
|
240
|
+
return isAvailable()
|
|
241
|
+
},
|
|
242
|
+
deliverPacket,
|
|
243
|
+
/**
|
|
244
|
+
* @param {object} dialOptions dial 选项
|
|
245
|
+
* @returns {Promise<import('./index.mjs').LinkHandle | null>} link
|
|
246
|
+
*/
|
|
247
|
+
async dial(dialOptions) {
|
|
248
|
+
if (!isAvailable()) return null
|
|
249
|
+
const remoteNodeHash = normalizeHex64(dialOptions.nodeHash)
|
|
250
|
+
if (!remoteNodeHash) return null
|
|
251
|
+
localIdentity = dialOptions.localIdentity || localIdentity
|
|
252
|
+
if (!localIdentity?.nodeHash) throw new Error('p2p: nostr dial requires localIdentity')
|
|
253
|
+
const linkId = randomBytes(32).toString('hex')
|
|
254
|
+
const pipe = openPipe({
|
|
255
|
+
linkId,
|
|
256
|
+
remoteNodeHash,
|
|
257
|
+
initiator: true,
|
|
258
|
+
localIdentity,
|
|
259
|
+
})
|
|
260
|
+
await sendOp(remoteNodeHash, linkId, 'open')
|
|
261
|
+
await pipe.startHandshake()
|
|
262
|
+
return asLinkHandle(pipe)
|
|
263
|
+
},
|
|
264
|
+
/**
|
|
265
|
+
* @param {{ onInbound: (link: import('./index.mjs').LinkHandle) => void, localIdentity: object }} handlers 回调
|
|
266
|
+
* @returns {() => void} 停止 listening
|
|
267
|
+
*/
|
|
268
|
+
ensureListening(handlers) {
|
|
269
|
+
onInbound = handlers.onInbound
|
|
270
|
+
localIdentity = handlers.localIdentity
|
|
271
|
+
return () => {
|
|
272
|
+
onInbound = null
|
|
273
|
+
for (const session of sessions.values())
|
|
274
|
+
void session.pipe.close('listen-stop')
|
|
275
|
+
sessions.clear()
|
|
276
|
+
pendingByLinkId.clear()
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -34,6 +34,13 @@ function formatErrorReason(error) {
|
|
|
34
34
|
|
|
35
35
|
let cachedAvailable = null
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* @returns {boolean} 是否跑在 Deno
|
|
39
|
+
*/
|
|
40
|
+
function isDenoRuntime() {
|
|
41
|
+
return typeof globalThis.Deno !== 'undefined'
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
/**
|
|
38
45
|
* 探测 WebRTC(node-datachannel)是否可用。
|
|
39
46
|
* @returns {Promise<boolean>} 可用为 true
|
|
@@ -46,7 +53,12 @@ export async function canUseWebRtcLink() {
|
|
|
46
53
|
}
|
|
47
54
|
catch (error) {
|
|
48
55
|
cachedAvailable = false
|
|
49
|
-
|
|
56
|
+
const reason = formatErrorReason(error)
|
|
57
|
+
nodeDebug('p2p:webrtc unavailable', {
|
|
58
|
+
err: isDenoRuntime()
|
|
59
|
+
? `${reason} | deno: see docs/runtime.md (node-datachannel scripts only)`
|
|
60
|
+
: reason,
|
|
61
|
+
})
|
|
50
62
|
}
|
|
51
63
|
return cachedAvailable
|
|
52
64
|
}
|
package/link/rtc.mjs
CHANGED
|
@@ -4,15 +4,23 @@ import { toBytes } from '../core/bytes_codec.mjs'
|
|
|
4
4
|
import { getSignalingRuntimeConfig } from '../node/instance.mjs'
|
|
5
5
|
import { wrapRtcPeerConnectionForIceLocalHostname } from '../transport/rtc_ice_local_hostname.mjs'
|
|
6
6
|
|
|
7
|
+
/** @type {boolean} */
|
|
8
|
+
let exitCleanupHooked = false
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
|
-
* 注册进程退出时销毁 libdatachannel
|
|
11
|
+
* 注册进程退出时销毁 libdatachannel 原生资源(首次成功加载后挂一次)。
|
|
9
12
|
* libdatachannel 的原生线程在 pc.close() 后仍需时间回收;进程退出时若原生资源未同步销毁,
|
|
10
13
|
* Windows 上会触发堆损坏(退出码 0xC0000374)。
|
|
14
|
+
* @returns {Promise<void>}
|
|
11
15
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
})
|
|
16
|
+
async function ensureNodeDatachannelExitCleanup() {
|
|
17
|
+
if (exitCleanupHooked) return
|
|
18
|
+
exitCleanupHooked = true
|
|
19
|
+
const { cleanup = undefined } = await import('node-datachannel').catch(() => ({}))
|
|
20
|
+
process.on('exit', () => {
|
|
21
|
+
try { cleanup?.() } catch { /* already torn down */ }
|
|
22
|
+
})
|
|
23
|
+
}
|
|
16
24
|
|
|
17
25
|
/**
|
|
18
26
|
* 加载 node-datachannel polyfill,并按配置包装 RTCPeerConnection。
|
|
@@ -20,6 +28,7 @@ process.on('exit', () => {
|
|
|
20
28
|
*/
|
|
21
29
|
export async function loadNodeRtcPolyfill() {
|
|
22
30
|
const mod = await import('node-datachannel/polyfill')
|
|
31
|
+
await ensureNodeDatachannelExitCleanup()
|
|
23
32
|
const { iceLocalHostnamePolicy } = getSignalingRuntimeConfig()
|
|
24
33
|
return {
|
|
25
34
|
RTCPeerConnection: wrapRtcPeerConnectionForIceLocalHostname(mod.RTCPeerConnection, mod.RTCIceCandidate, iceLocalHostnamePolicy),
|
package/package.json
CHANGED
|
@@ -3,11 +3,13 @@ import { Buffer } from 'node:buffer'
|
|
|
3
3
|
import { compareHex64Asc, normalizeHex64 } from '../core/hexIds.mjs'
|
|
4
4
|
import { keyPairFromSeed } from '../crypto/crypto.mjs'
|
|
5
5
|
import { watchVerifiedNodeAdvert, setDiscoveryLinkDialer, prepareConnectToNode } from '../discovery/index.mjs'
|
|
6
|
+
import { setDiscoveryPeerClueListener } from '../discovery/peer_clue.mjs'
|
|
6
7
|
import { listLinkProviders } from '../link/providers/index.mjs'
|
|
7
8
|
import { ensureNodeSeed, getNodeHash } from '../node/identity.mjs'
|
|
8
9
|
import { nodeDebug, shortHash } from '../node/log.mjs'
|
|
9
10
|
import { loadPeerPoolView } from '../node/network.mjs'
|
|
10
11
|
import { createOverlayRouter } from '../overlay/index.mjs'
|
|
12
|
+
import { ms } from '../utils/duration.mjs'
|
|
11
13
|
import { emitSafe } from '../utils/emit_safe.mjs'
|
|
12
14
|
import { createLruMap } from '../utils/lru.mjs'
|
|
13
15
|
|
|
@@ -18,6 +20,11 @@ import { createOfferAnswerDial } from './offer_answer.mjs'
|
|
|
18
20
|
import { pickMeshEvictionVictim } from './peer_pool.mjs'
|
|
19
21
|
import { createRuntimeBootstrap } from './runtime_bootstrap.mjs'
|
|
20
22
|
|
|
23
|
+
/** dial exhausted 后的初始冷却。 */
|
|
24
|
+
const DIAL_COOLDOWN_BASE_MS = ms('30s')
|
|
25
|
+
/** dial 冷却上限。 */
|
|
26
|
+
const DIAL_COOLDOWN_MAX_MS = ms('10m')
|
|
27
|
+
|
|
21
28
|
/**
|
|
22
29
|
* 解析或从节点种子推导本地身份。
|
|
23
30
|
* @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array } | undefined} localIdentity 可选的预置身份
|
|
@@ -84,6 +91,8 @@ export function createLinkRegistry(options = {}) {
|
|
|
84
91
|
const links = new Map()
|
|
85
92
|
/** @type {Map<string, Promise<object | null>>} */
|
|
86
93
|
const inflights = new Map()
|
|
94
|
+
/** @type {Map<string, { until: number, failures: number }> & { touch: (key: string, value: { until: number, failures: number }) => void }} */
|
|
95
|
+
const dialCooldown = createLruMap(1024)
|
|
87
96
|
/** @type {Map<string, ReturnType<typeof import('./offer_answer.mjs').createBufferedSignalSession>>} */
|
|
88
97
|
const signalSessions = new Map()
|
|
89
98
|
/** @type {Map<string, Set<string>>} */
|
|
@@ -265,6 +274,19 @@ export function createLinkRegistry(options = {}) {
|
|
|
265
274
|
if (!normalized || normalized === localIdentity.nodeHash) return null
|
|
266
275
|
if (links.has(normalized)) return links.get(normalized)
|
|
267
276
|
if (inflights.has(normalized)) return await inflights.get(normalized)
|
|
277
|
+
const cool = dialCooldown.get(normalized)
|
|
278
|
+
const now = Date.now()
|
|
279
|
+
if (cool) {
|
|
280
|
+
if (now < cool.until) {
|
|
281
|
+
nodeDebug('p2p:dial cooldown', {
|
|
282
|
+
peer: shortHash(normalized),
|
|
283
|
+
failures: cool.failures,
|
|
284
|
+
retryInMs: cool.until - now,
|
|
285
|
+
})
|
|
286
|
+
return null
|
|
287
|
+
}
|
|
288
|
+
dialCooldown.delete(normalized)
|
|
289
|
+
}
|
|
268
290
|
const task = (async () => {
|
|
269
291
|
nodeDebug('p2p:dial start', { peer: shortHash(normalized) })
|
|
270
292
|
await prepareConnectToNode(normalized)
|
|
@@ -289,6 +311,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
289
311
|
if (provider.caps?.needsOfferAnswer) {
|
|
290
312
|
const link = await dialOfferAnswer(provider, normalized)
|
|
291
313
|
if (link) {
|
|
314
|
+
dialCooldown.delete(normalized)
|
|
292
315
|
nodeDebug('p2p:dial ok', { peer: shortHash(normalized), provider: provider.id })
|
|
293
316
|
return link
|
|
294
317
|
}
|
|
@@ -297,6 +320,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
297
320
|
}
|
|
298
321
|
const link = await dialProvider(provider, normalized)
|
|
299
322
|
if (link) {
|
|
323
|
+
dialCooldown.delete(normalized)
|
|
300
324
|
nodeDebug('p2p:dial ok', { peer: shortHash(normalized), provider: provider.id })
|
|
301
325
|
return link
|
|
302
326
|
}
|
|
@@ -310,7 +334,10 @@ export function createLinkRegistry(options = {}) {
|
|
|
310
334
|
})
|
|
311
335
|
}
|
|
312
336
|
|
|
313
|
-
|
|
337
|
+
const failures = (cool?.failures || 0) + 1
|
|
338
|
+
const delay = Math.min(DIAL_COOLDOWN_MAX_MS, DIAL_COOLDOWN_BASE_MS * (2 ** Math.min(failures - 1, 5)))
|
|
339
|
+
dialCooldown.touch(normalized, { until: Date.now() + delay, failures })
|
|
340
|
+
nodeDebug('p2p:dial exhausted', { peer: shortHash(normalized), cooldownMs: delay, failures })
|
|
314
341
|
return null
|
|
315
342
|
})().finally(() => inflights.delete(normalized))
|
|
316
343
|
inflights.set(normalized, task)
|
|
@@ -351,6 +378,12 @@ export function createLinkRegistry(options = {}) {
|
|
|
351
378
|
})
|
|
352
379
|
exploreLinkHashes = meshKeepalive.exploreLinkHashes
|
|
353
380
|
setDiscoveryLinkDialer(ensureDirectLinkToNode)
|
|
381
|
+
setDiscoveryPeerClueListener(nodeHash => {
|
|
382
|
+
const hash = normalizeHex64(nodeHash)
|
|
383
|
+
if (!hash) return
|
|
384
|
+
dialCooldown.delete(hash)
|
|
385
|
+
recentAdverts.touch(hash, Date.now())
|
|
386
|
+
})
|
|
354
387
|
|
|
355
388
|
/**
|
|
356
389
|
*
|
|
@@ -619,6 +652,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
619
652
|
return await watchVerifiedNodeAdvert(hash, async (verifiedNodeHash, body, meta) => {
|
|
620
653
|
applyAdvertPeerHints(verifiedNodeHash, body, meta)
|
|
621
654
|
recentAdverts.touch(verifiedNodeHash, Date.now())
|
|
655
|
+
dialCooldown.delete(verifiedNodeHash)
|
|
622
656
|
await Promise.resolve(onAdvert(verifiedNodeHash, body))
|
|
623
657
|
})
|
|
624
658
|
},
|
|
@@ -631,6 +665,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
631
665
|
async shutdown() {
|
|
632
666
|
await meshKeepalive?.stop()
|
|
633
667
|
setDiscoveryLinkDialer(null)
|
|
668
|
+
setDiscoveryPeerClueListener(null)
|
|
634
669
|
await bootstrap.shutdown()
|
|
635
670
|
overlayRouter?.close()
|
|
636
671
|
overlayRouter = null
|
|
@@ -638,6 +673,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
638
673
|
await link.close('registry-shutdown')
|
|
639
674
|
links.clear()
|
|
640
675
|
inflights.clear()
|
|
676
|
+
dialCooldown.clear()
|
|
641
677
|
for (const session of signalSessions.values()) session.clear()
|
|
642
678
|
signalSessions.clear()
|
|
643
679
|
},
|
|
@@ -150,11 +150,10 @@ export function createOfferAnswerDial(deps) {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
|
-
* @param {
|
|
153
|
+
* @param {object} packet 已解密的 signal 包
|
|
154
154
|
* @returns {Promise<void>}
|
|
155
155
|
*/
|
|
156
|
-
async function
|
|
157
|
-
const packet = decryptNodeSignalPacket(localIdentity.nodeHash, bytes)
|
|
156
|
+
async function handleSignalPacket(packet) {
|
|
158
157
|
if (packet?.type !== 'signal') return
|
|
159
158
|
const remoteNodeHash = normalizeHex64(packet.from)
|
|
160
159
|
const connId = String(packet.connId || '')
|
|
@@ -175,6 +174,21 @@ export function createOfferAnswerDial(deps) {
|
|
|
175
174
|
session.deliver(packet.body)
|
|
176
175
|
}
|
|
177
176
|
|
|
177
|
+
/**
|
|
178
|
+
* @param {Uint8Array} bytes 加密信令
|
|
179
|
+
* @returns {Promise<void>}
|
|
180
|
+
*/
|
|
181
|
+
async function handleIncomingSignal(bytes) {
|
|
182
|
+
const packet = decryptNodeSignalPacket(localIdentity.nodeHash, bytes)
|
|
183
|
+
if (!packet) return
|
|
184
|
+
if (packet.type === 'link') {
|
|
185
|
+
const provider = listLinkProviders().find(entry => entry.id === 'nostr')
|
|
186
|
+
provider?.deliverPacket?.(packet)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
await handleSignalPacket(packet)
|
|
190
|
+
}
|
|
191
|
+
|
|
178
192
|
/**
|
|
179
193
|
* @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
|
|
180
194
|
* @param {string} remoteNodeHash 远端 nodeHash
|
|
@@ -187,5 +201,5 @@ export function createOfferAnswerDial(deps) {
|
|
|
187
201
|
return await buildConnLink({ provider, remoteNodeHash, connId, session, initiator: true })
|
|
188
202
|
}
|
|
189
203
|
|
|
190
|
-
return { handleIncomingSignal, dialOfferAnswer }
|
|
204
|
+
return { handleIncomingSignal, handleSignalPacket, dialOfferAnswer }
|
|
191
205
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
unregisterLinkProvider,
|
|
19
19
|
} from '../link/providers/index.mjs'
|
|
20
20
|
import { createLanTcpLinkProvider } from '../link/providers/lan_tcp.mjs'
|
|
21
|
+
import { createNostrLinkProvider } from '../link/providers/nostr.mjs'
|
|
21
22
|
import { createWebRtcLinkProvider } from '../link/providers/webrtc.mjs'
|
|
22
23
|
import { getNodeTransportSettings } from '../node/identity.mjs'
|
|
23
24
|
import { getSignalingRuntimeConfig, onNodeChange } from '../node/instance.mjs'
|
|
@@ -116,7 +117,9 @@ export function createRuntimeBootstrap(deps) {
|
|
|
116
117
|
function registerDiscoveryDefaults() {
|
|
117
118
|
const providerIds = new Set(listDiscoveryProviders().map(provider => provider.id))
|
|
118
119
|
if (!providerIds.has('lan'))
|
|
119
|
-
registerDiscoveryProvider(createLanDiscoveryProvider(
|
|
120
|
+
registerDiscoveryProvider(createLanDiscoveryProvider({
|
|
121
|
+
localNodeHash: localIdentity.nodeHash,
|
|
122
|
+
}))
|
|
120
123
|
if (!providerIds.has('nostr'))
|
|
121
124
|
registerNostrProvider()
|
|
122
125
|
}
|
|
@@ -128,6 +131,7 @@ export function createRuntimeBootstrap(deps) {
|
|
|
128
131
|
unregisterDiscoveryProvider('nostr')
|
|
129
132
|
registerDiscoveryProvider(createNostrDiscoveryProvider({
|
|
130
133
|
getRelayUrls: resolveNostrRelayUrls,
|
|
134
|
+
localNodeHash: localIdentity.nodeHash,
|
|
131
135
|
}))
|
|
132
136
|
}
|
|
133
137
|
|
|
@@ -154,6 +158,8 @@ export function createRuntimeBootstrap(deps) {
|
|
|
154
158
|
const ids = new Set(listLinkProviders().map(provider => provider.id))
|
|
155
159
|
if (!ids.has('webrtc'))
|
|
156
160
|
registerLinkProvider(createWebRtcLinkProvider())
|
|
161
|
+
if (!ids.has('nostr'))
|
|
162
|
+
registerLinkProvider(createNostrLinkProvider({ getRelayUrls: resolveNostrRelayUrls }))
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
/**
|