@steve02081504/fount-p2p 0.0.44 → 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.
@@ -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
- const { id } = provider
49
- if (id.startsWith('lan_tcp') || id.startsWith('ble_gatt')) continue
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
- if (ownedLanTcp && !stopLinkListeners.has(ownedLanTcp.id))
328
- await startProviderListening(ownedLanTcp)
329
- if (ownedBleGatt && !stopLinkListeners.has(ownedBleGatt.id)
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
@@ -25,13 +25,21 @@ function mutexState(lockKey) {
25
25
  */
26
26
  function releaseMutex(lockKey, state) {
27
27
  const next = state.queue.shift()
28
- if (next) setTimeout(next, 0)
28
+ if (next) queueMicrotask(next)
29
29
  else {
30
30
  state.locked = false
31
31
  mutexes.delete(lockKey)
32
32
  }
33
33
  }
34
34
 
35
+ /**
36
+ * 当前存活的互斥键数量(仅供测试观察锁表是否回落到基线)。
37
+ * @returns {number} `mutexes` 中的键数量
38
+ */
39
+ export function activeMutexCount() {
40
+ return mutexes.size
41
+ }
42
+
35
43
  /**
36
44
  * @param {string} key 锁键
37
45
  * @param {() => Promise<T>} criticalSection 临界区
@@ -39,15 +47,14 @@ function releaseMutex(lockKey, state) {
39
47
  * @template T
40
48
  */
41
49
  export async function withAsyncMutex(key, criticalSection) {
42
- const lockKey = key
43
- const state = mutexState(lockKey)
50
+ const state = mutexState(key)
44
51
  return new Promise((resolve, reject) => {
45
52
  /** 执行临界区并最终释放锁 */
46
53
  const run = () => {
47
54
  Promise.resolve()
48
55
  .then(criticalSection)
49
56
  .then(resolve, reject)
50
- .finally(() => releaseMutex(lockKey, state))
57
+ .finally(() => releaseMutex(key, state))
51
58
  }
52
59
  if (state.locked) state.queue.push(run)
53
60
  else {
package/utils/shuffle.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  export function shuffleInPlace(arr) {
8
8
  for (let i = arr.length - 1; i > 0; i--) {
9
9
  const j = Math.floor(Math.random() * (i + 1))
10
- ;[arr[i], arr[j]] = [arr[j], arr[i]]
10
+ ;[arr[i], arr[j]] = [arr[j], arr[i]]
11
11
  }
12
12
  return arr
13
13
  }
package/wire/adapter.mjs CHANGED
@@ -10,4 +10,4 @@
10
10
  */
11
11
 
12
12
  /** Wire 适配器与上下文的类型导出(运行时代码见各 wire 模块)。 */
13
- export {}
13
+ export { }
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,62 +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
- onUpdate: snapshot => {
36
- render(snapshot)
37
- statusEl.textContent = `监听 ${snapshot.relayUrl}(census,共 ${snapshot.relays} 个 relay)`
38
- },
39
- })
40
- }
41
- catch (error) {
42
- statusEl.textContent = `中继配置无效:${String(error?.message || error)}`
43
- }
44
- }
45
-
46
- /** @returns {void} 停止监控 */
47
- function stopLive() {
48
- monitor?.stop()
49
- monitor = null
50
- }
51
-
52
- toggleEl.addEventListener('change', () => {
53
- if (!toggleEl.checked) {
54
- stopLive()
55
- render({ estimate: 0, sampleSize: 0, eventsInWindow: 0 })
56
- statusEl.textContent = '已关闭'
57
- return
58
- }
59
- startLive()
60
- })
61
-
62
- if (toggleEl.checked) startLive()