@steve02081504/fount-p2p 0.0.29 → 0.0.30

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.
@@ -297,9 +297,7 @@ export function createBluetoothDiscoveryProvider() {
297
297
  cleanup()
298
298
  reject(new Error('p2p: bt signal peripheral timeout'))
299
299
  }, 8_000)
300
- /**
301
- * @returns {void}
302
- */
300
+ /** 清理信号超时计时器并移除 discover 监听器 */
303
301
  function cleanup() {
304
302
  clearTimeout(deadline)
305
303
  noble.removeListener('discover', onDiscover)
@@ -243,6 +243,7 @@ function connectRelay(relayUrl, timeoutMs = NOSTR_CONNECT_TIMEOUT_MS, signal) {
243
243
  reject(error)
244
244
  }
245
245
  /**
246
+ * 收到 abort 信号时失败
246
247
  * @returns {void}
247
248
  */
248
249
  const onAbort = () => fail(new Error('nostr: aborted'))
@@ -294,6 +295,7 @@ function publishEventOnRelay(ws, relayUrl, event, signal, isCurrent) {
294
295
  else resolve(ok)
295
296
  }
296
297
  /**
298
+ * 收到 abort 信号时以失败结束
297
299
  * @returns {void}
298
300
  */
299
301
  const onAbort = () => finish(false, new Error('nostr: aborted'))
@@ -492,9 +494,7 @@ function attachSharedRelaySocket(relayUrl, session, ws) {
492
494
  function scheduleSharedRelayConnect(relayUrl, session, delayMs = 0) {
493
495
  if (!isLiveSharedSession(relayUrl, session) || session.connecting || session.ws) return
494
496
  clearSharedRelayReconnect(session)
495
- /**
496
- * @returns {void}
497
- */
497
+ /** 启动/恢复共享会话连接 */
498
498
  const start = () => {
499
499
  session.reconnectTimer = null
500
500
  if (!isLiveSharedSession(relayUrl, session) || session.connecting || session.ws) return
@@ -5,6 +5,7 @@ import { dirname, join } from 'node:path'
5
5
  import { pipeline } from 'node:stream/promises'
6
6
 
7
7
  import { isHex64 } from '../../core/hexIds.mjs'
8
+ import { trackFileStream } from '../../node/handles.mjs'
8
9
  import { getNodeDir } from '../../node/instance.mjs'
9
10
 
10
11
  /**
@@ -50,7 +51,7 @@ export async function getChunk(hash) {
50
51
  * @returns {import('node:fs').ReadStream} 可读流
51
52
  */
52
53
  export function createChunkReadStream(hash) {
53
- return fs.createReadStream(chunkStorePath(hash))
54
+ return trackFileStream(fs.createReadStream(chunkStorePath(hash)))
54
55
  }
55
56
 
56
57
  /**
@@ -72,5 +73,5 @@ export async function putChunk(hash, data) {
72
73
  export async function putChunkFromStream(hash, readable) {
73
74
  const filePath = chunkStorePath(hash)
74
75
  await fsp.mkdir(dirname(filePath), { recursive: true })
75
- await pipeline(readable, fs.createWriteStream(filePath))
76
+ await pipeline(readable, trackFileStream(fs.createWriteStream(filePath)))
76
77
  }
package/index.mjs CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  import { registerLinkProvider } from './link/providers/index.mjs'
12
12
  import { ensureNodeDefaults, getNodeHash } from './node/identity.mjs'
13
13
  import {
14
+ closeNode,
14
15
  getNodeDir,
15
16
  initNode,
16
17
  isNodeInitialized,
@@ -58,6 +59,7 @@ import {
58
59
  export {
59
60
  attachReputationSyncWire,
60
61
  attachNodeScopeDefaultFeatures,
62
+ closeNode,
61
63
  configureLinkRegistry,
62
64
  createGroupLinkSet,
63
65
  createScopedLinkRoom,
@@ -28,9 +28,7 @@ export function getInfraPriority() {
28
28
  return { ...priorityConfig }
29
29
  }
30
30
 
31
- /**
32
- * @returns {void}
33
- */
31
+ /** 将优先级配置应用到 link registry */
34
32
  export function applyPriorityToRegistry() {
35
33
  const registry = getLinkRegistry()
36
34
  if (!priorityConfig.useLocalReputation) {
package/infra/service.mjs CHANGED
@@ -42,9 +42,7 @@ export function consumeOverlayRateToken(buckets, sender, now, limits) {
42
42
  return true
43
43
  }
44
44
 
45
- /**
46
- * @returns {void}
47
- */
45
+ /** 安装 overlay 速率门 */
48
46
  function installOverlayRateLimit() {
49
47
  const limits = {
50
48
  perMin: Math.max(1, Number(infraTunables.overlayRatePerMin) || 120),
@@ -56,9 +54,7 @@ function installOverlayRateLimit() {
56
54
  })
57
55
  }
58
56
 
59
- /**
60
- * @returns {void}
61
- */
57
+ /** 移除 overlay 速率门并清空桶 */
62
58
  function removeOverlayRateLimit() {
63
59
  clearOverlayRateGate()
64
60
  overlayRateBuckets.clear()
@@ -80,9 +80,7 @@ async function dialBleGatt(options) {
80
80
  cleanup()
81
81
  reject(new Error('p2p: ble_gatt peripheral scan timeout'))
82
82
  }, 8_000)
83
- /**
84
- * @returns {void}
85
- */
83
+ /** 清理扫描超时计时器与监听器并停止 BLE 扫描 */
86
84
  function cleanup() {
87
85
  clearTimeout(deadline)
88
86
  noble.removeListener('discover', onDiscover)
@@ -249,9 +247,7 @@ export function createBleGattLinkProvider() {
249
247
  sessionInbound = handler
250
248
  return () => { sessionInbound = null }
251
249
  },
252
- /**
253
- * @returns {void}
254
- */
250
+ /** 关闭入站 transport */
255
251
  closeTransport() {
256
252
  activeInbound = null
257
253
  sessionInbound = null
@@ -35,9 +35,7 @@ function connectTcp(host, port, timeoutMs = LAN_TCP_CONNECT_TIMEOUT_MS) {
35
35
  conn.off('connect', onConnect)
36
36
  reject(error)
37
37
  }
38
- /**
39
- * @returns {void}
40
- */
38
+ /** TCP 连接成功并返回 socket */
41
39
  function onConnect() {
42
40
  clearTimeout(timer)
43
41
  conn.off('error', onError)
@@ -111,9 +109,7 @@ function attachLengthPrefix(socket, onPayload) {
111
109
  header.writeUInt32BE(body.length)
112
110
  socket.write(Buffer.concat([header, body]))
113
111
  },
114
- /**
115
- * @returns {void}
116
- */
112
+ /** 仅移除 data 监听器 */
117
113
  destroy() {
118
114
  socket.off('data', onData)
119
115
  },
@@ -149,9 +145,7 @@ function createTcpPipe(options) {
149
145
  sendFrame(_action, frame) {
150
146
  codec.write(frame)
151
147
  },
152
- /**
153
- * @returns {void}
154
- */
148
+ /** 关闭 transport 与 socket */
155
149
  closeTransport() {
156
150
  codec.destroy()
157
151
  socket.destroy()
@@ -0,0 +1,49 @@
1
+ /**
2
+ * 节点生命周期内打开的文件流注册表。
3
+ *
4
+ * 文件流(chunk 读流 / 写流)在 Windows 上若未 destroy 会占用文件句柄,
5
+ * 导致节点目录删除失败。这里集中跟踪,`closeNode()` 通过
6
+ * `closeAllFileStreams()` 一次性释放全部仍打开的流。
7
+ */
8
+
9
+ /** @type {Set<import('node:stream').Readable | import('node:stream').Writable>} */
10
+ const streams = new Set()
11
+
12
+ /**
13
+ * 跟踪一个文件流:销毁/关闭/出错时自动从注册表移除。
14
+ * @template {import('node:stream').Readable | import('node:stream').Writable} T
15
+ * @param {T} stream 文件流
16
+ * @returns {T} 原流
17
+ */
18
+ export function trackFileStream(stream) {
19
+ streams.add(stream)
20
+ /** @returns {void} */
21
+ const forget = () => streams.delete(stream)
22
+ stream.once('close', forget)
23
+ stream.once('error', forget)
24
+ return stream
25
+ }
26
+
27
+ /**
28
+ * 关闭并销毁所有仍在打开的文件流(测试 teardown / 节点关闭)。
29
+ * @returns {Promise<number>} 被销毁的流数量
30
+ */
31
+ export async function closeAllFileStreams() {
32
+ const targeted = [...streams]
33
+ const pending = []
34
+ for (const stream of targeted) {
35
+ if (!stream.closed)
36
+ pending.push(new Promise(resolve => stream.once('close', resolve)))
37
+ stream.destroy()
38
+ }
39
+ await Promise.all(pending)
40
+ for (const stream of targeted) streams.delete(stream)
41
+ return targeted.length
42
+ }
43
+
44
+ /**
45
+ * @returns {boolean} 是否仍有未释放的文件流(测试断言用)
46
+ */
47
+ export function hasOpenFileStreams() {
48
+ return streams.size > 0
49
+ }
package/node/instance.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path'
2
2
 
3
3
  import { createFsEntityStore } from './entity_store.mjs'
4
+ import { closeAllFileStreams } from './handles.mjs'
4
5
  import { defaultSignalingRuntimeConfig, resolveSignalingRuntimeConfig } from './signaling_config.mjs'
5
6
 
6
7
  /** @typedef {{ warn?: (...args: unknown[]) => void, error?: (...args: unknown[]) => void, info?: (...args: unknown[]) => void, log?: (...args: unknown[]) => void }} NodeLogger */
@@ -37,7 +38,7 @@ export function getRtcPolyfillCacheEpoch() {
37
38
  * @returns {NodeRuntime} 初始化后的运行时
38
39
  */
39
40
  export function initNode(options) {
40
- if (runtime) throw new Error('p2p: initNode already called — use setNodeLogger / setSignalingRuntimeConfig or resetNodeForTests')
41
+ if (runtime) throw new Error('p2p: initNode already called — use setNodeLogger / setSignalingRuntimeConfig or closeNode')
41
42
  if (options?.logger !== undefined || options?.signaling !== undefined)
42
43
  throw new Error('p2p: initNode only accepts nodeDir/entityStore — use setNodeLogger / setSignalingRuntimeConfig')
43
44
  const nodeDir = path.resolve(options.nodeDir)
@@ -138,11 +139,13 @@ export function onNodeChange(listener) {
138
139
  }
139
140
 
140
141
  /**
141
- * 测试专用:重置节点运行时。
142
- * @returns {void}
142
+ * 关闭节点:释放全部文件句柄(chunk 读/写流等)并清空运行时与监听器。
143
+ * 之后可用 initNode 重新引导。
144
+ * @returns {Promise<void>}
143
145
  */
144
- export function resetNodeForTests() {
146
+ export async function closeNode() {
145
147
  runtime = null
146
148
  changeListeners.clear()
147
149
  rtcPolyfillCacheEpoch++
150
+ await closeAllFileStreams()
148
151
  }
@@ -78,9 +78,7 @@ function normalizeHashList(list) {
78
78
  .filter(id => isHex64(id)))]
79
79
  }
80
80
 
81
- /**
82
- * @returns {void}
83
- */
81
+ /** 将当前 sync 配置写盘 */
84
82
  function persistSyncConfig() {
85
83
  writeNodeJsonSync(SYNC_DATA_NAME, loadSyncConfig())
86
84
  }
@@ -304,9 +302,7 @@ export async function pullReputationFromNode(nodeHash) {
304
302
  return await resultPromise
305
303
  }
306
304
 
307
- /**
308
- * @returns {void}
309
- */
305
+ /** 重置 sync 运行时(测试用) */
310
306
  export function resetReputationSyncForTests() {
311
307
  syncConfig = null
312
308
  detachReputationSyncWire()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve02081504/fount-p2p",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
5
5
  "keywords": [
6
6
  "network",
@@ -99,9 +99,7 @@ export function createGroupLinkSet(options) {
99
99
  scheduleDial()
100
100
  }
101
101
 
102
- /**
103
- * @returns {void}
104
- */
102
+ /** 按成员选择目标并拨号 */
105
103
  function selectAndDial() {
106
104
  if (!autoconnectEnabled || !active) return
107
105
  const targets = dialAll
@@ -119,9 +117,7 @@ export function createGroupLinkSet(options) {
119
117
  void registry.ensureLinkToNode(nodeHash).catch(() => null)
120
118
  }
121
119
 
122
- /**
123
- * @returns {void}
124
- */
120
+ /** 延迟调度一次拨号 */
125
121
  function scheduleDial() {
126
122
  if (!autoconnectEnabled || !active || dialTimer) return
127
123
  dialTimer = setTimeout(() => { dialTimer = null; selectAndDial() }, 200)
@@ -719,9 +719,7 @@ export function configureLinkRegistry(options = {}) {
719
719
  pendingRegistryOptions = { ...pendingRegistryOptions, ...options }
720
720
  }
721
721
 
722
- /**
723
- * @returns {void}
724
- */
722
+ /** 重置 link registry 运行时(测试用) */
725
723
  export function resetLinkRegistryForTests() {
726
724
  defaultRegistry = null
727
725
  pendingRegistryOptions = null
@@ -48,9 +48,7 @@ export function createBufferedSignalSession(sendRemote) {
48
48
  for (const handler of handlers)
49
49
  handler(message)
50
50
  },
51
- /**
52
- * @returns {void}
53
- */
51
+ /** 清空积压消息 */
54
52
  clear() {
55
53
  backlog.length = 0
56
54
  handlers.clear()
@@ -111,9 +111,7 @@ export function createRuntimeBootstrap(deps) {
111
111
  ?? mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls)
112
112
  }
113
113
 
114
- /**
115
- * @returns {void}
116
- */
114
+ /** 注册默认 discovery provider(lan/nostr) */
117
115
  function registerDiscoveryDefaults() {
118
116
  const providerIds = new Set(listDiscoveryProviders().map(provider => provider.id))
119
117
  if (!providerIds.has('lan'))
@@ -124,9 +122,7 @@ export function createRuntimeBootstrap(deps) {
124
122
  registerNostrProvider()
125
123
  }
126
124
 
127
- /**
128
- * @returns {void}
129
- */
125
+ /** 注册/替换 nostr discovery provider */
130
126
  function registerNostrProvider() {
131
127
  unregisterDiscoveryProvider('nostr')
132
128
  registerDiscoveryProvider(createNostrDiscoveryProvider({
@@ -42,9 +42,7 @@ export async function withAsyncMutex(key, criticalSection) {
42
42
  const lockKey = key
43
43
  const state = mutexState(lockKey)
44
44
  return new Promise((resolve, reject) => {
45
- /**
46
- * @returns {void}
47
- */
45
+ /** 执行临界区并最终释放锁 */
48
46
  const run = () => {
49
47
  Promise.resolve()
50
48
  .then(criticalSection)
package/utils/ttl_map.mjs CHANGED
@@ -56,9 +56,7 @@ export function createTtlMap(ttlMs, maxSize = 4096) {
56
56
  }
57
57
  return entry.value
58
58
  },
59
- /**
60
- * @returns {void}
61
- */
59
+ /** 清空全部条目 */
62
60
  clear() {
63
61
  map.clear()
64
62
  },