test-fns 1.15.5 → 1.15.6
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.
|
@@ -1,10 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .what = result of findsertSymlink operation
|
|
3
|
+
* .why = observability into which code path was taken for tests and debug
|
|
4
|
+
*/
|
|
5
|
+
export interface FindsertSymlinkResult {
|
|
6
|
+
/** whether the symlink was created or already found */
|
|
7
|
+
effect: 'created' | 'found';
|
|
8
|
+
/** race condition that was overcome, if any */
|
|
9
|
+
overcame: 'EEXIST' | 'ENOENT' | null;
|
|
10
|
+
}
|
|
1
11
|
/**
|
|
2
12
|
* .what = creates a symlink if absent, no-op if correct symlink exists
|
|
3
13
|
* .why = idempotent symlink creation safe for parallel workers
|
|
4
14
|
*
|
|
15
|
+
* .note = handles three race scenarios:
|
|
16
|
+
* 1. EEXIST: another worker created symlink → verify target, return if correct
|
|
17
|
+
* 2. ENOENT on verify: another worker deleted symlink → retry
|
|
18
|
+
* 3. stale symlink/file: remove and create
|
|
19
|
+
*
|
|
5
20
|
* @throws UnexpectedCodePathError if path exists but is not the expected symlink
|
|
6
21
|
*/
|
|
7
22
|
export declare const findsertSymlink: (input: {
|
|
8
23
|
target: string;
|
|
9
24
|
path: string;
|
|
10
|
-
}) =>
|
|
25
|
+
}) => FindsertSymlinkResult;
|
|
@@ -31,50 +31,78 @@ const isSymlinkEexistError_1 = require("./isSymlinkEexistError");
|
|
|
31
31
|
* .what = creates a symlink if absent, no-op if correct symlink exists
|
|
32
32
|
* .why = idempotent symlink creation safe for parallel workers
|
|
33
33
|
*
|
|
34
|
+
* .note = handles three race scenarios:
|
|
35
|
+
* 1. EEXIST: another worker created symlink → verify target, return if correct
|
|
36
|
+
* 2. ENOENT on verify: another worker deleted symlink → retry
|
|
37
|
+
* 3. stale symlink/file: remove and create
|
|
38
|
+
*
|
|
34
39
|
* @throws UnexpectedCodePathError if path exists but is not the expected symlink
|
|
35
40
|
*/
|
|
36
41
|
const findsertSymlink = (input) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
const MAX_RETRIES = 3;
|
|
43
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
44
|
+
// check if correct symlink already exists
|
|
45
|
+
const stat = (() => {
|
|
46
|
+
try {
|
|
47
|
+
return fs.lstatSync(input.path);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
if (stat?.isSymbolicLink()) {
|
|
54
|
+
try {
|
|
55
|
+
const currentTarget = fs.readlinkSync(input.path);
|
|
56
|
+
if (currentTarget === input.target) {
|
|
57
|
+
return { effect: 'found', overcame: null }; // correct symlink exists
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// symlink deleted between lstat and readlink — retry
|
|
62
|
+
const code = error.code;
|
|
63
|
+
if (code === 'ENOENT')
|
|
64
|
+
continue;
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
41
67
|
}
|
|
42
|
-
|
|
43
|
-
|
|
68
|
+
// remove stale symlink, directory, or file
|
|
69
|
+
if (stat) {
|
|
70
|
+
fs.rmSync(input.path, { recursive: true, force: true });
|
|
44
71
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return; // correct symlink exists
|
|
72
|
+
// create symlink
|
|
73
|
+
try {
|
|
74
|
+
fs.symlinkSync(input.target, input.path);
|
|
75
|
+
return { effect: 'created', overcame: null }; // success
|
|
50
76
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
catch (error) {
|
|
78
|
+
// handle race: another worker created symlink between our check and create
|
|
79
|
+
if (!(0, isSymlinkEexistError_1.isSymlinkEexistError)(error))
|
|
80
|
+
throw error;
|
|
81
|
+
// another worker created symlink — verify it's correct
|
|
82
|
+
try {
|
|
83
|
+
const raceStat = fs.lstatSync(input.path);
|
|
84
|
+
if (raceStat.isSymbolicLink()) {
|
|
85
|
+
const currentTarget = fs.readlinkSync(input.path);
|
|
86
|
+
if (currentTarget === input.target) {
|
|
87
|
+
return { effect: 'found', overcame: 'EEXIST' }; // correct symlink created by another worker
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw new helpful_errors_1.UnexpectedCodePathError('symlink exists but points to unexpected target', {
|
|
91
|
+
target: input.target,
|
|
92
|
+
path: input.path,
|
|
93
|
+
isSymlink: raceStat.isSymbolicLink(),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (verifyError) {
|
|
97
|
+
// symlink deleted between EEXIST and verify — retry
|
|
98
|
+
const code = verifyError.code;
|
|
99
|
+
if (code === 'ENOENT')
|
|
100
|
+
continue;
|
|
101
|
+
throw verifyError;
|
|
70
102
|
}
|
|
71
103
|
}
|
|
72
|
-
throw new helpful_errors_1.UnexpectedCodePathError('symlink exists but points to unexpected target', {
|
|
73
|
-
target: input.target,
|
|
74
|
-
path: input.path,
|
|
75
|
-
isSymlink: raceStat.isSymbolicLink(),
|
|
76
|
-
});
|
|
77
104
|
}
|
|
105
|
+
throw new helpful_errors_1.UnexpectedCodePathError('findsertSymlink exceeded max retries due to concurrent deletes', { target: input.target, path: input.path, maxRetries: MAX_RETRIES });
|
|
78
106
|
};
|
|
79
107
|
exports.findsertSymlink = findsertSymlink;
|
|
80
108
|
//# sourceMappingURL=findsertSymlink.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findsertSymlink.js","sourceRoot":"","sources":["../../../src/infra/isomorph.fs/findsertSymlink.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,mDAAyD;AAEzD,4CAA8B;AAC9B,iEAA8D;
|
|
1
|
+
{"version":3,"file":"findsertSymlink.js","sourceRoot":"","sources":["../../../src/infra/isomorph.fs/findsertSymlink.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "test-fns",
|
|
3
3
|
"author": "ehmpathy",
|
|
4
4
|
"description": "write usecase driven tests systematically for simpler, safer, and more readable code",
|
|
5
|
-
"version": "1.15.
|
|
5
|
+
"version": "1.15.6",
|
|
6
6
|
"repository": "ehmpathy/test-fns",
|
|
7
7
|
"homepage": "https://github.com/ehmpathy/test-fns",
|
|
8
8
|
"keywords": [
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"preversion": "npm run prepush",
|
|
69
69
|
"postversion": "git push origin HEAD --tags --no-verify",
|
|
70
70
|
"prepare:husky": "husky install && chmod ug+x .husky/*",
|
|
71
|
-
"prepare:rhachet": "rhachet init --hooks --roles behaver driver
|
|
71
|
+
"prepare:rhachet": "rhachet init --hooks --roles mechanic behaver driver reviewer librarian ergonomist architect",
|
|
72
72
|
"prepare": "if [ -e .git ] && [ -z $CI ]; then npm run prepare:husky && npm run prepare:rhachet; fi",
|
|
73
73
|
"upgrade:rhachet": "rhachet upgrade"
|
|
74
74
|
},
|
|
@@ -99,11 +99,11 @@
|
|
|
99
99
|
"esbuild-register": "3.6.0",
|
|
100
100
|
"husky": "8.0.3",
|
|
101
101
|
"jest": "30.2.0",
|
|
102
|
-
"rhachet": "1.
|
|
102
|
+
"rhachet": "1.37.15",
|
|
103
103
|
"rhachet-brains-anthropic": "0.3.3",
|
|
104
|
-
"rhachet-roles-bhrain": "0.
|
|
105
|
-
"rhachet-roles-bhuild": "0.
|
|
106
|
-
"rhachet-roles-ehmpathy": "1.
|
|
104
|
+
"rhachet-roles-bhrain": "0.19.0",
|
|
105
|
+
"rhachet-roles-bhuild": "0.14.1",
|
|
106
|
+
"rhachet-roles-ehmpathy": "1.27.13",
|
|
107
107
|
"tsc-alias": "1.8.10",
|
|
108
108
|
"tsx": "4.21.0",
|
|
109
109
|
"typescript": "5.4.5",
|