@agfpd/iapeer 0.4.34 → 0.4.35
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/package.json +1 -1
- package/src/registry/index.ts +6 -5
- package/src/storage/atomicWrite.ts +60 -0
- package/src/storage/index.ts +4 -37
- package/src/storage/rotatelog.ts +0 -0
package/package.json
CHANGED
package/src/registry/index.ts
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
// is no unlocked path to the registry file anywhere in the package.
|
|
10
10
|
|
|
11
11
|
import * as lockfile from 'proper-lockfile'
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
13
13
|
import { homedir } from 'os'
|
|
14
14
|
import { join } from 'path'
|
|
15
|
-
import {
|
|
15
|
+
import { writeFileAtomicRaw } from '../storage/atomicWrite.ts'
|
|
16
16
|
import {
|
|
17
17
|
IAPEER_DIR,
|
|
18
18
|
MAX_DESCRIPTION_LEN,
|
|
@@ -313,7 +313,6 @@ export async function withPeersLock<T>(
|
|
|
313
313
|
* sanctioned write path for the registry file (#3).
|
|
314
314
|
*/
|
|
315
315
|
function writePeersIndexAtomic(paths: PeersPaths, index: PeersIndex): void {
|
|
316
|
-
const tmp = join(paths.tmpDir, `.${PEERS_PROFILES_FILE}.${process.pid}.${randomUUID()}.tmp`)
|
|
317
316
|
// Persist the RAW intelligence verbatim (legacy-safe round-trip) and DROP the
|
|
318
317
|
// intelligenceRaw shadow field from the on-disk shape — the file carries the
|
|
319
318
|
// legacy vocab the live IAP reads, never the foundation's in-memory normalization.
|
|
@@ -333,8 +332,10 @@ function writePeersIndexAtomic(paths: PeersPaths, index: PeersIndex): void {
|
|
|
333
332
|
...(peer.interfaces ? { interfaces: peer.interfaces } : {}),
|
|
334
333
|
}))
|
|
335
334
|
const normalized = { version: PEERS_SCHEMA_VERSION, peers: peersForDisk }
|
|
336
|
-
|
|
337
|
-
|
|
335
|
+
// Route through the SHARED atomic-write core (fsync-before-rename + looped writeSync) — the private
|
|
336
|
+
// registry writer no longer keeps its own copy that drifted off the durability protocol. tmpDir is
|
|
337
|
+
// paths.tmpDir (same filesystem as peersFile → the rename stays atomic).
|
|
338
|
+
writeFileAtomicRaw(paths.peersFile, `${JSON.stringify(normalized, null, 2)}\n`, 0o600, paths.tmpDir)
|
|
338
339
|
}
|
|
339
340
|
|
|
340
341
|
export async function updatePeersIndex(
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// The guard-FREE atomic-write CORE — the single durability implementation shared by
|
|
2
|
+
// storage.writeFileAtomic (public, basename-guarded) and registry's private
|
|
3
|
+
// peers-profiles writer (which bypasses that guard legitimately: it is the sole writer
|
|
4
|
+
// under withPeersLock). Factored out because the registry kept its OWN copy of the
|
|
5
|
+
// atomic-rename, and that copy DRIFTED — it lost the fsync the storage primitive has,
|
|
6
|
+
// re-opening the power-loss hole on the most critical file of the host (the fleet
|
|
7
|
+
// registry). One implementation → the durability protocol can never drift again.
|
|
8
|
+
|
|
9
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, renameSync, unlinkSync, writeSync } from 'node:fs'
|
|
10
|
+
import { basename, dirname, join } from 'node:path'
|
|
11
|
+
import { randomUUID } from 'node:crypto'
|
|
12
|
+
|
|
13
|
+
const DIR_MODE = 0o700
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Atomically write `data` to `target`:
|
|
17
|
+
* 1. write to a unique tmp in `tmpDir` (default: the target's directory — which MUST be the same
|
|
18
|
+
* filesystem as `target`, or the rename is not atomic),
|
|
19
|
+
* 2. fsync the tmp BEFORE the rename — rename is atomic on one fs, but without flushing the bytes
|
|
20
|
+
* first a power-loss can publish a zero-length / partial file,
|
|
21
|
+
* 3. rename the tmp over `target`.
|
|
22
|
+
*
|
|
23
|
+
* writeSync is LOOPED over a Buffer: a single writeSync can write FEWER bytes than requested (on
|
|
24
|
+
* ENOSPC / a signal, write(2) returns a partial count WITHOUT throwing), which would otherwise publish
|
|
25
|
+
* a truncated file — the exact failure the atomic primitive exists to prevent. The tmp is always
|
|
26
|
+
* cleaned up on failure so a partial write never leaks.
|
|
27
|
+
*/
|
|
28
|
+
export function writeFileAtomicRaw(
|
|
29
|
+
target: string,
|
|
30
|
+
data: string,
|
|
31
|
+
mode: number,
|
|
32
|
+
tmpDir: string = dirname(target),
|
|
33
|
+
): void {
|
|
34
|
+
mkdirSync(dirname(target), { recursive: true, mode: DIR_MODE })
|
|
35
|
+
if (tmpDir !== dirname(target)) mkdirSync(tmpDir, { recursive: true, mode: DIR_MODE })
|
|
36
|
+
const tmp = join(tmpDir, `.${basename(target)}.${process.pid}.${randomUUID()}.tmp`)
|
|
37
|
+
try {
|
|
38
|
+
const fd = openSync(tmp, 'w', mode)
|
|
39
|
+
try {
|
|
40
|
+
const buf = Buffer.from(data, 'utf8')
|
|
41
|
+
let written = 0
|
|
42
|
+
while (written < buf.length) {
|
|
43
|
+
const n = writeSync(fd, buf, written, buf.length - written)
|
|
44
|
+
if (n <= 0) throw new Error(`writeSync made no progress writing ${target} (${written}/${buf.length} bytes)`)
|
|
45
|
+
written += n
|
|
46
|
+
}
|
|
47
|
+
fsyncSync(fd)
|
|
48
|
+
} finally {
|
|
49
|
+
closeSync(fd)
|
|
50
|
+
}
|
|
51
|
+
renameSync(tmp, target)
|
|
52
|
+
} catch (e) {
|
|
53
|
+
try {
|
|
54
|
+
if (existsSync(tmp)) unlinkSync(tmp)
|
|
55
|
+
} catch {
|
|
56
|
+
/* best-effort cleanup */
|
|
57
|
+
}
|
|
58
|
+
throw e
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/storage/index.ts
CHANGED
|
@@ -10,21 +10,10 @@
|
|
|
10
10
|
// reaching for the generic atomic-write primitive. The registry keeps its own
|
|
11
11
|
// private writer (see registry/index.ts) used only inside withPeersLock.
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
closeSync,
|
|
15
|
-
existsSync,
|
|
16
|
-
fsyncSync,
|
|
17
|
-
mkdirSync,
|
|
18
|
-
openSync,
|
|
19
|
-
readFileSync,
|
|
20
|
-
readdirSync,
|
|
21
|
-
renameSync,
|
|
22
|
-
unlinkSync,
|
|
23
|
-
writeSync,
|
|
24
|
-
} from 'fs'
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync } from 'fs'
|
|
25
14
|
import { homedir } from 'os'
|
|
26
15
|
import { basename, dirname, join } from 'path'
|
|
27
|
-
import {
|
|
16
|
+
import { writeFileAtomicRaw } from './atomicWrite.ts'
|
|
28
17
|
import {
|
|
29
18
|
CACHE_DIR,
|
|
30
19
|
IAPEER_DIR,
|
|
@@ -304,28 +293,6 @@ export function writeFileAtomic(path: string, data: string, mode: number = FILE_
|
|
|
304
293
|
`refusing to write ${PEERS_PROFILES_FILE} via storage.writeFileAtomic — it is the registry's locked write target; use registry.upsertPeer/removePeer/renamePeer`,
|
|
305
294
|
)
|
|
306
295
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const tmp = join(dir, `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`)
|
|
310
|
-
try {
|
|
311
|
-
// fsync the tmp file BEFORE the rename: rename is atomic on a single fs, but
|
|
312
|
-
// without flushing the bytes first a power-loss can publish a zero-length /
|
|
313
|
-
// partial shared file (codex config.toml, .mcp.json, router.json, plists).
|
|
314
|
-
const fd = openSync(tmp, 'w', mode)
|
|
315
|
-
try {
|
|
316
|
-
writeSync(fd, data)
|
|
317
|
-
fsyncSync(fd)
|
|
318
|
-
} finally {
|
|
319
|
-
closeSync(fd)
|
|
320
|
-
}
|
|
321
|
-
renameSync(tmp, path)
|
|
322
|
-
} catch (e) {
|
|
323
|
-
// Never leak the tmp file when the write/rename fails (#audit).
|
|
324
|
-
try {
|
|
325
|
-
if (existsSync(tmp)) unlinkSync(tmp)
|
|
326
|
-
} catch {
|
|
327
|
-
/* best-effort cleanup */
|
|
328
|
-
}
|
|
329
|
-
throw e
|
|
330
|
-
}
|
|
296
|
+
// The durability core (fsync-before-rename + looped writeSync) lives in ONE place — see atomicWrite.ts.
|
|
297
|
+
writeFileAtomicRaw(path, data, mode)
|
|
331
298
|
}
|
package/src/storage/rotatelog.ts
CHANGED
|
Binary file
|