@steve02081504/fount-p2p 0.0.32 → 0.0.33
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 +1 -1
- package/discovery/nostr.mjs +11 -0
- package/docs/runtime.md +15 -3
- package/docs/signaling.md +12 -2
- package/docs/transports.md +1 -1
- package/index.mjs +2 -0
- package/link/providers/nostr.mjs +2 -12
- package/link/providers/webrtc.mjs +1 -1
- package/link/rtc/polyfill.mjs +1 -1
- package/node/instance.mjs +7 -3
- package/node/signaling_config.mjs +52 -29
- package/package.json +5 -2
- package/transport/link_registry.mjs +7 -0
- package/transport/runtime_bootstrap.mjs +120 -103
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ Facade entry: `index.mjs` (`startNode`, `createGroupLinkSet`, `registerDiscovery
|
|
|
88
88
|
| `user_room.mjs` / `group_link_set.mjs` / `node_scope/wire` + `node_scope/features` | rooms + composable node-scope wires (`registerNodeScopeWireHook`, attaches) |
|
|
89
89
|
| `room_scopes.mjs` / `remote_user_room.mjs` | scope constants / remote user slot |
|
|
90
90
|
|
|
91
|
-
`runtime_bootstrap`, `offer_answer` are **internal** (transport). Signal crypto / rendezvous live under `discovery/internal/signal_crypto.mjs` (used by discovery `nostr.mjs` / `adverts.mjs`; not a package export). The Nostr **link** provider (`link/providers/nostr.mjs`) reuses the same discovery signal path (`type: 'link'`) as a last-resort duplex pipe. `ensureRuntime` returns after registration and scheduling warm-up; it does not await lan_tcp listen, public relays, or Bluetooth. `setSignalingRuntimeConfig` → `reloadDiscoveryRelays`. See [docs/runtime.md](./docs/runtime.md) and [docs/transports.md](./docs/transports.md).
|
|
91
|
+
`runtime_bootstrap`, `offer_answer` are **internal** (transport). Signal crypto / rendezvous live under `discovery/internal/signal_crypto.mjs` (used by discovery `nostr.mjs` / `adverts.mjs`; not a package export). The Nostr **link** provider (`link/providers/nostr.mjs`) reuses the same discovery signal path (`type: 'link'`) as a last-resort duplex pipe. `ensureRuntime` returns after registration and scheduling warm-up; it does not await lan_tcp listen, public relays, or Bluetooth. Call `ensureChannelAvailable('bt')` to await BT discovery readiness. `setSignalingRuntimeConfig` → `reloadDiscoveryRelays`. See [docs/runtime.md](./docs/runtime.md) and [docs/transports.md](./docs/transports.md).
|
|
92
92
|
|
|
93
93
|
Root contains only the facade and package metadata; all modules live in layered subdirectories.
|
|
94
94
|
|
package/discovery/nostr.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import WebSocket from 'ws'
|
|
|
6
6
|
import { base64ToBytes, hexToBytes, bytesToBase64, bytesToHex } from '../core/bytes_codec.mjs'
|
|
7
7
|
import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
8
8
|
import { sha256Hex } from '../crypto/crypto.mjs'
|
|
9
|
+
import { getNodeTransportSettings } from '../node/identity.mjs'
|
|
10
|
+
import { getSignalingRuntimeConfig } from '../node/instance.mjs'
|
|
9
11
|
import { nodeDebug, shortHash } from '../node/log.mjs'
|
|
10
12
|
|
|
11
13
|
import { noteAdvertPeerHints } from './advert_peer_hints.mjs'
|
|
@@ -200,6 +202,15 @@ export function mergeSignalingRelayUrls(userRelayUrls) {
|
|
|
200
202
|
return merged.length ? merged : [...DEFAULT_RELAY_URLS]
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
/**
|
|
206
|
+
* 当前可用 nostr 中继:nostr 通道配置 relay(替换默认),否则节点默认 + 公共中继。
|
|
207
|
+
* @returns {string[]} relay URL 列表
|
|
208
|
+
*/
|
|
209
|
+
export function resolveNostrRelayUrls() {
|
|
210
|
+
const relay = getSignalingRuntimeConfig().channels?.nostr?.relay
|
|
211
|
+
return relay?.length ? relay : mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls)
|
|
212
|
+
}
|
|
213
|
+
|
|
203
214
|
/**
|
|
204
215
|
* @param {number} kind 事件 kind
|
|
205
216
|
* @param {string[][]} tags 事件标签
|
package/docs/runtime.md
CHANGED
|
@@ -4,15 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
## `ensureRuntime` contract
|
|
6
6
|
|
|
7
|
-
Returns after registering lan / nostr
|
|
7
|
+
Returns after registering lan / nostr discovery providers (subject to the signaling `channels` whitelist), registering all enabled link providers (including `ble_gatt`), and scheduling background warm — does **not** await lan_tcp listen, Nostr relays, or BT availability.
|
|
8
8
|
|
|
9
9
|
| Who waits | For what |
|
|
10
10
|
| --- | --- |
|
|
11
11
|
| Shells (`startNode` / `ensureUserRoom`) | Nothing beyond `ensureRuntime` itself — never read `lanTcpPort` or await public-signaling warm-up |
|
|
12
12
|
| `buildLocalAdvert` / `whenListening` | Local lan_tcp listen only |
|
|
13
13
|
| `ensureLinkToNode` | `whenSignalListening` (Nostr `listenNodeSignals` attached) before dial, so offer/answer does not drop the first signal |
|
|
14
|
+
| `ensureChannelAvailable('bt')` | BT discovery provider registered and BT signal/presence fan-in restarted |
|
|
14
15
|
|
|
15
|
-
Nostr / LAN
|
|
16
|
+
Nostr / LAN hooks are progressive. The `bt` discovery provider is **not** registered by `ensureRuntime`; it is registered on demand via `ensureChannelAvailable('bt')`.
|
|
17
|
+
|
|
18
|
+
## `ensureChannelAvailable(channel)` contract
|
|
19
|
+
|
|
20
|
+
Await this to make a specific channel available:
|
|
21
|
+
|
|
22
|
+
- `bt` — probes the Bluetooth runtime, registers the `bt` discovery provider if available, and restarts the signal/presence fan-in so BT participates.
|
|
23
|
+
- `lan` — awaits `whenListening` (local lan_tcp listen).
|
|
24
|
+
- `nostr` — awaits `whenSignalListening` (Nostr `listenNodeSignals` attached).
|
|
25
|
+
- `webrtc` — awaits `whenSignalListening` (the shared signal medium WebRTC depends on).
|
|
26
|
+
|
|
27
|
+
`startNode` does not call `ensureChannelAvailable` automatically; shells that need BT discovery should `await getLinkRegistry().ensureChannelAvailable('bt')` (or the top-level `ensureChannelAvailable('bt')` export) after `startNode`.
|
|
16
28
|
|
|
17
29
|
### Fast-listen & dial path
|
|
18
30
|
|
|
@@ -28,7 +40,7 @@ Nostr / LAN / BT hooks are progressive. BT discovery / `ble_gatt` always warm in
|
|
|
28
40
|
| init → shutdown → natural exit | ≤10s | `test/pure/shutdown_exit.test.mjs` |
|
|
29
41
|
| 10s warm → shutdown → exit | ≤2s | same |
|
|
30
42
|
|
|
31
|
-
Shutdown-exit tests use the production path (default public Nostr + lan). Do not use
|
|
43
|
+
Shutdown-exit tests use the production path (default public Nostr + lan). Do not use per-channel relay / dead-relay crutches there.
|
|
32
44
|
|
|
33
45
|
## Nostr cleanup
|
|
34
46
|
|
package/docs/signaling.md
CHANGED
|
@@ -18,6 +18,16 @@ Frames: `hello` then `auth`. On simultaneous dial, peer `auth` can arrive before
|
|
|
18
18
|
|
|
19
19
|
When set: send final offer/answer after ICE gathering, dedupe remote signals, queue remote ICE until both descriptions are ready.
|
|
20
20
|
|
|
21
|
-
## Runtime
|
|
21
|
+
## Runtime channels
|
|
22
22
|
|
|
23
|
-
`setSignalingRuntimeConfig({
|
|
23
|
+
`setSignalingRuntimeConfig({ channels })` after `initNode` (or pass `signaling` once on first `startNode`). Changes emit `signaling-changed` and trigger `reloadDiscoveryRelays` (rebind discovery providers + node presence/signals).
|
|
24
|
+
|
|
25
|
+
`channels` selects which discovery/link media are active — `nostr`, `lan`, `bt`, `webrtc`. Semantics per channel key: `false` **disables**; any other value (`undefined`, `true`, or an object) **enables** — an object is merged over that channel's default config. Channels not mentioned keep their default enabled state.
|
|
26
|
+
|
|
27
|
+
Channel → components:
|
|
28
|
+
- `nostr` (nostr discovery + nostr link) — config `relay` **replaces** the default public relay list (do not merge defaults back in).
|
|
29
|
+
- `lan` (lan discovery + lan_tcp link)
|
|
30
|
+
- `bt` (bt discovery + ble_gatt link)
|
|
31
|
+
- `webrtc` (webrtc link data-transport fallback) — config `iceLocalHostnamePolicy` (`none` / `rewrite-loopback` / `drop`, Windows defaults `drop`) and `trickleIceOff` (send final offer/answer after ICE gathering).
|
|
32
|
+
|
|
33
|
+
To run a node on only a few channels, use the `disableAllChannels` helper: `{ channels: disableAllChannels({ nostr: { relay: [...] } }) }` enables only `nostr` (with its per-channel relay) and disables the rest; use `true` to enable a channel with its default config (`disableAllChannels({ nostr: true, webrtc: true })`).
|
package/docs/transports.md
CHANGED
|
@@ -33,7 +33,7 @@ Topic / rendezvous / signal crypto live under `discovery/` (`nostr.mjs`, `intern
|
|
|
33
33
|
| **Discovery** (`discovery/`) | Per medium: `listVisibleNodeHashes` + `connectToNode`. Encrypted adverts/signals via `adverts.mjs` + `index.mjs` helpers. |
|
|
34
34
|
| **Link providers** (`link/providers/`) | Open a duplex pipe; sorted by **`level` (descending)** — not a shell import |
|
|
35
35
|
| **Registry** (`transport/link_registry.mjs`) | fount-network facade: dial fallback, scope/overlay, one canonical link per peer |
|
|
36
|
-
| **Bootstrap** (`transport/runtime_bootstrap.mjs`) | `ensureRuntime` register + progressive listen/discovery
|
|
36
|
+
| **Bootstrap** (`transport/runtime_bootstrap.mjs`) | `ensureRuntime` register + progressive listen/discovery; `ensureChannelAvailable` for BT discovery; `reloadDiscoveryRelays` on `signaling-changed` |
|
|
37
37
|
| **Offer/answer** (`transport/offer_answer.mjs`) | Discovery-signal glare path for `caps.needsOfferAnswer` (**internal**; uses `sendNodeSignalPacket`) |
|
|
38
38
|
| **Mesh keepalive** (`transport/mesh_keepalive.mjs`) | N/K pool, explore eviction, stable promote to `trustedPeers` |
|
|
39
39
|
|
package/index.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import { getRoutingProfile, setRoutingProfile } from './node/routing_profile.mjs
|
|
|
36
36
|
import { createGroupLinkSet } from './transport/group_link_set.mjs'
|
|
37
37
|
import {
|
|
38
38
|
configureLinkRegistry,
|
|
39
|
+
ensureChannelAvailable,
|
|
39
40
|
ensureLinkToNode,
|
|
40
41
|
ensureOverlayRouter,
|
|
41
42
|
getLinkRegistry,
|
|
@@ -63,6 +64,7 @@ export {
|
|
|
63
64
|
configureLinkRegistry,
|
|
64
65
|
createGroupLinkSet,
|
|
65
66
|
createScopedLinkRoom,
|
|
67
|
+
ensureChannelAvailable,
|
|
66
68
|
ensureLinkToNode,
|
|
67
69
|
ensureNodeDefaults,
|
|
68
70
|
ensureNodeScope,
|
package/link/providers/nostr.mjs
CHANGED
|
@@ -4,9 +4,7 @@ import { randomBytes } from 'node:crypto'
|
|
|
4
4
|
import { base64ToBytes, bytesToBase64 } from '../../core/bytes_codec.mjs'
|
|
5
5
|
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
6
6
|
import { getDiscoveryProvider, sendNodeSignalPacket } from '../../discovery/index.mjs'
|
|
7
|
-
import {
|
|
8
|
-
import { getNodeTransportSettings } from '../../node/identity.mjs'
|
|
9
|
-
import { getSignalingRuntimeConfig } from '../../node/instance.mjs'
|
|
7
|
+
import { resolveNostrRelayUrls } from '../../discovery/nostr.mjs'
|
|
10
8
|
import { ms } from '../../utils/duration.mjs'
|
|
11
9
|
import { createLruMap } from '../../utils/lru.mjs'
|
|
12
10
|
import { asLinkHandle } from '../pipe.mjs'
|
|
@@ -25,14 +23,6 @@ const NOSTR_HEARTBEAT_MS = ms('60s')
|
|
|
25
23
|
/** Nostr 链空闲超时。 */
|
|
26
24
|
const NOSTR_IDLE_TIMEOUT_MS = ms('3m')
|
|
27
25
|
|
|
28
|
-
/**
|
|
29
|
-
* @returns {string[]} 当前可用中继 URL
|
|
30
|
-
*/
|
|
31
|
-
function resolveDefaultRelayUrls() {
|
|
32
|
-
return getSignalingRuntimeConfig().relayOverride
|
|
33
|
-
?? mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
26
|
/**
|
|
37
27
|
* @param {string} remoteNodeHash 对端
|
|
38
28
|
* @param {object} packet link 包
|
|
@@ -48,7 +38,7 @@ async function publishLinkPacket(remoteNodeHash, packet) {
|
|
|
48
38
|
* @returns {import('./index.mjs').LinkProvider & { deliverPacket: (packet: object) => void }} provider
|
|
49
39
|
*/
|
|
50
40
|
export function createNostrLinkProvider(options = {}) {
|
|
51
|
-
const resolveRelayUrls = options.getRelayUrls ||
|
|
41
|
+
const resolveRelayUrls = options.getRelayUrls || resolveNostrRelayUrls
|
|
52
42
|
|
|
53
43
|
/** @type {((link: import('./index.mjs').LinkHandle) => void) | null} */
|
|
54
44
|
let onInbound = null
|
|
@@ -68,7 +68,7 @@ export async function createWebRtcLink(options) {
|
|
|
68
68
|
const handshakeTimeoutMs = Number(options.handshakeTimeoutMs) || ms('10s')
|
|
69
69
|
const channelOpenTimeoutMs = Math.max(handshakeTimeoutMs, ms('30s'))
|
|
70
70
|
const rtc = options.rtc ?? await loadNodeRtcPolyfill()
|
|
71
|
-
const trickleIceOff = getSignalingRuntimeConfig().trickleIceOff === true
|
|
71
|
+
const trickleIceOff = getSignalingRuntimeConfig().channels.webrtc?.trickleIceOff === true
|
|
72
72
|
const peerConnection = new rtc.RTCPeerConnection(options.iceServers?.length ? { iceServers: options.iceServers } : undefined)
|
|
73
73
|
const remoteSignalQueue = []
|
|
74
74
|
const seenRemoteSignals = createLruMap(1024)
|
package/link/rtc/polyfill.mjs
CHANGED
|
@@ -99,7 +99,7 @@ async function loadNodeRtcPolyfillUncached(options) {
|
|
|
99
99
|
for (const backend of backends)
|
|
100
100
|
try {
|
|
101
101
|
const mod = await backend.load()
|
|
102
|
-
const
|
|
102
|
+
const iceLocalHostnamePolicy = getSignalingRuntimeConfig().channels.webrtc?.iceLocalHostnamePolicy
|
|
103
103
|
return {
|
|
104
104
|
RTCPeerConnection: wrapRtcPeerConnectionForIceLocalHostname(
|
|
105
105
|
mod.RTCPeerConnection,
|
package/node/instance.mjs
CHANGED
|
@@ -83,9 +83,13 @@ export function setNodeLogger(logger) {
|
|
|
83
83
|
*/
|
|
84
84
|
export function setSignalingRuntimeConfig(config) {
|
|
85
85
|
if (!runtime) throw new Error('p2p: setSignalingRuntimeConfig requires initNode')
|
|
86
|
-
const previousPolicy = runtime.signaling.iceLocalHostnamePolicy
|
|
87
|
-
runtime.signaling = resolveSignalingRuntimeConfig({
|
|
88
|
-
|
|
86
|
+
const previousPolicy = runtime.signaling.channels.webrtc?.iceLocalHostnamePolicy
|
|
87
|
+
runtime.signaling = resolveSignalingRuntimeConfig({
|
|
88
|
+
...runtime.signaling,
|
|
89
|
+
...config,
|
|
90
|
+
channels: { ...runtime.signaling.channels, ...config?.channels },
|
|
91
|
+
})
|
|
92
|
+
if (runtime.signaling.channels.webrtc?.iceLocalHostnamePolicy !== previousPolicy)
|
|
89
93
|
rtcPolyfillCacheEpoch++
|
|
90
94
|
emitNodeChange('signaling-changed', runtime.signaling)
|
|
91
95
|
}
|
|
@@ -4,25 +4,64 @@ import process from 'node:process'
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @typedef {{
|
|
7
|
-
*
|
|
8
|
-
* iceLocalHostnamePolicy: IceLocalHostnamePolicy
|
|
9
|
-
* trickleIceOff: boolean
|
|
7
|
+
* channels: ChannelsConfig
|
|
10
8
|
* }} SignalingRuntimeConfig
|
|
11
9
|
*/
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Record<string, object | boolean>} ChannelsConfig
|
|
13
|
+
*/
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
* @returns {
|
|
16
|
+
* webrtc 通道默认配置。
|
|
17
|
+
* @returns {{ iceLocalHostnamePolicy: IceLocalHostnamePolicy, trickleIceOff: boolean }} webrtc 通道默认配置
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
function defaultWebRtcConfig() {
|
|
20
20
|
const iceLocalHostnamePolicy = process.platform === 'win32' ? 'drop' : 'none'
|
|
21
|
-
return {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
return { iceLocalHostnamePolicy, trickleIceOff: iceLocalHostnamePolicy !== 'none' }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 各介质的默认 channel 配置;对象值是启用并覆盖默认。 */
|
|
25
|
+
const DEFAULT_CHANNEL_CONFIG = {
|
|
26
|
+
nostr: true,
|
|
27
|
+
lan: true,
|
|
28
|
+
bt: true,
|
|
29
|
+
webrtc: defaultWebRtcConfig(),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 归一到完整 channels 记录:false 禁用,其余(undefined / true / object)启用并合并默认配置。
|
|
34
|
+
* 未提及的通道保持默认启用。
|
|
35
|
+
* @param {object | undefined} [raw] 原始 channels 配置
|
|
36
|
+
* @returns {ChannelsConfig} 每个已知通道都有明确值(object | false)的完整记录
|
|
37
|
+
*/
|
|
38
|
+
function resolveChannels(raw = {}) {
|
|
39
|
+
/** @type {ChannelsConfig} */
|
|
40
|
+
const out = {}
|
|
41
|
+
for (const name of Object.keys(DEFAULT_CHANNEL_CONFIG)) {
|
|
42
|
+
const value = raw[name]
|
|
43
|
+
out[name] = value !== false && { ...Object(DEFAULT_CHANNEL_CONFIG[name]), ...Object(value) }
|
|
25
44
|
}
|
|
45
|
+
return out
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 生成禁用全部通道的 channels 配置;`overrides` 可重新启用/覆盖指定通道。
|
|
50
|
+
* @param {Record<string, object | boolean>} [overrides] 需覆盖的通道配置
|
|
51
|
+
* @returns {ChannelsConfig} 除覆盖外全部 false 的 channels 配置
|
|
52
|
+
*/
|
|
53
|
+
export function disableAllChannels(overrides = {}) {
|
|
54
|
+
/** @type {ChannelsConfig} */
|
|
55
|
+
const disabledChannels = {}
|
|
56
|
+
for (const name of Object.keys(DEFAULT_CHANNEL_CONFIG)) disabledChannels[name] = false
|
|
57
|
+
return { ...disabledChannels, ...overrides }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @returns {SignalingRuntimeConfig} 默认信令运行时配置
|
|
62
|
+
*/
|
|
63
|
+
export function defaultSignalingRuntimeConfig() {
|
|
64
|
+
return { channels: resolveChannels() }
|
|
26
65
|
}
|
|
27
66
|
|
|
28
67
|
/**
|
|
@@ -30,22 +69,6 @@ export function defaultSignalingRuntimeConfig() {
|
|
|
30
69
|
* @returns {SignalingRuntimeConfig} 合并后的信令运行时配置
|
|
31
70
|
*/
|
|
32
71
|
export function resolveSignalingRuntimeConfig(patch = {}) {
|
|
33
|
-
|
|
34
|
-
|
|
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 || ''))
|
|
45
|
-
.filter(url => url.startsWith('wss://')))]
|
|
46
|
-
return {
|
|
47
|
-
relayOverride,
|
|
48
|
-
iceLocalHostnamePolicy,
|
|
49
|
-
trickleIceOff: patch.trickleIceOff !== undefined ? !!patch.trickleIceOff : iceLocalHostnamePolicy !== 'none',
|
|
50
|
-
}
|
|
72
|
+
if (!patch || typeof patch !== 'object') return defaultSignalingRuntimeConfig()
|
|
73
|
+
return { channels: resolveChannels(patch.channels) }
|
|
51
74
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steve02081504/fount-p2p",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.33",
|
|
4
4
|
"description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"network",
|
|
7
7
|
"p2p",
|
|
8
8
|
"fount",
|
|
9
|
-
"AI"
|
|
9
|
+
"AI",
|
|
10
|
+
"nostr",
|
|
11
|
+
"LAN",
|
|
12
|
+
"bluetooth"
|
|
10
13
|
],
|
|
11
14
|
"type": "module",
|
|
12
15
|
"repository": {
|
|
@@ -536,6 +536,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
536
536
|
lanTcpPort: bootstrap.lanTcpPort,
|
|
537
537
|
whenListening: bootstrap.whenListening,
|
|
538
538
|
ensureRuntime: ensureRuntimeWithMesh,
|
|
539
|
+
ensureChannelAvailable: bootstrap.ensureChannelAvailable,
|
|
539
540
|
reloadDiscoveryRelays: bootstrap.reloadDiscoveryRelays,
|
|
540
541
|
ensureOverlayRouter,
|
|
541
542
|
getOverlayRouter,
|
|
@@ -759,6 +760,12 @@ export const ensureOverlayRouter = bindRegistryMethod('ensureOverlayRouter')
|
|
|
759
760
|
export const reloadDiscoveryRelays = bindRegistryMethod('reloadDiscoveryRelays')
|
|
760
761
|
/** 确保到 nodeHash 的活跃链路。 @type {(...methodArguments: unknown[]) => unknown} */
|
|
761
762
|
export const ensureLinkToNode = bindRegistryMethod('ensureLinkToNode')
|
|
763
|
+
/**
|
|
764
|
+
* 确保指定 channel 可用。
|
|
765
|
+
* @param {string} channel 通道名
|
|
766
|
+
* @returns {Promise<boolean>} 该 channel 可用为 true
|
|
767
|
+
*/
|
|
768
|
+
export const ensureChannelAvailable = bindRegistryMethod('ensureChannelAvailable')
|
|
762
769
|
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
763
770
|
export const getLink = bindRegistryMethod('getLink')
|
|
764
771
|
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
unregisterDiscoveryProvider,
|
|
11
11
|
} from '../discovery/index.mjs'
|
|
12
12
|
import { createLanDiscoveryProvider } from '../discovery/lan.mjs'
|
|
13
|
-
import {
|
|
13
|
+
import { createNostrDiscoveryProvider, resolveNostrRelayUrls } from '../discovery/nostr.mjs'
|
|
14
14
|
import { createBleGattLinkProvider } from '../link/providers/ble_gatt.mjs'
|
|
15
15
|
import {
|
|
16
16
|
listLinkProviders,
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
import { createLanTcpLinkProvider } from '../link/providers/lan_tcp.mjs'
|
|
21
21
|
import { createNostrLinkProvider } from '../link/providers/nostr.mjs'
|
|
22
22
|
import { createWebRtcLinkProvider } from '../link/providers/webrtc.mjs'
|
|
23
|
-
import { getNodeTransportSettings } from '../node/identity.mjs'
|
|
24
23
|
import { getSignalingRuntimeConfig, onNodeChange } from '../node/instance.mjs'
|
|
25
24
|
import { isConnectivityDebug, nodeDebug, shortHash } from '../node/log.mjs'
|
|
26
25
|
|
|
@@ -85,14 +84,12 @@ export function createRuntimeBootstrap(deps) {
|
|
|
85
84
|
let signalListenReady = null
|
|
86
85
|
/** @type {Promise<void> | null} */
|
|
87
86
|
let runtimeWarm = null
|
|
88
|
-
/** @type {Promise<void> | null} */
|
|
89
|
-
let bluetoothWarm = null
|
|
90
87
|
/** @type {(() => void) | null} */
|
|
91
88
|
let stopPresence = null
|
|
92
89
|
/** @type {(() => void) | null} */
|
|
93
90
|
let stopSignalListener = null
|
|
94
|
-
/** @type {
|
|
95
|
-
const stopLinkListeners =
|
|
91
|
+
/** @type {Map<string, () => void>} */
|
|
92
|
+
const stopLinkListeners = new Map()
|
|
96
93
|
/** @type {ReturnType<typeof createLanTcpLinkProvider> | null} */
|
|
97
94
|
let ownedLanTcp = null
|
|
98
95
|
/** @type {ReturnType<typeof createBleGattLinkProvider> | null} */
|
|
@@ -104,22 +101,11 @@ export function createRuntimeBootstrap(deps) {
|
|
|
104
101
|
let stopSignalingWatch = null
|
|
105
102
|
|
|
106
103
|
/**
|
|
107
|
-
* @
|
|
104
|
+
* @param {string} name 通道名
|
|
105
|
+
* @returns {boolean} 该通道是否启用(默认启用,false 禁用)
|
|
108
106
|
*/
|
|
109
|
-
function
|
|
110
|
-
return getSignalingRuntimeConfig().
|
|
111
|
-
?? mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** 注册默认 discovery provider(lan/nostr) */
|
|
115
|
-
function registerDiscoveryDefaults() {
|
|
116
|
-
const providerIds = new Set(listDiscoveryProviders().map(provider => provider.id))
|
|
117
|
-
if (!providerIds.has('lan'))
|
|
118
|
-
registerDiscoveryProvider(createLanDiscoveryProvider({
|
|
119
|
-
localNodeHash: localIdentity.nodeHash,
|
|
120
|
-
}))
|
|
121
|
-
if (!providerIds.has('nostr'))
|
|
122
|
-
registerNostrProvider()
|
|
107
|
+
function isChannelEnabled(name) {
|
|
108
|
+
return getSignalingRuntimeConfig().channels[name] !== false
|
|
123
109
|
}
|
|
124
110
|
|
|
125
111
|
/** 注册/替换 nostr discovery provider */
|
|
@@ -138,26 +124,6 @@ export function createRuntimeBootstrap(deps) {
|
|
|
138
124
|
return runtimeStarted
|
|
139
125
|
}
|
|
140
126
|
|
|
141
|
-
/**
|
|
142
|
-
* @returns {Promise<void>}
|
|
143
|
-
*/
|
|
144
|
-
async function ensureLinkProviders() {
|
|
145
|
-
if (!autoRegisterLinkProviders) return
|
|
146
|
-
if (!ownedLanTcp) {
|
|
147
|
-
ownedLanTcp = createLanTcpLinkProvider()
|
|
148
|
-
registerLinkProvider(ownedLanTcp)
|
|
149
|
-
}
|
|
150
|
-
if (!ownedBleGatt) {
|
|
151
|
-
ownedBleGatt = createBleGattLinkProvider()
|
|
152
|
-
registerLinkProvider(ownedBleGatt)
|
|
153
|
-
}
|
|
154
|
-
const ids = new Set(listLinkProviders().map(provider => provider.id))
|
|
155
|
-
if (!ids.has('webrtc'))
|
|
156
|
-
registerLinkProvider(createWebRtcLinkProvider())
|
|
157
|
-
if (!ids.has('nostr'))
|
|
158
|
-
registerLinkProvider(createNostrLinkProvider({ getRelayUrls: resolveNostrRelayUrls }))
|
|
159
|
-
}
|
|
160
|
-
|
|
161
127
|
/**
|
|
162
128
|
* @returns {number | null} 本机 lan_tcp 监听端口,未就绪为 null
|
|
163
129
|
*/
|
|
@@ -196,12 +162,13 @@ export function createRuntimeBootstrap(deps) {
|
|
|
196
162
|
*/
|
|
197
163
|
async function startProviderListening(provider) {
|
|
198
164
|
if (!provider.ensureListening) return
|
|
165
|
+
stopLinkListeners.get(provider.id)?.()
|
|
199
166
|
try {
|
|
200
167
|
const stop = await provider.ensureListening({
|
|
201
168
|
localIdentity,
|
|
202
169
|
onInbound: onInboundLink,
|
|
203
170
|
})
|
|
204
|
-
if (stop) stopLinkListeners.
|
|
171
|
+
if (stop) stopLinkListeners.set(provider.id, stop)
|
|
205
172
|
}
|
|
206
173
|
catch { /* provider listen unavailable */ }
|
|
207
174
|
}
|
|
@@ -214,60 +181,6 @@ export function createRuntimeBootstrap(deps) {
|
|
|
214
181
|
return encryptAdvertForScope('network', localIdentity, body)
|
|
215
182
|
}
|
|
216
183
|
|
|
217
|
-
/**
|
|
218
|
-
* @param {number} gen 启动世代
|
|
219
|
-
* @returns {Promise<void>}
|
|
220
|
-
*/
|
|
221
|
-
async function warmBluetoothTask(gen) {
|
|
222
|
-
if (autoRegisterDiscoveryProviders) {
|
|
223
|
-
const providerIds = new Set(listDiscoveryProviders().map(provider => provider.id))
|
|
224
|
-
if (!providerIds.has('bt')) {
|
|
225
|
-
const bt = await import('../discovery/bt/index.mjs').catch(() => null)
|
|
226
|
-
if (generation !== gen || !isLive()) return
|
|
227
|
-
if (await bt?.canUseBluetoothRuntime?.()) {
|
|
228
|
-
if (generation !== gen || !isLive()) return
|
|
229
|
-
const provider = bt.createBluetoothDiscoveryProvider()
|
|
230
|
-
registerDiscoveryProvider(provider)
|
|
231
|
-
if (generation !== gen || !isLive()) return
|
|
232
|
-
if (provider.startPresence)
|
|
233
|
-
try {
|
|
234
|
-
const stop = await provider.startPresence(async () => ({
|
|
235
|
-
nodeHash: localIdentity.nodeHash,
|
|
236
|
-
advertBytes: await buildNetworkAdvertBytes(),
|
|
237
|
-
}))
|
|
238
|
-
if (stop && generation === gen && isLive()) {
|
|
239
|
-
const prev = stopPresence
|
|
240
|
-
/**
|
|
241
|
-
* 停止 presence 广播并链式调用上一轮清理。
|
|
242
|
-
*/
|
|
243
|
-
stopPresence = () => { try { stop() } catch { /* ignore */ }; prev?.() }
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
catch { /* ignore */ }
|
|
247
|
-
if (provider.caps?.canSignal && provider.listenNodeSignals)
|
|
248
|
-
try {
|
|
249
|
-
const stop = await provider.listenNodeSignals(localIdentity.nodeHash, bytes => {
|
|
250
|
-
void handleIncomingSignal(bytes).catch(() => { })
|
|
251
|
-
})
|
|
252
|
-
if (stop && generation === gen && isLive()) {
|
|
253
|
-
const prev = stopSignalListener
|
|
254
|
-
/**
|
|
255
|
-
* 停止信令监听并链式调用上一轮清理。
|
|
256
|
-
*/
|
|
257
|
-
stopSignalListener = () => { try { stop() } catch { /* ignore */ }; prev?.() }
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
catch { /* ignore */ }
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (generation !== gen || !isLive()) return
|
|
265
|
-
if (ownedBleGatt && await Promise.resolve(ownedBleGatt.isAvailable())) {
|
|
266
|
-
if (generation !== gen || !isLive()) return
|
|
267
|
-
await startProviderListening(ownedBleGatt)
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
184
|
/**
|
|
272
185
|
* @param {number} gen 启动世代
|
|
273
186
|
* @returns {Promise<void>}
|
|
@@ -306,20 +219,91 @@ export function createRuntimeBootstrap(deps) {
|
|
|
306
219
|
})()
|
|
307
220
|
}
|
|
308
221
|
|
|
222
|
+
/**
|
|
223
|
+
* 释放指定 id 的 link provider 监听与注册
|
|
224
|
+
* @param {id: string} id link provider id
|
|
225
|
+
*/
|
|
226
|
+
function releaseLinkProvider(id) {
|
|
227
|
+
const stop = stopLinkListeners.get(id)
|
|
228
|
+
stopLinkListeners.delete(id)
|
|
229
|
+
try { stop?.() } catch { /* ignore */ }
|
|
230
|
+
unregisterLinkProvider(id)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** 按 channels 配置同步 link provider(启用注册 / 禁用注销) */
|
|
234
|
+
function reconcileLinkProviders() {
|
|
235
|
+
if (!autoRegisterLinkProviders) return
|
|
236
|
+
const present = new Set(listLinkProviders().map(provider => provider.id.split(':')[0]))
|
|
237
|
+
if (isChannelEnabled('nostr')) {
|
|
238
|
+
if (!present.has('nostr'))
|
|
239
|
+
registerLinkProvider(createNostrLinkProvider({ getRelayUrls: resolveNostrRelayUrls }))
|
|
240
|
+
}
|
|
241
|
+
else releaseLinkProvider('nostr')
|
|
242
|
+
|
|
243
|
+
if (isChannelEnabled('lan')) {
|
|
244
|
+
if (!ownedLanTcp) {
|
|
245
|
+
ownedLanTcp = createLanTcpLinkProvider()
|
|
246
|
+
registerLinkProvider(ownedLanTcp)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (ownedLanTcp) {
|
|
250
|
+
releaseLinkProvider(ownedLanTcp.id)
|
|
251
|
+
ownedLanTcp = null
|
|
252
|
+
}
|
|
253
|
+
if (isChannelEnabled('bt')) {
|
|
254
|
+
if (!ownedBleGatt) {
|
|
255
|
+
ownedBleGatt = createBleGattLinkProvider()
|
|
256
|
+
registerLinkProvider(ownedBleGatt)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else if (ownedBleGatt) {
|
|
260
|
+
releaseLinkProvider(ownedBleGatt.id)
|
|
261
|
+
ownedBleGatt = null
|
|
262
|
+
}
|
|
263
|
+
if (isChannelEnabled('webrtc')) {
|
|
264
|
+
if (!listLinkProviders().some(provider => provider.id.split(':')[0] === 'webrtc'))
|
|
265
|
+
registerLinkProvider(createWebRtcLinkProvider())
|
|
266
|
+
}
|
|
267
|
+
else releaseLinkProvider('webrtc')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** 按 channels 配置同步 discovery provider(启用注册 / 禁用注销;BT discovery 由 ensureChannelAvailable 显式注册) */
|
|
271
|
+
function reconcileDiscoveryProviders() {
|
|
272
|
+
if (!autoRegisterDiscoveryProviders) return
|
|
273
|
+
const present = new Set(listDiscoveryProviders().map(provider => provider.id))
|
|
274
|
+
if (isChannelEnabled('lan')) {
|
|
275
|
+
if (!present.has('lan'))
|
|
276
|
+
registerDiscoveryProvider(createLanDiscoveryProvider({ localNodeHash: localIdentity.nodeHash }))
|
|
277
|
+
}
|
|
278
|
+
else unregisterDiscoveryProvider('lan')
|
|
279
|
+
|
|
280
|
+
if (isChannelEnabled('nostr')) registerNostrProvider()
|
|
281
|
+
else unregisterDiscoveryProvider('nostr')
|
|
282
|
+
if (!isChannelEnabled('bt')) unregisterDiscoveryProvider('bt')
|
|
283
|
+
}
|
|
284
|
+
|
|
309
285
|
/**
|
|
310
286
|
* @returns {Promise<void>}
|
|
311
287
|
*/
|
|
312
288
|
async function reloadDiscoveryRelays() {
|
|
313
|
-
if (!runtimeStarted || !autoRegisterDiscoveryProviders) return
|
|
314
289
|
if (reloadInflight) return await reloadInflight
|
|
315
290
|
reloadInflight = (async () => {
|
|
291
|
+
if (runtimeStart) await runtimeStart
|
|
292
|
+
await runtimeWarm?.catch(() => { })
|
|
316
293
|
const gen = generation
|
|
294
|
+
if (!isLive()) return
|
|
317
295
|
stopPresence?.()
|
|
318
296
|
stopSignalListener?.()
|
|
319
297
|
stopPresence = null
|
|
320
298
|
stopSignalListener = null
|
|
321
|
-
|
|
299
|
+
reconcileLinkProviders()
|
|
300
|
+
reconcileDiscoveryProviders()
|
|
322
301
|
if (generation !== gen || !isLive()) return
|
|
302
|
+
if (ownedLanTcp && !stopLinkListeners.has(ownedLanTcp.id))
|
|
303
|
+
await startProviderListening(ownedLanTcp)
|
|
304
|
+
if (ownedBleGatt && !stopLinkListeners.has(ownedBleGatt.id)
|
|
305
|
+
&& await Promise.resolve(ownedBleGatt.isAvailable()))
|
|
306
|
+
await startProviderListening(ownedBleGatt)
|
|
323
307
|
signalListenReady = (async () => {
|
|
324
308
|
if (generation !== gen || !isLive()) return
|
|
325
309
|
if (!listDiscoveryProviders().length) return
|
|
@@ -353,9 +337,9 @@ export function createRuntimeBootstrap(deps) {
|
|
|
353
337
|
runtimeStart = (async () => {
|
|
354
338
|
runtimeStarted = true
|
|
355
339
|
const gen = generation
|
|
356
|
-
|
|
340
|
+
reconcileLinkProviders()
|
|
357
341
|
if (autoRegisterDiscoveryProviders)
|
|
358
|
-
|
|
342
|
+
reconcileDiscoveryProviders()
|
|
359
343
|
if (isConnectivityDebug())
|
|
360
344
|
nodeDebug('p2p:runtime ensure', {
|
|
361
345
|
self: shortHash(localIdentity.nodeHash),
|
|
@@ -367,7 +351,6 @@ export function createRuntimeBootstrap(deps) {
|
|
|
367
351
|
if (event === 'signaling-changed')
|
|
368
352
|
void reloadDiscoveryRelays().catch(() => { })
|
|
369
353
|
})
|
|
370
|
-
bluetoothWarm = warmBluetoothTask(gen).catch(() => { })
|
|
371
354
|
runtimeWarm = warmListenAndDiscovery(gen)
|
|
372
355
|
void runtimeWarm.catch(() => { })
|
|
373
356
|
})()
|
|
@@ -379,6 +362,39 @@ export function createRuntimeBootstrap(deps) {
|
|
|
379
362
|
}
|
|
380
363
|
}
|
|
381
364
|
|
|
365
|
+
/**
|
|
366
|
+
* 确保指定 channel 可用:重 channel(bt)做探测并注册 discovery;其余 await 其渐进式就绪。
|
|
367
|
+
* @param {string} channel 通道名
|
|
368
|
+
* @returns {Promise<boolean>} 该 channel 可用为 true
|
|
369
|
+
*/
|
|
370
|
+
async function ensureChannelAvailable(channel) {
|
|
371
|
+
if (!autoRegisterDiscoveryProviders) return false
|
|
372
|
+
if (runtimeStart) await runtimeStart
|
|
373
|
+
if (!isLive()) return false
|
|
374
|
+
switch (channel) {
|
|
375
|
+
case 'lan':
|
|
376
|
+
await whenListening()
|
|
377
|
+
return isChannelEnabled('lan')
|
|
378
|
+
case 'nostr':
|
|
379
|
+
case 'webrtc':
|
|
380
|
+
await whenSignalListening()
|
|
381
|
+
return isChannelEnabled(channel)
|
|
382
|
+
case 'bt': {
|
|
383
|
+
if (!isChannelEnabled('bt')) return false
|
|
384
|
+
if (listDiscoveryProviders().some(provider => provider.id === 'bt')) return true
|
|
385
|
+
const bt = await import('../discovery/bt/index.mjs')
|
|
386
|
+
if (!isLive()) return false
|
|
387
|
+
if (await bt.canUseBluetoothRuntime()) {
|
|
388
|
+
registerDiscoveryProvider(bt.createBluetoothDiscoveryProvider())
|
|
389
|
+
await reloadDiscoveryRelays()
|
|
390
|
+
}
|
|
391
|
+
return listDiscoveryProviders().some(provider => provider.id === 'bt')
|
|
392
|
+
}
|
|
393
|
+
default:
|
|
394
|
+
throw new Error(`p2p: unknown channel ${channel}`)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
382
398
|
/**
|
|
383
399
|
* @returns {Promise<void>}
|
|
384
400
|
*/
|
|
@@ -395,8 +411,9 @@ export function createRuntimeBootstrap(deps) {
|
|
|
395
411
|
runtimeWarm?.catch(() => { }) ?? Promise.resolve(),
|
|
396
412
|
new Promise(resolve => setTimeout(resolve, 500)),
|
|
397
413
|
])
|
|
398
|
-
for (const stop of stopLinkListeners.
|
|
414
|
+
for (const stop of stopLinkListeners.values())
|
|
399
415
|
try { stop() } catch { /* ignore */ }
|
|
416
|
+
stopLinkListeners.clear()
|
|
400
417
|
clearDiscoveryProviders()
|
|
401
418
|
if (ownedLanTcp) {
|
|
402
419
|
unregisterLinkProvider(ownedLanTcp.id)
|
|
@@ -409,12 +426,12 @@ export function createRuntimeBootstrap(deps) {
|
|
|
409
426
|
lanListenReady = null
|
|
410
427
|
signalListenReady = null
|
|
411
428
|
runtimeWarm = null
|
|
412
|
-
bluetoothWarm = null
|
|
413
429
|
reloadInflight = null
|
|
414
430
|
}
|
|
415
431
|
|
|
416
432
|
return {
|
|
417
433
|
ensureRuntime,
|
|
434
|
+
ensureChannelAvailable,
|
|
418
435
|
whenListening,
|
|
419
436
|
whenSignalListening,
|
|
420
437
|
buildLocalAdvert,
|