@steve02081504/fount-p2p 0.0.23 → 0.0.25

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 (67) hide show
  1. package/README.md +8 -6
  2. package/core/object.mjs +9 -0
  3. package/core/partpath.mjs +10 -0
  4. package/dag/storage.mjs +0 -5
  5. package/dag/strip_extensions.mjs +1 -3
  6. package/{wire/part_query_cache.mjs → federation/part_query/cache.mjs} +7 -7
  7. package/{wire/part_query.mjs → federation/part_query/runtime.mjs} +36 -77
  8. package/files/assemble.mjs +5 -5
  9. package/files/assemble_stream.mjs +2 -2
  10. package/files/chunk/fetch.mjs +101 -0
  11. package/files/chunk/pending.mjs +59 -0
  12. package/files/{chunk_provider_registry.mjs → chunk/provider_registry.mjs} +4 -5
  13. package/files/chunk/responder.mjs +104 -0
  14. package/files/{chunk_store.mjs → chunk/store.mjs} +2 -2
  15. package/files/{chunk_fetch_verify.mjs → chunk/verify.mjs} +2 -2
  16. package/files/evfs.mjs +18 -19
  17. package/files/fed/fetch_shared.mjs +39 -0
  18. package/files/fed/responder.mjs +47 -0
  19. package/files/{acl.mjs → manifest/acl.mjs} +4 -4
  20. package/files/{manifest_acl_registry.mjs → manifest/acl_registry.mjs} +14 -17
  21. package/files/{manifest_fetch.mjs → manifest/fetch.mjs} +47 -52
  22. package/files/{manifest.mjs → manifest/normalize.mjs} +2 -2
  23. package/files/manifest/pending.mjs +53 -0
  24. package/files/{public_manifest.mjs → manifest/public.mjs} +12 -12
  25. package/files/transfer_key.mjs +2 -2
  26. package/files/transfer_key_registry.mjs +7 -7
  27. package/index.mjs +4 -4
  28. package/infra/service.mjs +1 -1
  29. package/link/handshake.mjs +0 -3
  30. package/mailbox/deliver_or_store.mjs +0 -2
  31. package/mailbox/parse.mjs +1 -1
  32. package/mailbox/store.mjs +0 -9
  33. package/mailbox/wire.mjs +24 -16
  34. package/node/reputation_sync.mjs +33 -29
  35. package/package.json +10 -10
  36. package/registries/inbound.mjs +19 -9
  37. package/registries/part_path.mjs +3 -8
  38. package/schemas/discovery.mjs +1 -1
  39. package/schemas/federation_pull.mjs +1 -1
  40. package/schemas/part_query.mjs +6 -5
  41. package/transport/group_link_set.mjs +2 -2
  42. package/transport/link_registry.mjs +2 -2
  43. package/transport/node_scope/features.mjs +166 -0
  44. package/transport/node_scope/wire.mjs +143 -0
  45. package/transport/remote_user_room.mjs +0 -1
  46. package/{rooms → transport}/scoped_link.mjs +1 -1
  47. package/transport/user_room.mjs +15 -28
  48. package/utils/fetch_wait.mjs +98 -0
  49. package/wire/adapter.mjs +13 -0
  50. package/wire/ingress.mjs +1 -8
  51. package/wire/part/fanout.mjs +99 -0
  52. package/wire/part/group.mjs +45 -0
  53. package/wire/{part_ingress.mjs → part/ingress.mjs} +58 -67
  54. package/wire/{part_invoke.mjs → part/invoke.mjs} +1 -22
  55. package/wire/part/pending.mjs +32 -0
  56. package/wire/part/query.mjs +91 -0
  57. package/wire/subscribe.mjs +13 -0
  58. package/federation/chunk_fetch_pending.mjs +0 -124
  59. package/federation/manifest_fetch_pending.mjs +0 -88
  60. package/files/chunk_fetch.mjs +0 -99
  61. package/files/chunk_responder.mjs +0 -142
  62. package/transport/advert_ingest.mjs +0 -17
  63. package/transport/node_scope.mjs +0 -289
  64. package/wire/group_part.mjs +0 -43
  65. package/wire/part_common.mjs +0 -55
  66. package/wire/part_fanout.mjs +0 -52
  67. /package/{wire → schemas}/part_query.tunables.json +0 -0
@@ -0,0 +1,104 @@
1
+ import { handleIncomingManifestGet } from '../manifest/fetch.mjs'
2
+ import { resolvePendingManifestFetch } from '../manifest/pending.mjs'
3
+
4
+ import { handleIncomingChunkGet } from './fetch.mjs'
5
+ import { resolvePendingChunkFetch } from './pending.mjs'
6
+
7
+ /**
8
+ * 群联邦 / TrustGraph 路径:响应带 requestId 的 fed_chunk_get。
9
+ * @param {object} data 入站 fed_chunk_get
10
+ * @param {string} peerId 对端 id
11
+ * @param {(resp: object, peerId: string) => void} sendChunkData 发送 fed_chunk_data
12
+ * @returns {Promise<void>}
13
+ */
14
+ export async function handleFedChunkGetIngress(data, peerId, sendChunkData) {
15
+ await handleIncomingChunkGet(data, sendChunkData, peerId)
16
+ }
17
+
18
+ /**
19
+ * @param {object} data 入站 fed_chunk_data(含 requestId 时 resolve pending fetch)
20
+ * @returns {boolean} 是否命中 pending
21
+ */
22
+ export function handleFedChunkDataIngress(data) {
23
+ return resolvePendingChunkFetch(data)
24
+ }
25
+
26
+ /**
27
+ * @param {object} data 入站 fed_manifest_get
28
+ * @param {string} peerId 对端 id
29
+ * @param {(resp: object, peerId: string) => void} sendManifestData 发送 fed_manifest_data
30
+ * @returns {Promise<void>}
31
+ */
32
+ export async function handleFedManifestGetIngress(data, peerId, sendManifestData) {
33
+ await handleIncomingManifestGet(data, sendManifestData, peerId)
34
+ }
35
+
36
+ /**
37
+ * @param {object} data 入站 fed_manifest_data
38
+ * @returns {Promise<boolean>} 是否命中 pending
39
+ */
40
+ export function handleFedManifestDataIngress(data) {
41
+ return resolvePendingManifestFetch(data)
42
+ }
43
+
44
+ /**
45
+ * @param {(resp: object, peerId: string) => void} sendData 发送
46
+ * @param {{ enqueue: (prio: number, cleanup: () => void) => void }} [fedOut] 出站限速队列
47
+ * @returns {(resp: object, peerId: string) => void} 可入队的发送
48
+ */
49
+ function wrapSend(sendData, fedOut) {
50
+ if (!fedOut) return sendData
51
+ return (resp, peerId) => {
52
+ fedOut.enqueue(6, () => {
53
+ try { sendData(resp, peerId) }
54
+ catch { /* peer gone */ }
55
+ })
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Trystero room:注册带 requestId 的 fed_chunk_* + fed_manifest_*(TrustGraph / 群全局 miss)。
61
+ * @param {object} room Trystero room
62
+ * @param {{ enqueue: (prio: number, cleanup: () => void) => void }} [fedOut] 出站队列
63
+ * @param {(roomKey: string, action: string, rtcLimits: object) => boolean} [guardGet] RTC 负载守卫
64
+ * @param {object} [rtcLimits] RTC 限额
65
+ * @param {string} [roomKey] 房间键
66
+ * @returns {void}
67
+ */
68
+ export function attachTrustGraphFedChunkResponder(room, fedOut, guardGet, rtcLimits = {}, roomKey = '') {
69
+ const [sendChunkData, getChunkData] = room.makeAction('fed_chunk_data')
70
+ const [, getChunkGet] = room.makeAction('fed_chunk_get')
71
+ const [sendManifestData, getManifestData] = room.makeAction('fed_manifest_data')
72
+ const [, getManifestGet] = room.makeAction('fed_manifest_get')
73
+
74
+ const sendChunk = wrapSend((resp, peerId) => {
75
+ try { sendChunkData(resp, peerId) }
76
+ catch { /* peer gone */ }
77
+ }, fedOut)
78
+ const sendManifest = wrapSend((resp, peerId) => {
79
+ try { sendManifestData(resp, peerId) }
80
+ catch { /* peer gone */ }
81
+ }, fedOut)
82
+
83
+ getChunkGet((data, peerId) => {
84
+ if (guardGet && !guardGet(roomKey, 'fed_chunk_get', rtcLimits)) return
85
+ if (!data?.requestId) return
86
+ void handleFedChunkGetIngress(data, peerId, sendChunk)
87
+ })
88
+
89
+ getChunkData(data => {
90
+ if (!data?.requestId) return
91
+ handleFedChunkDataIngress(data)
92
+ })
93
+
94
+ getManifestGet((data, peerId) => {
95
+ if (guardGet && !guardGet(roomKey, 'fed_manifest_get', rtcLimits)) return
96
+ if (!data?.requestId) return
97
+ void handleFedManifestGetIngress(data, peerId, sendManifest)
98
+ })
99
+
100
+ getManifestData(data => {
101
+ if (!data?.requestId) return
102
+ handleFedManifestDataIngress(data)
103
+ })
104
+ }
@@ -4,8 +4,8 @@ import fsp from 'node:fs/promises'
4
4
  import { dirname, join } from 'node:path'
5
5
  import { pipeline } from 'node:stream/promises'
6
6
 
7
- import { isHex64 } from '../core/hexIds.mjs'
8
- import { getNodeDir } from '../node/instance.mjs'
7
+ import { isHex64 } from '../../core/hexIds.mjs'
8
+ import { getNodeDir } from '../../node/instance.mjs'
9
9
 
10
10
  /**
11
11
  * @returns {string} `{nodeDir}/chunks`
@@ -1,5 +1,5 @@
1
- import { isHex64 } from '../core/hexIds.mjs'
2
- import { sha256Hex } from '../crypto/crypto.mjs'
1
+ import { isHex64 } from '../../core/hexIds.mjs'
2
+ import { sha256Hex } from '../../crypto/crypto.mjs'
3
3
 
4
4
  /**
5
5
  * @param {string} chunkHash 期望的 64 hex 密文哈希
package/files/evfs.mjs CHANGED
@@ -6,15 +6,15 @@ import { getEntityStore } from '../node/instance.mjs'
6
6
 
7
7
  import { buildFileManifestFromEnc, encryptPlaintextToMultiPartsAsync, encryptPlaintextToParts, manifestPartsForPersist } from './assemble.mjs'
8
8
  import { createManifestPlaintextStream, encryptReadableToParts } from './assemble_stream.mjs'
9
- import { fetchChunk } from './chunk_fetch.mjs'
10
- import { createChunkReadStream, getChunk, hasChunk, putChunk } from './chunk_store.mjs'
11
- import { normalizeFileManifest, publicTransferKeyDescriptor } from './manifest.mjs'
9
+ import { fetchChunk } from './chunk/fetch.mjs'
10
+ import { createChunkReadStream, getChunk, hasChunk, putChunk } from './chunk/store.mjs'
11
+ import { normalizeFileManifest, publicTransferKeyDescriptor } from './manifest/normalize.mjs'
12
12
  import { assembleManifestPlaintext, resolveContentKey } from './transfer_key.mjs'
13
13
  import { readDagManifestPlaintext, resolveTransferKeyDependencies } from './transfer_key_registry.mjs'
14
14
 
15
15
  /**
16
16
  * @param {string} replicaUsername 副本用户名
17
- * @param {import('./manifest.mjs').FileManifest} manifest 清单
17
+ * @param {import('./manifest/normalize.mjs').FileManifest} manifest 清单
18
18
  * @returns {{ getGroupFileMasterKey?: Function, getVaultMasterKey?: Function }} 密钥依赖
19
19
  */
20
20
  function transferKeyDependenciesForReplica(replicaUsername, manifest) {
@@ -31,7 +31,7 @@ function transferKeyDependenciesForReplica(replicaUsername, manifest) {
31
31
 
32
32
  /**
33
33
  * @param {string} username 拉取身份
34
- * @param {import('./manifest.mjs').FileManifest} manifest 清单
34
+ * @param {import('./manifest/normalize.mjs').FileManifest} manifest 清单
35
35
  * @param {{ fetchChunk?: Function }} [options] miss 拉取
36
36
  * @returns {Promise<boolean>} 全部 part 是否已就位
37
37
  */
@@ -53,15 +53,14 @@ async function ensureManifestPartsLocal(username, manifest, options = {}) {
53
53
  /**
54
54
  * @param {string} ownerEntityHash 所有者
55
55
  * @param {string} logicalPath 路径
56
- * @returns {Promise<import('./manifest.mjs').FileManifest | null>} 归一化 manifest
56
+ * @returns {Promise<import('./manifest/normalize.mjs').FileManifest | null>} 本机已写盘的 manifest
57
57
  */
58
58
  export async function loadFileManifest(ownerEntityHash, logicalPath) {
59
- const manifest = await getEntityStore().readManifest(ownerEntityHash, logicalPath)
60
- return manifest ? normalizeFileManifest(manifest) : null
59
+ return getEntityStore().readManifest(ownerEntityHash, logicalPath)
61
60
  }
62
61
 
63
62
  /**
64
- * @param {import('./manifest.mjs').FileManifest} manifest 清单
63
+ * @param {import('./manifest/normalize.mjs').FileManifest} manifest 清单
65
64
  * @returns {Promise<void>}
66
65
  */
67
66
  export async function saveFileManifest(manifest) {
@@ -69,7 +68,7 @@ export async function saveFileManifest(manifest) {
69
68
  }
70
69
 
71
70
  /**
72
- * @param {import('./manifest.mjs').FileManifest} manifest 清单
71
+ * @param {import('./manifest/normalize.mjs').FileManifest} manifest 清单
73
72
  * @param {Array<Buffer | Uint8Array>} partBytes 密文块
74
73
  * @returns {Promise<void>}
75
74
  */
@@ -80,7 +79,7 @@ export async function storeManifestParts(manifest, partBytes) {
80
79
 
81
80
  /**
82
81
  * @param {string} replicaUsername 副本用户名
83
- * @param {import('./manifest.mjs').FileManifest} manifest 清单
82
+ * @param {import('./manifest/normalize.mjs').FileManifest} manifest 清单
84
83
  * @param {{ username?: string, fetchChunk?: Function }} [options] miss 拉取
85
84
  * @returns {Promise<Buffer | null>} 明文内容
86
85
  */
@@ -104,7 +103,7 @@ export async function readManifestPlaintext(replicaUsername, manifest, options =
104
103
 
105
104
  /**
106
105
  * @param {string} replicaUsername 副本用户名
107
- * @param {import('./manifest.mjs').FileManifest} manifest 清单
106
+ * @param {import('./manifest/normalize.mjs').FileManifest} manifest 清单
108
107
  * @param {{ username?: string, fetchChunk?: Function }} [options] miss 拉取
109
108
  * @returns {Promise<import('node:stream').Readable | null>} 明文流
110
109
  */
@@ -134,10 +133,10 @@ export async function readManifestPlaintextStream(replicaUsername, manifest, opt
134
133
  * @param {Buffer | Uint8Array} parameters.plaintext 明文
135
134
  * @param {string} [parameters.name] 文件名
136
135
  * @param {string} [parameters.mimeType] MIME
137
- * @param {import('./manifest.mjs').CeMode} [parameters.ceMode] 模式
138
- * @param {import('./manifest.mjs').TransferKeyDescriptor} [parameters.transferKeyDescriptor] 传递密钥
136
+ * @param {import('./manifest/normalize.mjs').CeMode} [parameters.ceMode] 模式
137
+ * @param {import('./manifest/normalize.mjs').TransferKeyDescriptor} [parameters.transferKeyDescriptor] 传递密钥
139
138
  * @param {object} [parameters.meta] meta
140
- * @returns {Promise<import('./manifest.mjs').FileManifest>} 写入后的 manifest
139
+ * @returns {Promise<import('./manifest/normalize.mjs').FileManifest>} 写入后的 manifest
141
140
  */
142
141
  export async function putFileManifest(parameters) {
143
142
  const {
@@ -178,10 +177,10 @@ export async function putFileManifest(parameters) {
178
177
  * @param {number} parameters.plainSize 明文字节数
179
178
  * @param {string} [parameters.name] 文件名
180
179
  * @param {string} [parameters.mimeType] MIME
181
- * @param {import('./manifest.mjs').CeMode} [parameters.ceMode] 模式
182
- * @param {import('./manifest.mjs').TransferKeyDescriptor} [parameters.transferKeyDescriptor] 传递密钥
180
+ * @param {import('./manifest/normalize.mjs').CeMode} [parameters.ceMode] 模式
181
+ * @param {import('./manifest/normalize.mjs').TransferKeyDescriptor} [parameters.transferKeyDescriptor] 传递密钥
183
182
  * @param {object} [parameters.meta] meta
184
- * @returns {Promise<import('./manifest.mjs').FileManifest>} 写入后的 manifest
183
+ * @returns {Promise<import('./manifest/normalize.mjs').FileManifest>} 写入后的 manifest
185
184
  */
186
185
  export async function putFileManifestFromStream(parameters) {
187
186
  const {
@@ -223,7 +222,7 @@ export async function putFileManifestFromStream(parameters) {
223
222
  * @returns {Promise<Buffer | null>} 明文或 null
224
223
  */
225
224
  export async function readPublicFile(replicaUsername, entityHash, logicalPath, options = {}) {
226
- const { fetchPublicManifest } = await import('./manifest_fetch.mjs')
225
+ const { fetchPublicManifest } = await import('./manifest/fetch.mjs')
227
226
  const manifest = await fetchPublicManifest({
228
227
  username: options.username || replicaUsername,
229
228
  ownerEntityHash: entityHash,
@@ -0,0 +1,39 @@
1
+ import { randomUUID } from 'node:crypto'
2
+
3
+ import { resolveNodeHash } from '../chunk/provider_registry.mjs'
4
+ import { fanoutFedFetch } from '../fetch_fanout.mjs'
5
+
6
+ /**
7
+ * inflight + pending wait + fanout 共用骨架。
8
+ * @template T
9
+ * @param {{
10
+ * inflight: { acquire: (key: string, start: () => { done: Promise<T | null>, cancel: () => void }) => Promise<T | null> | null },
11
+ * inflightKey: string,
12
+ * username: string,
13
+ * action: string,
14
+ * registerWait: (requestId: string) => { done: Promise<T | null>, cancel: () => void },
15
+ * buildPayload: (requestId: string, nodeHash: string) => object,
16
+ * }} options 选项
17
+ * @returns {Promise<T | null> | null} 共享 Promise;队满为 null
18
+ */
19
+ export function beginFedFanoutFetch({
20
+ inflight,
21
+ inflightKey,
22
+ username,
23
+ action,
24
+ registerWait,
25
+ buildPayload,
26
+ }) {
27
+ return inflight.acquire(inflightKey, () => {
28
+ const requestId = randomUUID()
29
+ const wait = registerWait(requestId)
30
+ void (async () => {
31
+ try {
32
+ const { nodeHash } = await resolveNodeHash(username)
33
+ await fanoutFedFetch(username, action, buildPayload(requestId, nodeHash))
34
+ }
35
+ catch { /* pending wait 超时/cancel 负责 settle */ }
36
+ })()
37
+ return { done: wait.done, cancel: wait.cancel }
38
+ })
39
+ }
@@ -0,0 +1,47 @@
1
+ import { subscribeWire } from '../../wire/subscribe.mjs'
2
+ import { handleIncomingChunkGet } from '../chunk/fetch.mjs'
3
+ import { resolvePendingChunkFetch } from '../chunk/pending.mjs'
4
+ import { handleIncomingManifestGet } from '../manifest/fetch.mjs'
5
+ import { resolvePendingManifestFetch } from '../manifest/pending.mjs'
6
+
7
+ /** @typedef {import('../../wire/adapter.mjs').WireAdapter} WireAdapter */
8
+
9
+ /**
10
+ * node scope wire:注册 fed_chunk_* + fed_manifest_*。
11
+ * @param {WireAdapter} wire action 表
12
+ * @returns {() => void} 取消挂载的 dispose
13
+ */
14
+ export function attachNodeScopeFedResponder(wire) {
15
+ return subscribeWire(wire, {
16
+ /**
17
+ * @param {unknown} data chunk 请求载荷
18
+ * @param {string} peerId 对端 nodeHash
19
+ */
20
+ fed_chunk_get(data, peerId) {
21
+ void handleIncomingChunkGet(data, (resp, pid) => {
22
+ try { wire.send('fed_chunk_data', resp, pid) }
23
+ catch { /* disconnected */ }
24
+ }, peerId)
25
+ },
26
+ /**
27
+ * @param {unknown} data chunk 响应载荷
28
+ */
29
+ fed_chunk_data(data) { resolvePendingChunkFetch(data) },
30
+ /**
31
+ * @param {unknown} data manifest 请求载荷
32
+ * @param {string} peerId 对端 nodeHash
33
+ */
34
+ fed_manifest_get(data, peerId) {
35
+ void handleIncomingManifestGet(data, (resp, pid) => {
36
+ try { wire.send('fed_manifest_data', resp, pid) }
37
+ catch { /* disconnected */ }
38
+ }, peerId)
39
+ },
40
+ /**
41
+ * @param {unknown} data manifest 响应载荷
42
+ */
43
+ fed_manifest_data(data) {
44
+ void resolvePendingManifestFetch(data)
45
+ },
46
+ })
47
+ }
@@ -1,12 +1,12 @@
1
- import { isLogicalEntityHash } from '../core/logical_entity.mjs'
2
- import { isWritableLocalEntity } from '../node/identity.mjs'
1
+ import { isLogicalEntityHash } from '../../core/logical_entity.mjs'
2
+ import { isWritableLocalEntity } from '../../node/identity.mjs'
3
3
 
4
- import { checkManifestAcl, resolveManifestAclType } from './manifest_acl_registry.mjs'
4
+ import { checkManifestAcl, resolveManifestAclType } from './acl_registry.mjs'
5
5
 
6
6
  /**
7
7
  * @param {string} replicaUsername 请求 replica
8
8
  * @param {string} ownerEntityHash 文件 owner
9
- * @param {import('./manifest.mjs').FileManifest} manifest 清单
9
+ * @param {import('./normalize.mjs').FileManifest} manifest 清单
10
10
  * @returns {Promise<boolean>} 是否允许读
11
11
  */
12
12
  export async function canReadManifest(replicaUsername, ownerEntityHash, manifest) {
@@ -5,14 +5,14 @@
5
5
  /** @type {Map<string, { ownerId: string, handler: (context: ManifestAclContext, logicalPath?: string) => Promise<boolean> }>} */
6
6
  const handlersByType = new Map()
7
7
 
8
- /** @type {Map<string, { ownerId: string, match: (manifest: import('./manifest.mjs').FileManifest | null | undefined, ownerEntityHash: string) => string | null }>} */
8
+ /** @type {Map<string, { ownerId: string, match: (manifest: import('./normalize.mjs').FileManifest | null | undefined, ownerEntityHash: string) => string | null }>} */
9
9
  const matchersByOwner = new Map()
10
10
 
11
11
  /**
12
12
  * @typedef {{
13
13
  * replicaUsername: string,
14
14
  * ownerEntityHash: string,
15
- * manifest: import('./manifest.mjs').FileManifest,
15
+ * manifest: import('./normalize.mjs').FileManifest,
16
16
  * }} ManifestAclContext
17
17
  */
18
18
 
@@ -23,12 +23,10 @@ const matchersByOwner = new Map()
23
23
  * @returns {void}
24
24
  */
25
25
  export function registerManifestAcl(type, ownerId, handler) {
26
- const key = String(type)
27
- const owner = String(ownerId)
28
- const existing = handlersByType.get(key)
29
- if (existing && existing.ownerId !== owner)
30
- throw new Error(`manifest acl handler for '${key}' already registered by '${existing.ownerId}'`)
31
- handlersByType.set(key, { ownerId: owner, handler })
26
+ const existing = handlersByType.get(type)
27
+ if (existing && existing.ownerId !== ownerId)
28
+ throw new Error(`manifest acl handler for '${type}' already registered by '${existing.ownerId}'`)
29
+ handlersByType.set(type, { ownerId, handler })
32
30
  }
33
31
 
34
32
  /**
@@ -37,20 +35,19 @@ export function registerManifestAcl(type, ownerId, handler) {
37
35
  * @returns {void}
38
36
  */
39
37
  export function unregisterManifestAcl(type, ownerId) {
40
- const key = String(type)
41
- const existing = handlersByType.get(key)
38
+ const existing = handlersByType.get(type)
42
39
  if (!existing) return
43
- if (existing.ownerId === String(ownerId))
44
- handlersByType.delete(key)
40
+ if (existing.ownerId === ownerId)
41
+ handlersByType.delete(type)
45
42
  }
46
43
 
47
44
  /**
48
45
  * @param {string} ownerId 注册方
49
- * @param {(manifest: import('./manifest.mjs').FileManifest | null | undefined, ownerEntityHash: string) => string | null} match ACL 类型匹配
46
+ * @param {(manifest: import('./normalize.mjs').FileManifest | null | undefined, ownerEntityHash: string) => string | null} match ACL 类型匹配
50
47
  * @returns {void}
51
48
  */
52
49
  export function registerManifestAclMatcher(ownerId, match) {
53
- matchersByOwner.set(String(ownerId), { ownerId: String(ownerId), match })
50
+ matchersByOwner.set(ownerId, { ownerId, match })
54
51
  }
55
52
 
56
53
  /**
@@ -58,11 +55,11 @@ export function registerManifestAclMatcher(ownerId, match) {
58
55
  * @returns {void}
59
56
  */
60
57
  export function unregisterManifestAclMatcher(ownerId) {
61
- matchersByOwner.delete(String(ownerId))
58
+ matchersByOwner.delete(ownerId)
62
59
  }
63
60
 
64
61
  /**
65
- * @param {import('./manifest.mjs').FileManifest | null | undefined} manifest manifest
62
+ * @param {import('./normalize.mjs').FileManifest | null | undefined} manifest manifest
66
63
  * @param {string} ownerEntityHash 所有者
67
64
  * @returns {string | null} ACL 类型
68
65
  */
@@ -81,7 +78,7 @@ export function resolveManifestAclType(manifest, ownerEntityHash) {
81
78
  * @returns {Promise<boolean>} 是否允许(未注册为 false)
82
79
  */
83
80
  export async function checkManifestAcl(type, context, logicalPath) {
84
- const row = handlersByType.get(String(type))
81
+ const row = handlersByType.get(type)
85
82
  if (!row?.handler) return false
86
83
  return row.handler(context, logicalPath)
87
84
  }
@@ -1,20 +1,19 @@
1
- import { randomUUID } from 'node:crypto'
1
+ import { parseEntityHash } from '../../core/entity_id.mjs'
2
+ import { assertSafeEvfsLogicalPath } from '../../core/evfs_logical_path.mjs'
3
+ import { isWritableLocalEntity } from '../../node/identity.mjs'
4
+ import { getEntityStore } from '../../node/instance.mjs'
5
+ import { ms } from '../../utils/duration.mjs'
6
+ import { createInflightTable } from '../../utils/inflight_table.mjs'
7
+ import { loadFileManifest, saveFileManifest } from '../evfs.mjs'
8
+ import { beginFedFanoutFetch } from '../fed/fetch_shared.mjs'
2
9
 
10
+ import { normalizeFileManifest } from './normalize.mjs'
3
11
  import {
4
12
  manifestFetchExpectedKey,
5
13
  MAX_PENDING_MANIFEST_FETCHES,
6
14
  registerManifestFetchWait,
7
- } from '../federation/manifest_fetch_pending.mjs'
8
- import { isWritableLocalEntity } from '../node/identity.mjs'
9
- import { getEntityStore } from '../node/instance.mjs'
10
- import { ms } from '../utils/duration.mjs'
11
- import { createInflightTable } from '../utils/inflight_table.mjs'
12
-
13
- import { resolveNodeHash } from './chunk_provider_registry.mjs'
14
- import { loadFileManifest, saveFileManifest } from './evfs.mjs'
15
- import { fanoutFedFetch } from './fetch_fanout.mjs'
16
- import { normalizeFileManifest } from './manifest.mjs'
17
- import { shouldPreferIncomingPublicManifest } from './public_manifest.mjs'
15
+ } from './pending.mjs'
16
+ import { shouldPreferIncomingPublicManifest } from './public.mjs'
18
17
 
19
18
  const DEFAULT_MANIFEST_FETCH_TIMEOUT_MS = ms('8s')
20
19
 
@@ -29,7 +28,7 @@ const manifestInflight = createInflightTable({
29
28
  * 本地已有 publicSig 时立即返回,并后台 fanout;`cache: true` 时按 publishedAt 择新写盘。
30
29
  * 冷 miss 仍等 fanout。同 key in-flight 去重;调用方外层超时不 abort,后台继续填缓存。
31
30
  * @param {{ username: string, ownerEntityHash: string, logicalPath: string, cache?: boolean, timeoutMs?: number }} context - 拉取上下文
32
- * @returns {Promise<import('./manifest.mjs').FileManifest | null>} 验签后的 manifest,失败为 null
31
+ * @returns {Promise<import('./normalize.mjs').FileManifest | null>} 验签后的 manifest,失败为 null
33
32
  */
34
33
  export async function fetchPublicManifest(context) {
35
34
  const ownerEntityHash = String(context.ownerEntityHash || '').trim().toLowerCase()
@@ -41,27 +40,31 @@ export async function fetchPublicManifest(context) {
41
40
  ? Number(context.timeoutMs)
42
41
  : DEFAULT_MANIFEST_FETCH_TIMEOUT_MS
43
42
  const expectedKey = manifestFetchExpectedKey(ownerEntityHash, logicalPath)
44
- const inflightKey = `${username}\0${expectedKey}`
45
43
  const wantCache = context.cache === true
46
44
 
47
45
  // 先挂 in-flight(同步),再读本地 — 避免并发调用在 await 间隙各自 start
48
46
  const localPromise = loadFileManifest(ownerEntityHash, logicalPath)
49
- const shared = manifestInflight.acquire(inflightKey, () => {
50
- const requestId = randomUUID()
51
- const wait = registerManifestFetchWait(requestId, expectedKey, timeoutMs)
52
- void (async () => {
53
- try {
54
- const { nodeHash } = await resolveNodeHash(username)
55
- await fanoutFedFetch(username, 'fed_manifest_get', {
56
- requestId,
57
- nodeHash,
58
- ownerEntityHash,
59
- logicalPath,
60
- })
61
- }
62
- catch { /* pending wait 超时/cancel 负责 settle */ }
63
- })()
64
- return { done: wait.done, cancel: wait.cancel }
47
+ const shared = beginFedFanoutFetch({
48
+ inflight: manifestInflight,
49
+ inflightKey: `${username}\0${expectedKey}`,
50
+ username,
51
+ action: 'fed_manifest_get',
52
+ /**
53
+ * @param {string} requestId 请求 id
54
+ * @returns {{ done: Promise<import('./normalize.mjs').FileManifest | null>, cancel: () => void }} 等待句柄
55
+ */
56
+ registerWait: requestId => registerManifestFetchWait(requestId, expectedKey, timeoutMs),
57
+ /**
58
+ * @param {string} requestId 请求 id
59
+ * @param {string} nodeHash 目标节点 hash
60
+ * @returns {object} fanout 载荷
61
+ */
62
+ buildPayload: (requestId, nodeHash) => ({
63
+ requestId,
64
+ nodeHash,
65
+ ownerEntityHash,
66
+ logicalPath,
67
+ }),
65
68
  })
66
69
 
67
70
  const local = await localPromise
@@ -69,13 +72,12 @@ export async function fetchPublicManifest(context) {
69
72
  if (!shared) return hasLocalPublic ? local : null
70
73
 
71
74
  if (hasLocalPublic) {
72
- // SWR:立刻回本地;后台 fanout 择新写盘(需 cache: true)
73
- if (wantCache) {
75
+ if (wantCache)
74
76
  void shared.then(async result => {
75
77
  if (result && shouldPreferIncomingPublicManifest(local, result))
76
78
  await cachePublicManifest(ownerEntityHash, logicalPath, result)
77
79
  })
78
- }
80
+
79
81
  return local
80
82
  }
81
83
 
@@ -89,41 +91,34 @@ export async function fetchPublicManifest(context) {
89
91
  * 将已验签公开 manifest 写入本地缓存(显式 apply;`fetchPublicManifest` 默认不调用)。
90
92
  * @param {string} ownerEntityHash owner
91
93
  * @param {string} logicalPath 路径
92
- * @param {import('./manifest.mjs').FileManifest} incoming 已验签入站清单
94
+ * @param {import('./normalize.mjs').FileManifest} incoming 已验签入站清单
93
95
  * @returns {Promise<void>}
94
96
  */
95
97
  export async function cachePublicManifest(ownerEntityHash, logicalPath, incoming) {
96
- await maybeCacheIncomingPublicManifest(ownerEntityHash, logicalPath, incoming)
97
- }
98
-
99
- /**
100
- * @param {string} ownerEntityHash owner
101
- * @param {string} logicalPath 路径
102
- * @param {import('./manifest.mjs').FileManifest} incoming 已验签入站清单
103
- * @returns {Promise<void>}
104
- */
105
- async function maybeCacheIncomingPublicManifest(ownerEntityHash, logicalPath, incoming) {
106
98
  if (isWritableLocalEntity(ownerEntityHash)) return
107
- const store = getEntityStore()
108
- const existing = await store.readManifest(ownerEntityHash, logicalPath)
99
+ const existing = await getEntityStore().readManifest(ownerEntityHash, logicalPath)
109
100
  if (existing && !shouldPreferIncomingPublicManifest(existing, incoming)) return
110
101
  await saveFileManifest(incoming)
111
102
  }
112
103
 
113
104
  /**
114
105
  * 若本机有已签名公开 manifest 则响应 fed_manifest_get。
115
- * @param {string} username 用户
116
106
  * @param {object} payload 请求
117
107
  * @param {(response: object, peerId: string) => void} sendResponse 发送
118
108
  * @param {string} peerId 对端
119
109
  * @returns {Promise<void>}
120
110
  */
121
- export async function handleIncomingManifestGet(username, payload, sendResponse, peerId) {
122
- void username
123
- const ownerEntityHash = String(payload?.ownerEntityHash || '').trim().toLowerCase()
124
- const logicalPath = String(payload?.logicalPath || '').trim().replace(/^\/+/, '')
111
+ export async function handleIncomingManifestGet(payload, sendResponse, peerId) {
112
+ const parsedOwner = parseEntityHash(payload?.ownerEntityHash)
113
+ if (!parsedOwner) return
114
+ let logicalPath
115
+ try {
116
+ logicalPath = assertSafeEvfsLogicalPath(payload?.logicalPath)
117
+ }
118
+ catch { return }
125
119
  const requestId = String(payload?.requestId || '')
126
- if (!ownerEntityHash || !logicalPath || !requestId) return
120
+ if (!requestId) return
121
+ const { entityHash: ownerEntityHash } = parsedOwner
127
122
 
128
123
  const raw = await getEntityStore().readManifest(ownerEntityHash, logicalPath)
129
124
  const manifest = normalizeFileManifest(raw)
@@ -1,5 +1,5 @@
1
- import { isEntityHash128 } from '../core/entity_id.mjs'
2
- import { isHex64 } from '../core/hexIds.mjs'
1
+ import { isEntityHash128 } from '../../core/entity_id.mjs'
2
+ import { isHex64 } from '../../core/hexIds.mjs'
3
3
 
4
4
  /** @typedef {'plain' | 'convergent' | 'random'} CeMode */
5
5
 
@@ -0,0 +1,53 @@
1
+ import { compositeKey } from '../../core/composite_key.mjs'
2
+ import { createFetchWaitTable } from '../../utils/fetch_wait.mjs'
3
+
4
+ import { verifySignedPublicManifest } from './public.mjs'
5
+
6
+ /** 并发 pending manifest fetch 上限。 */
7
+ export const MAX_PENDING_MANIFEST_FETCHES = 512
8
+
9
+ const table = createFetchWaitTable({ maxSize: MAX_PENDING_MANIFEST_FETCHES })
10
+
11
+ /** @type {typeof table.pending} */
12
+ export const pendingManifestFetches = table.pending
13
+
14
+ /**
15
+ * @param {string} ownerEntityHash owner
16
+ * @param {string} logicalPath 路径
17
+ * @returns {string} 期望键
18
+ */
19
+ export function manifestFetchExpectedKey(ownerEntityHash, logicalPath) {
20
+ return compositeKey(
21
+ String(ownerEntityHash || '').trim().toLowerCase(),
22
+ String(logicalPath || '').trim().replace(/^\/+/, ''),
23
+ )
24
+ }
25
+
26
+ /**
27
+ * @param {string} key requestId
28
+ * @param {string} expectedKey owner+path 复合键
29
+ * @param {number} timeoutMs 超时毫秒
30
+ * @returns {{ done: Promise<object | null>, cancel: () => void }} 等待 Promise 与取消
31
+ */
32
+ export function registerManifestFetchWait(key, expectedKey, timeoutMs) {
33
+ return table.register(key, expectedKey, timeoutMs)
34
+ }
35
+
36
+ /**
37
+ * 处理 fed_manifest_data:验签通过后 resolve pending。
38
+ * @param {object} payload 入站载荷
39
+ * @returns {Promise<boolean>} 是否命中并完成等待
40
+ */
41
+ export async function resolvePendingManifestFetch(payload) {
42
+ const requestId = String(payload?.requestId || '')
43
+ if (!requestId) return false
44
+ const entry = table.peek(requestId)
45
+ if (!entry) return false
46
+
47
+ const verified = await verifySignedPublicManifest(payload?.manifest)
48
+ if (!verified) return false
49
+ const key = manifestFetchExpectedKey(verified.ownerEntityHash, verified.logicalPath)
50
+ if (key !== entry.expectedKey) return false
51
+
52
+ return table.settle(requestId, verified)
53
+ }