@tinycloud/operations 0.3.3-beta.12 → 0.3.3-beta.14
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/coverage.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"source": "packages/cli/src/command-registry.ts",
|
|
4
4
|
"commands": [
|
|
5
5
|
{ "command": "init", "status": "legacy" },
|
|
6
|
+
{ "command": "context", "status": "excluded", "reason": "Read-only local CLI context diagnostics; performs no storage operation." },
|
|
6
7
|
{ "command": "enable", "status": "legacy" },
|
|
7
8
|
{ "command": "enable share", "status": "legacy" },
|
|
8
9
|
{ "command": "auth", "status": "legacy" },
|
package/dist/artifacts.cjs
CHANGED
|
@@ -6958,8 +6958,18 @@ var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
|
|
|
6958
6958
|
var DEFAULT_LOCK_RETRY_MS = 25;
|
|
6959
6959
|
var DEFAULT_STALE_LOCK_MS = 3e4;
|
|
6960
6960
|
var TEST_LOCK_CONTENTION_SIGNAL_PATH = "TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH";
|
|
6961
|
+
var PRIVATE_FILE_MODE = 384;
|
|
6962
|
+
var PRIVATE_DIR_MODE = 448;
|
|
6961
6963
|
var TEST_LOCK_RECOVERY_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR";
|
|
6964
|
+
var TEST_LOCK_OWNERLESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR";
|
|
6965
|
+
var TEST_LOCK_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR";
|
|
6966
|
+
var TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
|
|
6967
|
+
var TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
|
|
6968
|
+
var TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
|
|
6969
|
+
var TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
|
|
6970
|
+
var TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
|
|
6962
6971
|
var invocationStateRoot = new import_node_async_hooks.AsyncLocalStorage();
|
|
6972
|
+
var heldProfileLocks = new import_node_async_hooks.AsyncLocalStorage();
|
|
6963
6973
|
var ProfileLockTimeoutError = class extends Error {
|
|
6964
6974
|
constructor(profile, timeoutMs) {
|
|
6965
6975
|
super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
|
|
@@ -6986,9 +6996,6 @@ function profileStoreMetadataPath(profile, store) {
|
|
|
6986
6996
|
function profileLockPath(profile) {
|
|
6987
6997
|
return (0, import_node_path.join)(profilePath(profile), ".lock");
|
|
6988
6998
|
}
|
|
6989
|
-
function profileLockMetadataPath(profile) {
|
|
6990
|
-
return (0, import_node_path.join)(profileLockPath(profile), "owner.json");
|
|
6991
|
-
}
|
|
6992
6999
|
async function readJson(filePath) {
|
|
6993
7000
|
try {
|
|
6994
7001
|
return JSON.parse(await (0, import_promises.readFile)(filePath, "utf8"));
|
|
@@ -7005,9 +7012,9 @@ async function writeJsonAtomic(filePath, value) {
|
|
|
7005
7012
|
);
|
|
7006
7013
|
const contents = `${JSON.stringify(value, null, 2)}
|
|
7007
7014
|
`;
|
|
7008
|
-
await (0, import_promises.mkdir)(directory, { recursive: true });
|
|
7015
|
+
await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7009
7016
|
try {
|
|
7010
|
-
await (0, import_promises.writeFile)(temporaryPath, contents, "utf8");
|
|
7017
|
+
await (0, import_promises.writeFile)(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
7011
7018
|
await (0, import_promises.rename)(temporaryPath, filePath);
|
|
7012
7019
|
} catch (error) {
|
|
7013
7020
|
await (0, import_promises.rm)(temporaryPath, { force: true }).catch(() => void 0);
|
|
@@ -7037,10 +7044,15 @@ async function readProfileStore(profile, store) {
|
|
|
7037
7044
|
}
|
|
7038
7045
|
async function withProfileLock(profile, action, options = {}) {
|
|
7039
7046
|
const normalizedProfile = validateProfileName(profile);
|
|
7047
|
+
const lockPath = profileLockPath(normalizedProfile);
|
|
7048
|
+
const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
|
|
7049
|
+
if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
|
|
7040
7050
|
const release = await acquireProfileLock(normalizedProfile, options);
|
|
7051
|
+
const ownership = { lockPath, active: true };
|
|
7041
7052
|
try {
|
|
7042
|
-
return await action
|
|
7053
|
+
return await heldProfileLocks.run([...held, ownership], action);
|
|
7043
7054
|
} finally {
|
|
7055
|
+
ownership.active = false;
|
|
7044
7056
|
await release();
|
|
7045
7057
|
}
|
|
7046
7058
|
}
|
|
@@ -7067,28 +7079,18 @@ async function acquireProfileLock(profile, options) {
|
|
|
7067
7079
|
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
|
|
7068
7080
|
const startedAt = Date.now();
|
|
7069
7081
|
const lockPath = profileLockPath(profile);
|
|
7070
|
-
await (0, import_promises.mkdir)(profilePath(profile), { recursive: true });
|
|
7082
|
+
await (0, import_promises.mkdir)(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7083
|
+
for (const directory of [tinycloudHomePath(), profilesPath(), profilePath(profile)]) {
|
|
7084
|
+
await (0, import_promises.chmod)(directory, PRIVATE_DIR_MODE);
|
|
7085
|
+
}
|
|
7071
7086
|
while (true) {
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
const token = (0, import_node_crypto.randomUUID)();
|
|
7075
|
-
try {
|
|
7076
|
-
await writeJsonAtomic(profileLockMetadataPath(profile), {
|
|
7077
|
-
pid: process.pid,
|
|
7078
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7079
|
-
token
|
|
7080
|
-
});
|
|
7081
|
-
} catch (error) {
|
|
7082
|
-
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7083
|
-
throw error;
|
|
7084
|
-
}
|
|
7087
|
+
const token = (0, import_node_crypto.randomUUID)();
|
|
7088
|
+
if (await publishProfileLock(profile, lockPath, token)) {
|
|
7085
7089
|
return async () => {
|
|
7086
|
-
await releaseProfileLock(lockPath, token);
|
|
7090
|
+
await releaseProfileLock(profile, lockPath, token);
|
|
7087
7091
|
};
|
|
7088
|
-
} catch (error) {
|
|
7089
|
-
if (!isLockAlreadyHeld(error)) throw error;
|
|
7090
|
-
await signalTestLockContention(profile);
|
|
7091
7092
|
}
|
|
7093
|
+
await signalTestLockContention(profile);
|
|
7092
7094
|
if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
|
|
7093
7095
|
const elapsedMs = Date.now() - startedAt;
|
|
7094
7096
|
if (elapsedMs >= timeoutMs) {
|
|
@@ -7097,21 +7099,35 @@ async function acquireProfileLock(profile, options) {
|
|
|
7097
7099
|
await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
|
|
7098
7100
|
}
|
|
7099
7101
|
}
|
|
7100
|
-
async function
|
|
7101
|
-
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
7102
|
-
const claimPath = (0, import_node_path.join)(lockPath, `.release-${token}.json`);
|
|
7102
|
+
async function publishProfileLock(profile, lockPath, token) {
|
|
7103
7103
|
try {
|
|
7104
|
-
await (0, import_promises.
|
|
7104
|
+
await (0, import_promises.mkdir)(lockPath, { mode: PRIVATE_DIR_MODE });
|
|
7105
7105
|
} catch (error) {
|
|
7106
|
-
if (isErrno(error, "
|
|
7106
|
+
if (isErrno(error, "EEXIST")) return false;
|
|
7107
7107
|
throw error;
|
|
7108
7108
|
}
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7109
|
+
await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
|
|
7110
|
+
const staged = (0, import_node_path.join)((0, import_node_path.dirname)(lockPath), `.lock-owner-${token}.tmp`);
|
|
7111
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token };
|
|
7112
|
+
try {
|
|
7113
|
+
await (0, import_promises.writeFile)(staged, `${JSON.stringify(owner, null, 2)}
|
|
7114
|
+
`, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
7115
|
+
await (0, import_promises.link)(staged, (0, import_node_path.join)(lockPath, "owner.json"));
|
|
7116
|
+
return true;
|
|
7117
|
+
} catch (error) {
|
|
7118
|
+
if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
|
|
7119
|
+
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7120
|
+
throw error;
|
|
7121
|
+
} finally {
|
|
7122
|
+
await (0, import_promises.rm)(staged, { force: true }).catch(() => void 0);
|
|
7113
7123
|
}
|
|
7114
|
-
|
|
7124
|
+
}
|
|
7125
|
+
async function releaseProfileLock(profile, lockPath, token) {
|
|
7126
|
+
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
7127
|
+
const owner = await readJson(ownerPath).catch(() => null);
|
|
7128
|
+
if (owner?.token !== token) return;
|
|
7129
|
+
await (0, import_promises.rm)(ownerPath, { force: true });
|
|
7130
|
+
await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);
|
|
7115
7131
|
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7116
7132
|
}
|
|
7117
7133
|
async function signalTestLockContention(profile) {
|
|
@@ -7128,32 +7144,79 @@ async function signalTestLockContention(profile) {
|
|
|
7128
7144
|
await (0, import_promises.writeFile)(signalPath, `${profile}
|
|
7129
7145
|
`, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
7130
7146
|
}
|
|
7147
|
+
async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
7148
|
+
try {
|
|
7149
|
+
const { mtimeMs } = await (0, import_promises.stat)(lockPath);
|
|
7150
|
+
if (Date.now() - mtimeMs < staleAfterMs) return false;
|
|
7151
|
+
} catch {
|
|
7152
|
+
return false;
|
|
7153
|
+
}
|
|
7154
|
+
const checkedAt = performance.now();
|
|
7155
|
+
await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);
|
|
7156
|
+
try {
|
|
7157
|
+
const entries = await (0, import_promises.readdir)(lockPath);
|
|
7158
|
+
if (entries.includes("owner.json")) return false;
|
|
7159
|
+
if (recoveryFenced(checkedAt, staleAfterMs)) return false;
|
|
7160
|
+
for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
|
|
7161
|
+
await (0, import_promises.rm)((0, import_node_path.join)(lockPath, name), { force: true });
|
|
7162
|
+
}
|
|
7163
|
+
await (0, import_promises.rmdir)(lockPath);
|
|
7164
|
+
} catch {
|
|
7165
|
+
return false;
|
|
7166
|
+
}
|
|
7167
|
+
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7168
|
+
return true;
|
|
7169
|
+
}
|
|
7170
|
+
function recoveryFenced(since, staleAfterMs) {
|
|
7171
|
+
return performance.now() - since >= staleAfterMs / 2;
|
|
7172
|
+
}
|
|
7173
|
+
var ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
|
|
7174
|
+
var STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
|
|
7175
|
+
async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
|
|
7176
|
+
const directory = (0, import_node_path.dirname)(lockPath);
|
|
7177
|
+
const names = await (0, import_promises.readdir)(directory).catch(() => []);
|
|
7178
|
+
for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {
|
|
7179
|
+
const path = (0, import_node_path.join)(directory, name);
|
|
7180
|
+
const aged = await (0, import_promises.stat)(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);
|
|
7181
|
+
if (aged) await (0, import_promises.rm)(path, { force: true }).catch(() => void 0);
|
|
7182
|
+
}
|
|
7183
|
+
}
|
|
7131
7184
|
async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
7132
7185
|
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
7133
7186
|
const owner = await readJson(ownerPath).catch(() => null);
|
|
7187
|
+
if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
|
|
7134
7188
|
if (!isStaleOwner(owner, staleAfterMs)) return false;
|
|
7135
7189
|
const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
|
|
7136
|
-
await
|
|
7190
|
+
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
7137
7191
|
const claimPath = (0, import_node_path.join)(lockPath, `.stale-${(0, import_node_crypto.randomUUID)()}.json`);
|
|
7138
7192
|
try {
|
|
7139
|
-
await (0, import_promises.
|
|
7140
|
-
} catch
|
|
7141
|
-
if (isErrno(error, "ENOENT")) return false;
|
|
7193
|
+
await (0, import_promises.link)(ownerPath, claimPath);
|
|
7194
|
+
} catch {
|
|
7142
7195
|
return false;
|
|
7143
7196
|
}
|
|
7197
|
+
const claimedAt = performance.now();
|
|
7198
|
+
await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
|
|
7144
7199
|
const claimed = await readJson(claimPath).catch(() => null);
|
|
7145
7200
|
const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
|
|
7146
|
-
|
|
7147
|
-
|
|
7201
|
+
await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
|
|
7202
|
+
if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {
|
|
7203
|
+
await (0, import_promises.rm)(claimPath, { force: true });
|
|
7204
|
+
await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);
|
|
7148
7205
|
return false;
|
|
7149
7206
|
}
|
|
7207
|
+
if (sameInstance) {
|
|
7208
|
+
await (0, import_promises.rm)(ownerPath, { force: true });
|
|
7209
|
+
await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
|
|
7210
|
+
}
|
|
7150
7211
|
await (0, import_promises.rm)(claimPath, { force: true });
|
|
7151
7212
|
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7213
|
+
if (!sameInstance) return false;
|
|
7214
|
+
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7152
7215
|
return true;
|
|
7153
7216
|
}
|
|
7154
|
-
async function
|
|
7217
|
+
async function waitForTestBarrier(environmentName, profile) {
|
|
7155
7218
|
if (process.env.NODE_ENV !== "test") return;
|
|
7156
|
-
const barrierDirectory = process.env[
|
|
7219
|
+
const barrierDirectory = process.env[environmentName];
|
|
7157
7220
|
if (!barrierDirectory) return;
|
|
7158
7221
|
await (0, import_promises.mkdir)(barrierDirectory, { recursive: true });
|
|
7159
7222
|
const readyPath = (0, import_node_path.join)(barrierDirectory, `ready-${process.pid}-${profile}`);
|
|
@@ -7177,9 +7240,6 @@ function isStaleOwner(owner, staleAfterMs) {
|
|
|
7177
7240
|
}
|
|
7178
7241
|
return !isProcessAlive(owner.pid);
|
|
7179
7242
|
}
|
|
7180
|
-
function isLockAlreadyHeld(error) {
|
|
7181
|
-
return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
|
|
7182
|
-
}
|
|
7183
7243
|
function isProcessAlive(pid) {
|
|
7184
7244
|
try {
|
|
7185
7245
|
process.kill(pid, 0);
|