@steve02081504/fount-p2p 0.0.11 → 0.0.13

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 (102) hide show
  1. package/README.md +42 -11
  2. package/core/bytes_codec.mjs +65 -10
  3. package/core/composite_key.mjs +5 -5
  4. package/core/tcp_port.mjs +1 -1
  5. package/crypto/checkpoint_sign.mjs +2 -2
  6. package/crypto/crypto.mjs +3 -3
  7. package/crypto/key.mjs +12 -12
  8. package/dag/canonicalize_row.mjs +3 -3
  9. package/dag/index.mjs +2 -3
  10. package/dag/storage.mjs +36 -36
  11. package/discovery/advert_peer_hints.mjs +1 -1
  12. package/discovery/adverts.mjs +109 -0
  13. package/discovery/bt/index.mjs +154 -113
  14. package/discovery/bt/probe_child.mjs +2 -1
  15. package/discovery/bt/runtime.mjs +5 -12
  16. package/discovery/index.mjs +267 -62
  17. package/discovery/internal/signal_crypto.mjs +109 -0
  18. package/discovery/lan.mjs +228 -0
  19. package/discovery/nostr.mjs +430 -141
  20. package/federation/chunk_fetch_pending.mjs +14 -18
  21. package/federation/dag_order_cache.mjs +1 -1
  22. package/federation/entity_key_chain.mjs +3 -4
  23. package/federation/manifest_fetch_pending.mjs +1 -3
  24. package/federation/topo_order_memo.mjs +11 -4
  25. package/files/assemble.mjs +14 -14
  26. package/files/assemble_stream.mjs +6 -6
  27. package/files/chunk_fetch.mjs +2 -2
  28. package/files/chunk_responder.mjs +29 -20
  29. package/files/evfs.mjs +31 -30
  30. package/files/manifest_fetch.mjs +16 -3
  31. package/files/public_manifest.mjs +11 -11
  32. package/files/transfer_key_registry.mjs +2 -2
  33. package/index.mjs +86 -11
  34. package/infra/cli.mjs +62 -0
  35. package/infra/debug_log.mjs +56 -0
  36. package/infra/default_node_dir.mjs +22 -0
  37. package/infra/priority.mjs +56 -0
  38. package/infra/service.mjs +140 -0
  39. package/infra/tunables.json +5 -0
  40. package/link/channel_mux.mjs +10 -10
  41. package/link/frame.mjs +76 -131
  42. package/link/handshake.mjs +19 -20
  43. package/link/pipe.mjs +53 -79
  44. package/link/providers/ble_gatt.mjs +19 -31
  45. package/link/providers/lan_tcp.mjs +17 -32
  46. package/link/providers/link_id_pipe.mjs +46 -0
  47. package/link/providers/webrtc.mjs +42 -51
  48. package/link/rtc.mjs +24 -12
  49. package/mailbox/consumer_registry.mjs +2 -2
  50. package/mailbox/deliver_or_store.mjs +6 -6
  51. package/mailbox/wire.mjs +28 -24
  52. package/node/entity_store.mjs +6 -6
  53. package/node/identity.mjs +4 -4
  54. package/node/instance.mjs +46 -27
  55. package/node/local_data_revision.mjs +26 -0
  56. package/node/log.mjs +56 -0
  57. package/node/network.mjs +46 -14
  58. package/node/reputation_store.mjs +4 -4
  59. package/node/reputation_sync.mjs +318 -0
  60. package/node/routing_profile.mjs +21 -0
  61. package/node/signaling_config.mjs +30 -11
  62. package/overlay/index.mjs +35 -14
  63. package/package.json +13 -2
  64. package/rooms/scoped_link.mjs +17 -182
  65. package/schemas/discovery.mjs +1 -2
  66. package/schemas/federation_pull.mjs +2 -2
  67. package/schemas/part_query.mjs +1 -1
  68. package/schemas/remote_event.mjs +1 -1
  69. package/transport/advert_ingest.mjs +17 -0
  70. package/transport/group_link_set.mjs +165 -134
  71. package/transport/ice_servers.mjs +2 -2
  72. package/transport/link_registry.mjs +283 -124
  73. package/transport/mesh_keepalive.mjs +217 -0
  74. package/transport/node_scope.mjs +289 -0
  75. package/transport/offer_answer.mjs +48 -47
  76. package/transport/peer_pool.mjs +169 -78
  77. package/transport/remote_user_room.mjs +18 -23
  78. package/transport/rtc_connection_budget.mjs +18 -4
  79. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  80. package/transport/runtime_bootstrap.mjs +172 -104
  81. package/transport/tunables.json +11 -0
  82. package/transport/tunables.mjs +18 -0
  83. package/transport/user_room.mjs +93 -161
  84. package/trust_graph/build.mjs +0 -2
  85. package/trust_graph/cache.mjs +12 -3
  86. package/trust_graph/registry.mjs +6 -6
  87. package/trust_graph/send.mjs +1 -4
  88. package/utils/async_mutex.mjs +4 -4
  89. package/utils/emit_safe.mjs +11 -0
  90. package/utils/json_io.mjs +12 -12
  91. package/utils/map_pool.mjs +5 -5
  92. package/utils/shuffle.mjs +13 -0
  93. package/utils/ttl_map.mjs +29 -4
  94. package/wire/ingress.mjs +1 -1
  95. package/wire/part_ingress.mjs +38 -36
  96. package/wire/part_invoke.mjs +4 -4
  97. package/wire/part_query.mjs +28 -23
  98. package/wire/part_query.tunables.json +6 -1
  99. package/wire/part_query_cache.mjs +1 -1
  100. package/wire/volatile_signature.mjs +1 -1
  101. package/discovery/mdns.mjs +0 -191
  102. package/transport/signal_crypto.mjs +0 -82
package/infra/cli.mjs ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { on_shutdown } from 'on-shutdown'
4
+
5
+ import { ensureNodeDefaults, getNodeHash } from '../node/identity.mjs'
6
+ import { initNode, setNodeLogger } from '../node/instance.mjs'
7
+ import { setConnectivityDebug } from '../node/log.mjs'
8
+ import { getLinkRegistry } from '../transport/link_registry.mjs'
9
+
10
+ import { resolveNodeDir } from './default_node_dir.mjs'
11
+ import { setInfraPriority, startInfra, stopInfra } from './service.mjs'
12
+
13
+ /**
14
+ * @param {string[]} argv - 命令行参数(不含 node 路径)
15
+ * @returns {{ nodeDir?: string, quiet?: boolean, useLocalReputation?: boolean, help?: boolean }} 解析结果
16
+ */
17
+ function parseArgs(argv) {
18
+ /** @type {ReturnType<typeof parseArgs>} */
19
+ const out = {}
20
+ for (let i = 0; i < argv.length; i++) {
21
+ const arg = argv[i]
22
+ if (arg === '--help' || arg === '-h') out.help = true
23
+ else if (arg === '--quiet') out.quiet = true
24
+ else if (arg === '--use-local-reputation') out.useLocalReputation = true
25
+ else if (arg === '--node-dir') out.nodeDir = argv[++i]
26
+ }
27
+ return out
28
+ }
29
+
30
+ /** 打印 CLI 用法。 */
31
+ function printHelp() {
32
+ console.log(`Usage: fount-p2p [--node-dir PATH] [--use-local-reputation] [--quiet]
33
+
34
+ Public-good infra relay node (overlay + mailbox).
35
+ Connectivity debug logs on by default (Nostr/LAN/mesh/dial); --quiet silences.
36
+ Non-CLI shells: setConnectivityDebug(true).
37
+ Default data dir: Windows %LOCALAPPDATA%/fount-p2p/node, else ~/.local/share/fount-p2p/node`)
38
+ }
39
+
40
+ const args = parseArgs(process.argv.slice(2))
41
+ if (args.help) {
42
+ printHelp()
43
+ process.exit(0)
44
+ }
45
+
46
+ const nodeDir = resolveNodeDir(args.nodeDir)
47
+ initNode({ nodeDir })
48
+ if (args.quiet) {
49
+ setNodeLogger(null)
50
+ setConnectivityDebug(false)
51
+ }
52
+ else
53
+ setConnectivityDebug(true)
54
+ ensureNodeDefaults()
55
+ const nodeHash = getNodeHash()
56
+ await getLinkRegistry().ensureRuntime()
57
+ if (args.useLocalReputation) setInfraPriority({ useLocalReputation: true })
58
+ await startInfra({ logger: args.quiet ? null : console })
59
+
60
+ console.log(`p2p infra running (nodeDir=${nodeDir} nodeHash=${nodeHash})`)
61
+
62
+ on_shutdown(stopInfra)
@@ -0,0 +1,56 @@
1
+ import { getLinkRegistry } from '../transport/link_registry.mjs'
2
+
3
+ /** @type {Array<() => void>} */
4
+ const cleanups = []
5
+
6
+ /**
7
+ * 挂载 registry link/overlay/node scope 调试日志。
8
+ * @param {{ info?: Function, warn?: Function, error?: Function, log?: Function } | null} logger - 日志输出目标,null 表示静默
9
+ * @returns {() => void} 取消 debug 监听的 dispose
10
+ */
11
+ export function attachInfraDebugLog(logger) {
12
+ detachInfraDebugLog()
13
+ if (!logger) return () => { }
14
+ const registry = getLinkRegistry()
15
+ /**
16
+ * @param {'info' | 'warn' | 'error' | 'log'} level - 日志级别
17
+ * @param {string} message - 日志消息
18
+ * @param {object} [extra] - 附加字段
19
+ */
20
+ const log = (level, message, extra) => {
21
+ logger?.[level]?.(message, extra)
22
+ }
23
+ cleanups.push(registry.onLinkUp((nodeHash, link) => {
24
+ log('info', 'p2p:infra link up', {
25
+ nodeHash,
26
+ providerId: link?.providerId,
27
+ level: link?.level,
28
+ })
29
+ }))
30
+ cleanups.push(registry.onLinkDown((nodeHash, reason) => {
31
+ log('info', 'p2p:infra link down', { nodeHash, reason })
32
+ }))
33
+ cleanups.push(registry.subscribeScope('overlay', (from, envelope) => {
34
+ log('info', 'p2p:infra overlay', {
35
+ from,
36
+ action: envelope?.action,
37
+ path: envelope?.payload?.path,
38
+ })
39
+ }))
40
+ cleanups.push(registry.subscribeScope('node', (from, envelope) => {
41
+ log('info', 'p2p:infra node', {
42
+ from,
43
+ action: envelope?.action,
44
+ })
45
+ }))
46
+ return detachInfraDebugLog
47
+ }
48
+
49
+ /**
50
+ * 卸掉 infra debug 监听。
51
+ * @returns {void}
52
+ */
53
+ export function detachInfraDebugLog() {
54
+ for (const cleanup of cleanups.splice(0))
55
+ try { cleanup() } catch { /* ignore */ }
56
+ }
@@ -0,0 +1,22 @@
1
+ import os from 'node:os'
2
+ import path from 'node:path'
3
+
4
+ /**
5
+ * @returns {string} 平台默认 node 数据目录
6
+ */
7
+ export function defaultNodeDir() {
8
+ if (process.platform === 'win32') {
9
+ const base = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
10
+ return path.join(base, 'fount-p2p', 'node')
11
+ }
12
+ return path.join(os.homedir(), '.local', 'share', 'fount-p2p', 'node')
13
+ }
14
+
15
+ /**
16
+ * @param {string | undefined} override - CLI 或调用方覆盖
17
+ * @returns {string} 解析后的绝对 node 目录
18
+ */
19
+ export function resolveNodeDir(override) {
20
+ const trimmed = String(override || '').trim()
21
+ return trimmed ? path.resolve(trimmed) : defaultNodeDir()
22
+ }
@@ -0,0 +1,56 @@
1
+ import { normalizeHex64 } from '../core/hexIds.mjs'
2
+ import { getReputationTable } from '../node/reputation_sync.mjs'
3
+ import { pickNodeScoreFromReputation } from '../reputation/pick_score.mjs'
4
+ import { getLinkRegistry } from '../transport/link_registry.mjs'
5
+
6
+
7
+ /** @type {{ useLocalReputation: boolean }} */
8
+ let priorityConfig = { useLocalReputation: false }
9
+
10
+ const PRIORITY_BOOST = 1000
11
+
12
+ /**
13
+ * 配置 infra 路由加权(是否用本地 reputation)。
14
+ * @param {{ useLocalReputation?: boolean }} config - 是否用本地 reputation 加权路由
15
+ * @returns {void}
16
+ */
17
+ export function setInfraPriority(config = {}) {
18
+ priorityConfig = {
19
+ useLocalReputation: Boolean(config.useLocalReputation),
20
+ }
21
+ applyPriorityToRegistry()
22
+ }
23
+
24
+ /**
25
+ * @returns {{ useLocalReputation: boolean }} 当前 priority 配置副本
26
+ */
27
+ export function getInfraPriority() {
28
+ return { ...priorityConfig }
29
+ }
30
+
31
+ /**
32
+ * @returns {void}
33
+ */
34
+ export function applyPriorityToRegistry() {
35
+ const registry = getLinkRegistry()
36
+ if (!priorityConfig.useLocalReputation) {
37
+ registry.setPriorityWeightFunction(null)
38
+ return
39
+ }
40
+ registry.setPriorityWeightFunction(nodeHash => {
41
+ const score = pickNodeScoreFromReputation(
42
+ getReputationTable(),
43
+ normalizeHex64(nodeHash) || nodeHash,
44
+ )
45
+ return Math.floor(score * PRIORITY_BOOST)
46
+ })
47
+ }
48
+
49
+ /**
50
+ * stopInfra:卸 weight,并重置 priority 配置,避免再次 startInfra 幽灵恢复加权。
51
+ * @returns {void}
52
+ */
53
+ export function clearInfraPriorityFromRegistry() {
54
+ priorityConfig = { useLocalReputation: false }
55
+ getLinkRegistry().setPriorityWeightFunction(null)
56
+ }
@@ -0,0 +1,140 @@
1
+
2
+ import { isNodeInitialized } from '../node/instance.mjs'
3
+ import { setOverlayRateGate, clearOverlayRateGate } from '../overlay/index.mjs'
4
+ import { getLinkRegistry } from '../transport/link_registry.mjs'
5
+ import { attachNodeScopeMailbox } from '../transport/node_scope.mjs'
6
+
7
+ import { attachInfraDebugLog, detachInfraDebugLog } from './debug_log.mjs'
8
+ import {
9
+ applyPriorityToRegistry,
10
+ clearInfraPriorityFromRegistry,
11
+ getInfraPriority,
12
+ setInfraPriority,
13
+ } from './priority.mjs'
14
+ import infraTunables from './tunables.json' with { type: 'json' }
15
+
16
+ /** @type {Map<string, { tokens: number, updatedAt: number }>} */
17
+ const overlayRateBuckets = new Map()
18
+
19
+ /**
20
+ * Token bucket:桶容量 = burst,补充速率 = perMin/min。
21
+ * @param {Map<string, { tokens: number, updatedAt: number }>} buckets - 每 sender 桶状态
22
+ * @param {string} sender - 发送方 nodeHash
23
+ * @param {number} now - 当前时间戳(ms)
24
+ * @param {{ perMin: number, burst: number }} limits - 限速参数
25
+ * @returns {boolean} 是否允许本次 overlay 动作
26
+ */
27
+ export function consumeOverlayRateToken(buckets, sender, now, limits) {
28
+ const perMin = Math.max(1, limits.perMin)
29
+ const burst = Math.max(1, limits.burst)
30
+ const refillPerMs = perMin / 60_000
31
+ let bucket = buckets.get(sender)
32
+ if (!bucket) bucket = { tokens: burst, updatedAt: now }
33
+ const elapsed = Math.max(0, now - bucket.updatedAt)
34
+ bucket.tokens = Math.min(burst, bucket.tokens + elapsed * refillPerMs)
35
+ bucket.updatedAt = now
36
+ if (bucket.tokens < 1) {
37
+ buckets.set(sender, bucket)
38
+ return false
39
+ }
40
+ bucket.tokens -= 1
41
+ buckets.set(sender, bucket)
42
+ return true
43
+ }
44
+
45
+ /**
46
+ * @returns {void}
47
+ */
48
+ function installOverlayRateLimit() {
49
+ const limits = {
50
+ perMin: Math.max(1, Number(infraTunables.overlayRatePerMin) || 120),
51
+ burst: Math.max(1, Number(infraTunables.overlayRateBurst) || 30),
52
+ }
53
+ setOverlayRateGate((sender, action) => {
54
+ if (action !== 'route_req' && action !== 'relay') return true
55
+ return consumeOverlayRateToken(overlayRateBuckets, sender, Date.now(), limits)
56
+ })
57
+ }
58
+
59
+ /**
60
+ * @returns {void}
61
+ */
62
+ function removeOverlayRateLimit() {
63
+ clearOverlayRateGate()
64
+ overlayRateBuckets.clear()
65
+ }
66
+
67
+ /** @type {boolean} */
68
+ let infraRunning = false
69
+
70
+ /** @type {(() => void) | null} */
71
+ let mailboxDispose = null
72
+
73
+ /** @type {(() => void) | null} */
74
+ let debugDispose = null
75
+
76
+ /** @type {number | null} */
77
+ let savedMaxActive = null
78
+
79
+ /**
80
+ * @returns {boolean} infra relay 是否在运行
81
+ */
82
+ export function isInfraRunning() {
83
+ return infraRunning
84
+ }
85
+
86
+ /**
87
+ * 启动 public-good infra:overlay、mailbox、rate limit、priority、debug log。
88
+ * @param {{ maxActive?: number, logger?: { info?: Function, warn?: Function, error?: Function, log?: Function } | null }} [options] - maxActive 与 debug logger
89
+ * @returns {Promise<void>}
90
+ */
91
+ export async function startInfra(options = {}) {
92
+ if (!isNodeInitialized()) throw new Error('p2p: startInfra requires initNode')
93
+ if (infraRunning) {
94
+ if (options.maxActive != null) await getLinkRegistry().setMaxActive(options.maxActive)
95
+ if (options.logger !== undefined) {
96
+ detachInfraDebugLog()
97
+ debugDispose = attachInfraDebugLog(options.logger ?? null)
98
+ }
99
+ applyPriorityToRegistry()
100
+ return
101
+ }
102
+ const registry = getLinkRegistry()
103
+ await registry.ensureRuntime()
104
+ registry.ensureOverlayRouter()
105
+ installOverlayRateLimit()
106
+ savedMaxActive = registry.getMaxActive()
107
+ if (options.maxActive != null)
108
+ await registry.setMaxActive(options.maxActive)
109
+ else
110
+ await registry.setMaxActive(infraTunables.defaultMaxActive ?? savedMaxActive)
111
+ if (!mailboxDispose) mailboxDispose = attachNodeScopeMailbox()
112
+ const logger = options.logger === undefined ? console : options.logger ?? null
113
+ debugDispose = attachInfraDebugLog(logger)
114
+ applyPriorityToRegistry()
115
+ infraRunning = true
116
+ }
117
+
118
+ /**
119
+ * 卸掉 infra 自己挂的面(mailbox / rate / debug / priority / maxActive 恢复)。
120
+ * 不碰用户另行 attach 的 wires,不绑信誉同步。
121
+ * @returns {Promise<void>}
122
+ */
123
+ export async function stopInfra() {
124
+ if (!infraRunning) return
125
+ debugDispose?.()
126
+ debugDispose = null
127
+ detachInfraDebugLog()
128
+ clearInfraPriorityFromRegistry()
129
+ removeOverlayRateLimit()
130
+ mailboxDispose?.()
131
+ mailboxDispose = null
132
+ const registry = getLinkRegistry()
133
+ if (savedMaxActive != null)
134
+ await registry.setMaxActive(savedMaxActive)
135
+ savedMaxActive = null
136
+ infraRunning = false
137
+ }
138
+
139
+ /** 再导出:infra 路由加权配置(见 `priority.mjs`)。 */
140
+ export { getInfraPriority, setInfraPriority }
@@ -0,0 +1,5 @@
1
+ {
2
+ "defaultMaxActive": 96,
3
+ "overlayRatePerMin": 120,
4
+ "overlayRateBurst": 30
5
+ }
@@ -57,7 +57,7 @@ const DEFAULT_ACTION_PRIORITIES = Object.freeze({
57
57
  * @returns {number} 优先级(未知 action 默认 5)
58
58
  */
59
59
  export function resolveActionPriority(action) {
60
- return DEFAULT_ACTION_PRIORITIES[String(action || '').trim()] ?? 5
60
+ return DEFAULT_ACTION_PRIORITIES[action] ?? 5
61
61
  }
62
62
 
63
63
  /**
@@ -67,12 +67,11 @@ export function resolveActionPriority(action) {
67
67
  * @returns {'control' | 'bulk'} 目标通道名
68
68
  */
69
69
  export function pickChannel(action, byteLength) {
70
- const normalized = String(action || '').trim()
71
- if (normalized === 'ping' || normalized === 'pong' || normalized.startsWith('route_'))
70
+ if (action === 'ping' || action === 'pong' || action.startsWith('route_'))
72
71
  return CHANNEL_CONTROL
73
- if (Number(byteLength) > BULK_CHANNEL_MIN_BYTES)
72
+ if (byteLength > BULK_CHANNEL_MIN_BYTES)
74
73
  return CHANNEL_BULK
75
- return resolveActionPriority(normalized) <= 3 ? CHANNEL_CONTROL : CHANNEL_BULK
74
+ return resolveActionPriority(action) <= 3 ? CHANNEL_CONTROL : CHANNEL_BULK
76
75
  }
77
76
 
78
77
  /**
@@ -162,13 +161,14 @@ export function createChannelSendQueues(options) {
162
161
  const flush = channelName => {
163
162
  scheduled[channelName] = false
164
163
  const channel = getChannel(channelName)
165
- if (!channel || channel.readyState !== 'open') return
164
+ if (channel?.readyState !== 'open') return
166
165
  const queue = queues[channelName]
167
- while (queue.length) {
168
- if (readBufferedAmount(channel) > highWatermarkBytes) return
169
- const item = queue.shift()
170
- channel.send(item.bytes)
166
+ let i = 0
167
+ while (i < queue.length) {
168
+ if (readBufferedAmount(channel) > highWatermarkBytes) break
169
+ channel.send(queue[i++].bytes)
171
170
  }
171
+ if (i > 0) queue.splice(0, i)
172
172
  }
173
173
 
174
174
  return {