@steve02081504/fount-p2p 0.0.10 → 0.0.12
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 +25 -7
- package/core/bytes_codec.mjs +65 -10
- package/core/tcp_port.mjs +1 -1
- package/crypto/checkpoint_sign.mjs +2 -2
- package/dag/canonicalize_row.mjs +3 -3
- package/dag/index.mjs +2 -3
- package/discovery/bt/index.mjs +5 -5
- package/discovery/bt/probe_child.mjs +20 -0
- package/discovery/bt/runtime.mjs +59 -15
- package/discovery/index.mjs +45 -48
- package/discovery/mdns.mjs +72 -17
- package/discovery/nostr.mjs +165 -129
- package/federation/chunk_fetch_pending.mjs +4 -5
- package/federation/dag_order_cache.mjs +1 -1
- package/federation/entity_key_chain.mjs +3 -4
- package/federation/topo_order_memo.mjs +11 -4
- package/files/chunk_fetch.mjs +2 -2
- package/files/evfs.mjs +2 -2
- package/link/channel_mux.mjs +10 -10
- package/link/frame.mjs +76 -124
- package/link/handshake.mjs +5 -5
- package/link/pipe.mjs +49 -75
- package/link/providers/ble_gatt.mjs +15 -27
- package/link/providers/index.mjs +7 -9
- package/link/providers/lan_tcp.mjs +18 -33
- package/link/providers/link_id_pipe.mjs +46 -0
- package/link/providers/webrtc.mjs +43 -52
- package/link/rtc.mjs +21 -9
- package/mailbox/deliver_or_store.mjs +1 -1
- package/node/identity.mjs +4 -4
- package/node/network.mjs +9 -9
- package/overlay/index.mjs +13 -13
- package/package.json +3 -2
- package/rooms/scoped_link.mjs +22 -38
- package/schemas/discovery.mjs +1 -2
- package/schemas/federation_pull.mjs +2 -2
- package/schemas/part_query.mjs +1 -1
- package/schemas/remote_event.mjs +1 -1
- package/transport/advert_ingest.mjs +20 -0
- package/transport/group_link_set.mjs +26 -45
- package/transport/ice_servers.mjs +12 -3
- package/transport/link_registry.mjs +181 -523
- package/transport/offer_answer.mjs +194 -0
- package/transport/peer_pool.mjs +53 -64
- package/transport/remote_user_room.mjs +13 -15
- package/transport/rtc_connection_budget.mjs +18 -4
- package/transport/runtime_bootstrap.mjs +369 -0
- package/transport/signal_crypto.mjs +104 -0
- package/transport/user_room.mjs +22 -15
- package/trust_graph/send.mjs +1 -1
- package/utils/emit_safe.mjs +11 -0
- package/utils/shuffle.mjs +13 -0
- package/utils/ttl_map.mjs +29 -4
- package/wire/ingress.mjs +1 -1
- package/wire/part_ingress.mjs +6 -8
- package/wire/part_invoke.mjs +4 -4
- package/wire/part_query.mjs +2 -3
- package/wire/part_query.tunables.json +6 -1
- package/wire/part_query_cache.mjs +1 -1
- package/wire/volatile_signature.mjs +1 -1
package/overlay/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
2
|
|
|
3
3
|
import { pubKeyHash, sign, verify } from '../crypto/crypto.mjs'
|
|
4
|
-
import {
|
|
4
|
+
import { randomFrameIdHex } from '../link/frame.mjs'
|
|
5
5
|
import { createLruMap } from '../utils/lru.mjs'
|
|
6
6
|
|
|
7
7
|
const ROUTE_DOMAIN = 'fount-route'
|
|
@@ -50,12 +50,12 @@ export function createOverlayRouter(registry, ttl = 3) {
|
|
|
50
50
|
*/
|
|
51
51
|
async function handleOverlay(senderNodeHash, envelope) {
|
|
52
52
|
const payload = envelope?.payload
|
|
53
|
-
const action =
|
|
54
|
-
if (!payload
|
|
53
|
+
const action = envelope?.action || ''
|
|
54
|
+
if (!payload) return
|
|
55
55
|
if (action === 'route_req') {
|
|
56
|
-
const reqId =
|
|
57
|
-
const target =
|
|
58
|
-
const hops =
|
|
56
|
+
const reqId = payload.reqId || ''
|
|
57
|
+
const target = payload.target || ''
|
|
58
|
+
const hops = payload.path || []
|
|
59
59
|
const remainingTtl = Number(payload.ttl)
|
|
60
60
|
if (!reqId || !target || !hops.length || remainingTtl <= 0) return
|
|
61
61
|
if (seenReqs.has(reqId) || hops.includes(selfNodeHash) || hops.length > 6) return
|
|
@@ -86,10 +86,10 @@ export function createOverlayRouter(registry, ttl = 3) {
|
|
|
86
86
|
return
|
|
87
87
|
}
|
|
88
88
|
if (action === 'route_resp') {
|
|
89
|
-
const reqId =
|
|
90
|
-
const path =
|
|
91
|
-
const nodePubKey =
|
|
92
|
-
const sigHex =
|
|
89
|
+
const reqId = payload.reqId || ''
|
|
90
|
+
const path = payload.path || []
|
|
91
|
+
const nodePubKey = payload.nodePubKey || ''
|
|
92
|
+
const sigHex = payload.sig || ''
|
|
93
93
|
if (!reqId || path.length < 2 || !nodePubKey || !sigHex) return
|
|
94
94
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== path[path.length - 1]) return
|
|
95
95
|
const ok = await verify(Buffer.from(sigHex, 'hex'), routeSignBytes(reqId, path), Buffer.from(nodePubKey, 'hex'))
|
|
@@ -108,7 +108,7 @@ export function createOverlayRouter(registry, ttl = 3) {
|
|
|
108
108
|
return
|
|
109
109
|
}
|
|
110
110
|
if (action === 'relay') {
|
|
111
|
-
const path =
|
|
111
|
+
const path = payload.path || []
|
|
112
112
|
const index = Number(payload.idx)
|
|
113
113
|
if (!path.length || path[index] !== selfNodeHash) return
|
|
114
114
|
if (index === path.length - 1) {
|
|
@@ -137,7 +137,7 @@ export function createOverlayRouter(registry, ttl = 3) {
|
|
|
137
137
|
* @returns {Promise<string[]>} 从本节点到目标的 nodeHash 路径
|
|
138
138
|
*/
|
|
139
139
|
async discoverRoute(targetNodeHash, options = {}) {
|
|
140
|
-
const reqId =
|
|
140
|
+
const reqId = randomFrameIdHex()
|
|
141
141
|
const maxTtl = Number(options.ttl) || ttl
|
|
142
142
|
const timeoutMs = Number(options.timeoutMs) || 10_000
|
|
143
143
|
const promise = new Promise((resolve, reject) => {
|
|
@@ -164,7 +164,7 @@ export function createOverlayRouter(registry, ttl = 3) {
|
|
|
164
164
|
* @returns {Promise<void>}
|
|
165
165
|
*/
|
|
166
166
|
async relay(path, body) {
|
|
167
|
-
if (
|
|
167
|
+
if (path?.[0] !== selfNodeHash || path.length < 2)
|
|
168
168
|
throw new Error('overlay: invalid relay path')
|
|
169
169
|
await sendOverlay(path[1], { action: 'relay', path, idx: 1, body })
|
|
170
170
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steve02081504/fount-p2p",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"network",
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@noble/curves": "latest",
|
|
64
|
-
"node-datachannel": "latest"
|
|
64
|
+
"node-datachannel": "latest",
|
|
65
|
+
"ws": "latest"
|
|
65
66
|
},
|
|
66
67
|
"devDependencies": {
|
|
67
68
|
"@steve02081504/exec": "latest"
|
package/rooms/scoped_link.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
|
|
2
1
|
import { advertiseTopic, subscribeTopic } from '../discovery/index.mjs'
|
|
3
|
-
import {
|
|
2
|
+
import { ingestSignedAdvert } from '../transport/advert_ingest.mjs'
|
|
3
|
+
import { getLinkRegistry } from '../transport/link_registry.mjs'
|
|
4
4
|
import {
|
|
5
|
-
groupRendezvousTopic,
|
|
6
|
-
decryptSignalPacket,
|
|
7
5
|
encryptSignalPacket,
|
|
8
|
-
|
|
9
|
-
} from '../transport/
|
|
6
|
+
groupRendezvousTopic,
|
|
7
|
+
} from '../transport/signal_crypto.mjs'
|
|
8
|
+
import { emitSafe } from '../utils/emit_safe.mjs'
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* 在指定 scope 与 roomSecret 下创建 link 层房间(discovery + registry 转发)。
|
|
@@ -18,9 +17,9 @@ import {
|
|
|
18
17
|
*/
|
|
19
18
|
export function createScopedLinkRoom(options) {
|
|
20
19
|
const registry = getLinkRegistry()
|
|
21
|
-
const scope =
|
|
20
|
+
const { scope } = options
|
|
22
21
|
const topic = groupRendezvousTopic(options.roomSecret)
|
|
23
|
-
const allowNode =
|
|
22
|
+
const allowNode = options.allowNode ?? (() => true)
|
|
24
23
|
/** @type {Set<string>} */
|
|
25
24
|
const discoveredPeers = new Set()
|
|
26
25
|
/** @type {Set<string>} */
|
|
@@ -41,16 +40,6 @@ export function createScopedLinkRoom(options) {
|
|
|
41
40
|
return [...discoveredPeers].filter(nodeHash => allowNode(nodeHash) && registry.getLink(nodeHash))
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
/**
|
|
45
|
-
* @param {Set<(peerId: string) => void>} listeners 回调集合
|
|
46
|
-
* @param {string} peerId 节点 hash
|
|
47
|
-
* @returns {void}
|
|
48
|
-
*/
|
|
49
|
-
function emit(listeners, peerId) {
|
|
50
|
-
for (const listener of listeners)
|
|
51
|
-
try { listener(peerId) } catch { /* ignore */ }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
43
|
/**
|
|
55
44
|
* @param {string} peerId 节点 hash
|
|
56
45
|
* @returns {void}
|
|
@@ -58,7 +47,7 @@ export function createScopedLinkRoom(options) {
|
|
|
58
47
|
function notePeerJoin(peerId) {
|
|
59
48
|
if (!peerId || announcedPeers.has(peerId)) return
|
|
60
49
|
announcedPeers.add(peerId)
|
|
61
|
-
|
|
50
|
+
emitSafe(joinListeners, peerId)
|
|
62
51
|
}
|
|
63
52
|
|
|
64
53
|
/**
|
|
@@ -68,7 +57,7 @@ export function createScopedLinkRoom(options) {
|
|
|
68
57
|
function notePeerLeave(peerId) {
|
|
69
58
|
if (!peerId || !announcedPeers.has(peerId)) return
|
|
70
59
|
announcedPeers.delete(peerId)
|
|
71
|
-
|
|
60
|
+
emitSafe(leaveListeners, peerId)
|
|
72
61
|
}
|
|
73
62
|
|
|
74
63
|
/**
|
|
@@ -76,10 +65,9 @@ export function createScopedLinkRoom(options) {
|
|
|
76
65
|
* @returns {{ handler: ((payload: unknown, peerId: string) => void) | null, backlog: Array<{ payload: unknown, peerId: string }> }} action 槽(含待处理 backlog)
|
|
77
66
|
*/
|
|
78
67
|
function getActionEntry(name) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return actionEntries.get(key)
|
|
68
|
+
if (!actionEntries.has(name))
|
|
69
|
+
actionEntries.set(name, { handler: null, backlog: [] })
|
|
70
|
+
return actionEntries.get(name)
|
|
83
71
|
}
|
|
84
72
|
|
|
85
73
|
return {
|
|
@@ -90,7 +78,7 @@ export function createScopedLinkRoom(options) {
|
|
|
90
78
|
await registry.ensureRuntime()
|
|
91
79
|
cleanups.add(registry.subscribeScope(scope, (senderNodeHash, envelope) => {
|
|
92
80
|
if (!allowNode(senderNodeHash)) return
|
|
93
|
-
const entry = actionEntries.get(
|
|
81
|
+
const entry = actionEntries.get(envelope.action)
|
|
94
82
|
if (!entry) return
|
|
95
83
|
if (entry.handler) entry.handler(envelope.payload, senderNodeHash)
|
|
96
84
|
else entry.backlog.push({ payload: envelope.payload, peerId: senderNodeHash })
|
|
@@ -104,14 +92,11 @@ export function createScopedLinkRoom(options) {
|
|
|
104
92
|
notePeerLeave(nodeHash)
|
|
105
93
|
}))
|
|
106
94
|
cleanups.add(await subscribeTopic(topic, async (bytes, meta) => {
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
discoveredPeers.add(verifiedNodeHash)
|
|
113
|
-
await registry.ensureLinkToNode(verifiedNodeHash).catch(() => null)
|
|
114
|
-
if (registry.getLink(verifiedNodeHash)) notePeerJoin(verifiedNodeHash)
|
|
95
|
+
const ingested = await ingestSignedAdvert(topic, bytes, meta)
|
|
96
|
+
if (!ingested || !allowNode(ingested.verifiedNodeHash)) return
|
|
97
|
+
discoveredPeers.add(ingested.verifiedNodeHash)
|
|
98
|
+
await registry.ensureLinkToNode(ingested.verifiedNodeHash).catch(() => null)
|
|
99
|
+
if (registry.getLink(ingested.verifiedNodeHash)) notePeerJoin(ingested.verifiedNodeHash)
|
|
115
100
|
}))
|
|
116
101
|
cleanups.add(await advertiseTopic(topic, encryptSignalPacket(topic, {
|
|
117
102
|
type: 'advert',
|
|
@@ -135,22 +120,21 @@ export function createScopedLinkRoom(options) {
|
|
|
135
120
|
* @returns {[(payload: unknown, peerId?: string | string[] | null) => Promise<void>, (handler: (payload: unknown, peerId: string) => void) => void]} [send, onReceive] 发送与订阅元组
|
|
136
121
|
*/
|
|
137
122
|
makeAction(name) {
|
|
138
|
-
const actionName = String(name)
|
|
139
123
|
return [
|
|
140
124
|
async (payload, peerId = null) => {
|
|
141
125
|
if (Array.isArray(peerId)) {
|
|
142
126
|
await Promise.all(peerId.map(targetPeerId =>
|
|
143
|
-
registry.sendToNodeLink(targetPeerId, { scope, action:
|
|
127
|
+
registry.sendToNodeLink(targetPeerId, { scope, action: name, payload })))
|
|
144
128
|
return
|
|
145
129
|
}
|
|
146
130
|
if (peerId)
|
|
147
|
-
await registry.sendToNodeLink(peerId, { scope, action:
|
|
131
|
+
await registry.sendToNodeLink(peerId, { scope, action: name, payload })
|
|
148
132
|
else
|
|
149
133
|
await Promise.all(activePeerIds().map(targetPeerId =>
|
|
150
|
-
registry.sendToNodeLink(targetPeerId, { scope, action:
|
|
134
|
+
registry.sendToNodeLink(targetPeerId, { scope, action: name, payload })))
|
|
151
135
|
},
|
|
152
136
|
handler => {
|
|
153
|
-
const entry = getActionEntry(
|
|
137
|
+
const entry = getActionEntry(name)
|
|
154
138
|
entry.handler = handler
|
|
155
139
|
for (const pending of entry.backlog.splice(0))
|
|
156
140
|
handler(pending.payload, pending.peerId)
|
package/schemas/discovery.mjs
CHANGED
|
@@ -34,8 +34,7 @@ function sanitizeDiscoveryAdvertisement(ad) {
|
|
|
34
34
|
* @returns {object[]} 已清扫广告
|
|
35
35
|
*/
|
|
36
36
|
function sanitizeDiscoveryAdvertisements(ads) {
|
|
37
|
-
|
|
38
|
-
return ads.slice(0, MAX_DISCOVERY_ADS).map(sanitizeDiscoveryAdvertisement).filter(Boolean)
|
|
37
|
+
return (ads || []).slice(0, MAX_DISCOVERY_ADS).map(sanitizeDiscoveryAdvertisement).filter(Boolean)
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
/**
|
|
@@ -30,8 +30,8 @@ export function parsePullAttestation(attestation) {
|
|
|
30
30
|
const signature = String(attestation.signature || '').trim().toLowerCase()
|
|
31
31
|
if (!isHex64(requesterPubKeyHash) || !groupId || !Number.isFinite(timestamp) || !isSignatureHex128(signature))
|
|
32
32
|
return null
|
|
33
|
-
const wantIds =
|
|
34
|
-
? [...new Set(attestation.wantIds.map(id =>
|
|
33
|
+
const wantIds = attestation.wantIds?.length
|
|
34
|
+
? [...new Set(attestation.wantIds.map(id => id.trim().toLowerCase()).filter(id => EVENT_ID_HEX.test(id)))]
|
|
35
35
|
: undefined
|
|
36
36
|
return {
|
|
37
37
|
requesterPubKeyHash,
|
package/schemas/part_query.mjs
CHANGED
|
@@ -107,7 +107,7 @@ export function parsePartQueryReq(value, tunables = partQueryTunables) {
|
|
|
107
107
|
if (!Object.prototype.hasOwnProperty.call(value, 'query')) return null
|
|
108
108
|
if (measureJsonBytes(value.query) > (tunables.maxQueryBytes ?? 2048)) return null
|
|
109
109
|
const ttl = clampPartQueryTtl(value.ttl, tunables.maxTtl)
|
|
110
|
-
if (ttl
|
|
110
|
+
if (!ttl) return null
|
|
111
111
|
const budget = clampPartQueryBudget(value.budget, tunables.maxHits)
|
|
112
112
|
return {
|
|
113
113
|
requestId,
|
package/schemas/remote_event.mjs
CHANGED
|
@@ -9,7 +9,7 @@ export function validateRemoteEventShape(event) {
|
|
|
9
9
|
if (!event?.type || typeof event.type !== 'string')
|
|
10
10
|
throw new Error('remote event: type required')
|
|
11
11
|
assertHex64(event.id, 'remote event.id')
|
|
12
|
-
if (!
|
|
12
|
+
if (!event.prev_event_ids)
|
|
13
13
|
throw new Error('remote event: prev_event_ids must be array')
|
|
14
14
|
for (const parentId of event.prev_event_ids)
|
|
15
15
|
assertHex64(parentId, 'remote event.prev_event_id')
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
|
|
2
|
+
import { verifySignedAdvert } from '../link/handshake.mjs'
|
|
3
|
+
|
|
4
|
+
import { decryptSignalPacket } from './signal_crypto.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 解密验签 advert,写入 peer hints;失败返回 null。
|
|
8
|
+
* @param {string} topic rendezvous 主题
|
|
9
|
+
* @param {Uint8Array} bytes 加密 advert 载荷
|
|
10
|
+
* @param {object} [meta] 发现元数据(如 relayUrl)
|
|
11
|
+
* @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签后的节点与 body
|
|
12
|
+
*/
|
|
13
|
+
export async function ingestSignedAdvert(topic, bytes, meta) {
|
|
14
|
+
const packet = decryptSignalPacket(topic, bytes)
|
|
15
|
+
if (packet?.type !== 'advert' || !packet.body) return null
|
|
16
|
+
const verifiedNodeHash = await verifySignedAdvert(topic, packet.body)
|
|
17
|
+
if (!verifiedNodeHash) return null
|
|
18
|
+
noteAdvertPeerHints(verifiedNodeHash, packet.body, meta)
|
|
19
|
+
return { verifiedNodeHash, body: packet.body }
|
|
20
|
+
}
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
|
|
2
1
|
import { advertiseTopic, subscribeTopic } from '../discovery/index.mjs'
|
|
3
|
-
import { verifySignedAdvert } from '../link/handshake.mjs'
|
|
4
2
|
import { loadPeerPoolView } from '../node/network.mjs'
|
|
5
3
|
import { loadReputation } from '../node/reputation_store.mjs'
|
|
4
|
+
import { emitSafe } from '../utils/emit_safe.mjs'
|
|
6
5
|
|
|
6
|
+
import { ingestSignedAdvert } from './advert_ingest.mjs'
|
|
7
|
+
import { getLinkRegistry } from './link_registry.mjs'
|
|
8
|
+
import { resolveFederationPoolLimits, selectLinkTargetsFromMembers } from './peer_pool.mjs'
|
|
7
9
|
import {
|
|
8
|
-
groupRendezvousTopic,
|
|
9
|
-
decryptSignalPacket,
|
|
10
10
|
encryptSignalPacket,
|
|
11
|
-
|
|
12
|
-
} from './
|
|
13
|
-
import { resolveFederationPoolLimits, selectLinkTargetsFromMembers } from './peer_pool.mjs'
|
|
14
|
-
|
|
11
|
+
groupRendezvousTopic,
|
|
12
|
+
} from './signal_crypto.mjs'
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
15
|
* 创建基于 link registry 的群组联邦房间。
|
|
@@ -25,10 +23,10 @@ import { resolveFederationPoolLimits, selectLinkTargetsFromMembers } from './pee
|
|
|
25
23
|
export function createGroupLinkSet(options) {
|
|
26
24
|
const registry = options.registry ?? getLinkRegistry()
|
|
27
25
|
const autoconnect = options.autoconnect !== false
|
|
28
|
-
const groupId =
|
|
26
|
+
const { groupId } = options
|
|
29
27
|
const scope = `group:${groupId}`
|
|
30
28
|
const topic = groupRendezvousTopic(options.roomSecret)
|
|
31
|
-
const members = new Set(
|
|
29
|
+
const members = new Set(options.members || [])
|
|
32
30
|
const selfNodeHash = registry.localIdentity.nodeHash
|
|
33
31
|
const groupSettings = options.groupSettings ?? {}
|
|
34
32
|
// 初始成员是调用方明确知道的引导集合(如 introducer/creator/seed),作为必连锚点保证引导期连通。
|
|
@@ -59,17 +57,6 @@ export function createGroupLinkSet(options) {
|
|
|
59
57
|
cleanups.add(cleanup)
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
/**
|
|
63
|
-
* 向 peer 事件监听器集合广播。
|
|
64
|
-
* @param {Set<(peerId: string) => void>} listeners 监听器集合
|
|
65
|
-
* @param {string} peerId 目标 peer id
|
|
66
|
-
* @returns {void}
|
|
67
|
-
*/
|
|
68
|
-
function emitPeerListeners(listeners, peerId) {
|
|
69
|
-
for (const listener of listeners)
|
|
70
|
-
try { listener(peerId) } catch { /* ignore */ }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
60
|
/**
|
|
74
61
|
* 记录 peer 加入并通知监听器。
|
|
75
62
|
* @param {string} peerId 加入的 peer id
|
|
@@ -78,7 +65,7 @@ export function createGroupLinkSet(options) {
|
|
|
78
65
|
function notePeerJoin(peerId) {
|
|
79
66
|
if (!peerId || announcedPeers.has(peerId)) return
|
|
80
67
|
announcedPeers.add(peerId)
|
|
81
|
-
|
|
68
|
+
emitSafe(peerJoinListeners, peerId)
|
|
82
69
|
}
|
|
83
70
|
|
|
84
71
|
/**
|
|
@@ -89,7 +76,7 @@ export function createGroupLinkSet(options) {
|
|
|
89
76
|
function notePeerLeave(peerId) {
|
|
90
77
|
if (!peerId || !announcedPeers.has(peerId)) return
|
|
91
78
|
announcedPeers.delete(peerId)
|
|
92
|
-
|
|
79
|
+
emitSafe(peerLeaveListeners, peerId)
|
|
93
80
|
}
|
|
94
81
|
|
|
95
82
|
/**
|
|
@@ -98,13 +85,12 @@ export function createGroupLinkSet(options) {
|
|
|
98
85
|
* @returns {void}
|
|
99
86
|
*/
|
|
100
87
|
function notePeerCandidate(nodeHash) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
members.add(normalized)
|
|
88
|
+
if (!nodeHash || nodeHash === selfNodeHash || members.has(nodeHash)) return
|
|
89
|
+
members.add(nodeHash)
|
|
104
90
|
registry.registerScopeInterest(scope, [...members])
|
|
105
91
|
// 若链路先于"得知其群成员身份"建立(如 warmup/连节点先连上,之后才收到其群 advert/envelope),
|
|
106
92
|
// onLinkUp 当时因 !members.has 被跳过,这里补发 peer-join,触发 bootstrap flush(member_join 自证推送)。
|
|
107
|
-
if (registry.getLink(
|
|
93
|
+
if (registry.getLink(nodeHash)) notePeerJoin(nodeHash)
|
|
108
94
|
scheduleDial()
|
|
109
95
|
}
|
|
110
96
|
|
|
@@ -143,10 +129,9 @@ export function createGroupLinkSet(options) {
|
|
|
143
129
|
* @returns {{ handler: ((payload: unknown, peerId: string) => void) | null, backlog: Array<{ payload: unknown, peerId: string }> }} action 条目
|
|
144
130
|
*/
|
|
145
131
|
function getActionEntry(name) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return actionEntries.get(key)
|
|
132
|
+
if (!actionEntries.has(name))
|
|
133
|
+
actionEntries.set(name, { handler: null, backlog: [] })
|
|
134
|
+
return actionEntries.get(name)
|
|
150
135
|
}
|
|
151
136
|
|
|
152
137
|
/**
|
|
@@ -171,7 +156,7 @@ export function createGroupLinkSet(options) {
|
|
|
171
156
|
registry.registerScopeInterest(scope, [...members])
|
|
172
157
|
registerCleanup(registry.subscribeScope(scope, (senderNodeHash, envelope) => {
|
|
173
158
|
notePeerCandidate(senderNodeHash)
|
|
174
|
-
const entry = actionEntries.get(
|
|
159
|
+
const entry = actionEntries.get(envelope.action)
|
|
175
160
|
if (entry)
|
|
176
161
|
if (entry.handler) entry.handler(envelope.payload, senderNodeHash)
|
|
177
162
|
else entry.backlog.push({ payload: envelope.payload, peerId: senderNodeHash })
|
|
@@ -188,13 +173,10 @@ export function createGroupLinkSet(options) {
|
|
|
188
173
|
notePeerLeave(nodeHash)
|
|
189
174
|
}))
|
|
190
175
|
registerCleanup(await subscribeTopic(topic, async (bytes, meta) => {
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
193
|
-
const verifiedNodeHash = await verifySignedAdvert(topic, packet.body)
|
|
194
|
-
if (!verifiedNodeHash || verifiedNodeHash === selfNodeHash) return
|
|
195
|
-
noteAdvertPeerHints(verifiedNodeHash, packet.body, meta)
|
|
176
|
+
const ingested = await ingestSignedAdvert(topic, bytes, meta)
|
|
177
|
+
if (!ingested || ingested.verifiedNodeHash === selfNodeHash) return
|
|
196
178
|
// 发现新成员:并入成员集并去抖重算稀疏建链目标(notePeerCandidate 内部会 scheduleDial)。
|
|
197
|
-
notePeerCandidate(verifiedNodeHash)
|
|
179
|
+
notePeerCandidate(ingested.verifiedNodeHash)
|
|
198
180
|
}))
|
|
199
181
|
registerCleanup(await advertiseTopic(topic, encryptSignalPacket(topic, {
|
|
200
182
|
type: 'advert',
|
|
@@ -229,7 +211,7 @@ export function createGroupLinkSet(options) {
|
|
|
229
211
|
* @returns {string | null} 对端 id;无链路时为 null
|
|
230
212
|
*/
|
|
231
213
|
getPeerIdByNodeHash(nodeHash) {
|
|
232
|
-
return registry.getLink(nodeHash) ?
|
|
214
|
+
return registry.getLink(nodeHash) ? nodeHash : null
|
|
233
215
|
},
|
|
234
216
|
/**
|
|
235
217
|
* 向单个 peer 发送 scope action。
|
|
@@ -239,7 +221,7 @@ export function createGroupLinkSet(options) {
|
|
|
239
221
|
* @returns {Promise<boolean>} 是否发送成功
|
|
240
222
|
*/
|
|
241
223
|
async sendToPeer(peerId, actionName, payload) {
|
|
242
|
-
return await registry.sendToNodeLink(peerId, { scope, action:
|
|
224
|
+
return await registry.sendToNodeLink(peerId, { scope, action: actionName, payload })
|
|
243
225
|
},
|
|
244
226
|
/**
|
|
245
227
|
* 向单个或全部在线成员发送 action。
|
|
@@ -252,7 +234,7 @@ export function createGroupLinkSet(options) {
|
|
|
252
234
|
if (peerId) return await this.sendToPeer(peerId, actionName, payload) ? 1 : 0
|
|
253
235
|
let sent = 0
|
|
254
236
|
for (const { peerId: targetPeerId } of activeRoster())
|
|
255
|
-
if (await registry.sendToNodeLink(targetPeerId, { scope, action:
|
|
237
|
+
if (await registry.sendToNodeLink(targetPeerId, { scope, action: actionName, payload })) sent++
|
|
256
238
|
return sent
|
|
257
239
|
},
|
|
258
240
|
/**
|
|
@@ -299,17 +281,16 @@ export function createGroupLinkSet(options) {
|
|
|
299
281
|
* @returns {[(payload: unknown, peerId?: string | string[] | null) => Promise<void>, (handler: (payload: unknown, peerId: string) => void) => void]} send 与 on 函数对
|
|
300
282
|
*/
|
|
301
283
|
makeAction(name) {
|
|
302
|
-
const actionName = String(name)
|
|
303
284
|
return [
|
|
304
285
|
async (payload, peerId = null) => {
|
|
305
286
|
if (Array.isArray(peerId)) {
|
|
306
|
-
await Promise.all(peerId.map(targetPeerId => this.sendToPeer(targetPeerId,
|
|
287
|
+
await Promise.all(peerId.map(targetPeerId => this.sendToPeer(targetPeerId, name, payload)))
|
|
307
288
|
return
|
|
308
289
|
}
|
|
309
|
-
await this.send(
|
|
290
|
+
await this.send(name, payload, peerId)
|
|
310
291
|
},
|
|
311
292
|
handler => {
|
|
312
|
-
const entry = getActionEntry(
|
|
293
|
+
const entry = getActionEntry(name)
|
|
313
294
|
entry.handler = handler
|
|
314
295
|
for (const pending of entry.backlog.splice(0))
|
|
315
296
|
handler(pending.payload, pending.peerId)
|
|
@@ -3,11 +3,20 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const ICE_URL_RE = /^(stun|turn|turns):/iu
|
|
6
|
-
const MAX_ICE_SERVERS =
|
|
6
|
+
const MAX_ICE_SERVERS = 12
|
|
7
7
|
|
|
8
8
|
/** @type {{ urls: string, username?: string, credential?: string }[]} */
|
|
9
9
|
export const DEFAULT_ICE_SERVERS = [
|
|
10
|
+
// Global first — ICE tries in order; regional CN servers are fallback when global STUN is blocked.
|
|
10
11
|
{ urls: 'stun:stun.l.google.com:19302' },
|
|
12
|
+
{ urls: 'stun:stun.cloudflare.com:3478' },
|
|
13
|
+
{ urls: 'stun:global.stun.twilio.com:3478' },
|
|
14
|
+
{ urls: 'stun:stun.nextcloud.com:3478' },
|
|
15
|
+
{ urls: 'stun:stun.voip.blackberry.com:3478' },
|
|
16
|
+
{ urls: 'stun:stun.freeswitch.org:3478' },
|
|
17
|
+
{ urls: 'stun:stun.chat.bilibili.com:3478' },
|
|
18
|
+
{ urls: 'stun:stun.hitv.com:3478' },
|
|
19
|
+
{ urls: 'stun:stun.miwifi.com:3478' },
|
|
11
20
|
]
|
|
12
21
|
|
|
13
22
|
/**
|
|
@@ -37,7 +46,7 @@ function normalizeIceEntry(raw) {
|
|
|
37
46
|
* @returns {{ urls: string, username?: string, credential?: string }[]} 合法 ICE 列表
|
|
38
47
|
*/
|
|
39
48
|
export function resolveIceServers(groupSettings) {
|
|
40
|
-
const fromSettings =
|
|
49
|
+
const fromSettings = groupSettings?.iceServers || []
|
|
41
50
|
const out = []
|
|
42
51
|
for (const raw of fromSettings) {
|
|
43
52
|
const entry = normalizeIceEntry(raw)
|
|
@@ -53,7 +62,7 @@ export function resolveIceServers(groupSettings) {
|
|
|
53
62
|
* @returns {{ urls: string, username?: string, credential?: string }[]} 校验后的 ICE 列表
|
|
54
63
|
*/
|
|
55
64
|
export function sanitizeIceServersForSettings(raw) {
|
|
56
|
-
if (!
|
|
65
|
+
if (!raw?.length) return [...DEFAULT_ICE_SERVERS]
|
|
57
66
|
const out = []
|
|
58
67
|
for (const item of raw) {
|
|
59
68
|
const entry = normalizeIceEntry(item)
|