@tinycloud/operations 0.3.3-beta.13 → 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 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" },
@@ -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
- try {
7073
- await (0, import_promises.mkdir)(lockPath);
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 releaseProfileLock(lockPath, token) {
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.rename)(ownerPath, claimPath);
7104
+ await (0, import_promises.mkdir)(lockPath, { mode: PRIVATE_DIR_MODE });
7105
7105
  } catch (error) {
7106
- if (isErrno(error, "ENOENT")) return;
7106
+ if (isErrno(error, "EEXIST")) return false;
7107
7107
  throw error;
7108
7108
  }
7109
- const claimed = await readJson(claimPath).catch(() => null);
7110
- if (claimed?.token !== token) {
7111
- await (0, import_promises.rename)(claimPath, ownerPath).catch(() => void 0);
7112
- return;
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
- await (0, import_promises.rm)(claimPath, { force: true });
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 waitForTestStaleRecoveryBarrier(profile);
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.rename)(ownerPath, claimPath);
7140
- } catch (error) {
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
- if (!sameInstance) {
7147
- await (0, import_promises.rename)(claimPath, ownerPath).catch(() => void 0);
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 waitForTestStaleRecoveryBarrier(profile) {
7217
+ async function waitForTestBarrier(environmentName, profile) {
7155
7218
  if (process.env.NODE_ENV !== "test") return;
7156
- const barrierDirectory = process.env[TEST_LOCK_RECOVERY_BARRIER_DIR];
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);