@steve02081504/fount-p2p 0.0.12 → 0.0.14

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 (79) hide show
  1. package/README.md +41 -10
  2. package/core/composite_key.mjs +5 -5
  3. package/crypto/crypto.mjs +3 -3
  4. package/crypto/key.mjs +12 -12
  5. package/dag/storage.mjs +36 -36
  6. package/discovery/advert_peer_hints.mjs +9 -3
  7. package/discovery/adverts.mjs +115 -0
  8. package/discovery/bt/index.mjs +153 -112
  9. package/discovery/bt/probe_child.mjs +2 -1
  10. package/discovery/bt/runtime.mjs +2 -9
  11. package/discovery/index.mjs +267 -62
  12. package/discovery/internal/signal_crypto.mjs +109 -0
  13. package/discovery/lan.mjs +279 -0
  14. package/discovery/lan_interfaces.mjs +78 -0
  15. package/discovery/lan_peer_hints.mjs +22 -5
  16. package/discovery/nostr.mjs +474 -65
  17. package/federation/chunk_fetch_pending.mjs +10 -13
  18. package/federation/manifest_fetch_pending.mjs +1 -3
  19. package/files/assemble.mjs +14 -14
  20. package/files/assemble_stream.mjs +6 -6
  21. package/files/chunk_responder.mjs +29 -20
  22. package/files/evfs.mjs +29 -28
  23. package/files/manifest_fetch.mjs +16 -3
  24. package/files/public_manifest.mjs +11 -11
  25. package/files/transfer_key_registry.mjs +2 -2
  26. package/index.mjs +86 -11
  27. package/infra/cli.mjs +62 -0
  28. package/infra/debug_log.mjs +56 -0
  29. package/infra/default_node_dir.mjs +22 -0
  30. package/infra/priority.mjs +56 -0
  31. package/infra/service.mjs +140 -0
  32. package/infra/tunables.json +5 -0
  33. package/link/frame.mjs +9 -16
  34. package/link/handshake.mjs +25 -18
  35. package/link/pipe.mjs +8 -8
  36. package/link/providers/ble_gatt.mjs +6 -6
  37. package/link/providers/lan_tcp.mjs +62 -33
  38. package/link/providers/webrtc.mjs +3 -1
  39. package/link/rtc.mjs +3 -3
  40. package/mailbox/consumer_registry.mjs +2 -2
  41. package/mailbox/deliver_or_store.mjs +5 -5
  42. package/mailbox/wire.mjs +28 -24
  43. package/node/entity_store.mjs +6 -6
  44. package/node/instance.mjs +46 -27
  45. package/node/local_data_revision.mjs +26 -0
  46. package/node/log.mjs +56 -0
  47. package/node/network.mjs +37 -5
  48. package/node/reputation_store.mjs +4 -4
  49. package/node/reputation_sync.mjs +318 -0
  50. package/node/routing_profile.mjs +21 -0
  51. package/node/signaling_config.mjs +30 -11
  52. package/overlay/index.mjs +22 -1
  53. package/package.json +13 -2
  54. package/rooms/scoped_link.mjs +17 -166
  55. package/transport/advert_ingest.mjs +11 -14
  56. package/transport/group_link_set.mjs +149 -99
  57. package/transport/link_registry.mjs +211 -60
  58. package/transport/mesh_keepalive.mjs +217 -0
  59. package/transport/node_scope.mjs +289 -0
  60. package/transport/offer_answer.mjs +45 -48
  61. package/transport/peer_pool.mjs +116 -14
  62. package/transport/remote_user_room.mjs +6 -9
  63. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  64. package/transport/runtime_bootstrap.mjs +170 -108
  65. package/transport/tunables.json +11 -0
  66. package/transport/tunables.mjs +18 -0
  67. package/transport/user_room.mjs +83 -158
  68. package/trust_graph/build.mjs +0 -2
  69. package/trust_graph/cache.mjs +12 -3
  70. package/trust_graph/registry.mjs +6 -6
  71. package/trust_graph/send.mjs +0 -3
  72. package/utils/async_mutex.mjs +4 -4
  73. package/utils/emit_safe.mjs +3 -3
  74. package/utils/json_io.mjs +12 -12
  75. package/utils/map_pool.mjs +5 -5
  76. package/wire/part_ingress.mjs +32 -28
  77. package/wire/part_query.mjs +26 -20
  78. package/discovery/mdns.mjs +0 -197
  79. package/transport/signal_crypto.mjs +0 -104
@@ -0,0 +1,318 @@
1
+ import { randomUUID } from 'node:crypto'
2
+
3
+ import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
4
+ import { sendToNodeLink } from '../transport/link_registry.mjs'
5
+ import { attachNodeScopeFeature, ensureNodeScope, getNodeScopeWire } from '../transport/node_scope.mjs'
6
+
7
+ import {
8
+ loadReputation,
9
+ mutateReputation,
10
+ } from './reputation_store.mjs'
11
+ import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
12
+
13
+ const SYNC_DATA_NAME = 'reputation_sync'
14
+ const MAX_LOCKED_SCORE = 1
15
+
16
+ /**
17
+ * @typedef {{
18
+ * trustSyncDonors: string[]
19
+ * reputationExportAllowlist: string[]
20
+ * lockedMaxNodeHashes: string[]
21
+ * lockedMaxPrevByNodeHash: Record<string, number>
22
+ * }} ReputationSyncConfig
23
+ */
24
+
25
+ /** @type {ReputationSyncConfig | null} */
26
+ let syncConfig = null
27
+
28
+ /** @type {Map<string, { resolve: (v: object) => void, reject: (e: Error) => void, timer: ReturnType<typeof setTimeout>, donor: string }>} */
29
+ const pendingPulls = new Map()
30
+
31
+ /** @type {Set<() => void>} */
32
+ const syncWireDisposers = new Set()
33
+
34
+ let pullTimeoutMs = 8000
35
+
36
+ /**
37
+ * @param {number} ms 超时毫秒;测试用
38
+ * @returns {void}
39
+ */
40
+ export function setReputationPullTimeoutMsForTests(ms) {
41
+ pullTimeoutMs = Math.max(1, Number(ms) || 8000)
42
+ }
43
+
44
+ /**
45
+ * @returns {ReputationSyncConfig} 内存中的 sync 配置(首次从盘加载)
46
+ */
47
+ function loadSyncConfig() {
48
+ if (syncConfig) return syncConfig
49
+ const raw = readNodeJsonSync(SYNC_DATA_NAME) || {}
50
+ /** @type {Record<string, number>} */
51
+ const lockedMaxPrevByNodeHash = {}
52
+ const prevRaw = raw.lockedMaxPrevByNodeHash && typeof raw.lockedMaxPrevByNodeHash === 'object'
53
+ ? raw.lockedMaxPrevByNodeHash
54
+ : {}
55
+ for (const [nodeHash, score] of Object.entries(prevRaw)) {
56
+ const id = normalizeHex64(nodeHash)
57
+ const n = Number(score)
58
+ if (id && isHex64(id) && Number.isFinite(n)) lockedMaxPrevByNodeHash[id] = n
59
+ }
60
+ syncConfig = {
61
+ trustSyncDonors: normalizeHashList(raw.trustSyncDonors),
62
+ reputationExportAllowlist: normalizeHashList(raw.reputationExportAllowlist),
63
+ lockedMaxNodeHashes: normalizeHashList(raw.lockedMaxNodeHashes),
64
+ lockedMaxPrevByNodeHash,
65
+ }
66
+ return syncConfig
67
+ }
68
+
69
+ /**
70
+ * @param {unknown} list - 原始 hash 列表
71
+ * @returns {string[]} 规范化去重后的 64-hex 列表
72
+ */
73
+ function normalizeHashList(list) {
74
+ return [...new Set((Array.isArray(list) ? list : [])
75
+ .map(id => normalizeHex64(id))
76
+ .filter(id => isHex64(id)))]
77
+ }
78
+
79
+ /**
80
+ * @returns {void}
81
+ */
82
+ function persistSyncConfig() {
83
+ writeNodeJsonSync(SYNC_DATA_NAME, loadSyncConfig())
84
+ }
85
+
86
+ /**
87
+ * @returns {object} 当前 reputation 表(byNodeHash)
88
+ */
89
+ export function getReputationTable() {
90
+ return loadReputation()
91
+ }
92
+
93
+ /**
94
+ * @param {object} table - 含 byNodeHash 的信誉表或裸 byNodeHash 对象
95
+ * @returns {void}
96
+ */
97
+ export async function setReputationTable(table) {
98
+ const incoming = table?.byNodeHash && typeof table.byNodeHash === 'object' ? table.byNodeHash : table
99
+ if (!incoming || typeof incoming !== 'object') throw new Error('p2p: setReputationTable requires byNodeHash object')
100
+ await mutateReputation(data => {
101
+ data.byNodeHash = data.byNodeHash || {}
102
+ for (const [nodeHash, row] of Object.entries(incoming)) {
103
+ const id = normalizeHex64(nodeHash)
104
+ if (!id || !isHex64(id)) continue
105
+ const score = Number(row?.score ?? row)
106
+ if (!Number.isFinite(score)) continue
107
+ data.byNodeHash[id] = { ...data.byNodeHash[id] || {}, score }
108
+ }
109
+ applyLocksToReputation(data)
110
+ })
111
+ }
112
+
113
+ /**
114
+ * @param {object} data - reputation 存储对象
115
+ * @returns {void}
116
+ */
117
+ function applyLocksToReputation(data) {
118
+ for (const nodeHash of loadSyncConfig().lockedMaxNodeHashes)
119
+ if (!data.byNodeHash[nodeHash]) data.byNodeHash[nodeHash] = { score: MAX_LOCKED_SCORE }
120
+ else data.byNodeHash[nodeHash].score = MAX_LOCKED_SCORE
121
+ }
122
+
123
+ /**
124
+ * 将节点分数钳到上限;首次 lock 时记下原分,unlock 时还原。
125
+ * @param {string[]} nodeHashes - 要 lock 的节点 hash 列表
126
+ * @returns {Promise<void>}
127
+ */
128
+ export function lockReputationMax(nodeHashes) {
129
+ const config = loadSyncConfig()
130
+ const hashes = normalizeHashList(nodeHashes)
131
+ return mutateReputation(data => {
132
+ data.byNodeHash = data.byNodeHash || {}
133
+ for (const hash of hashes) {
134
+ if (config.lockedMaxNodeHashes.includes(hash)) continue
135
+ const prev = Number(data.byNodeHash[hash]?.score)
136
+ config.lockedMaxPrevByNodeHash[hash] = Number.isFinite(prev) ? prev : 0
137
+ config.lockedMaxNodeHashes.push(hash)
138
+ }
139
+ persistSyncConfig()
140
+ applyLocksToReputation(data)
141
+ })
142
+ }
143
+
144
+ /**
145
+ * 解除上限钳;还原 lock 前记下的分数。
146
+ * @param {string[]} nodeHashes - 要 unlock 的节点 hash 列表
147
+ * @returns {Promise<void>}
148
+ */
149
+ export function unlockReputationMax(nodeHashes) {
150
+ const config = loadSyncConfig()
151
+ const remove = new Set(normalizeHashList(nodeHashes))
152
+ if (!remove.size) return Promise.resolve()
153
+ config.lockedMaxNodeHashes = config.lockedMaxNodeHashes.filter(id => !remove.has(id))
154
+ /** @type {Record<string, number>} */
155
+ const restore = {}
156
+ for (const hash of remove)
157
+ if (Object.hasOwn(config.lockedMaxPrevByNodeHash, hash)) {
158
+ restore[hash] = config.lockedMaxPrevByNodeHash[hash]
159
+ delete config.lockedMaxPrevByNodeHash[hash]
160
+ }
161
+
162
+ persistSyncConfig()
163
+ return mutateReputation(data => {
164
+ data.byNodeHash = data.byNodeHash || {}
165
+ for (const [hash, score] of Object.entries(restore))
166
+ if (!data.byNodeHash[hash]) data.byNodeHash[hash] = { score }
167
+ else data.byNodeHash[hash].score = score
168
+ })
169
+ }
170
+
171
+ /**
172
+ * @returns {string[]} 当前锁定为满分的节点 hash 列表
173
+ */
174
+ export function getReputationLocks() {
175
+ return [...loadSyncConfig().lockedMaxNodeHashes]
176
+ }
177
+
178
+ /**
179
+ * @param {string[]} donors - 允许拉取信誉的 donor 节点
180
+ * @returns {void}
181
+ */
182
+ export function setTrustSyncDonors(donors) {
183
+ loadSyncConfig().trustSyncDonors = normalizeHashList(donors)
184
+ persistSyncConfig()
185
+ }
186
+
187
+ /**
188
+ * @returns {string[]} 当前 trustSyncDonors 副本
189
+ */
190
+ export function getTrustSyncDonors() {
191
+ return [...loadSyncConfig().trustSyncDonors]
192
+ }
193
+
194
+ /**
195
+ * @param {string[]} allowlist - 允许导出本机信誉表的节点
196
+ * @returns {void}
197
+ */
198
+ export function setReputationExportAllowlist(allowlist) {
199
+ loadSyncConfig().reputationExportAllowlist = normalizeHashList(allowlist)
200
+ persistSyncConfig()
201
+ }
202
+
203
+ /**
204
+ * @returns {string[]} 当前 reputationExportAllowlist 副本
205
+ */
206
+ export function getReputationExportAllowlist() {
207
+ return [...loadSyncConfig().reputationExportAllowlist]
208
+ }
209
+
210
+ /**
211
+ * @returns {object} 仅含 score 的导出表
212
+ */
213
+ function exportScoreTable() {
214
+ const rep = loadReputation()
215
+ /** @type {Record<string, { score: number }>} */
216
+ const byNodeHash = {}
217
+ for (const [nodeHash, row] of Object.entries(rep.byNodeHash || {}))
218
+ byNodeHash[nodeHash] = { score: Number(row?.score ?? 0) }
219
+ return { byNodeHash }
220
+ }
221
+
222
+ /**
223
+ * 挂载信誉同步 wire(refcount;处理 rep_sync_req / rep_sync_res)。
224
+ * @returns {() => void} 取消 wire 挂载的 dispose
225
+ */
226
+ export function attachReputationSyncWire() {
227
+ ensureNodeScope()
228
+ if (!getNodeScopeWire()) throw new Error('p2p: attachReputationSyncWire requires node scope wire')
229
+ const dispose = attachNodeScopeFeature('rep_sync', wire => {
230
+ const offs = [
231
+ wire.on('rep_sync_req', (payload, peerId) => {
232
+ const requester = normalizeHex64(peerId)
233
+ if (!requester || !getReputationExportAllowlist().includes(requester)) return
234
+ try {
235
+ wire.send('rep_sync_res', {
236
+ requestId: payload?.requestId,
237
+ ...exportScoreTable(),
238
+ }, peerId)
239
+ }
240
+ catch { /* disconnected */ }
241
+ }),
242
+ wire.on('rep_sync_res', (payload, peerId) => {
243
+ const requestId = String(payload?.requestId || '')
244
+ const pending = pendingPulls.get(requestId)
245
+ if (!pending) return
246
+ if (normalizeHex64(peerId) !== pending.donor) return
247
+ clearTimeout(pending.timer)
248
+ pendingPulls.delete(requestId)
249
+ pending.resolve(payload)
250
+ }),
251
+ ]
252
+ return () => {
253
+ for (const off of offs)
254
+ try { off?.() } catch { /* ignore */ }
255
+ }
256
+ })
257
+ syncWireDisposers.add(dispose)
258
+ return () => {
259
+ if (!syncWireDisposers.delete(dispose)) return
260
+ dispose()
261
+ }
262
+ }
263
+
264
+ /**
265
+ * 卸掉信誉同步 wire(强制清掉全部 ref)。
266
+ * @returns {void}
267
+ */
268
+ export function detachReputationSyncWire() {
269
+ for (const dispose of [...syncWireDisposers]) {
270
+ syncWireDisposers.delete(dispose)
271
+ try { dispose() } catch { /* ignore */ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * 从 donor 拉取信誉表 JSON;不落盘。应用需自行 `setReputationTable`。
277
+ * @param {string} nodeHash - donor 节点 64-hex hash
278
+ * @returns {Promise<object>} donor 返回的信誉表
279
+ */
280
+ export async function pullReputationFromNode(nodeHash) {
281
+ const donor = normalizeHex64(nodeHash)
282
+ if (!donor || !isHex64(donor)) throw new Error('p2p: pullReputationFromNode requires valid nodeHash')
283
+ if (!getTrustSyncDonors().includes(donor))
284
+ throw new Error('p2p: node not in trustSyncDonors')
285
+ attachReputationSyncWire()
286
+ const requestId = randomUUID()
287
+ const resultPromise = new Promise((resolve, reject) => {
288
+ const timer = setTimeout(() => {
289
+ pendingPulls.delete(requestId)
290
+ reject(new Error('p2p: rep_sync timeout'))
291
+ }, pullTimeoutMs)
292
+ pendingPulls.set(requestId, { resolve, reject, timer, donor })
293
+ })
294
+ const ok = await sendToNodeLink(donor, {
295
+ scope: 'node',
296
+ action: 'rep_sync_req',
297
+ payload: { requestId },
298
+ })
299
+ if (!ok) {
300
+ const pending = pendingPulls.get(requestId)
301
+ if (pending) {
302
+ clearTimeout(pending.timer)
303
+ pendingPulls.delete(requestId)
304
+ }
305
+ throw new Error('p2p: rep_sync_req send failed')
306
+ }
307
+ return await resultPromise
308
+ }
309
+
310
+ /**
311
+ * @returns {void}
312
+ */
313
+ export function resetReputationSyncForTests() {
314
+ syncConfig = null
315
+ detachReputationSyncWire()
316
+ pendingPulls.clear()
317
+ pullTimeoutMs = 8000
318
+ }
@@ -0,0 +1,21 @@
1
+ import { getNodeTransportSettings, saveNodeTransportSettings } from './identity.mjs'
2
+
3
+ /** @typedef {'default' | 'low'} RoutingProfile */
4
+
5
+ /**
6
+ * @param {RoutingProfile} profile - `default` 或 `low`(省电)
7
+ * @returns {RoutingProfile} 写入后的当前 profile
8
+ */
9
+ export function setRoutingProfile(profile) {
10
+ if (profile !== 'default' && profile !== 'low')
11
+ throw new Error('p2p: setRoutingProfile expects default|low')
12
+ saveNodeTransportSettings({ batterySaver: profile === 'low' })
13
+ return getRoutingProfile()
14
+ }
15
+
16
+ /**
17
+ * @returns {RoutingProfile} 当前路由 profile
18
+ */
19
+ export function getRoutingProfile() {
20
+ return getNodeTransportSettings().batterySaver ? 'low' : 'default'
21
+ }
@@ -1,32 +1,51 @@
1
1
  import process from 'node:process'
2
2
 
3
- /** @typedef {'none' | 'rewrite-loopback' | 'drop'} MdnsCandidatePolicy */
3
+ /** @typedef {'none' | 'rewrite-loopback' | 'drop'} IceLocalHostnamePolicy */
4
4
 
5
5
  /**
6
6
  * @typedef {{
7
7
  * relayOverride: string[] | null
8
- * mdnsPolicy: MdnsCandidatePolicy
8
+ * iceLocalHostnamePolicy: IceLocalHostnamePolicy
9
9
  * trickleIceOff: boolean
10
10
  * }} SignalingRuntimeConfig
11
11
  */
12
12
 
13
+ const ICE_LOCAL_HOSTNAME_POLICIES = new Set(['none', 'rewrite-loopback', 'drop'])
14
+
13
15
  /**
14
- * 生产默认:win32 丢弃 .local host candidate;其它平台不过滤。
15
- * @returns {SignalingRuntimeConfig} 生产默认信令配置
16
+ * 生产默认:win32 丢弃 `.local` host candidate;其它平台不过滤。
17
+ * @returns {SignalingRuntimeConfig} 默认信令运行时配置
16
18
  */
17
19
  export function defaultSignalingRuntimeConfig() {
18
- const mdnsPolicy = process.platform === 'win32' ? 'drop' : 'none'
20
+ const iceLocalHostnamePolicy = process.platform === 'win32' ? 'drop' : 'none'
19
21
  return {
20
22
  relayOverride: null,
21
- mdnsPolicy,
22
- trickleIceOff: mdnsPolicy !== 'none',
23
+ iceLocalHostnamePolicy,
24
+ trickleIceOff: iceLocalHostnamePolicy !== 'none',
23
25
  }
24
26
  }
25
27
 
26
28
  /**
27
- * 生产默认信令配置;测试通过 initNode({ signaling }) 注入。
28
- * @returns {SignalingRuntimeConfig} 生产默认信令配置
29
+ * @param {Partial<SignalingRuntimeConfig>} [patch] 合并字段
30
+ * @returns {SignalingRuntimeConfig} 合并后的信令运行时配置
29
31
  */
30
- export function resolveSignalingRuntimeConfig() {
31
- return defaultSignalingRuntimeConfig()
32
+ export function resolveSignalingRuntimeConfig(patch = {}) {
33
+ const base = defaultSignalingRuntimeConfig()
34
+ if (!patch || typeof patch !== 'object') return base
35
+ const policyRaw = patch.iceLocalHostnamePolicy
36
+ const iceLocalHostnamePolicy = ICE_LOCAL_HOSTNAME_POLICIES.has(/** @type {string} */ policyRaw)
37
+ ? /** @type {IceLocalHostnamePolicy} */ policyRaw
38
+ : base.iceLocalHostnamePolicy
39
+ let { relayOverride } = base
40
+ if (Object.prototype.hasOwnProperty.call(patch, 'relayOverride'))
41
+ relayOverride = patch.relayOverride == null
42
+ ? null
43
+ : [...new Set((Array.isArray(patch.relayOverride) ? patch.relayOverride : [])
44
+ .map(url => String(url || '').trim())
45
+ .filter(url => url.startsWith('wss://')))]
46
+ return {
47
+ relayOverride,
48
+ iceLocalHostnamePolicy,
49
+ trickleIceOff: patch.trickleIceOff !== undefined ? !!patch.trickleIceOff : iceLocalHostnamePolicy !== 'none',
50
+ }
32
51
  }
package/overlay/index.mjs CHANGED
@@ -6,6 +6,26 @@ import { createLruMap } from '../utils/lru.mjs'
6
6
 
7
7
  const ROUTE_DOMAIN = 'fount-route'
8
8
 
9
+ /** @type {((senderNodeHash: string, action: string) => boolean) | null} */
10
+ let overlayRateGate = null
11
+
12
+ /**
13
+ * 安装 overlay 入站限速门(返回 false 则丢弃)。
14
+ * @param {((senderNodeHash: string, action: string) => boolean) | null} rateGate 返回 false 则丢弃
15
+ * @returns {void}
16
+ */
17
+ export function setOverlayRateGate(rateGate) {
18
+ overlayRateGate = typeof rateGate === 'function' ? rateGate : null
19
+ }
20
+
21
+ /**
22
+ * 清除 overlay 限速门。
23
+ * @returns {void}
24
+ */
25
+ export function clearOverlayRateGate() {
26
+ overlayRateGate = null
27
+ }
28
+
9
29
  /**
10
30
  * 构造 overlay 路由签名用的字节序列。
11
31
  * @param {string} reqId 路由请求 id
@@ -27,7 +47,7 @@ export function createOverlayRouter(registry, ttl = 3) {
27
47
  const selfPubKey = registry.localIdentity.nodePubKey
28
48
  const { secretKey } = registry.localIdentity
29
49
  const seenReqs = createLruMap(4096)
30
- /** @type {Map<string, { resolve: (path: string[]) => void, reject: (err: Error) => void, timer: number }>} */
50
+ /** @type {Map<string, { resolve: (path: string[]) => void, reject: (error: Error) => void, timer: number }>} */
31
51
  const pendingRoutes = new Map()
32
52
  /** @type {Set<(body: unknown, meta: { path: string[], from: string }) => void>} */
33
53
  const relayListeners = new Set()
@@ -52,6 +72,7 @@ export function createOverlayRouter(registry, ttl = 3) {
52
72
  const payload = envelope?.payload
53
73
  const action = envelope?.action || ''
54
74
  if (!payload) return
75
+ if (overlayRateGate && !overlayRateGate(senderNodeHash, action)) return
55
76
  if (action === 'route_req') {
56
77
  const reqId = payload.reqId || ''
57
78
  const target = payload.target || ''
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve02081504/fount-p2p",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
5
5
  "keywords": [
6
6
  "network",
@@ -30,8 +30,13 @@
30
30
  "!sim/**",
31
31
  "!scripts/**"
32
32
  ],
33
+ "bin": {
34
+ "fount-p2p": "./infra/cli.mjs"
35
+ },
33
36
  "exports": {
34
37
  ".": "./index.mjs",
38
+ "./infra": "./infra/service.mjs",
39
+ "./link": "./link/providers/index.mjs",
35
40
  "./core/*": "./core/*.mjs",
36
41
  "./crypto": "./crypto/crypto.mjs",
37
42
  "./crypto/*": "./crypto/*.mjs",
@@ -42,7 +47,12 @@
42
47
  "./discovery/bt": "./discovery/bt/index.mjs",
43
48
  "./discovery/bt/*": "./discovery/bt/*.mjs",
44
49
  "./discovery/*": "./discovery/*.mjs",
45
- "./transport/*": "./transport/*.mjs",
50
+ "./transport/link_registry": "./transport/link_registry.mjs",
51
+ "./transport/user_room": "./transport/user_room.mjs",
52
+ "./transport/group_link_set": "./transport/group_link_set.mjs",
53
+ "./transport/node_scope": "./transport/node_scope.mjs",
54
+ "./transport/room_scopes": "./transport/room_scopes.mjs",
55
+ "./transport/remote_user_room": "./transport/remote_user_room.mjs",
46
56
  "./rooms/*": "./rooms/*.mjs",
47
57
  "./overlay": "./overlay/index.mjs",
48
58
  "./overlay/*": "./overlay/*.mjs",
@@ -62,6 +72,7 @@
62
72
  "dependencies": {
63
73
  "@noble/curves": "latest",
64
74
  "node-datachannel": "latest",
75
+ "on-shutdown": "latest",
65
76
  "ws": "latest"
66
77
  },
67
78
  "devDependencies": {
@@ -1,171 +1,22 @@
1
- import { advertiseTopic, subscribeTopic } from '../discovery/index.mjs'
2
- import { ingestSignedAdvert } from '../transport/advert_ingest.mjs'
3
- import { getLinkRegistry } from '../transport/link_registry.mjs'
4
- import {
5
- encryptSignalPacket,
6
- groupRendezvousTopic,
7
- } from '../transport/signal_crypto.mjs'
8
- import { emitSafe } from '../utils/emit_safe.mjs'
1
+ import { createGroupLinkSet } from '../transport/group_link_set.mjs'
9
2
 
10
3
  /**
11
- * 在指定 scope roomSecret 下创建 link 层房间(discovery + registry 转发)。
12
- * @param {object} options 房间选项
13
- * @param {string} options.scope link registry scope(如 `group:{id}`)
14
- * @param {string} options.roomSecret 房间 rendezvous 密钥
15
- * @param {(nodeHash: string) => boolean} [options.allowNode] 是否允许与某 nodeHash 通信
16
- * @returns {{ start: () => Promise<void>, leave: () => Promise<void>, makeAction: (name: string) => [(payload: unknown, peerId?: string | string[] | null) => Promise<void>, (handler: (payload: unknown, peerId: string) => void) => void], onPeerJoin: (callback: (peerId: string) => void) => () => void, onPeerLeave: (callback: (peerId: string) => void) => () => void, getPeers: () => Record<string, true> }} 房间句柄
4
+ * scoped 房间:group_link_set 的薄预设(任意 scope + allowNode + 发现即拨)。
5
+ * @param {object} options - 房间选项
6
+ * @param {string} options.scope - link registry scope
7
+ * @param {string} options.roomSecret - rendezvous 密钥
8
+ * @param {(nodeHash: string) => boolean} [options.allowNode] - 节点准入过滤
9
+ * @returns {ReturnType<typeof createGroupLinkSet>} scoped link 房间句柄
17
10
  */
18
11
  export function createScopedLinkRoom(options) {
19
- const registry = getLinkRegistry()
20
- const { scope } = options
21
- const topic = groupRendezvousTopic(options.roomSecret)
22
- const allowNode = options.allowNode ?? (() => true)
23
- /** @type {Set<string>} */
24
- const discoveredPeers = new Set()
25
- /** @type {Set<string>} */
26
- const announcedPeers = new Set()
27
- /** @type {Set<(peerId: string) => void>} */
28
- const joinListeners = new Set()
29
- /** @type {Set<(peerId: string) => void>} */
30
- const leaveListeners = new Set()
31
- /** @type {Set<() => void>} */
32
- const cleanups = new Set()
33
- /** @type {Map<string, { handler: ((payload: unknown, peerId: string) => void) | null, backlog: Array<{ payload: unknown, peerId: string }> }>} */
34
- const actionEntries = new Map()
35
-
36
- /**
37
- * @returns {string[]} 当前已连接且通过 allowNode 过滤的 peer nodeHash 列表
38
- */
39
- function activePeerIds() {
40
- return [...discoveredPeers].filter(nodeHash => allowNode(nodeHash) && registry.getLink(nodeHash))
41
- }
42
-
43
- /**
44
- * @param {string} peerId 节点 hash
45
- * @returns {void}
46
- */
47
- function notePeerJoin(peerId) {
48
- if (!peerId || announcedPeers.has(peerId)) return
49
- announcedPeers.add(peerId)
50
- emitSafe(joinListeners, peerId)
51
- }
52
-
53
- /**
54
- * @param {string} peerId 节点 hash
55
- * @returns {void}
56
- */
57
- function notePeerLeave(peerId) {
58
- if (!peerId || !announcedPeers.has(peerId)) return
59
- announcedPeers.delete(peerId)
60
- emitSafe(leaveListeners, peerId)
61
- }
62
-
63
- /**
64
- * @param {string} name action 名称
65
- * @returns {{ handler: ((payload: unknown, peerId: string) => void) | null, backlog: Array<{ payload: unknown, peerId: string }> }} action 槽(含待处理 backlog)
66
- */
67
- function getActionEntry(name) {
68
- if (!actionEntries.has(name))
69
- actionEntries.set(name, { handler: null, backlog: [] })
70
- return actionEntries.get(name)
71
- }
72
-
73
- return {
74
- /**
75
- * @returns {Promise<void>}
76
- */
77
- async start() {
78
- await registry.ensureRuntime()
79
- cleanups.add(registry.subscribeScope(scope, (senderNodeHash, envelope) => {
80
- if (!allowNode(senderNodeHash)) return
81
- const entry = actionEntries.get(envelope.action)
82
- if (!entry) return
83
- if (entry.handler) entry.handler(envelope.payload, senderNodeHash)
84
- else entry.backlog.push({ payload: envelope.payload, peerId: senderNodeHash })
85
- }))
86
- cleanups.add(registry.onLinkUp(nodeHash => {
87
- if (!discoveredPeers.has(nodeHash) || !allowNode(nodeHash)) return
88
- notePeerJoin(nodeHash)
89
- }))
90
- cleanups.add(registry.onLinkDown(nodeHash => {
91
- if (!discoveredPeers.has(nodeHash)) return
92
- notePeerLeave(nodeHash)
93
- }))
94
- cleanups.add(await subscribeTopic(topic, async (bytes, meta) => {
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)
100
- }))
101
- cleanups.add(await advertiseTopic(topic, encryptSignalPacket(topic, {
102
- type: 'advert',
103
- body: await registry.buildLocalAdvert(topic),
104
- })))
105
- for (const peerId of activePeerIds())
106
- notePeerJoin(peerId)
107
- },
108
- /**
109
- * @returns {Promise<void>}
110
- */
111
- async leave() {
112
- for (const cleanup of cleanups)
113
- try { cleanup() } catch { /* ignore */ }
114
- cleanups.clear()
115
- for (const peerId of [...announcedPeers])
116
- notePeerLeave(peerId)
117
- },
118
- /**
119
- * @param {string} name action 名称
120
- * @returns {[(payload: unknown, peerId?: string | string[] | null) => Promise<void>, (handler: (payload: unknown, peerId: string) => void) => void]} [send, onReceive] 发送与订阅元组
121
- */
122
- makeAction(name) {
123
- return [
124
- async (payload, peerId = null) => {
125
- if (Array.isArray(peerId)) {
126
- await Promise.all(peerId.map(targetPeerId =>
127
- registry.sendToNodeLink(targetPeerId, { scope, action: name, payload })))
128
- return
129
- }
130
- if (peerId)
131
- await registry.sendToNodeLink(peerId, { scope, action: name, payload })
132
- else
133
- await Promise.all(activePeerIds().map(targetPeerId =>
134
- registry.sendToNodeLink(targetPeerId, { scope, action: name, payload })))
135
- },
136
- handler => {
137
- const entry = getActionEntry(name)
138
- entry.handler = handler
139
- for (const pending of entry.backlog.splice(0))
140
- handler(pending.payload, pending.peerId)
141
- },
142
- ]
143
- },
144
- /**
145
- * @param {(peerId: string) => void} callback 新 peer 上线回调
146
- * @returns {() => void} 取消订阅
147
- */
148
- onPeerJoin(callback) {
149
- joinListeners.add(callback)
150
- for (const peerId of activePeerIds())
151
- announcedPeers.add(peerId)
152
- for (const peerId of announcedPeers)
153
- try { callback(peerId) } catch { /* ignore */ }
154
- return () => joinListeners.delete(callback)
155
- },
156
- /**
157
- * @param {(peerId: string) => void} callback peer 离线回调
158
- * @returns {() => void} 取消订阅
159
- */
160
- onPeerLeave(callback) {
161
- leaveListeners.add(callback)
162
- return () => leaveListeners.delete(callback)
163
- },
164
- /**
165
- * @returns {Record<string, true>} 当前活跃 peer 的 nodeHash 集合
166
- */
167
- getPeers() {
168
- return Object.fromEntries(activePeerIds().map(peerId => [peerId, true]))
169
- },
170
- }
12
+ const { scope, roomSecret, allowNode } = options
13
+ return createGroupLinkSet({
14
+ groupId: scope,
15
+ scope,
16
+ roomSecret,
17
+ members: [],
18
+ allowNode,
19
+ dialAll: true,
20
+ autoconnect: true,
21
+ })
171
22
  }