@tinycloud/operations 0.3.3-beta.9 → 0.3.3
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 +115 -54
- package/dist/artifacts.cjs.map +1 -1
- package/dist/artifacts.js +119 -54
- package/dist/artifacts.js.map +1 -1
- package/dist/cli-runtime.cjs +117 -56
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.js +121 -56
- package/dist/cli-runtime.js.map +1 -1
- package/dist/index.cjs +117 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +121 -56
- 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
|
@@ -2201,16 +2201,17 @@ var KVService = class extends BaseService {
|
|
|
2201
2201
|
...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
|
|
2202
2202
|
}
|
|
2203
2203
|
);
|
|
2204
|
+
if (response.status === 401 || response.status === 403) {
|
|
2205
|
+
const errorText = await response.text();
|
|
2206
|
+
const message = errorText.trim().length > 0 ? errorText : `Failed to put key "${key}": ${response.status} - ${response.statusText || "authorization failed"}`;
|
|
2207
|
+
const { resource, action: requiredAction } = parseAuthError(errorText);
|
|
2208
|
+
return err(authUnauthorizedError("kv", message, {
|
|
2209
|
+
status: response.status,
|
|
2210
|
+
...requiredAction && { requiredAction },
|
|
2211
|
+
...resource && { resource }
|
|
2212
|
+
}));
|
|
2213
|
+
}
|
|
2204
2214
|
if (!response.ok) {
|
|
2205
|
-
if (response.status === 401) {
|
|
2206
|
-
const errorText2 = await response.text();
|
|
2207
|
-
const { resource, action } = parseAuthError(errorText2);
|
|
2208
|
-
return err(authUnauthorizedError("kv", errorText2, {
|
|
2209
|
-
status: response.status,
|
|
2210
|
-
...action && { requiredAction: action },
|
|
2211
|
-
...resource && { resource }
|
|
2212
|
-
}));
|
|
2213
|
-
}
|
|
2214
2215
|
const errorText = await response.text();
|
|
2215
2216
|
if (response.status === 412) {
|
|
2216
2217
|
return err(serviceError(
|
|
@@ -6957,8 +6958,18 @@ var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
|
|
|
6957
6958
|
var DEFAULT_LOCK_RETRY_MS = 25;
|
|
6958
6959
|
var DEFAULT_STALE_LOCK_MS = 3e4;
|
|
6959
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;
|
|
6960
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";
|
|
6961
6971
|
var invocationStateRoot = new import_node_async_hooks.AsyncLocalStorage();
|
|
6972
|
+
var heldProfileLocks = new import_node_async_hooks.AsyncLocalStorage();
|
|
6962
6973
|
var ProfileLockTimeoutError = class extends Error {
|
|
6963
6974
|
constructor(profile, timeoutMs) {
|
|
6964
6975
|
super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
|
|
@@ -6985,9 +6996,6 @@ function profileStoreMetadataPath(profile, store) {
|
|
|
6985
6996
|
function profileLockPath(profile) {
|
|
6986
6997
|
return (0, import_node_path.join)(profilePath(profile), ".lock");
|
|
6987
6998
|
}
|
|
6988
|
-
function profileLockMetadataPath(profile) {
|
|
6989
|
-
return (0, import_node_path.join)(profileLockPath(profile), "owner.json");
|
|
6990
|
-
}
|
|
6991
6999
|
async function readJson(filePath) {
|
|
6992
7000
|
try {
|
|
6993
7001
|
return JSON.parse(await (0, import_promises.readFile)(filePath, "utf8"));
|
|
@@ -7004,9 +7012,9 @@ async function writeJsonAtomic(filePath, value) {
|
|
|
7004
7012
|
);
|
|
7005
7013
|
const contents = `${JSON.stringify(value, null, 2)}
|
|
7006
7014
|
`;
|
|
7007
|
-
await (0, import_promises.mkdir)(directory, { recursive: true });
|
|
7015
|
+
await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7008
7016
|
try {
|
|
7009
|
-
await (0, import_promises.writeFile)(temporaryPath, contents, "utf8");
|
|
7017
|
+
await (0, import_promises.writeFile)(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
7010
7018
|
await (0, import_promises.rename)(temporaryPath, filePath);
|
|
7011
7019
|
} catch (error) {
|
|
7012
7020
|
await (0, import_promises.rm)(temporaryPath, { force: true }).catch(() => void 0);
|
|
@@ -7036,10 +7044,15 @@ async function readProfileStore(profile, store) {
|
|
|
7036
7044
|
}
|
|
7037
7045
|
async function withProfileLock(profile, action, options = {}) {
|
|
7038
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();
|
|
7039
7050
|
const release = await acquireProfileLock(normalizedProfile, options);
|
|
7051
|
+
const ownership = { lockPath, active: true };
|
|
7040
7052
|
try {
|
|
7041
|
-
return await action
|
|
7053
|
+
return await heldProfileLocks.run([...held, ownership], action);
|
|
7042
7054
|
} finally {
|
|
7055
|
+
ownership.active = false;
|
|
7043
7056
|
await release();
|
|
7044
7057
|
}
|
|
7045
7058
|
}
|
|
@@ -7066,28 +7079,18 @@ async function acquireProfileLock(profile, options) {
|
|
|
7066
7079
|
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
|
|
7067
7080
|
const startedAt = Date.now();
|
|
7068
7081
|
const lockPath = profileLockPath(profile);
|
|
7069
|
-
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
|
+
}
|
|
7070
7086
|
while (true) {
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
const token = (0, import_node_crypto.randomUUID)();
|
|
7074
|
-
try {
|
|
7075
|
-
await writeJsonAtomic(profileLockMetadataPath(profile), {
|
|
7076
|
-
pid: process.pid,
|
|
7077
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7078
|
-
token
|
|
7079
|
-
});
|
|
7080
|
-
} catch (error) {
|
|
7081
|
-
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7082
|
-
throw error;
|
|
7083
|
-
}
|
|
7087
|
+
const token = (0, import_node_crypto.randomUUID)();
|
|
7088
|
+
if (await publishProfileLock(profile, lockPath, token)) {
|
|
7084
7089
|
return async () => {
|
|
7085
|
-
await releaseProfileLock(lockPath, token);
|
|
7090
|
+
await releaseProfileLock(profile, lockPath, token);
|
|
7086
7091
|
};
|
|
7087
|
-
} catch (error) {
|
|
7088
|
-
if (!isLockAlreadyHeld(error)) throw error;
|
|
7089
|
-
await signalTestLockContention(profile);
|
|
7090
7092
|
}
|
|
7093
|
+
await signalTestLockContention(profile);
|
|
7091
7094
|
if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
|
|
7092
7095
|
const elapsedMs = Date.now() - startedAt;
|
|
7093
7096
|
if (elapsedMs >= timeoutMs) {
|
|
@@ -7096,21 +7099,35 @@ async function acquireProfileLock(profile, options) {
|
|
|
7096
7099
|
await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
|
|
7097
7100
|
}
|
|
7098
7101
|
}
|
|
7099
|
-
async function
|
|
7100
|
-
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
7101
|
-
const claimPath = (0, import_node_path.join)(lockPath, `.release-${token}.json`);
|
|
7102
|
+
async function publishProfileLock(profile, lockPath, token) {
|
|
7102
7103
|
try {
|
|
7103
|
-
await (0, import_promises.
|
|
7104
|
+
await (0, import_promises.mkdir)(lockPath, { mode: PRIVATE_DIR_MODE });
|
|
7104
7105
|
} catch (error) {
|
|
7105
|
-
if (isErrno(error, "
|
|
7106
|
+
if (isErrno(error, "EEXIST")) return false;
|
|
7106
7107
|
throw error;
|
|
7107
7108
|
}
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
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);
|
|
7112
7123
|
}
|
|
7113
|
-
|
|
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);
|
|
7114
7131
|
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7115
7132
|
}
|
|
7116
7133
|
async function signalTestLockContention(profile) {
|
|
@@ -7127,32 +7144,79 @@ async function signalTestLockContention(profile) {
|
|
|
7127
7144
|
await (0, import_promises.writeFile)(signalPath, `${profile}
|
|
7128
7145
|
`, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
7129
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
|
+
}
|
|
7130
7184
|
async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
7131
7185
|
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
7132
7186
|
const owner = await readJson(ownerPath).catch(() => null);
|
|
7187
|
+
if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
|
|
7133
7188
|
if (!isStaleOwner(owner, staleAfterMs)) return false;
|
|
7134
7189
|
const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
|
|
7135
|
-
await
|
|
7190
|
+
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
7136
7191
|
const claimPath = (0, import_node_path.join)(lockPath, `.stale-${(0, import_node_crypto.randomUUID)()}.json`);
|
|
7137
7192
|
try {
|
|
7138
|
-
await (0, import_promises.
|
|
7139
|
-
} catch
|
|
7140
|
-
if (isErrno(error, "ENOENT")) return false;
|
|
7193
|
+
await (0, import_promises.link)(ownerPath, claimPath);
|
|
7194
|
+
} catch {
|
|
7141
7195
|
return false;
|
|
7142
7196
|
}
|
|
7197
|
+
const claimedAt = performance.now();
|
|
7198
|
+
await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
|
|
7143
7199
|
const claimed = await readJson(claimPath).catch(() => null);
|
|
7144
7200
|
const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
|
|
7145
|
-
|
|
7146
|
-
|
|
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);
|
|
7147
7205
|
return false;
|
|
7148
7206
|
}
|
|
7207
|
+
if (sameInstance) {
|
|
7208
|
+
await (0, import_promises.rm)(ownerPath, { force: true });
|
|
7209
|
+
await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
|
|
7210
|
+
}
|
|
7149
7211
|
await (0, import_promises.rm)(claimPath, { force: true });
|
|
7150
7212
|
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7213
|
+
if (!sameInstance) return false;
|
|
7214
|
+
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7151
7215
|
return true;
|
|
7152
7216
|
}
|
|
7153
|
-
async function
|
|
7217
|
+
async function waitForTestBarrier(environmentName, profile) {
|
|
7154
7218
|
if (process.env.NODE_ENV !== "test") return;
|
|
7155
|
-
const barrierDirectory = process.env[
|
|
7219
|
+
const barrierDirectory = process.env[environmentName];
|
|
7156
7220
|
if (!barrierDirectory) return;
|
|
7157
7221
|
await (0, import_promises.mkdir)(barrierDirectory, { recursive: true });
|
|
7158
7222
|
const readyPath = (0, import_node_path.join)(barrierDirectory, `ready-${process.pid}-${profile}`);
|
|
@@ -7176,9 +7240,6 @@ function isStaleOwner(owner, staleAfterMs) {
|
|
|
7176
7240
|
}
|
|
7177
7241
|
return !isProcessAlive(owner.pid);
|
|
7178
7242
|
}
|
|
7179
|
-
function isLockAlreadyHeld(error) {
|
|
7180
|
-
return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
|
|
7181
|
-
}
|
|
7182
7243
|
function isProcessAlive(pid) {
|
|
7183
7244
|
try {
|
|
7184
7245
|
process.kill(pid, 0);
|