@steve02081504/fount-p2p 0.0.9 → 0.0.11

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 CHANGED
@@ -24,7 +24,9 @@ await startNode({ nodeDir: '/path/to/p2p/node' })
24
24
  await ensureUserRoom()
25
25
  ```
26
26
 
27
- Subpath exports mirror source directories, e.g. `@steve02081504/fount-p2p/dag`, `@steve02081504/fount-p2p/transport/link_registry`, `@steve02081504/fount-p2p/registries/event_type`.
27
+ Shells talk to the **fount network** (`ensureLinkToNode` / `sendToNodeLink` / rooms). Do not import `link/` or choose WebRTC / BLE / LAN yourself.
28
+
29
+ Subpath exports mirror source directories, e.g. `@steve02081504/fount-p2p/dag`, `@steve02081504/fount-p2p/transport/link_registry`, `@steve02081504/fount-p2p/transport/signal_crypto`, `@steve02081504/fount-p2p/registries/event_type`.
28
30
 
29
31
  ## Layout
30
32
 
@@ -33,31 +35,47 @@ Subpath exports mirror source directories, e.g. `@steve02081504/fount-p2p/dag`,
33
35
  | L0 | `core/` | Pure primitives: `hexIds`, `entity_id*`, `canonical_json` |
34
36
  | L1 | `crypto/`, `wire/`, `schemas/` | Cryptography, wire protocol, canonical validation |
35
37
  | L2 | `node/` | Node runtime: `identity`, `entity_store`, `denylist`, `reputation_store` |
36
- | L3 | `discovery/`, `link/`, `transport/`, `rooms/` | Discovery, RTC links, rooms |
37
- | L4 | `trust_graph/`, `mailbox/`, `dag/`, `federation/`, `files/` | Federation, store-and-forward, DAG, EVFS |
38
+ | L3 | `discovery/`, `link/`, `transport/`, `rooms/` | Discovery + fount-network registry/rooms (`link/providers` are package-private) |
39
+ | L4 | `trust_graph/`, `mailbox/`, `dag/`, `federation/`, `files/`, `governance/`, `reputation/` | Federation, store-and-forward, DAG, EVFS, tunables |
38
40
  | — | `registries/` | Pluggable registries (event type, part path, room provider, …) |
39
41
 
40
42
  Facade entry: `index.mjs` (`startNode`, `createGroupLinkSet`, `registerDiscoveryProvider`, …).
41
43
 
42
- Root contains only the facade and package metadata; all modules live in layered subdirectories. After a layout migration, run `node scripts/cleanup-root-duplicates.mjs` to drop stale root stubs.
44
+ **Transport modules** (exported under `./transport/*`):
45
+
46
+ | Module | Role |
47
+ |---|---|
48
+ | `link_registry.mjs` | Fount-network facade: dial fallback, scope/overlay |
49
+ | `runtime_bootstrap.mjs` | Progressive `ensureRuntime` (register + background listen / discovery / BT) |
50
+ | `offer_answer.mjs` | Discovery-signal glare for offer/answer providers |
51
+ | `signal_crypto.mjs` | Rendezvous topics + AES-GCM signal packets |
52
+
53
+ `ensureRuntime` returns after registration and scheduling warm-up; it does not await lan_tcp listen, public relays, or Bluetooth. See [docs/runtime.md](./docs/runtime.md) and [docs/transports.md](./docs/transports.md).
54
+
55
+ Root contains only the facade and package metadata; all modules live in layered subdirectories.
43
56
 
44
57
  ## Tests
45
58
 
46
59
  ```bash
47
60
  npm test # package pure + integration (Node)
48
61
  npm run test:fount # cross-repo bridge (Deno; requires fount on PATH)
49
- npm run test:live # RTC / link smoke (requires node-datachannel)
62
+ npm run test:live # link / LAN / glare smoke
50
63
  npm run test:sim # tunables co-evolution sim (dev only, not published; --social-tunables to write back)
51
64
  ```
52
65
 
53
66
  During development: `node scripts/check-imports.mjs` validates relative imports. After a layout migration, `node scripts/cleanup-root-duplicates.mjs` removes stale root-level stubs.
54
67
 
68
+ Maintainer notes for agents / contributors: [AGENTS.md](./AGENTS.md). Sim harness fidelity: [sim/AGENTS.md](./sim/AGENTS.md).
69
+
55
70
  ## Optional dependencies
56
71
 
57
- - `@stoprocent/noble` / `@stoprocent/bleno` — Bluetooth discovery (attempted by default; skipped when not installed)
72
+ - `@stoprocent/noble` / `@stoprocent/bleno` — Bluetooth (optionalDependencies). Hardware probe is subprocess-only; see [docs/runtime.md](./docs/runtime.md).
73
+ - `node-datachannel` — WebRTC DataChannels (dependency).
74
+ - `ws` — Nostr discovery/signaling WebSockets (dependency).
58
75
 
59
76
  Group chunk remote storage (S3, etc.) is implemented by the shell as `GroupStoragePlugin` and injected; see `node/storage_plugins.mjs` for the local reference implementation.
60
77
 
61
78
  ## Docs
62
79
 
63
- - Signaling and link setup: [`docs/signaling.md`](./docs/signaling.md)
80
+ - Transports and provider fallback: [`docs/transports.md`](./docs/transports.md)
81
+ - Signaling and WebRTC glare: [`docs/signaling.md`](./docs/signaling.md)
@@ -1,7 +1,7 @@
1
1
  import { Buffer } from 'node:buffer'
2
2
 
3
3
  import { getBtPeerHint } from './peer_hints.mjs'
4
- import { loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from './runtime.mjs'
4
+ import { canUseBluetoothRuntime, loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from './runtime.mjs'
5
5
 
6
6
  /** 重导出 waitPoweredOn,供 discovery 调用方使用。 */
7
7
  export { waitPoweredOn } from './runtime.mjs'
@@ -15,19 +15,11 @@ const MAX_SIGNAL_BLOB_BYTES = 8 * 1024
15
15
  const PERIPHERAL_RESCAN_MS = 15_000
16
16
 
17
17
  /**
18
- * 探测本机 noble 运行时是否具备 BT 扫描所需 API。
19
- * @returns {Promise<boolean>} noble 可加载且具备 startScanningAsync 与 waitForPoweredOn(Async) 时为 true
18
+ * 探测本机 BT discovery 是否可用;失败则回落其它 discovery,不抛错。
19
+ * @returns {Promise<boolean>} 可用为 true
20
20
  */
21
21
  export async function canUseBluetoothDiscovery() {
22
- try {
23
- const noble = await loadNoble()
24
- if (typeof noble.startScanningAsync !== 'function') return false
25
- const wait = noble.waitForPoweredOnAsync ?? noble.waitForPoweredOn
26
- return typeof wait === 'function'
27
- }
28
- catch {
29
- return false
30
- }
22
+ return canUseBluetoothRuntime()
31
23
  }
32
24
 
33
25
  /**
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 子进程 BT 可用性探测:load → poweredOn → stop → exit。
3
+ * 由 `canUseBluetoothRuntime` spawn;勿在父进程直接跑 waitPoweredOn(会拖住事件循环)。
4
+ */
5
+ import process from 'node:process'
6
+
7
+ import { loadNoble, waitPoweredOn } from './runtime.mjs'
8
+
9
+ const timeoutMs = Number(process.env.FOUNT_BT_PROBE_MS || 3000)
10
+
11
+ try {
12
+ const noble = await loadNoble()
13
+ if (!noble?.startScanningAsync) process.exit(2)
14
+ await waitPoweredOn(noble, timeoutMs)
15
+ try { noble.stop() } catch { /* Windows 上 stop 偶发崩;子进程反正要退出 */ }
16
+ process.exit(0)
17
+ }
18
+ catch {
19
+ process.exit(1)
20
+ }
@@ -1,4 +1,8 @@
1
+ import { existsSync, readdirSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
1
3
  import process from 'node:process'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { spawn } from 'node:child_process'
2
6
 
3
7
  /**
4
8
  * 解析 Bluetooth 角色(scan / dual)。
@@ -13,20 +17,110 @@ export function resolveBtRole() {
13
17
  }
14
18
 
15
19
  /**
16
- * 加载 Noble BLE central。
20
+ * 廉价硬件探测:明确无适配器时跳过 noble/bleno import(加载本身会拉起 native,无适配器时常在 teardown SIGSEGV)。
21
+ * @returns {boolean | null} true=有迹象,false=明确无,null=未知(继续尝试加载,失败则回落)
22
+ */
23
+ export function probeBluetoothHardware() {
24
+ if (process.platform === 'linux') try {
25
+ const dir = '/sys/class/bluetooth'
26
+ if (!existsSync(dir)) return false
27
+ return readdirSync(dir).some(name => name && !name.startsWith('.'))
28
+ }
29
+ catch {
30
+ return false
31
+ }
32
+ return null
33
+ }
34
+
35
+ /** @type {boolean | null} canUseBluetoothRuntime 缓存 */
36
+ let cachedRuntimeOk = null
37
+ /** @type {Promise<boolean> | null} 并发 canUse 合并为一次子进程探测 */
38
+ let probeInflight = null
39
+
40
+ const PROBE_CHILD = join(dirname(fileURLToPath(import.meta.url)), 'probe_child.mjs')
41
+
42
+ /**
43
+ * 在子进程中探测 BT(父进程 waitPoweredOn 会拖住事件循环;stop() 还可能 AV)。
44
+ * @param {number} timeoutMs waitPoweredOn 超时
45
+ * @returns {Promise<boolean>} 子进程 exit 0 为可用
46
+ */
47
+ function probeBluetoothInSubprocess(timeoutMs) {
48
+ return new Promise(resolve => {
49
+ const child = spawn(process.execPath, [PROBE_CHILD], {
50
+ stdio: 'ignore',
51
+ env: {
52
+ ...process.env,
53
+ FOUNT_BT_PROBE_MS: String(timeoutMs),
54
+ },
55
+ windowsHide: true,
56
+ })
57
+ // 探测是旁路缓存填充;勿拖住父进程事件循环 / shutdown 退出。
58
+ child.unref()
59
+ let settled = false
60
+ /**
61
+ * @param {boolean} ok 探测结果
62
+ * @returns {void}
63
+ */
64
+ const finish = ok => {
65
+ if (settled) return
66
+ settled = true
67
+ clearTimeout(timer)
68
+ resolve(ok)
69
+ }
70
+ const timer = setTimeout(() => {
71
+ child.kill('SIGKILL')
72
+ finish(false)
73
+ }, timeoutMs + 5_000)
74
+ timer.unref()
75
+ child.once('error', () => finish(false))
76
+ child.once('exit', (code, signal) => {
77
+ if (signal) finish(false)
78
+ else finish(code === 0)
79
+ })
80
+ })
81
+ }
82
+
83
+ /**
84
+ * 探测 BT 栈是否真正可用(硬件迹象 → 子进程 load → poweredOn)。
85
+ * 任一步失败返回 false;不抛错。
86
+ * @param {number} [timeoutMs=3000] waitPoweredOn 超时
87
+ * @returns {Promise<boolean>} 可用为 true
88
+ */
89
+ export async function canUseBluetoothRuntime(timeoutMs = 3000) {
90
+ if (cachedRuntimeOk !== null) return cachedRuntimeOk
91
+ if (probeBluetoothHardware() === false) {
92
+ cachedRuntimeOk = false
93
+ return false
94
+ }
95
+ if (!probeInflight) {
96
+ probeInflight = probeBluetoothInSubprocess(timeoutMs)
97
+ .then(ok => {
98
+ cachedRuntimeOk = ok
99
+ return ok
100
+ })
101
+ .finally(() => { probeInflight = null })
102
+ }
103
+ return probeInflight
104
+ }
105
+
106
+ /**
107
+ * 加载 Noble BLE central。明确无适配器时直接抛错,避免无意义的 native import。
17
108
  * @returns {Promise<any>} noble 运行时
18
109
  */
19
110
  export async function loadNoble() {
111
+ if (probeBluetoothHardware() === false)
112
+ throw new Error('p2p: no bluetooth adapter')
20
113
  const mod = await import('@stoprocent/noble')
21
- if (typeof mod.withBindings === 'function') return mod.withBindings('default')
22
- return mod.default ?? mod
114
+ return mod?.withBindings?.('default') || mod?.default || mod
23
115
  }
24
116
 
25
117
  /**
26
- * 加载 Bleno BLE peripheral。
118
+ * 加载 Bleno BLE peripheral。明确无适配器时直接抛错,避免无意义的 native import
27
119
  * @returns {Promise<any>} bleno 运行时
28
120
  */
29
121
  export async function loadBleno() {
122
+ if (probeBluetoothHardware() === false)
123
+ throw new Error('p2p: no bluetooth adapter')
30
124
  const mod = await import('@stoprocent/bleno')
31
125
  if (typeof mod.withBindings === 'function') return mod.withBindings('default')
32
126
  return mod.default ?? mod
@@ -40,7 +134,7 @@ export async function loadBleno() {
40
134
  */
41
135
  export async function waitPoweredOn(runtime, timeout) {
42
136
  const wait = runtime.waitForPoweredOnAsync ?? runtime.waitForPoweredOn
43
- if (typeof wait !== 'function')
137
+ if (!wait)
44
138
  throw new Error('p2p: bluetooth runtime missing waitForPoweredOn(Async)')
45
139
  return wait.call(runtime, timeout)
46
140
  }
@@ -23,6 +23,14 @@ export function unregisterDiscoveryProvider(id) {
23
23
  providers.delete(String(id))
24
24
  }
25
25
 
26
+ /**
27
+ * 清空全部 discovery provider。
28
+ * @returns {void}
29
+ */
30
+ export function clearDiscoveryProviders() {
31
+ providers.clear()
32
+ }
33
+
26
34
  /**
27
35
  * 列出已注册的 discovery provider(按 priority 排序)。
28
36
  * @returns {DiscoveryProvider[]} 提供者列表
@@ -32,29 +40,40 @@ export function listDiscoveryProviders() {
32
40
  }
33
41
 
34
42
  /**
35
- * 通过所有可用 provider 广播 topic advert。
36
- * 单路失败静默(正常降级);其它 provider 仍可成功。
37
- * @param {string} topic advert 主题
38
- * @param {Uint8Array} bytes advert 载荷
43
+ * 并行启动各 provider async 钩子;单路失败静默。
44
+ * @param {DiscoveryProvider[]} list 提供者列表
45
+ * @param {(provider: DiscoveryProvider) => Promise<(() => void) | null | undefined>} run 单路启动
39
46
  * @returns {Promise<() => void>} 统一取消函数
40
47
  */
41
- export async function advertiseTopic(topic, bytes) {
42
- const cleanups = []
43
- for (const provider of listDiscoveryProviders()) {
44
- if (!provider.caps?.canDiscover || typeof provider.advertise !== 'function') continue
45
- let cleanup = null
48
+ async function startProvidersParallel(list, run) {
49
+ const settled = await Promise.all(list.map(async provider => {
46
50
  try {
47
- cleanup = await provider.advertise(topic, bytes)
51
+ const cleanup = await run(provider)
52
+ return typeof cleanup === 'function' ? cleanup : null
48
53
  }
49
- catch { continue }
50
- if (typeof cleanup === 'function') cleanups.push(cleanup)
51
- }
54
+ catch {
55
+ return null
56
+ }
57
+ }))
58
+ const cleanups = settled.filter(Boolean)
52
59
  return () => {
53
60
  for (const cleanup of cleanups)
54
61
  try { cleanup() } catch { /* ignore */ }
55
62
  }
56
63
  }
57
64
 
65
+ /**
66
+ * 通过所有可用 provider 广播 topic advert。
67
+ * 单路失败静默(正常降级);其它 provider 仍可成功。
68
+ * @param {string} topic advert 主题
69
+ * @param {Uint8Array} bytes advert 载荷
70
+ * @returns {Promise<() => void>} 统一取消函数
71
+ */
72
+ export async function advertiseTopic(topic, bytes) {
73
+ const list = listDiscoveryProviders().filter(provider => provider.caps?.canDiscover && typeof provider.advertise === 'function')
74
+ return startProvidersParallel(list, provider => provider.advertise(topic, bytes))
75
+ }
76
+
58
77
  /**
59
78
  * 通过所有可用 provider 订阅 topic advert。
60
79
  * 单路失败静默(正常降级)。
@@ -63,20 +82,8 @@ export async function advertiseTopic(topic, bytes) {
63
82
  * @returns {Promise<() => void>} 统一取消函数
64
83
  */
65
84
  export async function subscribeTopic(topic, onAdvert) {
66
- const cleanups = []
67
- for (const provider of listDiscoveryProviders()) {
68
- if (!provider.caps?.canDiscover || typeof provider.subscribe !== 'function') continue
69
- let cleanup = null
70
- try {
71
- cleanup = await provider.subscribe(topic, onAdvert)
72
- }
73
- catch { continue }
74
- if (typeof cleanup === 'function') cleanups.push(cleanup)
75
- }
76
- return () => {
77
- for (const cleanup of cleanups)
78
- try { cleanup() } catch { /* ignore */ }
79
- }
85
+ const list = listDiscoveryProviders().filter(provider => provider.caps?.canDiscover && typeof provider.subscribe === 'function')
86
+ return startProvidersParallel(list, provider => provider.subscribe(topic, onAdvert))
80
87
  }
81
88
 
82
89
  /**
@@ -93,13 +100,15 @@ export async function sendSignal(topic, to, bytes) {
93
100
  if (!capable.length) throw new Error('p2p: no discovery provider can signal')
94
101
  let sent = false
95
102
  let lastError = null
96
- for (const provider of capable) try {
97
- if (await Promise.resolve(provider.sendSignal(topic, to, bytes)) !== false)
98
- sent = true
99
- }
100
- catch (error) {
101
- lastError = error
102
- }
103
+ await Promise.all(capable.map(async provider => {
104
+ try {
105
+ if (await Promise.resolve(provider.sendSignal(topic, to, bytes)) !== false)
106
+ sent = true
107
+ }
108
+ catch (error) {
109
+ lastError = error
110
+ }
111
+ }))
103
112
  if (!sent) throw lastError || new Error('p2p: no discovery provider delivered signal')
104
113
  }
105
114
 
@@ -111,18 +120,6 @@ export async function sendSignal(topic, to, bytes) {
111
120
  * @returns {Promise<() => void>} 统一取消函数
112
121
  */
113
122
  export async function listenSignals(topic, onSignal) {
114
- const cleanups = []
115
- for (const provider of listDiscoveryProviders()) {
116
- if (!provider.caps?.canSignal || typeof provider.onSignal !== 'function') continue
117
- let cleanup = null
118
- try {
119
- cleanup = await provider.onSignal(topic, onSignal)
120
- }
121
- catch { continue }
122
- if (typeof cleanup === 'function') cleanups.push(cleanup)
123
- }
124
- return () => {
125
- for (const cleanup of cleanups)
126
- try { cleanup() } catch { /* ignore */ }
127
- }
123
+ const list = listDiscoveryProviders().filter(provider => provider.caps?.canSignal && typeof provider.onSignal === 'function')
124
+ return startProvidersParallel(list, provider => provider.onSignal(topic, onSignal))
128
125
  }
@@ -6,6 +6,7 @@ const DEFAULT_GROUP = '239.255.42.99'
6
6
 
7
7
  /**
8
8
  * 轻量 multicast 发现插件:不做完整 DNS-SD,只复用 mDNS 的 LAN multicast 发现思路。
9
+ * 引用计数为 0 时关闭 UDP socket,避免 shutdown 后句柄拖住进程。
9
10
  *
10
11
  * @param {{ port?: number, group?: string }} [options] 组播端口与组地址
11
12
  * @returns {import('./index.mjs').DiscoveryProvider} mDNS 发现提供者
@@ -13,14 +14,24 @@ const DEFAULT_GROUP = '239.255.42.99'
13
14
  export function createMdnsDiscoveryProvider(options = {}) {
14
15
  const port = Number(options.port) || DEFAULT_PORT
15
16
  const group = String(options.group || DEFAULT_GROUP)
16
- const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true })
17
+ /** @type {import('node:dgram').Socket | null} */
18
+ let socket = null
17
19
  let bound = false
18
20
  let bindPromise = null
21
+ let refs = 0
19
22
  /** @type {Map<string, Set<Function>>} */
20
23
  const advertListeners = new Map()
21
24
  /** @type {Map<string, Set<Function>>} */
22
25
  const signalListeners = new Map()
23
26
 
27
+ /**
28
+ * 懒创建或返回当前 UDP socket。
29
+ * @returns {import('node:dgram').Socket} 用于组播 bind/send 的 socket
30
+ */
31
+ function getSocket() {
32
+ return socket ||= dgram.createSocket({ type: 'udp4', reuseAddr: true })
33
+ }
34
+
24
35
  /**
25
36
  * 绑定 UDP socket 并注册组播消息处理器。
26
37
  * @returns {Promise<void>}
@@ -29,16 +40,24 @@ export function createMdnsDiscoveryProvider(options = {}) {
29
40
  if (bound) return
30
41
  if (!bindPromise)
31
42
  bindPromise = (async () => {
43
+ const sock = getSocket()
32
44
  await new Promise((resolve, reject) => {
33
- socket.once('error', reject)
34
- socket.bind(port, '0.0.0.0', () => {
35
- socket.off('error', reject)
36
- socket.addMembership(group)
37
- socket.setMulticastTTL(1)
45
+ sock.once('error', reject)
46
+ sock.bind(port, '0.0.0.0', () => {
47
+ sock.off('error', reject)
48
+ sock.addMembership(group)
49
+ sock.setMulticastTTL(1)
38
50
  resolve()
39
51
  })
40
52
  })
41
- socket.on('message', (raw, rinfo) => {
53
+ // acquire 已全部释放:关掉刚 bind 的 socket,避免泄漏句柄。
54
+ if (refs <= 0) {
55
+ try { sock.close() } catch { /* ignore */ }
56
+ if (socket === sock) socket = null
57
+ bound = false
58
+ return
59
+ }
60
+ sock.on('message', (raw, rinfo) => {
42
61
  let packet
43
62
  try { packet = JSON.parse(String(raw)) } catch { return }
44
63
  const listeners = packet.type === 'advert'
@@ -70,16 +89,34 @@ export function createMdnsDiscoveryProvider(options = {}) {
70
89
  */
71
90
  async function multicast(type, topic, bytes) {
72
91
  await ensureBound()
92
+ const sock = getSocket()
73
93
  const packet = Buffer.from(JSON.stringify({
74
94
  type,
75
95
  topic,
76
96
  data: Buffer.from(bytes).toString('base64'),
77
97
  }))
78
98
  await new Promise((resolve, reject) => {
79
- socket.send(packet, port, group, error => error ? reject(error) : resolve())
99
+ sock.send(packet, port, group, error => error ? reject(error) : resolve())
80
100
  })
81
101
  }
82
102
 
103
+ /**
104
+ * 占用 socket;归还后若无引用则关闭。
105
+ * @returns {() => void} 释放函数
106
+ */
107
+ function acquire() {
108
+ refs++
109
+ void ensureBound().catch(() => { })
110
+ return () => {
111
+ refs = Math.max(0, refs - 1)
112
+ if (refs > 0 || !socket) return
113
+ try { socket.close() } catch { /* ignore */ }
114
+ socket = null
115
+ bound = false
116
+ bindPromise = null
117
+ }
118
+ }
119
+
83
120
  /**
84
121
  * 向 topic bucket 注册监听器。
85
122
  * @param {Map<string, Set<Function>>} bucket topic → 监听器集合
@@ -90,7 +127,11 @@ export function createMdnsDiscoveryProvider(options = {}) {
90
127
  function addListener(bucket, topic, listener) {
91
128
  if (!bucket.has(topic)) bucket.set(topic, new Set())
92
129
  bucket.get(topic).add(listener)
93
- return () => bucket.get(topic)?.delete(listener)
130
+ const release = acquire()
131
+ return () => {
132
+ bucket.get(topic)?.delete(listener)
133
+ release()
134
+ }
94
135
  }
95
136
 
96
137
  return {
@@ -98,24 +139,27 @@ export function createMdnsDiscoveryProvider(options = {}) {
98
139
  priority: 10,
99
140
  caps: { canDiscover: true, canSignal: true, canRelay: false },
100
141
  /**
101
- * 周期性组播广播 advert
142
+ * 周期性组播广播 advert(首发后台,不阻塞调用方)。
102
143
  * @param {string} topic advert 主题
103
144
  * @param {Uint8Array} bytes advert 载荷
104
145
  * @returns {Promise<() => void>} 取消广播函数
105
146
  */
106
147
  async advertise(topic, bytes) {
107
- await multicast('advert', topic, bytes)
148
+ const release = acquire()
149
+ void multicast('advert', topic, bytes).catch(() => { })
108
150
  const timer = setInterval(() => { void multicast('advert', topic, bytes).catch(() => { }) }, 30_000)
109
- return () => clearInterval(timer)
151
+ return () => {
152
+ clearInterval(timer)
153
+ release()
154
+ }
110
155
  },
111
156
  /**
112
- * 订阅组播 advert
157
+ * 订阅组播 advert(立即返回;UDP bind 后台完成)。
113
158
  * @param {string} topic advert 主题
114
159
  * @param {Function} onAdvert advert 回调
115
160
  * @returns {Promise<() => void>} 取消订阅函数
116
161
  */
117
162
  async subscribe(topic, onAdvert) {
118
- await ensureBound()
119
163
  return addListener(advertListeners, topic, onAdvert)
120
164
  },
121
165
  /**
@@ -126,16 +170,21 @@ export function createMdnsDiscoveryProvider(options = {}) {
126
170
  * @returns {Promise<void>}
127
171
  */
128
172
  async sendSignal(topic, _to, bytes) {
129
- await multicast('signal', topic, bytes)
173
+ const release = acquire()
174
+ try {
175
+ await multicast('signal', topic, bytes)
176
+ }
177
+ finally {
178
+ release()
179
+ }
130
180
  },
131
181
  /**
132
- * 订阅组播信令。
182
+ * 订阅组播信令(立即返回;UDP bind 后台完成)。
133
183
  * @param {string} topic 信令 topic
134
184
  * @param {Function} onSignal 信令回调
135
185
  * @returns {Promise<() => void>} 取消订阅函数
136
186
  */
137
187
  async onSignal(topic, onSignal) {
138
- await ensureBound()
139
188
  return addListener(signalListeners, topic, onSignal)
140
189
  },
141
190
  }