@steve02081504/fount-p2p 0.0.17 → 0.0.19

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/dag/storage.mjs CHANGED
@@ -1,62 +1,18 @@
1
1
  import { Buffer } from 'node:buffer'
2
- import { randomUUID } from 'node:crypto'
3
2
  import { createReadStream, createWriteStream } from 'node:fs'
4
- import { appendFile, mkdir, open, readFile, rename, unlink, writeFile } from 'node:fs/promises'
3
+ import { appendFile, mkdir, open, readFile, writeFile } from 'node:fs/promises'
5
4
  import { dirname } from 'node:path'
6
5
  import { createInterface } from 'node:readline'
7
6
  import { Readable } from 'node:stream'
8
7
  import { pipeline } from 'node:stream/promises'
9
- import { setTimeout as sleep } from 'node:timers/promises'
10
8
 
9
+ import { atomicTemporaryPath, finalizeAtomicRename } from '../utils/atomic_fs.mjs'
11
10
  import { withAsyncMutex } from '../utils/async_mutex.mjs'
12
11
 
12
+ export { finalizeAtomicRename } from '../utils/atomic_fs.mjs'
13
+
13
14
  /** 流式重写 JSONL 时分块写入的行数上限 */
14
15
  const WRITE_JSONL_CHUNK_LINES = 1000
15
- /** Windows 上 rename 可能被短暂占用,做几次短退避重试。 */
16
- const ATOMIC_RENAME_RETRY_DELAYS_MS = [0, 10, 25, 50, 100]
17
- const ATOMIC_RENAME_RETRY_CODES = new Set(['EPERM', 'EBUSY', 'EACCES'])
18
-
19
- /**
20
- * @param {string} filePath 目标路径
21
- * @returns {string} 唯一临时文件路径
22
- */
23
- function atomicTemporaryPath(filePath) {
24
- return `${filePath}.tmp.${process.pid}.${randomUUID()}`
25
- }
26
-
27
- /**
28
- * @param {string} temporaryPath 临时文件路径
29
- * @returns {Promise<void>}
30
- */
31
- async function cleanupAtomicTemporary(temporaryPath) {
32
- try { await unlink(temporaryPath) } catch { /* ok */ }
33
- }
34
-
35
- /**
36
- * 完成原子写的最终 rename;若目标目录已在 cleanup 中消失,则清理残余临时文件后静默返回。
37
- * @param {string} temporaryPath 临时文件路径
38
- * @param {string} filePath 最终目标路径
39
- * @returns {Promise<boolean>} 是否已成功落到目标路径
40
- */
41
- export async function finalizeAtomicRename(temporaryPath, filePath) {
42
- /** @type {NodeJS.ErrnoException | undefined} */
43
- let lastError
44
- for (const delayMs of ATOMIC_RENAME_RETRY_DELAYS_MS) {
45
- if (delayMs) await sleep(delayMs)
46
- try {
47
- await rename(temporaryPath, filePath)
48
- return true
49
- }
50
- catch (error) {
51
- lastError = error
52
- if (ATOMIC_RENAME_RETRY_CODES.has(error?.code)) continue
53
- break
54
- }
55
- }
56
- await cleanupAtomicTemporary(temporaryPath)
57
- if (lastError?.code === 'ENOENT') return false
58
- throw lastError
59
- }
60
16
 
61
17
  /**
62
18
  * @param {string} line JSONL 单行
@@ -15,9 +15,12 @@ import { fanoutFedFetch } from './fetch_fanout.mjs'
15
15
  import { normalizeFileManifest } from './manifest.mjs'
16
16
  import { shouldPreferIncomingPublicManifest } from './public_manifest.mjs'
17
17
 
18
+ const DEFAULT_MANIFEST_FETCH_TIMEOUT_MS = 8000
19
+
18
20
  /**
19
21
  * 拉取公开 manifest;默认不写盘。`cache: true` 或 `cachePublicManifest` 才缓存。
20
- * @param {{ username: string, ownerEntityHash: string, logicalPath: string, cache?: boolean }} context - 拉取上下文
22
+ * 本地已有 publicSig 时仍会 fanout 再校验,按 publishedAt 择新;超时则回退本地。
23
+ * @param {{ username: string, ownerEntityHash: string, logicalPath: string, cache?: boolean, timeoutMs?: number }} context - 拉取上下文
21
24
  * @returns {Promise<import('./manifest.mjs').FileManifest | null>} 验签后的 manifest,失败为 null
22
25
  */
23
26
  export async function fetchPublicManifest(context) {
@@ -27,16 +30,19 @@ export async function fetchPublicManifest(context) {
27
30
  if (!ownerEntityHash || !logicalPath || !username) return null
28
31
 
29
32
  const local = await loadFileManifest(ownerEntityHash, logicalPath)
30
- if (local?.transferKeyDescriptor?.type === 'public' && local?.meta?.publicSig)
31
- return local
33
+ const hasLocalPublic = local?.transferKeyDescriptor?.type === 'public' && !!local?.meta?.publicSig
32
34
 
33
- if (pendingManifestFetches.size >= MAX_PENDING_MANIFEST_FETCHES) return null
35
+ if (pendingManifestFetches.size >= MAX_PENDING_MANIFEST_FETCHES)
36
+ return hasLocalPublic ? local : null
34
37
 
38
+ const timeoutMs = Number(context.timeoutMs) > 0
39
+ ? Number(context.timeoutMs)
40
+ : DEFAULT_MANIFEST_FETCH_TIMEOUT_MS
35
41
  const requestId = randomUUID()
36
42
  const { done } = registerManifestFetchWait(
37
43
  requestId,
38
44
  manifestFetchExpectedKey(ownerEntityHash, logicalPath),
39
- 8000,
45
+ timeoutMs,
40
46
  )
41
47
  const { nodeHash } = await resolveNodeHash(username)
42
48
  const payload = {
@@ -47,11 +53,14 @@ export async function fetchPublicManifest(context) {
47
53
  }
48
54
  await fanoutFedFetch(username, 'fed_manifest_get', payload)
49
55
  const result = await done
50
- if (!result) return null
51
56
 
52
- if (context.cache === true)
53
- await cachePublicManifest(ownerEntityHash, logicalPath, result)
54
- return result
57
+ if (result && (!hasLocalPublic || shouldPreferIncomingPublicManifest(local, result))) {
58
+ if (context.cache === true)
59
+ await cachePublicManifest(ownerEntityHash, logicalPath, result)
60
+ return result
61
+ }
62
+ if (hasLocalPublic) return local
63
+ return null
55
64
  }
56
65
 
57
66
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve02081504/fount-p2p",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
5
5
  "keywords": [
6
6
  "network",
@@ -0,0 +1,87 @@
1
+ /**
2
+ * 原子落盘:唯一临时路径 + rename(Windows 短暂占用时短退避重试)。
3
+ */
4
+ import { randomUUID } from 'node:crypto'
5
+ import fs from 'node:fs'
6
+ import { rename, unlink } from 'node:fs/promises'
7
+ import { setTimeout as sleep } from 'node:timers/promises'
8
+
9
+ /** Windows 上 rename 可能被短暂占用,做几次短退避重试。 */
10
+ const ATOMIC_RENAME_RETRY_DELAYS_MS = [0, 10, 25, 50, 100]
11
+ const ATOMIC_RENAME_RETRY_CODES = new Set(['EPERM', 'EBUSY', 'EACCES'])
12
+
13
+ /**
14
+ * @param {string} filePath 目标路径
15
+ * @returns {string} 唯一临时文件路径
16
+ */
17
+ export function atomicTemporaryPath(filePath) {
18
+ return `${filePath}.tmp.${process.pid}.${randomUUID()}`
19
+ }
20
+
21
+ /**
22
+ * @param {string} temporaryPath 临时文件路径
23
+ * @returns {Promise<void>}
24
+ */
25
+ async function cleanupAtomicTemporary(temporaryPath) {
26
+ try { await unlink(temporaryPath) } catch { /* ok */ }
27
+ }
28
+
29
+ /**
30
+ * @param {string} temporaryPath 临时文件路径
31
+ * @returns {void}
32
+ */
33
+ function cleanupAtomicTemporarySync(temporaryPath) {
34
+ try { fs.unlinkSync(temporaryPath) } catch { /* ok */ }
35
+ }
36
+
37
+ /**
38
+ * 完成原子写的最终 rename;若目标目录已在 cleanup 中消失,则清理残余临时文件后静默返回。
39
+ * @param {string} temporaryPath 临时文件路径
40
+ * @param {string} filePath 最终目标路径
41
+ * @returns {Promise<boolean>} 是否已成功落到目标路径
42
+ */
43
+ export async function finalizeAtomicRename(temporaryPath, filePath) {
44
+ /** @type {NodeJS.ErrnoException | undefined} */
45
+ let lastError
46
+ for (const delayMs of ATOMIC_RENAME_RETRY_DELAYS_MS) {
47
+ if (delayMs) await sleep(delayMs)
48
+ try {
49
+ await rename(temporaryPath, filePath)
50
+ return true
51
+ }
52
+ catch (error) {
53
+ lastError = error
54
+ if (ATOMIC_RENAME_RETRY_CODES.has(error?.code)) continue
55
+ break
56
+ }
57
+ }
58
+ await cleanupAtomicTemporary(temporaryPath)
59
+ if (lastError?.code === 'ENOENT') return false
60
+ throw lastError
61
+ }
62
+
63
+ /**
64
+ * `finalizeAtomicRename` 的同步版。
65
+ * @param {string} temporaryPath 临时文件路径
66
+ * @param {string} filePath 最终目标路径
67
+ * @returns {boolean} 是否已成功落到目标路径
68
+ */
69
+ export function finalizeAtomicRenameSync(temporaryPath, filePath) {
70
+ /** @type {NodeJS.ErrnoException | undefined} */
71
+ let lastError
72
+ for (const delayMs of ATOMIC_RENAME_RETRY_DELAYS_MS) {
73
+ if (delayMs) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMs)
74
+ try {
75
+ fs.renameSync(temporaryPath, filePath)
76
+ return true
77
+ }
78
+ catch (error) {
79
+ lastError = error
80
+ if (ATOMIC_RENAME_RETRY_CODES.has(error?.code)) continue
81
+ break
82
+ }
83
+ }
84
+ cleanupAtomicTemporarySync(temporaryPath)
85
+ if (lastError?.code === 'ENOENT') return false
86
+ throw lastError
87
+ }
package/utils/json_io.mjs CHANGED
@@ -2,6 +2,8 @@ import fs from 'node:fs'
2
2
  import fsp from 'node:fs/promises'
3
3
  import path from 'node:path'
4
4
 
5
+ import { atomicTemporaryPath, finalizeAtomicRename, finalizeAtomicRenameSync } from './atomic_fs.mjs'
6
+
5
7
  /**
6
8
  * @param {string} filePath 绝对路径
7
9
  * @returns {Promise<object | null>} JSON 或 null
@@ -24,9 +26,10 @@ export async function readJsonFile(filePath) {
24
26
  */
25
27
  export async function writeJsonFile(filePath, data) {
26
28
  await fsp.mkdir(path.dirname(filePath), { recursive: true })
27
- const temporaryPath = `${filePath}.tmp`
29
+ const temporaryPath = atomicTemporaryPath(filePath)
28
30
  await fsp.writeFile(temporaryPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
29
- await fsp.rename(temporaryPath, filePath)
31
+ if (!await finalizeAtomicRename(temporaryPath, filePath))
32
+ throw Object.assign(new Error(`ENOENT: atomic rename failed for ${filePath}`), { code: 'ENOENT' })
30
33
  }
31
34
 
32
35
  /**
@@ -51,7 +54,8 @@ export function readJsonFileSync(filePath) {
51
54
  */
52
55
  export function writeJsonFileSync(filePath, data) {
53
56
  fs.mkdirSync(path.dirname(filePath), { recursive: true })
54
- const temporaryPath = `${filePath}.tmp`
57
+ const temporaryPath = atomicTemporaryPath(filePath)
55
58
  fs.writeFileSync(temporaryPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
56
- fs.renameSync(temporaryPath, filePath)
59
+ if (!finalizeAtomicRenameSync(temporaryPath, filePath))
60
+ throw Object.assign(new Error(`ENOENT: atomic rename failed for ${filePath}`), { code: 'ENOENT' })
57
61
  }