@steve02081504/fount-p2p 0.0.24 → 0.0.26

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
@@ -34,7 +34,7 @@ Shells talk to the **fount network** (`ensureLinkToNode` / `sendToNodeLink` / ro
34
34
 
35
35
  Dial order is descending link **`level`**: `lan_tcp` → `webrtc` → `ble_gatt` → `nostr` (−∞ last resort). Discovery **`priority`** only orders handshake / presence media. Details: [docs/transports.md](./docs/transports.md).
36
36
 
37
- Public transport subpaths: `link_registry`, `user_room`, `group_link_set`, `node_scope`, `room_scopes`, `remote_user_room`, `scoped_link`. Other `transport/*` modules are internal.
37
+ Public transport subpaths: `link_registry`, `user_room`, `group_link_set`, `node_scope/wire`, `node_scope/features`, `room_scopes`, `remote_user_room`, `scoped_link`. Other `transport/*` modules are internal.
38
38
 
39
39
  ## Infra relay (optional)
40
40
 
@@ -85,7 +85,7 @@ Facade entry: `index.mjs` (`startNode`, `createGroupLinkSet`, `registerDiscovery
85
85
  | Module | Role |
86
86
  |---|---|
87
87
  | `link_registry.mjs` | fount-network facade: dial fallback, scope/overlay |
88
- | `user_room.mjs` / `group_link_set.mjs` / `node_scope/` | rooms + composable node-scope wires |
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
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).
@@ -285,7 +285,6 @@ export async function processIncomingPartQueryRequest(wireContext, wire, request
285
285
 
286
286
  const selfHash = nodeHashOf()
287
287
  const exclude = new Set([selfHash, request.originNodeHash, String(peerId || '').trim().toLowerCase()].filter(Boolean))
288
- const neighbors = username ? await selectQueryNeighbors(username, exclude, dependencies) : []
289
288
  const forwardPayload = { ...request, ttl: nextTtl }
290
289
 
291
290
  /** @type {RelayPending} */
@@ -304,18 +303,25 @@ export async function processIncomingPartQueryRequest(wireContext, wire, request
304
303
  state,
305
304
  }
306
305
  state.relayPending.set(request.requestId, pending)
307
-
308
- let sent = 0
309
- for (const target of neighbors)
310
- if (await deliverQuery(target, 'part_query_req', forwardPayload, dependencies)) sent++
311
- pending.expected = sent
312
-
313
- if (sent === 0 || pending.received >= pending.expected) {
314
- flushRelayPending(pending)
315
- return
316
- }
317
-
306
+ // 先挂 hop 超时:勿等 select/deliver settle,否则 stuck send 永不 flush upstream(#13 同类)
318
307
  pending.timer = setTimeout(() => flushRelayPending(pending), resolvePartQueryHopTimeoutMs(request.ttl))
308
+
309
+ void (async () => {
310
+ try {
311
+ const neighbors = username ? await selectQueryNeighbors(username, exclude, dependencies) : []
312
+ if (pending.flushed) return
313
+ let sent = 0
314
+ for (const target of neighbors)
315
+ if (await deliverQuery(target, 'part_query_req', forwardPayload, dependencies)) sent++
316
+ if (pending.flushed) return
317
+ pending.expected = sent
318
+ if (sent === 0 || pending.received >= pending.expected)
319
+ flushRelayPending(pending)
320
+ }
321
+ catch {
322
+ if (!pending.flushed) flushRelayPending(pending)
323
+ }
324
+ })()
319
325
  }
320
326
 
321
327
  /**
@@ -353,6 +359,7 @@ export function handleIncomingPartQueryResponse(response, peerId = '', dependenc
353
359
 
354
360
  /**
355
361
  * 多跳查询:本地 handler + 网络回流(反向路径聚合);本地缓存命中则不广播。
362
+ * `timeoutMs` 端到端约束:select/deliver 挂起也不阻塞返回。
356
363
  * @param {string} username trust graph 上下文
357
364
  * @param {string} partpath part 路径
358
365
  * @param {string} kind 查询标签
@@ -420,14 +427,20 @@ export async function queryNetwork(username, partpath, kind, query, options = {}
420
427
 
421
428
  const selfHash = nodeHashOf()
422
429
  const exclude = new Set([selfHash, parsed.originNodeHash])
423
- const neighbors = await selectQueryNeighbors(username, exclude, options)
424
- let sent = 0
425
- for (const target of neighbors)
426
- if (await deliverQuery(target, 'part_query_req', parsed, options)) sent++
427
- bag.expected = sent
428
-
429
- if (sent === 0 || bag.received >= bag.expected)
430
- finishMultiWireWaiters(state.originWaits, parsed.requestId, '')
430
+ // await select/deliver:否则 timeout 已触发也要等扇出 settle(#13)
431
+ void (async () => {
432
+ try {
433
+ const neighbors = await selectQueryNeighbors(username, exclude, options)
434
+ let sent = 0
435
+ for (const target of neighbors)
436
+ if (await deliverQuery(target, 'part_query_req', parsed, options)) sent++
437
+ bag.expected = sent
438
+ if (sent === 0 || bag.received >= bag.expected)
439
+ finishMultiWireWaiters(state.originWaits, parsed.requestId, '')
440
+ }
441
+ catch { /* pending wait 超时负责 settle */ }
442
+ })()
443
+
431
444
  await waitPromise
432
445
  state.originBags.delete(parsed.requestId)
433
446
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve02081504/fount-p2p",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
5
5
  "keywords": [
6
6
  "network",
@@ -11,7 +11,7 @@
11
11
  "type": "module",
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "https://github.com/steve02081504/fount-p2p.git",
14
+ "url": "git+https://github.com/steve02081504/fount-p2p.git",
15
15
  "directory": "."
16
16
  },
17
17
  "engines": {
@@ -31,7 +31,7 @@
31
31
  "!scripts/**"
32
32
  ],
33
33
  "bin": {
34
- "fount-p2p": "./infra/cli.mjs"
34
+ "fount-p2p": "infra/cli.mjs"
35
35
  },
36
36
  "exports": {
37
37
  ".": "./index.mjs",
@@ -6,6 +6,9 @@ import { subscribeScope, sendToNodeLink } from '../link_registry.mjs'
6
6
  /** @type {Map<string, Set<(payload: unknown, peerId: string) => void>>} */
7
7
  const nodeActionHandlers = new Map()
8
8
 
9
+ /** @type {Set<(context: NodeScopeContext, wire: NodeScopeWire) => void>} */
10
+ const nodeScopeWireHooks = new Set()
11
+
9
12
  /** @type {NodeScopeContext} */
10
13
  const nodeScopeContext = { replicaUsername: '' }
11
14
 
@@ -108,10 +111,26 @@ export function ensureNodeScope(options = {}) {
108
111
  for (const handler of handlers)
109
112
  try { handler(envelope.payload, senderNodeHash) } catch { /* ignore */ }
110
113
  })
111
- nodeScopeWire ||= createNodeScopeWire()
114
+ if (!nodeScopeWire) {
115
+ nodeScopeWire = createNodeScopeWire()
116
+ for (const hook of nodeScopeWireHooks)
117
+ try { hook(nodeScopeContext, nodeScopeWire) } catch { /* ignore */ }
118
+ }
112
119
  return nodeScopeSubscribeCleanup
113
120
  }
114
121
 
122
+ /**
123
+ * 在 node scope wire 创建时(或已存在时立即)回调;shell 用此挂自定义 action。
124
+ * @param {(context: NodeScopeContext, wire: NodeScopeWire) => void} hook - wire 就绪回调
125
+ * @returns {() => void} 取消注册的 dispose
126
+ */
127
+ export function registerNodeScopeWireHook(hook) {
128
+ nodeScopeWireHooks.add(hook)
129
+ if (nodeScopeWire)
130
+ try { hook(nodeScopeContext, nodeScopeWire) } catch { /* ignore */ }
131
+ return () => nodeScopeWireHooks.delete(hook)
132
+ }
133
+
115
134
  /**
116
135
  * 卸掉 scope 订阅与 wire(feature 须先卸)。
117
136
  * @returns {void}
@@ -61,10 +61,11 @@ export function partInvokeErrorMessages(results) {
61
61
  /**
62
62
  * 向 trust-graph top-K 扇出 part_invoke,收集 part_invoke_response。
63
63
  * 调用方须已挂载 `attachPartWire`(否则收不到 response)。
64
+ * `timeoutMs` 端到端约束:fanout 挂起(discoverRoute / stuck send)也不阻塞返回。
64
65
  * @param {string} username trust graph 上下文
65
66
  * @param {string} partpath part 路径
66
67
  * @param {PartInvoke} invoke 调用体
67
- * @param {number} [timeoutMs=2500] 超时
68
+ * @param {number} [timeoutMs=2500] 端到端超时
68
69
  * @param {number} [maxResponses=6] 最多响应数
69
70
  * @returns {Promise<PartInvokeResponse[]>} 邻居 PartInvokeResponse(含 error)
70
71
  */
@@ -85,15 +86,16 @@ export async function collectPartInvokeResponses(username, partpath, invoke, tim
85
86
  pendingPartInvoke.set(requestId, { responses, finish, maxResponses, respondedPeers: new Set() })
86
87
  })
87
88
 
88
- const sent = await requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).fanoutToTopNodes(
89
+ // await fanout:否则 timeout 已触发也要等扇出 settle(#13)
90
+ void requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).fanoutToTopNodes(
89
91
  username,
90
92
  'part_invoke',
91
93
  buildPartInvokePayload(partpath, invoke, nodeHash, requestId),
92
94
  maxResponses,
93
- )
94
-
95
- const pending = pendingPartInvoke.get(requestId)
96
- if (pending && sent === 0) pending.finish()
95
+ ).then(sent => {
96
+ const pending = pendingPartInvoke.get(requestId)
97
+ if (pending && sent === 0) pending.finish()
98
+ }, () => { /* pending wait 超时负责 settle */ })
97
99
 
98
100
  return waitForResponses
99
101
  }