@agentxm/workspace-transactions 0.29.0 → 0.29.2
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/transition-lock.js +41 -27
- package/package.json +5 -5
|
@@ -60,17 +60,47 @@ const errorCode = (cause) => typeof cause === "object" && cause !== null && "cod
|
|
|
60
60
|
export const transitionLockPath = (path, workspaceDir) => path.join(workspaceDir, "tmp", TRANSITION_LOCK_FILENAME);
|
|
61
61
|
/**
|
|
62
62
|
* The filesystem handed to `proper-lockfile`. Identical to the platform fs
|
|
63
|
-
* except
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
63
|
+
* except for the transition-lock directory itself:
|
|
64
|
+
*
|
|
65
|
+
* - acquisition creates an empty holder file before `proper-lockfile` probes
|
|
66
|
+
* and records the directory mtime;
|
|
67
|
+
* - removal clears that owned holder file before the library's non-recursive
|
|
68
|
+
* `rmdir`.
|
|
69
|
+
*
|
|
70
|
+
* Recording holder metadata therefore overwrites an existing file instead of
|
|
71
|
+
* adding a directory entry, so it cannot change the lock directory mtime and
|
|
72
|
+
* compromise the first heartbeat. The empty placeholder is also safe during
|
|
73
|
+
* the narrow acquisition window: it is unreadable as holder metadata, so an
|
|
74
|
+
* old finalizer can never mistake a successor's acquisition for its own.
|
|
75
|
+
* Stale reclamation can remove crashed holders, while foreign content still
|
|
76
|
+
* blocks removal as it always did.
|
|
71
77
|
*/
|
|
72
78
|
const lockDirectoryFs = {
|
|
73
79
|
...nodeFs,
|
|
80
|
+
mkdir: (target, callback) => {
|
|
81
|
+
if (typeof target !== "string" || !target.endsWith(TRANSITION_LOCK_FILENAME)) {
|
|
82
|
+
nodeFs.mkdir(target, callback);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
nodeFs.mkdir(target, (directoryError) => {
|
|
86
|
+
if (directoryError !== null) {
|
|
87
|
+
callback(directoryError);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const holderPath = `${target}/holder.json`;
|
|
91
|
+
nodeFs.writeFile(holderPath, "", { flag: "wx" }, (holderError) => {
|
|
92
|
+
if (holderError === null) {
|
|
93
|
+
callback(null);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Surface the original placeholder failure after best-effort cleanup;
|
|
97
|
+
// leaving a half-created directory would turn it into false contention.
|
|
98
|
+
nodeFs.unlink(holderPath, () => {
|
|
99
|
+
nodeFs.rmdir(target, () => callback(holderError));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
},
|
|
74
104
|
rmdir: (target, callback) => {
|
|
75
105
|
if (typeof target === "string" && target.endsWith(TRANSITION_LOCK_FILENAME)) {
|
|
76
106
|
nodeFs.unlink(`${target}/holder.json`, () => {
|
|
@@ -202,25 +232,9 @@ const acquireWorkspaceTransitionLock = (registry, args) => Effect.gen(function*
|
|
|
202
232
|
}
|
|
203
233
|
const release = granted.success;
|
|
204
234
|
const token = randomBytes(16).toString("hex");
|
|
205
|
-
const holderStamped = yield*
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
.pipe(Effect.mapError((cause) => new TransitionLockError({ path: lockPath, step: "inspect-timestamp", cause })));
|
|
209
|
-
const lockMtime = yield* Option.match(lockInfo.mtime, {
|
|
210
|
-
onNone: () => Effect.fail(new TransitionLockError({ path: lockPath, step: "missing-timestamp" })),
|
|
211
|
-
onSome: Effect.succeed,
|
|
212
|
-
});
|
|
213
|
-
yield* writeHolder(fs, path, lockPath, {
|
|
214
|
-
...args.holder,
|
|
215
|
-
token,
|
|
216
|
-
});
|
|
217
|
-
// proper-lockfile proves ownership by comparing the directory's
|
|
218
|
-
// mtime with the timestamp it recorded at acquisition. Writing
|
|
219
|
-
// holder.json changes that directory mtime, so restore the exact
|
|
220
|
-
// acquired value before the first refresh observes it.
|
|
221
|
-
yield* fs
|
|
222
|
-
.utimes(lockPath, lockMtime, lockMtime)
|
|
223
|
-
.pipe(Effect.mapError((cause) => new TransitionLockError({ path: lockPath, step: "preserve-timestamp", cause })));
|
|
235
|
+
const holderStamped = yield* writeHolder(fs, path, lockPath, {
|
|
236
|
+
...args.holder,
|
|
237
|
+
token,
|
|
224
238
|
}).pipe(Effect.result);
|
|
225
239
|
if (holderStamped._tag === "Failure") {
|
|
226
240
|
// The holder metadata is the only ownership evidence this
|
package/package.json
CHANGED
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
"url": "https://github.com/agentxm/axm/issues"
|
|
5
5
|
},
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"effect": "4.0.0-rc.
|
|
7
|
+
"effect": "4.0.0-rc.115",
|
|
8
8
|
"proper-lockfile": "^4.1.2"
|
|
9
9
|
},
|
|
10
10
|
"description": "AXM workspace transactions: the process transition lock, per-closure snapshot ledger, restoration, atomic single-file publication, and footprint observation for the axm CLI. Unstable and unsupported — use the axm.sh CLI.",
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@effect/platform-node": "4.0.0-rc.
|
|
13
|
-
"@effect/vitest": "4.0.0-rc.
|
|
12
|
+
"@effect/platform-node": "4.0.0-rc.115",
|
|
13
|
+
"@effect/vitest": "4.0.0-rc.115",
|
|
14
14
|
"@types/proper-lockfile": "^4.1.4",
|
|
15
15
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
16
16
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
17
|
-
"vitest": "^
|
|
17
|
+
"vitest": "^5.0.0"
|
|
18
18
|
},
|
|
19
19
|
"engines": {
|
|
20
20
|
"node": ">=22.19.0"
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
},
|
|
52
52
|
"sideEffects": false,
|
|
53
53
|
"type": "module",
|
|
54
|
-
"version": "0.29.
|
|
54
|
+
"version": "0.29.2"
|
|
55
55
|
}
|