@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/dist/artifacts.js
CHANGED
|
@@ -2177,16 +2177,17 @@ var KVService = class extends BaseService {
|
|
|
2177
2177
|
...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
|
|
2178
2178
|
}
|
|
2179
2179
|
);
|
|
2180
|
+
if (response.status === 401 || response.status === 403) {
|
|
2181
|
+
const errorText = await response.text();
|
|
2182
|
+
const message = errorText.trim().length > 0 ? errorText : `Failed to put key "${key}": ${response.status} - ${response.statusText || "authorization failed"}`;
|
|
2183
|
+
const { resource, action: requiredAction } = parseAuthError(errorText);
|
|
2184
|
+
return err(authUnauthorizedError("kv", message, {
|
|
2185
|
+
status: response.status,
|
|
2186
|
+
...requiredAction && { requiredAction },
|
|
2187
|
+
...resource && { resource }
|
|
2188
|
+
}));
|
|
2189
|
+
}
|
|
2180
2190
|
if (!response.ok) {
|
|
2181
|
-
if (response.status === 401) {
|
|
2182
|
-
const errorText2 = await response.text();
|
|
2183
|
-
const { resource, action } = parseAuthError(errorText2);
|
|
2184
|
-
return err(authUnauthorizedError("kv", errorText2, {
|
|
2185
|
-
status: response.status,
|
|
2186
|
-
...action && { requiredAction: action },
|
|
2187
|
-
...resource && { resource }
|
|
2188
|
-
}));
|
|
2189
|
-
}
|
|
2190
2191
|
const errorText = await response.text();
|
|
2191
2192
|
if (response.status === 412) {
|
|
2192
2193
|
return err(serviceError(
|
|
@@ -6927,11 +6928,15 @@ function canonicalizeCaveats(caveats) {
|
|
|
6927
6928
|
import { randomUUID } from "crypto";
|
|
6928
6929
|
import { AsyncLocalStorage } from "async_hooks";
|
|
6929
6930
|
import {
|
|
6931
|
+
chmod,
|
|
6932
|
+
link,
|
|
6930
6933
|
mkdir,
|
|
6931
6934
|
readFile,
|
|
6935
|
+
readdir,
|
|
6932
6936
|
rename,
|
|
6933
6937
|
rmdir,
|
|
6934
6938
|
rm,
|
|
6939
|
+
stat,
|
|
6935
6940
|
writeFile
|
|
6936
6941
|
} from "fs/promises";
|
|
6937
6942
|
import { homedir } from "os";
|
|
@@ -6940,8 +6945,18 @@ var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
|
|
|
6940
6945
|
var DEFAULT_LOCK_RETRY_MS = 25;
|
|
6941
6946
|
var DEFAULT_STALE_LOCK_MS = 3e4;
|
|
6942
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;
|
|
6943
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";
|
|
6944
6958
|
var invocationStateRoot = new AsyncLocalStorage();
|
|
6959
|
+
var heldProfileLocks = new AsyncLocalStorage();
|
|
6945
6960
|
var ProfileLockTimeoutError = class extends Error {
|
|
6946
6961
|
constructor(profile, timeoutMs) {
|
|
6947
6962
|
super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
|
|
@@ -6968,9 +6983,6 @@ function profileStoreMetadataPath(profile, store) {
|
|
|
6968
6983
|
function profileLockPath(profile) {
|
|
6969
6984
|
return join(profilePath(profile), ".lock");
|
|
6970
6985
|
}
|
|
6971
|
-
function profileLockMetadataPath(profile) {
|
|
6972
|
-
return join(profileLockPath(profile), "owner.json");
|
|
6973
|
-
}
|
|
6974
6986
|
async function readJson(filePath) {
|
|
6975
6987
|
try {
|
|
6976
6988
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
@@ -6987,9 +6999,9 @@ async function writeJsonAtomic(filePath, value) {
|
|
|
6987
6999
|
);
|
|
6988
7000
|
const contents = `${JSON.stringify(value, null, 2)}
|
|
6989
7001
|
`;
|
|
6990
|
-
await mkdir(directory, { recursive: true });
|
|
7002
|
+
await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
6991
7003
|
try {
|
|
6992
|
-
await writeFile(temporaryPath, contents, "utf8");
|
|
7004
|
+
await writeFile(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
6993
7005
|
await rename(temporaryPath, filePath);
|
|
6994
7006
|
} catch (error) {
|
|
6995
7007
|
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
@@ -7019,10 +7031,15 @@ async function readProfileStore(profile, store) {
|
|
|
7019
7031
|
}
|
|
7020
7032
|
async function withProfileLock(profile, action, options = {}) {
|
|
7021
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();
|
|
7022
7037
|
const release = await acquireProfileLock(normalizedProfile, options);
|
|
7038
|
+
const ownership = { lockPath, active: true };
|
|
7023
7039
|
try {
|
|
7024
|
-
return await action
|
|
7040
|
+
return await heldProfileLocks.run([...held, ownership], action);
|
|
7025
7041
|
} finally {
|
|
7042
|
+
ownership.active = false;
|
|
7026
7043
|
await release();
|
|
7027
7044
|
}
|
|
7028
7045
|
}
|
|
@@ -7049,28 +7066,18 @@ async function acquireProfileLock(profile, options) {
|
|
|
7049
7066
|
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
|
|
7050
7067
|
const startedAt = Date.now();
|
|
7051
7068
|
const lockPath = profileLockPath(profile);
|
|
7052
|
-
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
|
+
}
|
|
7053
7073
|
while (true) {
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
const token = randomUUID();
|
|
7057
|
-
try {
|
|
7058
|
-
await writeJsonAtomic(profileLockMetadataPath(profile), {
|
|
7059
|
-
pid: process.pid,
|
|
7060
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7061
|
-
token
|
|
7062
|
-
});
|
|
7063
|
-
} catch (error) {
|
|
7064
|
-
await rmdir(lockPath).catch(() => void 0);
|
|
7065
|
-
throw error;
|
|
7066
|
-
}
|
|
7074
|
+
const token = randomUUID();
|
|
7075
|
+
if (await publishProfileLock(profile, lockPath, token)) {
|
|
7067
7076
|
return async () => {
|
|
7068
|
-
await releaseProfileLock(lockPath, token);
|
|
7077
|
+
await releaseProfileLock(profile, lockPath, token);
|
|
7069
7078
|
};
|
|
7070
|
-
} catch (error) {
|
|
7071
|
-
if (!isLockAlreadyHeld(error)) throw error;
|
|
7072
|
-
await signalTestLockContention(profile);
|
|
7073
7079
|
}
|
|
7080
|
+
await signalTestLockContention(profile);
|
|
7074
7081
|
if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
|
|
7075
7082
|
const elapsedMs = Date.now() - startedAt;
|
|
7076
7083
|
if (elapsedMs >= timeoutMs) {
|
|
@@ -7079,21 +7086,35 @@ async function acquireProfileLock(profile, options) {
|
|
|
7079
7086
|
await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
|
|
7080
7087
|
}
|
|
7081
7088
|
}
|
|
7082
|
-
async function
|
|
7083
|
-
const ownerPath = join(lockPath, "owner.json");
|
|
7084
|
-
const claimPath = join(lockPath, `.release-${token}.json`);
|
|
7089
|
+
async function publishProfileLock(profile, lockPath, token) {
|
|
7085
7090
|
try {
|
|
7086
|
-
await
|
|
7091
|
+
await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
|
|
7087
7092
|
} catch (error) {
|
|
7088
|
-
if (isErrno(error, "
|
|
7093
|
+
if (isErrno(error, "EEXIST")) return false;
|
|
7089
7094
|
throw error;
|
|
7090
7095
|
}
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
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);
|
|
7095
7110
|
}
|
|
7096
|
-
|
|
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);
|
|
7097
7118
|
await rmdir(lockPath).catch(() => void 0);
|
|
7098
7119
|
}
|
|
7099
7120
|
async function signalTestLockContention(profile) {
|
|
@@ -7110,32 +7131,79 @@ async function signalTestLockContention(profile) {
|
|
|
7110
7131
|
await writeFile(signalPath, `${profile}
|
|
7111
7132
|
`, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
7112
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
|
+
}
|
|
7113
7171
|
async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
7114
7172
|
const ownerPath = join(lockPath, "owner.json");
|
|
7115
7173
|
const owner = await readJson(ownerPath).catch(() => null);
|
|
7174
|
+
if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
|
|
7116
7175
|
if (!isStaleOwner(owner, staleAfterMs)) return false;
|
|
7117
7176
|
const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
|
|
7118
|
-
await
|
|
7177
|
+
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
7119
7178
|
const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);
|
|
7120
7179
|
try {
|
|
7121
|
-
await
|
|
7122
|
-
} catch
|
|
7123
|
-
if (isErrno(error, "ENOENT")) return false;
|
|
7180
|
+
await link(ownerPath, claimPath);
|
|
7181
|
+
} catch {
|
|
7124
7182
|
return false;
|
|
7125
7183
|
}
|
|
7184
|
+
const claimedAt = performance.now();
|
|
7185
|
+
await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
|
|
7126
7186
|
const claimed = await readJson(claimPath).catch(() => null);
|
|
7127
7187
|
const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
|
|
7128
|
-
|
|
7129
|
-
|
|
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);
|
|
7130
7192
|
return false;
|
|
7131
7193
|
}
|
|
7194
|
+
if (sameInstance) {
|
|
7195
|
+
await rm(ownerPath, { force: true });
|
|
7196
|
+
await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
|
|
7197
|
+
}
|
|
7132
7198
|
await rm(claimPath, { force: true });
|
|
7133
7199
|
await rmdir(lockPath).catch(() => void 0);
|
|
7200
|
+
if (!sameInstance) return false;
|
|
7201
|
+
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7134
7202
|
return true;
|
|
7135
7203
|
}
|
|
7136
|
-
async function
|
|
7204
|
+
async function waitForTestBarrier(environmentName, profile) {
|
|
7137
7205
|
if (process.env.NODE_ENV !== "test") return;
|
|
7138
|
-
const barrierDirectory = process.env[
|
|
7206
|
+
const barrierDirectory = process.env[environmentName];
|
|
7139
7207
|
if (!barrierDirectory) return;
|
|
7140
7208
|
await mkdir(barrierDirectory, { recursive: true });
|
|
7141
7209
|
const readyPath = join(barrierDirectory, `ready-${process.pid}-${profile}`);
|
|
@@ -7159,9 +7227,6 @@ function isStaleOwner(owner, staleAfterMs) {
|
|
|
7159
7227
|
}
|
|
7160
7228
|
return !isProcessAlive(owner.pid);
|
|
7161
7229
|
}
|
|
7162
|
-
function isLockAlreadyHeld(error) {
|
|
7163
|
-
return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
|
|
7164
|
-
}
|
|
7165
7230
|
function isProcessAlive(pid) {
|
|
7166
7231
|
try {
|
|
7167
7232
|
process.kill(pid, 0);
|