@steve02081504/fount-p2p 0.0.46 → 0.0.48
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/AGENTS.md +13 -1
- package/discovery/nostr/constants.mjs +1 -3
- package/discovery/nostr/index.mjs +23 -11
- package/discovery/nostr/session.mjs +96 -12
- package/docs/signaling.md +14 -3
- package/docs/transports.md +15 -1
- package/index.mjs +5 -0
- package/link/providers/ble_gatt.mjs +2 -2
- package/link/providers/index.mjs +2 -1
- package/link/providers/nostr/index.mjs +19 -6
- package/link/providers/webrtc.mjs +235 -56
- package/link/rtc/polyfill.mjs +9 -6
- package/node/signaling_config.mjs +30 -8
- package/node/verification.mjs +163 -0
- package/package.json +4 -2
- package/sim/AGENTS.md +1 -1
- package/sim/{cold_start.md → docs/cold_start.md} +1 -1
- package/transport/link_registry.mjs +75 -6
- package/transport/peer_health.mjs +43 -25
- package/transport/runtime_bootstrap.mjs +44 -9
- package/pages/index.html +0 -36
- package/pages/index.mjs +0 -66
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steve02081504/fount-p2p",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.48",
|
|
4
4
|
"description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"network",
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"test:fount": "deno test -A --config deno.json test/fount/",
|
|
26
26
|
"test:live": "node --test --test-concurrency=1 --test-force-exit test/live/*.test.mjs",
|
|
27
27
|
"test:sim": "node --test --test-concurrency=1 --test-force-exit sim/test/*.test.mjs",
|
|
28
|
-
"test:frontend": "node --test --test-concurrency=1 --test-force-exit test/frontend/*.test.mjs"
|
|
28
|
+
"test:frontend": "node --test --test-concurrency=1 --test-force-exit test/frontend/*.test.mjs",
|
|
29
|
+
"test:checks": "node --test --test-concurrency=1 --test-force-exit test/pure/text_lf.test.mjs test/pure/jsdoc_no_english.test.mjs test/pure/agents_md_english.test.mjs",
|
|
30
|
+
"check:text_lf:fix": "node scripts/checks/text_lf.mjs --fix .."
|
|
29
31
|
},
|
|
30
32
|
"bin": {
|
|
31
33
|
"fount-p2p": "infra/cli.mjs"
|
package/sim/AGENTS.md
CHANGED
|
@@ -15,7 +15,7 @@ In-process co-evolution of **tunables** (`*.tunables.json`) against an **attack
|
|
|
15
15
|
- **`PARAM_SPACE` ↔ defaults:** every `PARAM_SPACE` key must exist in `loadDefaultTunables()`; clear both sides when deleting a key.
|
|
16
16
|
- **`socialRepHideThreshold`:** hide when `score < threshold`. Default `0` (suppress negatives only). Raise the threshold to raise `falsePositiveRate` — never use a negative threshold for "stricter".
|
|
17
17
|
- **Heuristic proxy** (not the real path): `model.mjs`, `discovery.mjs`, `transport.mjs`, `integrity.mjs` — analytical "params → defense" only.
|
|
18
|
-
- **Mesh-first:** explore slots and cold-start join are mandatory; fanout/trust stay separate from link presence. Policy: [docs/mesh.md](../docs/mesh.md). Wiring: [cold_start.md](cold_start.md).
|
|
18
|
+
- **Mesh-first:** explore slots and cold-start join are mandatory; fanout/trust stay separate from link presence. Policy: [docs/mesh.md](../docs/mesh.md). Wiring: [docs/cold_start.md](docs/cold_start.md).
|
|
19
19
|
|
|
20
20
|
## Anti-drift
|
|
21
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sim cold-start (K = 0)
|
|
2
2
|
|
|
3
|
-
Mesh policy: [
|
|
3
|
+
Mesh policy: [mesh.md](../../docs/mesh.md). Day-to-day sim rules: [AGENTS.md](../AGENTS.md).
|
|
4
4
|
|
|
5
5
|
When the observer has no acquaintances (`trustedPeers=[]`), the sim must still join via discovery explore slots — not idle.
|
|
6
6
|
|
|
@@ -25,6 +25,24 @@ import { createRuntimeBootstrap } from './runtime_bootstrap.mjs'
|
|
|
25
25
|
const DIAL_COOLDOWN_BASE_MS = ms('30s')
|
|
26
26
|
/** dial 冷却上限。 */
|
|
27
27
|
const DIAL_COOLDOWN_MAX_MS = ms('10m')
|
|
28
|
+
/**
|
|
29
|
+
* 单次确保直连的整体上限:覆盖全部 provider 尝试(含 offer/answer 握手)后必须结算。
|
|
30
|
+
* 必须大于 nostr 入队 publish 等待上限,让「relay 连不上」走 provider 自身的失败路径,
|
|
31
|
+
* 而不是在这里被掐断(否则拿不到 provider 级诊断)。见 fount-p2p#37。
|
|
32
|
+
*/
|
|
33
|
+
export const LINK_DIAL_DEADLINE_MS = ms('90s')
|
|
34
|
+
|
|
35
|
+
/** 测试可缩短的单次直连上限。 */
|
|
36
|
+
let dialDeadlineMs = LINK_DIAL_DEADLINE_MS
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 覆盖单次直连上限(测试用;传 null 恢复默认)。
|
|
40
|
+
* @param {number | null} value 毫秒上限;null 恢复默认
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
export function setLinkDialDeadlineMsForTests(value) {
|
|
44
|
+
dialDeadlineMs = value == null ? LINK_DIAL_DEADLINE_MS : value
|
|
45
|
+
}
|
|
28
46
|
|
|
29
47
|
/**
|
|
30
48
|
* 解析或从节点种子推导本地身份。
|
|
@@ -263,8 +281,58 @@ export function createLinkRegistry(options = {}) {
|
|
|
263
281
|
return links.get(remoteNodeHash) ?? null
|
|
264
282
|
}
|
|
265
283
|
|
|
284
|
+
/**
|
|
285
|
+
* 记一次 dial 失败并进入指数退避。
|
|
286
|
+
* @param {string} normalized 远端节点 64 hex
|
|
287
|
+
* @returns {number} 本次冷却毫秒
|
|
288
|
+
*/
|
|
289
|
+
function noteDialFailure(normalized) {
|
|
290
|
+
const failures = (dialCooldown.get(normalized)?.failures || 0) + 1
|
|
291
|
+
const delay = Math.min(DIAL_COOLDOWN_MAX_MS, DIAL_COOLDOWN_BASE_MS * (2 ** Math.min(failures - 1, 5)))
|
|
292
|
+
dialCooldown.touch(normalized, { until: Date.now() + delay, failures })
|
|
293
|
+
return delay
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 为一个已登记的直连尝试套上整体上限:超时即返回 null(调用方不再等),
|
|
298
|
+
* 后台尝试照旧收敛,成功仍会注册链路。
|
|
299
|
+
*
|
|
300
|
+
* 超时后立刻从 inflights 摘除,后续 dial 走新尝试而不是复用这个卡住的 promise。
|
|
301
|
+
* @param {string} normalized 远端节点 64 hex
|
|
302
|
+
* @param {Promise<object | null>} task 进行中的 dial
|
|
303
|
+
* @returns {Promise<object | null>} dial 结果;超时 null
|
|
304
|
+
*/
|
|
305
|
+
async function awaitDialAttempt(normalized, task) {
|
|
306
|
+
if (dialDeadlineMs <= 0) return await task
|
|
307
|
+
/** @type {boolean} */
|
|
308
|
+
let expired = false
|
|
309
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
310
|
+
let timer = null
|
|
311
|
+
const timeout = new Promise(resolve => {
|
|
312
|
+
timer = setTimeout(() => {
|
|
313
|
+
expired = true
|
|
314
|
+
resolve(null)
|
|
315
|
+
}, dialDeadlineMs)
|
|
316
|
+
timer.unref?.()
|
|
317
|
+
})
|
|
318
|
+
const link = await Promise.race([task, timeout])
|
|
319
|
+
if (timer) clearTimeout(timer)
|
|
320
|
+
if (!expired) return link
|
|
321
|
+
// 超时:调用方不再等,但后台尝试仍会自行收敛(成功照样注册链路,失败自己记冷却)。
|
|
322
|
+
if (inflights.get(normalized) === task) inflights.delete(normalized)
|
|
323
|
+
const cooldownMs = noteDialFailure(normalized)
|
|
324
|
+
nodeDebug('p2p:dial deadline', {
|
|
325
|
+
peer: shortHash(normalized),
|
|
326
|
+
deadlineMs: dialDeadlineMs,
|
|
327
|
+
cooldownMs,
|
|
328
|
+
})
|
|
329
|
+
return null
|
|
330
|
+
}
|
|
331
|
+
|
|
266
332
|
/**
|
|
267
333
|
* 按 level 降序尝试各 LinkProvider,不可用/不可达/失败则回落。
|
|
334
|
+
* 整体有 LINK_DIAL_DEADLINE_MS 上限:卡住的 provider 既不能让调用方永久等待,
|
|
335
|
+
* 也不能把坏 promise 留在 inflights 里毒化后续 dial。
|
|
268
336
|
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
269
337
|
* @returns {Promise<object | null>} 链路实例;失败时 null
|
|
270
338
|
*/
|
|
@@ -274,7 +342,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
274
342
|
const normalized = remoteNodeHash
|
|
275
343
|
if (!normalized || normalized === localIdentity.nodeHash) return null
|
|
276
344
|
if (links.has(normalized)) return links.get(normalized)
|
|
277
|
-
if (inflights.has(normalized)) return await inflights.get(normalized)
|
|
345
|
+
if (inflights.has(normalized)) return await awaitDialAttempt(normalized, inflights.get(normalized))
|
|
278
346
|
const cool = dialCooldown.get(normalized)
|
|
279
347
|
const now = Date.now()
|
|
280
348
|
if (cool) {
|
|
@@ -335,14 +403,12 @@ export function createLinkRegistry(options = {}) {
|
|
|
335
403
|
})
|
|
336
404
|
}
|
|
337
405
|
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
dialCooldown.touch(normalized, { until: Date.now() + delay, failures })
|
|
341
|
-
nodeDebug('p2p:dial exhausted', { peer: shortHash(normalized), cooldownMs: delay, failures })
|
|
406
|
+
const cooldownMs = noteDialFailure(normalized)
|
|
407
|
+
nodeDebug('p2p:dial exhausted', { peer: shortHash(normalized), cooldownMs })
|
|
342
408
|
return null
|
|
343
409
|
})().finally(() => inflights.delete(normalized))
|
|
344
410
|
inflights.set(normalized, task)
|
|
345
|
-
return await task
|
|
411
|
+
return await awaitDialAttempt(normalized, task)
|
|
346
412
|
}
|
|
347
413
|
|
|
348
414
|
meshKeepalive = createMeshKeepalive({
|
|
@@ -387,9 +453,12 @@ export function createLinkRegistry(options = {}) {
|
|
|
387
453
|
|
|
388
454
|
/**
|
|
389
455
|
* 启动 discovery/link runtime 并开启 mesh keepalive。
|
|
456
|
+
* 顺带重启 peer health 订阅:`shutdown()` 会停掉它,重启运行时后必须重新挂上,
|
|
457
|
+
* 否则 getPeerHealth / listPeerHealth 会永远为空(见 fount-p2p#38)。
|
|
390
458
|
*/
|
|
391
459
|
const ensureRuntimeWithMesh = async () => {
|
|
392
460
|
await bootstrap.ensureRuntime()
|
|
461
|
+
peerHealth.start()
|
|
393
462
|
meshKeepalive?.start()
|
|
394
463
|
}
|
|
395
464
|
|
|
@@ -19,6 +19,7 @@ import { emitSafe } from '../utils/emit_safe.mjs'
|
|
|
19
19
|
* getPeerHealth: (nodeHash: string) => PeerHealthEntry | null
|
|
20
20
|
* listPeerHealth: () => PeerHealthEntry[]
|
|
21
21
|
* onPeerHealth: (listener: (nodeHash: string, entry: PeerHealthEntry) => void) => () => void
|
|
22
|
+
* start: () => void
|
|
22
23
|
* stop: () => void
|
|
23
24
|
* }} peer health 聚合器
|
|
24
25
|
*/
|
|
@@ -29,6 +30,10 @@ export function createPeerHealthTracker(registry) {
|
|
|
29
30
|
const cleanups = new Map()
|
|
30
31
|
/** @type {Set<(nodeHash: string, entry: PeerHealthEntry) => void>} */
|
|
31
32
|
const listeners = new Set()
|
|
33
|
+
/** @type {(() => void) | null} registry link up 退订 */
|
|
34
|
+
let stopUp = null
|
|
35
|
+
/** @type {(() => void) | null} registry link down 退订 */
|
|
36
|
+
let stopDown = null
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
39
|
* 合并补丁并通知订阅方。
|
|
@@ -54,37 +59,48 @@ export function createPeerHealthTracker(registry) {
|
|
|
54
59
|
emitSafe(listeners, nodeHash, entry)
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
/**
|
|
63
|
+
* 订阅 registry 的链路事件(幂等)。
|
|
64
|
+
* `stop()` 之后可用它重新挂上监听:registry 的 link up/down 桶在 shutdown 时不会被清空,
|
|
65
|
+
* 所以重启运行时后健康记录能自然恢复。
|
|
66
|
+
* @returns {void}
|
|
67
|
+
*/
|
|
68
|
+
function start() {
|
|
69
|
+
if (stopUp) return
|
|
70
|
+
stopUp = registry.onLinkUp?.((nodeHash, link) => {
|
|
71
|
+
if (!nodeHash) return
|
|
72
|
+
const stopRtt = link?.onRtt?.(() => {
|
|
73
|
+
const stats = link.stats?.() ?? {}
|
|
74
|
+
update(nodeHash, {
|
|
75
|
+
rttMs: stats.rttMs ?? null,
|
|
76
|
+
avgRttMs: stats.avgRttMs ?? null,
|
|
77
|
+
lastSeenAt: Date.now(),
|
|
78
|
+
})
|
|
79
|
+
}) ?? null
|
|
80
|
+
const stopLinkDown = link?.onDown?.(() => {
|
|
81
|
+
cleanups.get(nodeHash)?.()
|
|
82
|
+
cleanups.delete(nodeHash)
|
|
83
|
+
update(nodeHash, { connected: false, lastSeenAt: Date.now() })
|
|
84
|
+
}) ?? null
|
|
61
85
|
update(nodeHash, {
|
|
62
|
-
|
|
63
|
-
|
|
86
|
+
connected: true,
|
|
87
|
+
source: link?.providerId ?? null,
|
|
64
88
|
lastSeenAt: Date.now(),
|
|
65
89
|
})
|
|
90
|
+
cleanups.set(nodeHash, () => {
|
|
91
|
+
stopRtt?.()
|
|
92
|
+
stopLinkDown?.()
|
|
93
|
+
})
|
|
66
94
|
}) ?? null
|
|
67
|
-
|
|
95
|
+
stopDown = registry.onLinkDown?.(nodeHash => {
|
|
96
|
+
if (!nodeHash) return
|
|
68
97
|
cleanups.get(nodeHash)?.()
|
|
69
98
|
cleanups.delete(nodeHash)
|
|
70
99
|
update(nodeHash, { connected: false, lastSeenAt: Date.now() })
|
|
71
100
|
}) ?? null
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
lastSeenAt: Date.now(),
|
|
76
|
-
})
|
|
77
|
-
cleanups.set(nodeHash, () => {
|
|
78
|
-
stopRtt?.()
|
|
79
|
-
stopDown?.()
|
|
80
|
-
})
|
|
81
|
-
}) ?? null
|
|
82
|
-
const stopDown = registry.onLinkDown?.(nodeHash => {
|
|
83
|
-
if (!nodeHash) return
|
|
84
|
-
cleanups.get(nodeHash)?.()
|
|
85
|
-
cleanups.delete(nodeHash)
|
|
86
|
-
update(nodeHash, { connected: false, lastSeenAt: Date.now() })
|
|
87
|
-
}) ?? null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
start()
|
|
88
104
|
|
|
89
105
|
return {
|
|
90
106
|
/**
|
|
@@ -108,17 +124,19 @@ export function createPeerHealthTracker(registry) {
|
|
|
108
124
|
listeners.add(listener)
|
|
109
125
|
return () => listeners.delete(listener)
|
|
110
126
|
},
|
|
127
|
+
start,
|
|
111
128
|
/**
|
|
112
|
-
*
|
|
129
|
+
* 停止监听并清空聚合(listen 订阅保留,listeners 订阅者保留)。
|
|
113
130
|
* @returns {void}
|
|
114
131
|
*/
|
|
115
132
|
stop() {
|
|
116
133
|
stopUp?.()
|
|
117
134
|
stopDown?.()
|
|
135
|
+
stopUp = null
|
|
136
|
+
stopDown = null
|
|
118
137
|
for (const cleanup of cleanups.values()) cleanup()
|
|
119
138
|
cleanups.clear()
|
|
120
139
|
entries.clear()
|
|
121
|
-
listeners.clear()
|
|
122
140
|
},
|
|
123
141
|
}
|
|
124
142
|
}
|
|
@@ -28,6 +28,15 @@ import { createWebRtcLinkProvider } from '../link/providers/webrtc.mjs'
|
|
|
28
28
|
import { getSignalingRuntimeConfig, onNodeChange } from '../node/instance.mjs'
|
|
29
29
|
import { isConnectivityDebug, nodeDebug, shortHash } from '../node/log.mjs'
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
|
|
33
|
+
* @returns {boolean} 是否由本 registry 自己持有(lan_tcp / ble_gatt 走各自的快速监听路径)
|
|
34
|
+
*/
|
|
35
|
+
function providerIsRegistryOwned(provider) {
|
|
36
|
+
const { id } = provider
|
|
37
|
+
return id.startsWith('lan_tcp') || id.startsWith('ble_gatt')
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
/**
|
|
32
41
|
* @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
|
|
33
42
|
* @returns {boolean} 是否使用原生 probe 路径
|
|
@@ -36,6 +45,16 @@ export function providerHasNativeProbe(provider) {
|
|
|
36
45
|
return provider.caps?.probe === 'native'
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
/**
|
|
49
|
+
* @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
|
|
50
|
+
* @returns {boolean} 是否需要在 runtime 暖机时补挂监听
|
|
51
|
+
*/
|
|
52
|
+
export function providerNeedsListening(provider) {
|
|
53
|
+
if (providerIsRegistryOwned(provider)) return false
|
|
54
|
+
if (providerHasNativeProbe(provider)) return false
|
|
55
|
+
return !!provider.ensureListening
|
|
56
|
+
}
|
|
57
|
+
|
|
39
58
|
/**
|
|
40
59
|
* @param {import('../link/providers/index.mjs').LinkProvider | null} ownedLanTcp 本 registry 持有的 lan_tcp
|
|
41
60
|
* @returns {import('../link/providers/index.mjs').LinkProvider[]} 可快速启动监听的 provider 列表
|
|
@@ -45,10 +64,8 @@ export function collectFastListenProviders(ownedLanTcp) {
|
|
|
45
64
|
const listenProviders = []
|
|
46
65
|
if (ownedLanTcp) listenProviders.push(ownedLanTcp)
|
|
47
66
|
for (const provider of listLinkProviders()) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!provider.ensureListening) continue
|
|
51
|
-
if (providerHasNativeProbe(provider)) continue
|
|
67
|
+
if (!providerNeedsListening(provider)) continue
|
|
68
|
+
// 快速路径只接同步 isAvailable:要等异步探测的 provider 交给 startEnabledProviderListening。
|
|
52
69
|
if (provider.isAvailable)
|
|
53
70
|
try {
|
|
54
71
|
const available = provider.isAvailable()
|
|
@@ -202,6 +219,24 @@ export function createRuntimeBootstrap(deps) {
|
|
|
202
219
|
return encryptAdvertForScope('network', localIdentity, body)
|
|
203
220
|
}
|
|
204
221
|
|
|
222
|
+
/**
|
|
223
|
+
* 为所有「已注册、已启用、尚未监听」的 link provider 启动监听。
|
|
224
|
+
* `reconcileLinkProviders()` 重新注册的 provider 只有走到这里才会拿到 onInbound/localIdentity,
|
|
225
|
+
* 否则它会静默丢弃全部入站 link-open(见 fount-p2p#38)。
|
|
226
|
+
*
|
|
227
|
+
* 刻意不看 `isAvailable()`:relay 池为空/未探测时 nostr 的 `isAvailable()` 会是 false,而 relay 列表
|
|
228
|
+
* 是在发送时解析的;先挂上监听才能收到入站 link-open,暂时不可用不该让它变哑巴。
|
|
229
|
+
* @returns {Promise<void>}
|
|
230
|
+
*/
|
|
231
|
+
async function startEnabledProviderListening() {
|
|
232
|
+
if (!autoRegisterLinkProviders) return
|
|
233
|
+
await Promise.all(listLinkProviders()
|
|
234
|
+
.filter(provider => isChannelEnabled(provider.id.split(':')[0]))
|
|
235
|
+
.filter(provider => providerNeedsListening(provider))
|
|
236
|
+
.filter(provider => !stopLinkListeners.has(provider.id))
|
|
237
|
+
.map(provider => startProviderListening(provider)))
|
|
238
|
+
}
|
|
239
|
+
|
|
205
240
|
/**
|
|
206
241
|
* @param {number} gen 启动世代
|
|
207
242
|
* @returns {Promise<void>}
|
|
@@ -212,6 +247,8 @@ export function createRuntimeBootstrap(deps) {
|
|
|
212
247
|
signalListenReady = (async () => {
|
|
213
248
|
await lanListenReady.catch(() => { })
|
|
214
249
|
if (generation !== gen || !isLive()) return
|
|
250
|
+
await startEnabledProviderListening().catch(() => { })
|
|
251
|
+
if (generation !== gen || !isLive()) return
|
|
215
252
|
if (!listDiscoveryProviders().length) return
|
|
216
253
|
stopSignalListener = await listenNodeSignals(localIdentity.nodeHash, bytes => {
|
|
217
254
|
void handleIncomingSignal(bytes).catch(() => { })
|
|
@@ -324,11 +361,9 @@ export function createRuntimeBootstrap(deps) {
|
|
|
324
361
|
reconcileLinkProviders()
|
|
325
362
|
reconcileDiscoveryProviders()
|
|
326
363
|
if (generation !== gen || !isLive()) return
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
&& await Promise.resolve(ownedBleGatt.isAvailable()))
|
|
331
|
-
await startProviderListening(ownedBleGatt)
|
|
364
|
+
// 覆盖本轮新注册/重新注册的 provider(例如关掉再打开 nostr):只补 ownedLanTcp/BLE
|
|
365
|
+
// 会让新 provider 永远拿不到 onInbound(见 fount-p2p#38)。
|
|
366
|
+
await startEnabledProviderListening()
|
|
332
367
|
signalListenReady = (async () => {
|
|
333
368
|
if (generation !== gen || !isLive()) return
|
|
334
369
|
if (!listDiscoveryProviders().length) return
|
package/pages/index.html
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>fount 人口统计</title>
|
|
7
|
-
<style>
|
|
8
|
-
:root { color-scheme: light dark; }
|
|
9
|
-
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 3rem auto; padding: 0 1rem; line-height: 1.6; }
|
|
10
|
-
h1 { font-size: 1.4rem; }
|
|
11
|
-
.card { border: 1px solid #8884; border-radius: 8px; padding: 1rem 1.25rem; margin: 1rem 0; }
|
|
12
|
-
.metric { font-size: 2rem; font-weight: 700; }
|
|
13
|
-
label { display: flex; align-items: center; gap: .5rem; margin: 1rem 0; }
|
|
14
|
-
#status { opacity: .7; font-size: .9rem; }
|
|
15
|
-
code { background: #8882; padding: .1em .4em; border-radius: 4px; }
|
|
16
|
-
</style>
|
|
17
|
-
</head>
|
|
18
|
-
<body>
|
|
19
|
-
<h1>fount 节点人口统计</h1>
|
|
20
|
-
<p>基于 Nostr census 事件(kind 30789,<code>t=fount</code> / <code>x=census</code>)的采样估计。监听逻辑由包内 <code>census_monitor</code> 封装:自动监听默认/传入 relay 并经 NIP-66 发现更多,取人口最大的 relay 作为显示源。在线节点数 = <code>Σ(1/p)</code>(Horvitz–Thompson 估计)。</p>
|
|
21
|
-
<label>
|
|
22
|
-
<input type="checkbox" id="toggle" checked>
|
|
23
|
-
人口统计(census)
|
|
24
|
-
</label>
|
|
25
|
-
<div class="card">
|
|
26
|
-
<div>在线节点数估计</div>
|
|
27
|
-
<div class="metric" id="estimate">0</div>
|
|
28
|
-
<div id="status">初始化…</div>
|
|
29
|
-
</div>
|
|
30
|
-
<div class="card">
|
|
31
|
-
<div>采样事件数 <span id="sample-size">0</span> · 窗口内事件 <span id="window-events">0</span></div>
|
|
32
|
-
</div>
|
|
33
|
-
<footer id="footer" style="opacity:.6;font-size:.85rem">通过 URL 参数 ?relay=wss://... 指定中继;缺省监听默认 relay。</footer>
|
|
34
|
-
<script type="module" src="./index.mjs"></script>
|
|
35
|
-
</body>
|
|
36
|
-
</html>
|
package/pages/index.mjs
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* fount 人口统计前端(薄壳,逻辑由包内 `census_monitor` 封装)。
|
|
3
|
-
* - `createPopulationMonitor` 自动监听全部默认(或传入)relay、经 NIP-66 发现更多,
|
|
4
|
-
* 并取人口估计最大的 relay 作为显示源,把 `{ estimate, sampleSize, eventsInWindow, relayUrl, relays }`
|
|
5
|
-
* 快照连同 relay 地址传给 onUpdate。
|
|
6
|
-
* - 页面只负责:导入监控器、传显示函数与可选 relay 地址。
|
|
7
|
-
*/
|
|
8
|
-
import { createPopulationMonitor } from 'https://esm.sh/@steve02081504/fount-p2p/discovery/nostr/census_monitor.mjs'
|
|
9
|
-
|
|
10
|
-
const statusEl = document.querySelector('#status')
|
|
11
|
-
const toggleEl = document.querySelector('#toggle')
|
|
12
|
-
|
|
13
|
-
const params = new URLSearchParams(location.search)
|
|
14
|
-
const relayUrl = params.get('relay')
|
|
15
|
-
|
|
16
|
-
/** @type {{ stop: () => void } | null} */
|
|
17
|
-
let monitor = null
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @param {{ estimate: number, sampleSize: number, eventsInWindow: number }} snapshot 快照
|
|
21
|
-
* @returns {void}
|
|
22
|
-
*/
|
|
23
|
-
function render(snapshot) {
|
|
24
|
-
document.querySelector('#estimate').textContent = snapshot.estimate.toLocaleString('zh-CN', { maximumFractionDigits: 1 })
|
|
25
|
-
document.querySelector('#sample-size').textContent = String(snapshot.sampleSize)
|
|
26
|
-
document.querySelector('#window-events').textContent = String(snapshot.eventsInWindow)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** @returns {void} 启动 live 监控(默认/传入 relay 全交给库) */
|
|
30
|
-
function startLive() {
|
|
31
|
-
if (monitor) return
|
|
32
|
-
try {
|
|
33
|
-
monitor = createPopulationMonitor({
|
|
34
|
-
relays: relayUrl ? [relayUrl] : undefined,
|
|
35
|
-
/**
|
|
36
|
-
* @param {{ estimate: number, sampleSize: number, eventsInWindow: number, relayUrl: string, relays: number }} snapshot 人口快照
|
|
37
|
-
* @returns {void}
|
|
38
|
-
*/
|
|
39
|
-
onUpdate: snapshot => {
|
|
40
|
-
render(snapshot)
|
|
41
|
-
statusEl.textContent = `监听 ${snapshot.relayUrl}(census,共 ${snapshot.relays} 个 relay)`
|
|
42
|
-
},
|
|
43
|
-
})
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
statusEl.textContent = `中继配置无效:${String(error?.message || error)}`
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** @returns {void} 停止监控 */
|
|
51
|
-
function stopLive() {
|
|
52
|
-
monitor?.stop()
|
|
53
|
-
monitor = null
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
toggleEl.addEventListener('change', () => {
|
|
57
|
-
if (!toggleEl.checked) {
|
|
58
|
-
stopLive()
|
|
59
|
-
render({ estimate: 0, sampleSize: 0, eventsInWindow: 0 })
|
|
60
|
-
statusEl.textContent = '已关闭'
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
startLive()
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
if (toggleEl.checked) startLive()
|