@elaraai/e3-core 1.0.16 → 1.0.18
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/dist/src/records.d.ts +10 -0
- package/dist/src/records.d.ts.map +1 -1
- package/dist/src/records.js +37 -3
- package/dist/src/records.js.map +1 -1
- package/dist/src/records.spec.js +108 -1
- package/dist/src/records.spec.js.map +1 -1
- package/dist/src/storage/local/LocalDatasetRefStore.d.ts.map +1 -1
- package/dist/src/storage/local/LocalDatasetRefStore.js +2 -52
- package/dist/src/storage/local/LocalDatasetRefStore.js.map +1 -1
- package/dist/src/storage/local/LocalRefStore.d.ts.map +1 -1
- package/dist/src/storage/local/LocalRefStore.js +23 -18
- package/dist/src/storage/local/LocalRefStore.js.map +1 -1
- package/dist/src/storage/local/LocalRefStore.spec.d.ts +6 -0
- package/dist/src/storage/local/LocalRefStore.spec.d.ts.map +1 -0
- package/dist/src/storage/local/LocalRefStore.spec.js +159 -0
- package/dist/src/storage/local/LocalRefStore.spec.js.map +1 -0
- package/dist/src/storage/local/gc.d.ts.map +1 -1
- package/dist/src/storage/local/gc.js +62 -0
- package/dist/src/storage/local/gc.js.map +1 -1
- package/dist/src/storage/local/localHelpers.d.ts +40 -0
- package/dist/src/storage/local/localHelpers.d.ts.map +1 -1
- package/dist/src/storage/local/localHelpers.js +77 -0
- package/dist/src/storage/local/localHelpers.js.map +1 -1
- package/dist/src/storage/local/localHelpers.spec.d.ts +6 -0
- package/dist/src/storage/local/localHelpers.spec.d.ts.map +1 -0
- package/dist/src/storage/local/localHelpers.spec.js +83 -0
- package/dist/src/storage/local/localHelpers.spec.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localHelpers.spec.d.ts","sourceRoot":"","sources":["../../../../src/storage/local/localHelpers.spec.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Licensed under BSL 1.1. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Tests for the local atomic-write primitive.
|
|
7
|
+
*
|
|
8
|
+
* `atomicWriteFile` is the shared stage-and-rename helper behind every mutable
|
|
9
|
+
* ref/state file (execution status, dataflow runs, workspace state, dataset
|
|
10
|
+
* refs). The contract it guarantees — and the property a bare `fs.writeFile`
|
|
11
|
+
* violates — is that a concurrent reader of a path being overwritten in place
|
|
12
|
+
* never observes a truncated/empty file.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
15
|
+
import assert from 'node:assert';
|
|
16
|
+
import * as fs from 'node:fs/promises';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { atomicWriteFile } from './localHelpers.js';
|
|
19
|
+
import { createTempDir, removeTempDir } from '../../test-helpers.js';
|
|
20
|
+
describe('atomicWriteFile', () => {
|
|
21
|
+
let dir;
|
|
22
|
+
beforeEach(() => { dir = createTempDir(); });
|
|
23
|
+
afterEach(() => { removeTempDir(dir); });
|
|
24
|
+
// POSIX-only. This reproduces the O_TRUNC torn-read race a bare fs.writeFile
|
|
25
|
+
// exposes (reader sees a 0-byte/partial file). On Windows rename-over-an-open
|
|
26
|
+
// file is a sharing violation (handled by renameWithRetry), not a torn read,
|
|
27
|
+
// and a tight raw-byte reader there starves the rename via libuv-threadpool
|
|
28
|
+
// ordering rather than exposing any real bug. Windows concurrent-read atomicity
|
|
29
|
+
// is covered end-to-end by the LocalRefStore execution/dataflow tests, which
|
|
30
|
+
// read+decode (a natural file-closed gap). Matches the repo's existing
|
|
31
|
+
// Windows-skip pattern for concurrency specs (e.g. runDetached.spec.ts).
|
|
32
|
+
it('a concurrent reader never observes a torn or empty file while the path is overwritten in place', { skip: process.platform === 'win32' }, async () => {
|
|
33
|
+
const target = join(dir, 'status.beast2');
|
|
34
|
+
// Distinctly-sized multi-chunk payloads so each write spans multiple write()
|
|
35
|
+
// calls — this widens the truncation window a bare fs.writeFile would expose,
|
|
36
|
+
// making a torn read overwhelmingly likely if atomicity regresses. Kept
|
|
37
|
+
// moderate so a concurrent reader on Windows doesn't hold the file long
|
|
38
|
+
// enough to starve the writer's rename.
|
|
39
|
+
const payloadA = Buffer.alloc(48 * 1024, 0xab);
|
|
40
|
+
const payloadB = Buffer.alloc(32 * 1024, 0xcd);
|
|
41
|
+
await atomicWriteFile(target, payloadA); // seed so the file always exists
|
|
42
|
+
let stop = false;
|
|
43
|
+
const reader = (async () => {
|
|
44
|
+
const torn = [];
|
|
45
|
+
while (!stop) {
|
|
46
|
+
const data = await fs.readFile(target);
|
|
47
|
+
const isA = data.length === payloadA.length && data[0] === 0xab && data[data.length - 1] === 0xab;
|
|
48
|
+
const isB = data.length === payloadB.length && data[0] === 0xcd && data[data.length - 1] === 0xcd;
|
|
49
|
+
if (!isA && !isB)
|
|
50
|
+
torn.push(data.length);
|
|
51
|
+
// Yield between reads. A tight read loop holds the file open ~continuously,
|
|
52
|
+
// which on Windows starves the writer's rename (EPERM); a real poller reads
|
|
53
|
+
// periodically. This still overlaps writes often enough to catch a torn read.
|
|
54
|
+
await new Promise((r) => setTimeout(r, 1));
|
|
55
|
+
}
|
|
56
|
+
return torn;
|
|
57
|
+
})();
|
|
58
|
+
try {
|
|
59
|
+
for (let i = 0; i < 200; i++) {
|
|
60
|
+
await atomicWriteFile(target, i % 2 === 0 ? payloadB : payloadA);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
stop = true; // always release the reader, even if a write throws, or it spins forever
|
|
65
|
+
}
|
|
66
|
+
const torn = await reader;
|
|
67
|
+
assert.deepStrictEqual(torn, [], `reader observed ${torn.length} torn read(s); sizes: ${torn.slice(0, 5).join(',')}`);
|
|
68
|
+
});
|
|
69
|
+
it('leaves no .partial staging files behind after sequential overwrites', async () => {
|
|
70
|
+
const target = join(dir, 'sub', 'a.beast2');
|
|
71
|
+
await atomicWriteFile(target, Buffer.from('hello'));
|
|
72
|
+
await atomicWriteFile(target, Buffer.from('world'));
|
|
73
|
+
const entries = await fs.readdir(join(dir, 'sub'));
|
|
74
|
+
assert.deepStrictEqual(entries, ['a.beast2'], 'only the destination remains; staging files are renamed away');
|
|
75
|
+
assert.strictEqual((await fs.readFile(target)).toString(), 'world');
|
|
76
|
+
});
|
|
77
|
+
it('creates parent directories as needed', async () => {
|
|
78
|
+
const target = join(dir, 'deep', 'nested', 'path', 'x.beast2');
|
|
79
|
+
await atomicWriteFile(target, Buffer.from('ok'));
|
|
80
|
+
assert.strictEqual((await fs.readFile(target)).toString(), 'ok');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=localHelpers.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localHelpers.spec.js","sourceRoot":"","sources":["../../../../src/storage/local/localHelpers.spec.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elaraai/e3-core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "East Execution Engine Core - Programmatic API for e3 repository operations",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
"cross-spawn": "^7.0.6",
|
|
38
38
|
"yauzl": "^3.2.0",
|
|
39
39
|
"yazl": "^2.5.1",
|
|
40
|
-
"@elaraai/e3": "1.0.
|
|
41
|
-
"@elaraai/e3
|
|
40
|
+
"@elaraai/e3-types": "1.0.18",
|
|
41
|
+
"@elaraai/e3": "1.0.18"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@elaraai/east": "1.0.
|
|
44
|
+
"@elaraai/east": "1.0.18"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/cross-spawn": "^6.0.6",
|