@tinycloud/operations 0.3.3-beta.13 → 0.3.3-beta.15
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/coverage.json +1 -0
- package/dist/artifacts.cjs +105 -45
- package/dist/artifacts.cjs.map +1 -1
- package/dist/artifacts.js +109 -45
- package/dist/artifacts.js.map +1 -1
- package/dist/cli-runtime.cjs +107 -47
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.js +111 -47
- package/dist/cli-runtime.js.map +1 -1
- package/dist/index.cjs +107 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +111 -47
- package/dist/index.js.map +1 -1
- package/dist/profile.cjs +2 -1
- package/dist/profile.cjs.map +1 -1
- package/dist/profile.js +6 -1
- package/dist/profile.js.map +1 -1
- package/dist/state.cjs +105 -42
- package/dist/state.cjs.map +1 -1
- package/dist/state.d.cts +8 -0
- package/dist/state.d.ts +8 -0
- package/dist/state.js +109 -42
- package/dist/state.js.map +1 -1
- package/package.json +3 -3
package/dist/artifacts.js
CHANGED
|
@@ -6928,11 +6928,15 @@ function canonicalizeCaveats(caveats) {
|
|
|
6928
6928
|
import { randomUUID } from "crypto";
|
|
6929
6929
|
import { AsyncLocalStorage } from "async_hooks";
|
|
6930
6930
|
import {
|
|
6931
|
+
chmod,
|
|
6932
|
+
link,
|
|
6931
6933
|
mkdir,
|
|
6932
6934
|
readFile,
|
|
6935
|
+
readdir,
|
|
6933
6936
|
rename,
|
|
6934
6937
|
rmdir,
|
|
6935
6938
|
rm,
|
|
6939
|
+
stat,
|
|
6936
6940
|
writeFile
|
|
6937
6941
|
} from "fs/promises";
|
|
6938
6942
|
import { homedir } from "os";
|
|
@@ -6941,8 +6945,18 @@ var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
|
|
|
6941
6945
|
var DEFAULT_LOCK_RETRY_MS = 25;
|
|
6942
6946
|
var DEFAULT_STALE_LOCK_MS = 3e4;
|
|
6943
6947
|
var TEST_LOCK_CONTENTION_SIGNAL_PATH = "TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH";
|
|
6948
|
+
var PRIVATE_FILE_MODE = 384;
|
|
6949
|
+
var PRIVATE_DIR_MODE = 448;
|
|
6944
6950
|
var TEST_LOCK_RECOVERY_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR";
|
|
6951
|
+
var TEST_LOCK_OWNERLESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR";
|
|
6952
|
+
var TEST_LOCK_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR";
|
|
6953
|
+
var TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
|
|
6954
|
+
var TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
|
|
6955
|
+
var TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
|
|
6956
|
+
var TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
|
|
6957
|
+
var TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
|
|
6945
6958
|
var invocationStateRoot = new AsyncLocalStorage();
|
|
6959
|
+
var heldProfileLocks = new AsyncLocalStorage();
|
|
6946
6960
|
var ProfileLockTimeoutError = class extends Error {
|
|
6947
6961
|
constructor(profile, timeoutMs) {
|
|
6948
6962
|
super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
|
|
@@ -6969,9 +6983,6 @@ function profileStoreMetadataPath(profile, store) {
|
|
|
6969
6983
|
function profileLockPath(profile) {
|
|
6970
6984
|
return join(profilePath(profile), ".lock");
|
|
6971
6985
|
}
|
|
6972
|
-
function profileLockMetadataPath(profile) {
|
|
6973
|
-
return join(profileLockPath(profile), "owner.json");
|
|
6974
|
-
}
|
|
6975
6986
|
async function readJson(filePath) {
|
|
6976
6987
|
try {
|
|
6977
6988
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
@@ -6988,9 +6999,9 @@ async function writeJsonAtomic(filePath, value) {
|
|
|
6988
6999
|
);
|
|
6989
7000
|
const contents = `${JSON.stringify(value, null, 2)}
|
|
6990
7001
|
`;
|
|
6991
|
-
await mkdir(directory, { recursive: true });
|
|
7002
|
+
await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
6992
7003
|
try {
|
|
6993
|
-
await writeFile(temporaryPath, contents, "utf8");
|
|
7004
|
+
await writeFile(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
6994
7005
|
await rename(temporaryPath, filePath);
|
|
6995
7006
|
} catch (error) {
|
|
6996
7007
|
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
@@ -7020,10 +7031,15 @@ async function readProfileStore(profile, store) {
|
|
|
7020
7031
|
}
|
|
7021
7032
|
async function withProfileLock(profile, action, options = {}) {
|
|
7022
7033
|
const normalizedProfile = validateProfileName(profile);
|
|
7034
|
+
const lockPath = profileLockPath(normalizedProfile);
|
|
7035
|
+
const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
|
|
7036
|
+
if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
|
|
7023
7037
|
const release = await acquireProfileLock(normalizedProfile, options);
|
|
7038
|
+
const ownership = { lockPath, active: true };
|
|
7024
7039
|
try {
|
|
7025
|
-
return await action
|
|
7040
|
+
return await heldProfileLocks.run([...held, ownership], action);
|
|
7026
7041
|
} finally {
|
|
7042
|
+
ownership.active = false;
|
|
7027
7043
|
await release();
|
|
7028
7044
|
}
|
|
7029
7045
|
}
|
|
@@ -7050,28 +7066,18 @@ async function acquireProfileLock(profile, options) {
|
|
|
7050
7066
|
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
|
|
7051
7067
|
const startedAt = Date.now();
|
|
7052
7068
|
const lockPath = profileLockPath(profile);
|
|
7053
|
-
await mkdir(profilePath(profile), { recursive: true });
|
|
7069
|
+
await mkdir(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7070
|
+
for (const directory of [tinycloudHomePath(), profilesPath(), profilePath(profile)]) {
|
|
7071
|
+
await chmod(directory, PRIVATE_DIR_MODE);
|
|
7072
|
+
}
|
|
7054
7073
|
while (true) {
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
const token = randomUUID();
|
|
7058
|
-
try {
|
|
7059
|
-
await writeJsonAtomic(profileLockMetadataPath(profile), {
|
|
7060
|
-
pid: process.pid,
|
|
7061
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7062
|
-
token
|
|
7063
|
-
});
|
|
7064
|
-
} catch (error) {
|
|
7065
|
-
await rmdir(lockPath).catch(() => void 0);
|
|
7066
|
-
throw error;
|
|
7067
|
-
}
|
|
7074
|
+
const token = randomUUID();
|
|
7075
|
+
if (await publishProfileLock(profile, lockPath, token)) {
|
|
7068
7076
|
return async () => {
|
|
7069
|
-
await releaseProfileLock(lockPath, token);
|
|
7077
|
+
await releaseProfileLock(profile, lockPath, token);
|
|
7070
7078
|
};
|
|
7071
|
-
} catch (error) {
|
|
7072
|
-
if (!isLockAlreadyHeld(error)) throw error;
|
|
7073
|
-
await signalTestLockContention(profile);
|
|
7074
7079
|
}
|
|
7080
|
+
await signalTestLockContention(profile);
|
|
7075
7081
|
if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
|
|
7076
7082
|
const elapsedMs = Date.now() - startedAt;
|
|
7077
7083
|
if (elapsedMs >= timeoutMs) {
|
|
@@ -7080,21 +7086,35 @@ async function acquireProfileLock(profile, options) {
|
|
|
7080
7086
|
await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
|
|
7081
7087
|
}
|
|
7082
7088
|
}
|
|
7083
|
-
async function
|
|
7084
|
-
const ownerPath = join(lockPath, "owner.json");
|
|
7085
|
-
const claimPath = join(lockPath, `.release-${token}.json`);
|
|
7089
|
+
async function publishProfileLock(profile, lockPath, token) {
|
|
7086
7090
|
try {
|
|
7087
|
-
await
|
|
7091
|
+
await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
|
|
7088
7092
|
} catch (error) {
|
|
7089
|
-
if (isErrno(error, "
|
|
7093
|
+
if (isErrno(error, "EEXIST")) return false;
|
|
7090
7094
|
throw error;
|
|
7091
7095
|
}
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
+
await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
|
|
7097
|
+
const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
|
|
7098
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token };
|
|
7099
|
+
try {
|
|
7100
|
+
await writeFile(staged, `${JSON.stringify(owner, null, 2)}
|
|
7101
|
+
`, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
7102
|
+
await link(staged, join(lockPath, "owner.json"));
|
|
7103
|
+
return true;
|
|
7104
|
+
} catch (error) {
|
|
7105
|
+
if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
|
|
7106
|
+
await rmdir(lockPath).catch(() => void 0);
|
|
7107
|
+
throw error;
|
|
7108
|
+
} finally {
|
|
7109
|
+
await rm(staged, { force: true }).catch(() => void 0);
|
|
7096
7110
|
}
|
|
7097
|
-
|
|
7111
|
+
}
|
|
7112
|
+
async function releaseProfileLock(profile, lockPath, token) {
|
|
7113
|
+
const ownerPath = join(lockPath, "owner.json");
|
|
7114
|
+
const owner = await readJson(ownerPath).catch(() => null);
|
|
7115
|
+
if (owner?.token !== token) return;
|
|
7116
|
+
await rm(ownerPath, { force: true });
|
|
7117
|
+
await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);
|
|
7098
7118
|
await rmdir(lockPath).catch(() => void 0);
|
|
7099
7119
|
}
|
|
7100
7120
|
async function signalTestLockContention(profile) {
|
|
@@ -7111,32 +7131,79 @@ async function signalTestLockContention(profile) {
|
|
|
7111
7131
|
await writeFile(signalPath, `${profile}
|
|
7112
7132
|
`, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
7113
7133
|
}
|
|
7134
|
+
async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
7135
|
+
try {
|
|
7136
|
+
const { mtimeMs } = await stat(lockPath);
|
|
7137
|
+
if (Date.now() - mtimeMs < staleAfterMs) return false;
|
|
7138
|
+
} catch {
|
|
7139
|
+
return false;
|
|
7140
|
+
}
|
|
7141
|
+
const checkedAt = performance.now();
|
|
7142
|
+
await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);
|
|
7143
|
+
try {
|
|
7144
|
+
const entries = await readdir(lockPath);
|
|
7145
|
+
if (entries.includes("owner.json")) return false;
|
|
7146
|
+
if (recoveryFenced(checkedAt, staleAfterMs)) return false;
|
|
7147
|
+
for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
|
|
7148
|
+
await rm(join(lockPath, name), { force: true });
|
|
7149
|
+
}
|
|
7150
|
+
await rmdir(lockPath);
|
|
7151
|
+
} catch {
|
|
7152
|
+
return false;
|
|
7153
|
+
}
|
|
7154
|
+
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7155
|
+
return true;
|
|
7156
|
+
}
|
|
7157
|
+
function recoveryFenced(since, staleAfterMs) {
|
|
7158
|
+
return performance.now() - since >= staleAfterMs / 2;
|
|
7159
|
+
}
|
|
7160
|
+
var ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
|
|
7161
|
+
var STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
|
|
7162
|
+
async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
|
|
7163
|
+
const directory = dirname(lockPath);
|
|
7164
|
+
const names = await readdir(directory).catch(() => []);
|
|
7165
|
+
for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {
|
|
7166
|
+
const path = join(directory, name);
|
|
7167
|
+
const aged = await stat(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);
|
|
7168
|
+
if (aged) await rm(path, { force: true }).catch(() => void 0);
|
|
7169
|
+
}
|
|
7170
|
+
}
|
|
7114
7171
|
async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
7115
7172
|
const ownerPath = join(lockPath, "owner.json");
|
|
7116
7173
|
const owner = await readJson(ownerPath).catch(() => null);
|
|
7174
|
+
if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
|
|
7117
7175
|
if (!isStaleOwner(owner, staleAfterMs)) return false;
|
|
7118
7176
|
const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
|
|
7119
|
-
await
|
|
7177
|
+
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
7120
7178
|
const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);
|
|
7121
7179
|
try {
|
|
7122
|
-
await
|
|
7123
|
-
} catch
|
|
7124
|
-
if (isErrno(error, "ENOENT")) return false;
|
|
7180
|
+
await link(ownerPath, claimPath);
|
|
7181
|
+
} catch {
|
|
7125
7182
|
return false;
|
|
7126
7183
|
}
|
|
7184
|
+
const claimedAt = performance.now();
|
|
7185
|
+
await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
|
|
7127
7186
|
const claimed = await readJson(claimPath).catch(() => null);
|
|
7128
7187
|
const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
|
|
7129
|
-
|
|
7130
|
-
|
|
7188
|
+
await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
|
|
7189
|
+
if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {
|
|
7190
|
+
await rm(claimPath, { force: true });
|
|
7191
|
+
await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);
|
|
7131
7192
|
return false;
|
|
7132
7193
|
}
|
|
7194
|
+
if (sameInstance) {
|
|
7195
|
+
await rm(ownerPath, { force: true });
|
|
7196
|
+
await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
|
|
7197
|
+
}
|
|
7133
7198
|
await rm(claimPath, { force: true });
|
|
7134
7199
|
await rmdir(lockPath).catch(() => void 0);
|
|
7200
|
+
if (!sameInstance) return false;
|
|
7201
|
+
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7135
7202
|
return true;
|
|
7136
7203
|
}
|
|
7137
|
-
async function
|
|
7204
|
+
async function waitForTestBarrier(environmentName, profile) {
|
|
7138
7205
|
if (process.env.NODE_ENV !== "test") return;
|
|
7139
|
-
const barrierDirectory = process.env[
|
|
7206
|
+
const barrierDirectory = process.env[environmentName];
|
|
7140
7207
|
if (!barrierDirectory) return;
|
|
7141
7208
|
await mkdir(barrierDirectory, { recursive: true });
|
|
7142
7209
|
const readyPath = join(barrierDirectory, `ready-${process.pid}-${profile}`);
|
|
@@ -7160,9 +7227,6 @@ function isStaleOwner(owner, staleAfterMs) {
|
|
|
7160
7227
|
}
|
|
7161
7228
|
return !isProcessAlive(owner.pid);
|
|
7162
7229
|
}
|
|
7163
|
-
function isLockAlreadyHeld(error) {
|
|
7164
|
-
return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
|
|
7165
|
-
}
|
|
7166
7230
|
function isProcessAlive(pid) {
|
|
7167
7231
|
try {
|
|
7168
7232
|
process.kill(pid, 0);
|