@steve02081504/fount-p2p 0.0.40 → 0.0.41

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/AGENTS.md CHANGED
@@ -39,7 +39,7 @@ Deno / native / BT: [runtime.md](docs/runtime.md).
39
39
 
40
40
  ## Tests / tools
41
41
 
42
- - `npm test` — pure + integration (Node; `--test-force-exit`)
42
+ - `npm test` — pure + integration + frontend (Node; `--test-force-exit`)
43
43
  - `npm run test:live` — live link / LAN smoke
44
44
  - `npm run test:fount` — cross-repo Deno bridge (`test/fount/`); see [runtime.md](docs/runtime.md)
45
45
  - `npm run test:sim` — tunables co-evolution (dev-only; [sim/AGENTS.md](sim/AGENTS.md))
@@ -73,7 +73,7 @@ Deno / native / BT: [runtime.md](docs/runtime.md).
73
73
 
74
74
  | File | Directory |
75
75
  |---|---|
76
- | `tunables.json` | `reputation/`, `trust_graph/`, `mailbox/`, `governance/`, `dag/` |
76
+ | `tunables.json` | `transport/`, `infra/`, `reputation/`, `trust_graph/`, `mailbox/`, `governance/`, `dag/` |
77
77
  | `part_query.tunables.json` | `schemas/` |
78
78
 
79
79
  Sim harness: `sim/tunables_bundle.mjs` (dev-only). See [sim/AGENTS.md](sim/AGENTS.md).
package/docs/evfs.md CHANGED
@@ -40,6 +40,12 @@ Concurrency: `putChunk` / `putChunkFromStream` / `deleteChunk` / the GC delete p
40
40
 
41
41
  Outer caller timeouts must **not** abort the in-flight work — background fill continues after the caller gives up.
42
42
 
43
+ ## `fetchChunk` / public-file chunk reads
44
+
45
+ - `fetchChunk` accepts the same `fanoutTargets` as `fetchManifest`. With targets it fanouts `fed_chunk_get` **only** to that node set; without targets it falls back to node-scope fanout. Same `username`+chunk hash+mode (`targeted`/`public`) is deduped in-flight.
46
+ - `readPublicFile` / `readManifestPlaintext` forward `options.fanoutTargets` to both the manifest fetch and the chunk fetch, so a cross-node public read can pull profile/avatar content straight from the owner node instead of depending on the node-scope fanout.
47
+ - The public (non-targeted) fanout does **not** block the request window on dialing the whole peer pool: already-linked / group-room-reachable peers are sent immediately, and unreachable peers are dialed in the background and re-sent once linked. (`ensureLinkToNode` no longer gates the send.)
48
+
43
49
  ## Non-public (ACL-gated) manifests
44
50
 
45
51
  - `file-master-key-wrap` / `vault-wrap` / `identity-wrap` manifests carry no author signature, so the remote-acquisition trust boundary is **not** a signature — it is the **fanout target set** plus the **serving node's authorization**.
@@ -26,10 +26,14 @@ const chunkInflight = createInflightTable({
26
26
  * ciphertextHash: string,
27
27
  * ownerEntityHash?: string,
28
28
  * groupId?: string,
29
+ * fanoutTargets?: string[],
29
30
  * }} FetchChunkContext
30
31
  */
31
32
 
32
33
  /**
34
+ * 拉取密文块。默认走 node-scope public 语义;
35
+ * 传入 `fanoutTargets` 时只向目标集 fanout(public 文件定向拉取,如 profile/avatar 直接向 owner 节点取块)。
36
+ * 同 username+hash+模式(含规范化目标集)in-flight 去重;本地已缓存块直接返回。
33
37
  * @param {FetchChunkContext} context 上下文
34
38
  * @returns {Promise<Uint8Array | null>} 密文块
35
39
  */
@@ -50,7 +54,7 @@ export async function fetchChunk(context) {
50
54
 
51
55
  const shared = beginFedFanoutFetch({
52
56
  inflight: chunkInflight,
53
- inflightKey: `${username}\0${hash}`,
57
+ inflightKeyBase: `${username}\0${hash}`,
54
58
  username,
55
59
  action: 'fed_chunk_get',
56
60
  /**
@@ -69,6 +73,7 @@ export async function fetchChunk(context) {
69
73
  chunkHash: hash,
70
74
  ownerEntityHash: context.ownerEntityHash,
71
75
  }),
76
+ fanoutTargets: context.fanoutTargets,
72
77
  })
73
78
  if (!shared) return null
74
79
 
package/files/evfs.mjs CHANGED
@@ -32,7 +32,7 @@ function transferKeyDependenciesForReplica(replicaUsername, manifest) {
32
32
  /**
33
33
  * @param {string} username 拉取身份
34
34
  * @param {import('./manifest/normalize.mjs').FileManifest} manifest 清单
35
- * @param {{ fetchChunk?: Function }} [options] miss 拉取
35
+ * @param {{ fetchChunk?: Function, fanoutTargets?: string[] }} [options] miss 拉取
36
36
  * @returns {Promise<boolean>} 全部 part 是否已就位
37
37
  */
38
38
  async function ensureManifestPartsLocal(username, manifest, options = {}) {
@@ -43,6 +43,7 @@ async function ensureManifestPartsLocal(username, manifest, options = {}) {
43
43
  ciphertextHash: part.hash,
44
44
  ownerEntityHash: manifest.ownerEntityHash,
45
45
  groupId: manifest.transferKeyDescriptor.groupId,
46
+ fanoutTargets: options.fanoutTargets,
46
47
  })
47
48
  if (!fetchedChunk) return false
48
49
  await putChunk(part.hash, fetchedChunk)
@@ -94,7 +95,7 @@ export async function storeManifestParts(manifest, partBytes) {
94
95
  /**
95
96
  * @param {string} replicaUsername 副本用户名
96
97
  * @param {import('./manifest/normalize.mjs').FileManifest} manifest 清单
97
- * @param {{ username?: string, fetchChunk?: Function }} [options] miss 拉取
98
+ * @param {{ username?: string, fetchChunk?: Function, fanoutTargets?: string[] }} [options] miss 拉取
98
99
  * @returns {Promise<Buffer | null>} 明文内容
99
100
  */
100
101
  export async function readManifestPlaintext(replicaUsername, manifest, options = {}) {
@@ -118,7 +119,7 @@ export async function readManifestPlaintext(replicaUsername, manifest, options =
118
119
  /**
119
120
  * @param {string} replicaUsername 副本用户名
120
121
  * @param {import('./manifest/normalize.mjs').FileManifest} manifest 清单
121
- * @param {{ username?: string, fetchChunk?: Function }} [options] miss 拉取
122
+ * @param {{ username?: string, fetchChunk?: Function, fanoutTargets?: string[] }} [options] miss 拉取
122
123
  * @returns {Promise<import('node:stream').Readable | null>} 明文流
123
124
  */
124
125
  export async function readManifestPlaintextStream(replicaUsername, manifest, options = {}) {
@@ -229,10 +230,11 @@ export async function putFileManifestFromStream(parameters) {
229
230
 
230
231
  /**
231
232
  * 读取实体公开文件:本地 miss 时经网络取回签名 manifest,chunk miss 走既有 fetchChunk。
233
+ * 传入 `fanoutTargets` 时 manifest 与 chunk 拉取均定向到目标集(public 文件跨节点直取 owner 节点,避免 node-scope 大扇出)。
232
234
  * @param {string} replicaUsername 副本用户名
233
235
  * @param {string} entityHash owner entityHash
234
236
  * @param {string} logicalPath EVFS 逻辑路径
235
- * @param {{ username?: string, fetchChunk?: Function, revalidate?: boolean }} [options] miss 拉取;`revalidate` 强制阻塞等待 fanout 择新
237
+ * @param {{ username?: string, fetchChunk?: Function, revalidate?: boolean, fanoutTargets?: string[] }} [options] miss 拉取;`revalidate` 强制阻塞等待 fanout 择新
236
238
  * @returns {Promise<Buffer | null>} 明文或 null
237
239
  */
238
240
  export async function readPublicFile(replicaUsername, entityHash, logicalPath, options = {}) {
@@ -243,6 +245,7 @@ export async function readPublicFile(replicaUsername, entityHash, logicalPath, o
243
245
  logicalPath,
244
246
  cache: true,
245
247
  revalidate: options.revalidate === true,
248
+ fanoutTargets: options.fanoutTargets,
246
249
  })
247
250
  if (!manifest) return null
248
251
  return readManifestPlaintext(replicaUsername, manifest, options)
@@ -1,14 +1,15 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
 
3
3
  import { resolveNodeHash } from '../chunk/provider_registry.mjs'
4
- import { fanoutFedFetch } from '../fetch_fanout.mjs'
4
+ import { canonicalizeFanoutTargets, fanoutFedFetch } from '../fetch_fanout.mjs'
5
5
 
6
6
  /**
7
7
  * inflight + pending wait + fanout 共用骨架。
8
+ * 规范化目标集并拼入 inflight key:`fanoutTargets` 为数组(含空)即定向模式,undefined 为 node-scope public 模式。
8
9
  * @template T
9
10
  * @param {{
10
11
  * inflight: { acquire: (key: string, start: () => { done: Promise<T | null>, cancel: () => void }) => Promise<T | null> | null },
11
- * inflightKey: string,
12
+ * inflightKeyBase: string,
12
13
  * username: string,
13
14
  * action: string,
14
15
  * registerWait: (requestId: string) => { done: Promise<T | null>, cancel: () => void },
@@ -19,20 +20,23 @@ import { fanoutFedFetch } from '../fetch_fanout.mjs'
19
20
  */
20
21
  export function beginFedFanoutFetch({
21
22
  inflight,
22
- inflightKey,
23
+ inflightKeyBase,
23
24
  username,
24
25
  action,
25
26
  registerWait,
26
27
  buildPayload,
27
28
  fanoutTargets,
28
29
  }) {
30
+ const targeted = Array.isArray(fanoutTargets)
31
+ const canonicalTargets = targeted ? canonicalizeFanoutTargets(fanoutTargets) : undefined
32
+ const inflightKey = `${inflightKeyBase}\0${targeted ? 'targeted' : 'public'}` + (canonicalTargets?.length ? `\0${canonicalTargets.join('\0')}` : '')
29
33
  return inflight.acquire(inflightKey, () => {
30
34
  const requestId = randomUUID()
31
35
  const wait = registerWait(requestId)
32
36
  void (async () => {
33
37
  try {
34
38
  const { nodeHash } = await resolveNodeHash(username)
35
- await fanoutFedFetch(username, action, buildPayload(requestId, nodeHash), fanoutTargets)
39
+ await fanoutFedFetch(username, action, buildPayload(requestId, nodeHash), canonicalTargets)
36
40
  }
37
41
  catch { /* pending wait 超时/cancel 负责 settle */ }
38
42
  })()
@@ -32,6 +32,7 @@ export function canonicalizeFanoutTargets(targets) {
32
32
  /**
33
33
  * 全局 miss 请求扇出:先向已知 peer 定向发送,再 trust-graph top-K fanout。
34
34
  * 已直连 / follow hint peer 可能不在 trust-graph top-K(非成员 emoji CAS / Social 预览路径),故先定向发送。
35
+ * 拨号不阻塞请求窗口:已直连 peer 立即经现有链路投递,未直连 peer 先经群房间/overlay 尝试并后台拨号补发。
35
36
  * @param {string} username 用户
36
37
  * @param {string} action wire action 名
37
38
  * @param {object} payload 请求载荷
@@ -52,8 +53,20 @@ export async function fanoutFedFetch(username, action, payload, fanoutTargets) {
52
53
 
53
54
  const graph = await trustGraph.buildMergedGraph(username)
54
55
  const peerTargets = fetchPeerTargets()
55
- await Promise.all(peerTargets.map(nodeHash => ensureLinkToNode(nodeHash).catch(() => null)))
56
- for (const nodeHash of peerTargets)
57
- void trustGraph.sendToNode(username, nodeHash, action, payload, graph)
56
+ // 已直连 peer 立即经现有链路投递;其余 peer 后台并行:先经群房间/overlay 尝试投递,同时拨号,拨通且首投失败再补发。
57
+ // 全程不阻塞请求窗口(大 peer 池下 await 全量拨号会超窗口)。
58
+ const linked = new Set(listLinks().map(({ nodeHash }) => nodeHash))
59
+ for (const nodeHash of peerTargets) {
60
+ if (linked.has(nodeHash)) {
61
+ void trustGraph.sendToNode(username, nodeHash, action, payload, graph)
62
+ continue
63
+ }
64
+ const dialed = ensureLinkToNode(nodeHash).catch(() => null)
65
+ void trustGraph.sendToNode(username, nodeHash, action, payload, graph).then(async sent => {
66
+ if (sent) return
67
+ const link = await dialed
68
+ if (link) await trustGraph.sendToNode(username, nodeHash, action, payload, graph)
69
+ })
70
+ }
58
71
  await trustGraph.fanoutToTopNodes(username, action, payload, FEDERATION_CHUNK_FETCH_FANOUT_K)
59
72
  }
@@ -54,7 +54,7 @@ export async function fetchManifest(context) {
54
54
  const localPromise = loadFileManifest(ownerEntityHash, logicalPath)
55
55
  const shared = beginFedFanoutFetch({
56
56
  inflight: manifestInflight,
57
- inflightKey: `${username}\0${expectedKey}\0${targeted ? 'targeted' : 'public'}` + (fanoutTargets?.length ? `\0${fanoutTargets.join('\0')}` : ''),
57
+ inflightKeyBase: `${username}\0${expectedKey}`,
58
58
  username,
59
59
  action: 'fed_manifest_get',
60
60
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve02081504/fount-p2p",
3
- "version": "0.0.40",
3
+ "version": "0.0.41",
4
4
  "description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
5
5
  "keywords": [
6
6
  "network",