@agentxm/workspace-operations 0.28.11 → 0.28.13
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/index.d.ts +11 -10
- package/dist/src/index.js +16 -12
- package/dist/src/operations/scan-plan-readiness.d.ts +1 -1
- package/dist/src/operations/scan-plan-readiness.js +1 -1
- package/dist/src/plan/apply-plan.js +8 -8
- package/dist/src/plan/execution-candidate.d.ts +7 -1
- package/dist/src/plan/execution-candidate.js +9 -2
- package/dist/src/plan/interruption-resolution.d.ts +50 -0
- package/dist/src/plan/interruption-resolution.js +158 -0
- package/dist/src/plan/operation-journal.d.ts +9 -9
- package/dist/src/plan/operation-journal.js +5 -20
- package/dist/src/plan/operation-resolution.d.ts +1 -1
- package/dist/src/plan/plan-execution-fixtures.d.ts +2 -2
- package/dist/src/plan/plan-execution-fixtures.js +2 -2
- package/dist/src/plan/plan-execution.d.ts +48 -0
- package/dist/src/plan/plan-execution.js +40 -0
- package/dist/src/plan/plan.d.ts +9 -19
- package/dist/src/plan/resolve-plan-interaction.d.ts +1 -1
- package/dist/src/plan/resolve-plan-interaction.js +1 -1
- package/dist/src/plan/resolve-plan.d.ts +38 -23
- package/dist/src/plan/resolve-plan.js +114 -96
- package/dist/src/plan/step-failure-conversions.d.ts +2 -1
- package/dist/src/testing.d.ts +22 -0
- package/dist/src/testing.js +23 -0
- package/package.json +12 -16
- package/dist/src/live.d.ts +0 -13
- package/dist/src/live.js +0 -12
- package/dist/src/operations/load-workspace.d.ts +0 -43
- package/dist/src/operations/load-workspace.js +0 -77
- package/dist/src/operations/transaction.d.ts +0 -58
- package/dist/src/operations/transaction.js +0 -359
- package/dist/src/operations/transition-lock.d.ts +0 -66
- package/dist/src/operations/transition-lock.js +0 -294
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workspace transition lock.
|
|
3
|
-
*
|
|
4
|
-
* One cross-process lock serializes mutation-class operations on a workspace.
|
|
5
|
-
* A plan-family apply acquires it after confirmation — planning, network
|
|
6
|
-
* acquisition, preview, and the confirmation decision stay lock-free — then
|
|
7
|
-
* revalidates its candidate and applies while holding it; the workspace
|
|
8
|
-
* transaction reuses the invocation's hold, or acquires its own when no
|
|
9
|
-
* plan-family hold exists. A contending invocation waits with a visible
|
|
10
|
-
* reason up to a bound, then terminates blocked with a machine-readable
|
|
11
|
-
* reference to the holder.
|
|
12
|
-
*
|
|
13
|
-
* Each acquisition records a distinct owner token in the holder metadata.
|
|
14
|
-
* Ownership is proven only by an exact token match — never inferred from a
|
|
15
|
-
* pid or from missing or unreadable metadata — and a holder write that fails
|
|
16
|
-
* releases the acquisition instead of holding anonymously.
|
|
17
|
-
*
|
|
18
|
-
* The lock file lives at `.axm/tmp/workspace-transition.lock`; its path is
|
|
19
|
-
* load-bearing for operational tooling and must not move casually.
|
|
20
|
-
*
|
|
21
|
-
* Acquisition is atomic with respect to interruption: the narrow region from
|
|
22
|
-
* the library granting the hold through finalizer registration is masked, so
|
|
23
|
-
* an interrupt can never strand a granted lock, while contention waits stay
|
|
24
|
-
* interruptible. Only the lock-is-held error class is absorbed by the bounded
|
|
25
|
-
* wait; any other acquisition error surfaces immediately as a typed failure.
|
|
26
|
-
*
|
|
27
|
-
* @experimental This API is unstable and may change without notice.
|
|
28
|
-
*/
|
|
29
|
-
import * as Effect from "effect/Effect";
|
|
30
|
-
import * as FileSystem from "effect/FileSystem";
|
|
31
|
-
import * as Option from "effect/Option";
|
|
32
|
-
import * as Path from "effect/Path";
|
|
33
|
-
import type * as Scope from "effect/Scope";
|
|
34
|
-
import { WorkspaceTransitionCompromised, type TransitionContention, type TransitionLockHolder, type WorkspaceTransitionAcquireFailure } from "@agentxm/workspace-state";
|
|
35
|
-
export declare const TRANSITION_LOCK_FILENAME = "workspace-transition.lock";
|
|
36
|
-
/** How long a contending invocation serializes behind the holder. */
|
|
37
|
-
export declare const TRANSITION_WAIT_BOUND_MILLIS = 60000;
|
|
38
|
-
/** The live view of one acquisition this process currently holds. */
|
|
39
|
-
export interface HeldWorkspaceTransition {
|
|
40
|
-
/** Fails when the hold is compromised; never succeeds and never ends otherwise. */
|
|
41
|
-
readonly compromised: Effect.Effect<never, WorkspaceTransitionCompromised>;
|
|
42
|
-
/** Synchronous probe for boundaries that cannot race, such as restoration. */
|
|
43
|
-
readonly isCompromised: () => boolean;
|
|
44
|
-
}
|
|
45
|
-
export declare const isWorkspaceTransitionHeldByThisInvocation: (workspaceDir: string) => boolean;
|
|
46
|
-
/** The live hold for a workspace this process acquired, when one exists. */
|
|
47
|
-
export declare const heldWorkspaceTransition: (workspaceDir: string) => HeldWorkspaceTransition | undefined;
|
|
48
|
-
export declare const transitionLockPath: (path: Path.Path, workspaceDir: string) => string;
|
|
49
|
-
/**
|
|
50
|
-
* Acquire the workspace transition lock, waiting up to the bound while
|
|
51
|
-
* another invocation holds it. Resolves `None` when acquired (the release is
|
|
52
|
-
* a scope finalizer) and `Some(contention)` when the bound elapsed.
|
|
53
|
-
*/
|
|
54
|
-
export declare const acquireWorkspaceTransitionLock: (args: {
|
|
55
|
-
readonly workspaceDir: string;
|
|
56
|
-
readonly holder: TransitionLockHolder;
|
|
57
|
-
readonly waitBoundMillis?: number;
|
|
58
|
-
/** Called once when the invocation starts waiting on another holder. */
|
|
59
|
-
readonly onWaiting?: (holder: Option.Option<TransitionLockHolder>) => Effect.Effect<void>;
|
|
60
|
-
/** Staleness/refresh override for deterministic compromise tests only. */
|
|
61
|
-
readonly timingMillis?: {
|
|
62
|
-
readonly stale: number;
|
|
63
|
-
readonly update: number;
|
|
64
|
-
};
|
|
65
|
-
}) => Effect.Effect<Option.Option<TransitionContention>, WorkspaceTransitionAcquireFailure, FileSystem.FileSystem | Path.Path | Scope.Scope>;
|
|
66
|
-
//# sourceMappingURL=transition-lock.d.ts.map
|
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
// @effect-diagnostics nodeBuiltinImport:off — proper-lockfile requires a callback-style fs; the adapter is confined to the lock path
|
|
2
|
-
/**
|
|
3
|
-
* Workspace transition lock.
|
|
4
|
-
*
|
|
5
|
-
* One cross-process lock serializes mutation-class operations on a workspace.
|
|
6
|
-
* A plan-family apply acquires it after confirmation — planning, network
|
|
7
|
-
* acquisition, preview, and the confirmation decision stay lock-free — then
|
|
8
|
-
* revalidates its candidate and applies while holding it; the workspace
|
|
9
|
-
* transaction reuses the invocation's hold, or acquires its own when no
|
|
10
|
-
* plan-family hold exists. A contending invocation waits with a visible
|
|
11
|
-
* reason up to a bound, then terminates blocked with a machine-readable
|
|
12
|
-
* reference to the holder.
|
|
13
|
-
*
|
|
14
|
-
* Each acquisition records a distinct owner token in the holder metadata.
|
|
15
|
-
* Ownership is proven only by an exact token match — never inferred from a
|
|
16
|
-
* pid or from missing or unreadable metadata — and a holder write that fails
|
|
17
|
-
* releases the acquisition instead of holding anonymously.
|
|
18
|
-
*
|
|
19
|
-
* The lock file lives at `.axm/tmp/workspace-transition.lock`; its path is
|
|
20
|
-
* load-bearing for operational tooling and must not move casually.
|
|
21
|
-
*
|
|
22
|
-
* Acquisition is atomic with respect to interruption: the narrow region from
|
|
23
|
-
* the library granting the hold through finalizer registration is masked, so
|
|
24
|
-
* an interrupt can never strand a granted lock, while contention waits stay
|
|
25
|
-
* interruptible. Only the lock-is-held error class is absorbed by the bounded
|
|
26
|
-
* wait; any other acquisition error surfaces immediately as a typed failure.
|
|
27
|
-
*
|
|
28
|
-
* @experimental This API is unstable and may change without notice.
|
|
29
|
-
*/
|
|
30
|
-
import * as nodeFs from "node:fs";
|
|
31
|
-
import { randomBytes } from "node:crypto";
|
|
32
|
-
import * as Deferred from "effect/Deferred";
|
|
33
|
-
import * as Duration from "effect/Duration";
|
|
34
|
-
import * as Effect from "effect/Effect";
|
|
35
|
-
import * as FileSystem from "effect/FileSystem";
|
|
36
|
-
import * as Option from "effect/Option";
|
|
37
|
-
import * as Path from "effect/Path";
|
|
38
|
-
import * as lockfile from "proper-lockfile";
|
|
39
|
-
import { TransitionLockError, WorkspaceDirectoryError, WorkspaceTransitionCompromised, } from "@agentxm/workspace-state";
|
|
40
|
-
export const TRANSITION_LOCK_FILENAME = "workspace-transition.lock";
|
|
41
|
-
// Staleness must tolerate a saturated event loop: a heavy apply starves the
|
|
42
|
-
// mtime-update timer, and a slack smaller than the starvation lets a LIVE
|
|
43
|
-
// holder's lock self-declare compromised — after which release refuses and the
|
|
44
|
-
// dir is left for the next invocation to wait out. 25 s of slack covers the
|
|
45
|
-
// longest observed starvation with room; a crashed holder delays a contender
|
|
46
|
-
// at most this long, still inside the 60 s wait bound.
|
|
47
|
-
const LOCK_STALE_MILLIS = 30_000;
|
|
48
|
-
const LOCK_UPDATE_MILLIS = 5_000;
|
|
49
|
-
const WAIT_INTERVAL = Duration.millis(250);
|
|
50
|
-
/** How long a contending invocation serializes behind the holder. */
|
|
51
|
-
export const TRANSITION_WAIT_BOUND_MILLIS = 60_000;
|
|
52
|
-
/**
|
|
53
|
-
* Held transition locks in this process, keyed by resolved workspace
|
|
54
|
-
* directory. Owned exclusively by `acquireWorkspaceTransitionLock`, whose
|
|
55
|
-
* scope finalizer removes the entry, so an entry's lifetime is exactly the
|
|
56
|
-
* lock's; the workspace transaction consults it to avoid deadlocking on the
|
|
57
|
-
* invocation's own hold and to observe compromise of that hold.
|
|
58
|
-
*/
|
|
59
|
-
// eslint-disable-next-line no-restricted-syntax -- Owned exclusively by acquireWorkspaceTransitionLock: an entry's lifetime is exactly the lock's, and the acquisition's scope finalizer removes it.
|
|
60
|
-
const heldTransitions = new Map();
|
|
61
|
-
export const isWorkspaceTransitionHeldByThisInvocation = (workspaceDir) => heldTransitions.has(workspaceDir);
|
|
62
|
-
/** The live hold for a workspace this process acquired, when one exists. */
|
|
63
|
-
export const heldWorkspaceTransition = (workspaceDir) => heldTransitions.get(workspaceDir);
|
|
64
|
-
const errorCode = (cause) => typeof cause === "object" && cause !== null && "code" in cause && typeof cause.code === "string"
|
|
65
|
-
? cause.code
|
|
66
|
-
: undefined;
|
|
67
|
-
export const transitionLockPath = (path, workspaceDir) => path.join(workspaceDir, "tmp", TRANSITION_LOCK_FILENAME);
|
|
68
|
-
/**
|
|
69
|
-
* The filesystem handed to `proper-lockfile`. Identical to the platform fs
|
|
70
|
-
* except that removing a transition-lock directory first clears the tool's
|
|
71
|
-
* own holder metadata inside it. The library reclaims a stale lock and
|
|
72
|
-
* releases a held one with a non-recursive `rmdir`; without this, a lock
|
|
73
|
-
* directory left by a crashed process — which always still contains
|
|
74
|
-
* `holder.json` — could never be reclaimed, and every later mutation would
|
|
75
|
-
* wait out its bound and report a false contention. Removal stays
|
|
76
|
-
* non-recursive past that one owned name, so foreign content keeps blocking
|
|
77
|
-
* removal the way it always did.
|
|
78
|
-
*/
|
|
79
|
-
const lockDirectoryFs = {
|
|
80
|
-
...nodeFs,
|
|
81
|
-
rmdir: (target, callback) => {
|
|
82
|
-
if (typeof target === "string" && target.endsWith(TRANSITION_LOCK_FILENAME)) {
|
|
83
|
-
nodeFs.unlink(`${target}/holder.json`, () => {
|
|
84
|
-
nodeFs.rmdir(target, callback);
|
|
85
|
-
});
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
nodeFs.rmdir(target, callback);
|
|
89
|
-
},
|
|
90
|
-
rmdirSync: (target) => {
|
|
91
|
-
if (typeof target === "string" && target.endsWith(TRANSITION_LOCK_FILENAME)) {
|
|
92
|
-
try {
|
|
93
|
-
nodeFs.unlinkSync(`${target}/holder.json`);
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
// Absent metadata blocks nothing; foreign content still blocks rmdir.
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
nodeFs.rmdirSync(target);
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
/**
|
|
103
|
-
* Acquisition-error classes the bounded contention wait absorbs: the lock is
|
|
104
|
-
* held (`ELOCKED`), or the attempt raced the holder's own acquire, release,
|
|
105
|
-
* or staleness sweep (`ENOENT`/`EEXIST` from proper-lockfile's internal
|
|
106
|
-
* stat-remove-retry). Every other class — permissions, foreign state
|
|
107
|
-
* squatting the path — is not resolved by waiting and surfaces immediately.
|
|
108
|
-
*/
|
|
109
|
-
const isContentionErrorCode = (code) => code === "ELOCKED" || code === "ENOENT" || code === "EEXIST";
|
|
110
|
-
const readHolder = (fs, path, lockPath) => fs.readFileString(path.join(lockPath, "holder.json")).pipe(Effect.map((content) => {
|
|
111
|
-
const parsed = JSON.parse(content);
|
|
112
|
-
if (typeof parsed === "object" &&
|
|
113
|
-
parsed !== null &&
|
|
114
|
-
"command" in parsed &&
|
|
115
|
-
typeof parsed.command === "string" &&
|
|
116
|
-
"pid" in parsed &&
|
|
117
|
-
typeof parsed.pid === "number") {
|
|
118
|
-
return Option.some({
|
|
119
|
-
command: parsed.command,
|
|
120
|
-
pid: parsed.pid,
|
|
121
|
-
...("candidateId" in parsed && typeof parsed.candidateId === "string"
|
|
122
|
-
? { candidateId: parsed.candidateId }
|
|
123
|
-
: {}),
|
|
124
|
-
...("token" in parsed && typeof parsed.token === "string" ? { token: parsed.token } : {}),
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
return Option.none();
|
|
128
|
-
}), Effect.catch(() => Effect.succeed(Option.none())));
|
|
129
|
-
const writeHolder = (fs, path, lockPath, holder) => fs
|
|
130
|
-
.writeFileString(path.join(lockPath, "holder.json"), JSON.stringify(holder))
|
|
131
|
-
.pipe(Effect.mapError((cause) => new TransitionLockError({ path: lockPath, step: "record-holder", cause })));
|
|
132
|
-
/**
|
|
133
|
-
* Acquire the workspace transition lock, waiting up to the bound while
|
|
134
|
-
* another invocation holds it. Resolves `None` when acquired (the release is
|
|
135
|
-
* a scope finalizer) and `Some(contention)` when the bound elapsed.
|
|
136
|
-
*/
|
|
137
|
-
export const acquireWorkspaceTransitionLock = (args) => Effect.gen(function* () {
|
|
138
|
-
const fs = yield* FileSystem.FileSystem;
|
|
139
|
-
const path = yield* Path.Path;
|
|
140
|
-
const workspaceDir = path.resolve(args.workspaceDir);
|
|
141
|
-
const scratchDir = path.join(workspaceDir, "tmp");
|
|
142
|
-
const lockPath = path.join(scratchDir, TRANSITION_LOCK_FILENAME);
|
|
143
|
-
const waitBound = args.waitBoundMillis ?? TRANSITION_WAIT_BOUND_MILLIS;
|
|
144
|
-
const workspaceExisted = yield* fs
|
|
145
|
-
.exists(workspaceDir)
|
|
146
|
-
.pipe(Effect.mapError((cause) => new WorkspaceDirectoryError({ path: workspaceDir, step: "inspect", cause })));
|
|
147
|
-
const removeEmptyScratch = fs.readDirectory(scratchDir).pipe(Effect.flatMap((entries) => entries.length === 0
|
|
148
|
-
? fs.remove(scratchDir, { recursive: true, force: false })
|
|
149
|
-
: Effect.void), Effect.ignore);
|
|
150
|
-
const removeNewEmptyWorkspace = workspaceExisted
|
|
151
|
-
? Effect.void
|
|
152
|
-
: fs.readDirectory(workspaceDir).pipe(Effect.flatMap((entries) => entries.length === 0
|
|
153
|
-
? fs.remove(workspaceDir, { recursive: true, force: false })
|
|
154
|
-
: Effect.void), Effect.ignore);
|
|
155
|
-
const compromisedSignal = Deferred.makeUnsafe();
|
|
156
|
-
let waitedMillis = 0;
|
|
157
|
-
let reportedWaiting = false;
|
|
158
|
-
while (true) {
|
|
159
|
-
// The previous holder removes an empty scratch directory when it
|
|
160
|
-
// releases. Recreate it before every attempt so that a waiter can
|
|
161
|
-
// acquire after that cleanup instead of mistaking ENOENT for a hold.
|
|
162
|
-
yield* fs
|
|
163
|
-
.makeDirectory(scratchDir, { recursive: true })
|
|
164
|
-
.pipe(Effect.mapError((cause) => new TransitionLockError({ path: scratchDir, step: "create-scratch", cause })));
|
|
165
|
-
// One attempt is atomic with respect to interruption: from the library
|
|
166
|
-
// granting the hold through finalizer registration there is no
|
|
167
|
-
// interruptible gap, so an interrupt requested mid-acquisition defers
|
|
168
|
-
// until the release finalizer exists and then releases through it. The
|
|
169
|
-
// mask stays narrow — a single no-retry grant plus local metadata —
|
|
170
|
-
// and the contention wait below remains interruptible.
|
|
171
|
-
const attempt = yield* Effect.uninterruptible(Effect.gen(function* () {
|
|
172
|
-
const granted = yield* Effect.tryPromise({
|
|
173
|
-
try: () => lockfile.lock(workspaceDir, {
|
|
174
|
-
lockfilePath: lockPath,
|
|
175
|
-
realpath: false,
|
|
176
|
-
retries: 0,
|
|
177
|
-
fs: lockDirectoryFs,
|
|
178
|
-
stale: args.timingMillis?.stale ?? LOCK_STALE_MILLIS,
|
|
179
|
-
update: args.timingMillis?.update ?? LOCK_UPDATE_MILLIS,
|
|
180
|
-
// The default handler throws inside a timer: an uncatchable
|
|
181
|
-
// crash that sprays raw frames on stderr. Complete the typed
|
|
182
|
-
// compromise signal instead — the workspace transaction races
|
|
183
|
-
// against it and never continues mutating after ownership is
|
|
184
|
-
// lost.
|
|
185
|
-
onCompromised: (cause) => {
|
|
186
|
-
Deferred.doneUnsafe(compromisedSignal, Effect.fail(new WorkspaceTransitionCompromised({ workspaceDir, lockPath, cause })));
|
|
187
|
-
},
|
|
188
|
-
}),
|
|
189
|
-
catch: (cause) => ({ cause, code: errorCode(cause) }),
|
|
190
|
-
}).pipe(Effect.result);
|
|
191
|
-
if (granted._tag === "Failure") {
|
|
192
|
-
if (isContentionErrorCode(granted.failure.code)) {
|
|
193
|
-
return { _tag: "held" };
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
_tag: "failed",
|
|
197
|
-
error: new TransitionLockError({
|
|
198
|
-
path: lockPath,
|
|
199
|
-
step: "acquire",
|
|
200
|
-
cause: granted.failure.cause,
|
|
201
|
-
}),
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
const release = granted.success;
|
|
205
|
-
const token = randomBytes(16).toString("hex");
|
|
206
|
-
const holderStamped = yield* Effect.gen(function* () {
|
|
207
|
-
const lockInfo = yield* fs
|
|
208
|
-
.stat(lockPath)
|
|
209
|
-
.pipe(Effect.mapError((cause) => new TransitionLockError({ path: lockPath, step: "inspect-timestamp", cause })));
|
|
210
|
-
const lockMtime = yield* Option.match(lockInfo.mtime, {
|
|
211
|
-
onNone: () => Effect.fail(new TransitionLockError({ path: lockPath, step: "missing-timestamp" })),
|
|
212
|
-
onSome: Effect.succeed,
|
|
213
|
-
});
|
|
214
|
-
yield* writeHolder(fs, path, lockPath, {
|
|
215
|
-
...args.holder,
|
|
216
|
-
token,
|
|
217
|
-
});
|
|
218
|
-
// proper-lockfile proves ownership by comparing the directory's
|
|
219
|
-
// mtime with the timestamp it recorded at acquisition. Writing
|
|
220
|
-
// holder.json changes that directory mtime, so restore the exact
|
|
221
|
-
// acquired value before the first refresh observes it.
|
|
222
|
-
yield* fs
|
|
223
|
-
.utimes(lockPath, lockMtime, lockMtime)
|
|
224
|
-
.pipe(Effect.mapError((cause) => new TransitionLockError({ path: lockPath, step: "preserve-timestamp", cause })));
|
|
225
|
-
}).pipe(Effect.result);
|
|
226
|
-
if (holderStamped._tag === "Failure") {
|
|
227
|
-
// The holder metadata is the only ownership evidence this
|
|
228
|
-
// acquisition will ever have; holding without it would be
|
|
229
|
-
// indistinguishable from a successor's half-written acquisition.
|
|
230
|
-
// Release the lock and fail rather than hold anonymously.
|
|
231
|
-
yield* Effect.tryPromise({
|
|
232
|
-
try: () => release(),
|
|
233
|
-
catch: (cause) => new TransitionLockError({ path: lockPath, step: "release", cause }),
|
|
234
|
-
}).pipe(Effect.ignore);
|
|
235
|
-
return { _tag: "failed", error: holderStamped.failure };
|
|
236
|
-
}
|
|
237
|
-
heldTransitions.set(workspaceDir, {
|
|
238
|
-
compromised: Deferred.await(compromisedSignal),
|
|
239
|
-
isCompromised: () => Deferred.isDoneUnsafe(compromisedSignal),
|
|
240
|
-
});
|
|
241
|
-
yield* Effect.addFinalizer(() => Effect.gen(function* () {
|
|
242
|
-
heldTransitions.delete(workspaceDir);
|
|
243
|
-
// Removal requires an exact owner-token match, proven before
|
|
244
|
-
// release() — which would remove the directory unconditionally.
|
|
245
|
-
// Absent or unreadable holder metadata is indistinguishable from
|
|
246
|
-
// a successor that reclaimed the stale hold but has not yet
|
|
247
|
-
// written its holder file, and is never license to remove; the
|
|
248
|
-
// unowned in-process bookkeeping self-resolves as compromised on
|
|
249
|
-
// its next update tick without touching the directory.
|
|
250
|
-
const residualHolder = yield* readHolder(fs, path, lockPath);
|
|
251
|
-
const ownsResidual = Option.match(residualHolder, {
|
|
252
|
-
onNone: () => false,
|
|
253
|
-
onSome: (value) => value.token === token,
|
|
254
|
-
});
|
|
255
|
-
if (ownsResidual) {
|
|
256
|
-
yield* Effect.tryPromise({
|
|
257
|
-
try: () => release(),
|
|
258
|
-
catch: (cause) => new TransitionLockError({ path: lockPath, step: "release", cause }),
|
|
259
|
-
}).pipe(Effect.ignore);
|
|
260
|
-
// A compromised hold makes release() refuse; the directory is
|
|
261
|
-
// still ours by exact token match — remove it directly.
|
|
262
|
-
yield* fs.remove(lockPath, { recursive: true, force: true }).pipe(Effect.ignore);
|
|
263
|
-
}
|
|
264
|
-
yield* removeEmptyScratch;
|
|
265
|
-
yield* removeNewEmptyWorkspace;
|
|
266
|
-
}));
|
|
267
|
-
return { _tag: "acquired" };
|
|
268
|
-
}));
|
|
269
|
-
if (attempt._tag === "acquired") {
|
|
270
|
-
return Option.none();
|
|
271
|
-
}
|
|
272
|
-
if (attempt._tag === "failed") {
|
|
273
|
-
yield* removeEmptyScratch;
|
|
274
|
-
yield* removeNewEmptyWorkspace;
|
|
275
|
-
return yield* attempt.error;
|
|
276
|
-
}
|
|
277
|
-
// The lock is held: serialize behind the holder with a visible reason,
|
|
278
|
-
// up to the bound — the loser serializes or times out into a
|
|
279
|
-
// categorized blocked, never an internal crash.
|
|
280
|
-
const holder = yield* readHolder(fs, path, lockPath);
|
|
281
|
-
if (!reportedWaiting && args.onWaiting !== undefined) {
|
|
282
|
-
reportedWaiting = true;
|
|
283
|
-
yield* args.onWaiting(holder);
|
|
284
|
-
}
|
|
285
|
-
if (waitedMillis >= waitBound) {
|
|
286
|
-
yield* removeEmptyScratch;
|
|
287
|
-
yield* removeNewEmptyWorkspace;
|
|
288
|
-
return Option.some({ holder, waitedMillis });
|
|
289
|
-
}
|
|
290
|
-
yield* Effect.sleep(WAIT_INTERVAL);
|
|
291
|
-
waitedMillis += Duration.toMillis(WAIT_INTERVAL);
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
//# sourceMappingURL=transition-lock.js.map
|