@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/dist/index.js CHANGED
@@ -7090,11 +7090,15 @@ var init_authority = __esm({
7090
7090
  import { randomUUID } from "crypto";
7091
7091
  import { AsyncLocalStorage } from "async_hooks";
7092
7092
  import {
7093
+ chmod,
7094
+ link,
7093
7095
  mkdir,
7094
7096
  readFile,
7097
+ readdir,
7095
7098
  rename,
7096
7099
  rmdir,
7097
7100
  rm,
7101
+ stat,
7098
7102
  writeFile
7099
7103
  } from "fs/promises";
7100
7104
  import { homedir } from "os";
@@ -7137,9 +7141,6 @@ function profileStoreMetadataPath(profile, store) {
7137
7141
  function profileLockPath(profile) {
7138
7142
  return join(profilePath(profile), ".lock");
7139
7143
  }
7140
- function profileLockMetadataPath(profile) {
7141
- return join(profileLockPath(profile), "owner.json");
7142
- }
7143
7144
  async function readJson(filePath) {
7144
7145
  try {
7145
7146
  return JSON.parse(await readFile(filePath, "utf8"));
@@ -7156,9 +7157,9 @@ async function writeJsonAtomic(filePath, value) {
7156
7157
  );
7157
7158
  const contents = `${JSON.stringify(value, null, 2)}
7158
7159
  `;
7159
- await mkdir(directory, { recursive: true });
7160
+ await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
7160
7161
  try {
7161
- await writeFile(temporaryPath, contents, "utf8");
7162
+ await writeFile(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
7162
7163
  await rename(temporaryPath, filePath);
7163
7164
  } catch (error) {
7164
7165
  await rm(temporaryPath, { force: true }).catch(() => void 0);
@@ -7198,10 +7199,15 @@ async function readProfileStore(profile, store) {
7198
7199
  }
7199
7200
  async function withProfileLock(profile, action, options = {}) {
7200
7201
  const normalizedProfile = validateProfileName(profile);
7202
+ const lockPath = profileLockPath(normalizedProfile);
7203
+ const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
7204
+ if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
7201
7205
  const release = await acquireProfileLock(normalizedProfile, options);
7206
+ const ownership = { lockPath, active: true };
7202
7207
  try {
7203
- return await action();
7208
+ return await heldProfileLocks.run([...held, ownership], action);
7204
7209
  } finally {
7210
+ ownership.active = false;
7205
7211
  await release();
7206
7212
  }
7207
7213
  }
@@ -7228,28 +7234,18 @@ async function acquireProfileLock(profile, options) {
7228
7234
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
7229
7235
  const startedAt = Date.now();
7230
7236
  const lockPath = profileLockPath(profile);
7231
- await mkdir(profilePath(profile), { recursive: true });
7237
+ await mkdir(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });
7238
+ for (const directory of [tinycloudHomePath(), profilesPath(), profilePath(profile)]) {
7239
+ await chmod(directory, PRIVATE_DIR_MODE);
7240
+ }
7232
7241
  while (true) {
7233
- try {
7234
- await mkdir(lockPath);
7235
- const token = randomUUID();
7236
- try {
7237
- await writeJsonAtomic(profileLockMetadataPath(profile), {
7238
- pid: process.pid,
7239
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7240
- token
7241
- });
7242
- } catch (error) {
7243
- await rmdir(lockPath).catch(() => void 0);
7244
- throw error;
7245
- }
7242
+ const token = randomUUID();
7243
+ if (await publishProfileLock(profile, lockPath, token)) {
7246
7244
  return async () => {
7247
- await releaseProfileLock(lockPath, token);
7245
+ await releaseProfileLock(profile, lockPath, token);
7248
7246
  };
7249
- } catch (error) {
7250
- if (!isLockAlreadyHeld(error)) throw error;
7251
- await signalTestLockContention(profile);
7252
7247
  }
7248
+ await signalTestLockContention(profile);
7253
7249
  if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
7254
7250
  const elapsedMs = Date.now() - startedAt;
7255
7251
  if (elapsedMs >= timeoutMs) {
@@ -7258,21 +7254,35 @@ async function acquireProfileLock(profile, options) {
7258
7254
  await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
7259
7255
  }
7260
7256
  }
7261
- async function releaseProfileLock(lockPath, token) {
7262
- const ownerPath = join(lockPath, "owner.json");
7263
- const claimPath = join(lockPath, `.release-${token}.json`);
7257
+ async function publishProfileLock(profile, lockPath, token) {
7264
7258
  try {
7265
- await rename(ownerPath, claimPath);
7259
+ await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
7266
7260
  } catch (error) {
7267
- if (isErrno(error, "ENOENT")) return;
7261
+ if (isErrno(error, "EEXIST")) return false;
7268
7262
  throw error;
7269
7263
  }
7270
- const claimed = await readJson(claimPath).catch(() => null);
7271
- if (claimed?.token !== token) {
7272
- await rename(claimPath, ownerPath).catch(() => void 0);
7273
- return;
7264
+ await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
7265
+ const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
7266
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token };
7267
+ try {
7268
+ await writeFile(staged, `${JSON.stringify(owner, null, 2)}
7269
+ `, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
7270
+ await link(staged, join(lockPath, "owner.json"));
7271
+ return true;
7272
+ } catch (error) {
7273
+ if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
7274
+ await rmdir(lockPath).catch(() => void 0);
7275
+ throw error;
7276
+ } finally {
7277
+ await rm(staged, { force: true }).catch(() => void 0);
7274
7278
  }
7275
- await rm(claimPath, { force: true });
7279
+ }
7280
+ async function releaseProfileLock(profile, lockPath, token) {
7281
+ const ownerPath = join(lockPath, "owner.json");
7282
+ const owner = await readJson(ownerPath).catch(() => null);
7283
+ if (owner?.token !== token) return;
7284
+ await rm(ownerPath, { force: true });
7285
+ await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);
7276
7286
  await rmdir(lockPath).catch(() => void 0);
7277
7287
  }
7278
7288
  async function signalTestLockContention(profile) {
@@ -7289,32 +7299,77 @@ async function signalTestLockContention(profile) {
7289
7299
  await writeFile(signalPath, `${profile}
7290
7300
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
7291
7301
  }
7302
+ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
7303
+ try {
7304
+ const { mtimeMs } = await stat(lockPath);
7305
+ if (Date.now() - mtimeMs < staleAfterMs) return false;
7306
+ } catch {
7307
+ return false;
7308
+ }
7309
+ const checkedAt = performance.now();
7310
+ await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);
7311
+ try {
7312
+ const entries = await readdir(lockPath);
7313
+ if (entries.includes("owner.json")) return false;
7314
+ if (recoveryFenced(checkedAt, staleAfterMs)) return false;
7315
+ for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
7316
+ await rm(join(lockPath, name), { force: true });
7317
+ }
7318
+ await rmdir(lockPath);
7319
+ } catch {
7320
+ return false;
7321
+ }
7322
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
7323
+ return true;
7324
+ }
7325
+ function recoveryFenced(since, staleAfterMs) {
7326
+ return performance.now() - since >= staleAfterMs / 2;
7327
+ }
7328
+ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
7329
+ const directory = dirname(lockPath);
7330
+ const names = await readdir(directory).catch(() => []);
7331
+ for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {
7332
+ const path = join(directory, name);
7333
+ const aged = await stat(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);
7334
+ if (aged) await rm(path, { force: true }).catch(() => void 0);
7335
+ }
7336
+ }
7292
7337
  async function recoverStaleLock(profile, lockPath, staleAfterMs) {
7293
7338
  const ownerPath = join(lockPath, "owner.json");
7294
7339
  const owner = await readJson(ownerPath).catch(() => null);
7340
+ if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
7295
7341
  if (!isStaleOwner(owner, staleAfterMs)) return false;
7296
7342
  const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
7297
- await waitForTestStaleRecoveryBarrier(profile);
7343
+ await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
7298
7344
  const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);
7299
7345
  try {
7300
- await rename(ownerPath, claimPath);
7301
- } catch (error) {
7302
- if (isErrno(error, "ENOENT")) return false;
7346
+ await link(ownerPath, claimPath);
7347
+ } catch {
7303
7348
  return false;
7304
7349
  }
7350
+ const claimedAt = performance.now();
7351
+ await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
7305
7352
  const claimed = await readJson(claimPath).catch(() => null);
7306
7353
  const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
7307
- if (!sameInstance) {
7308
- await rename(claimPath, ownerPath).catch(() => void 0);
7354
+ await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
7355
+ if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {
7356
+ await rm(claimPath, { force: true });
7357
+ await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);
7309
7358
  return false;
7310
7359
  }
7360
+ if (sameInstance) {
7361
+ await rm(ownerPath, { force: true });
7362
+ await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
7363
+ }
7311
7364
  await rm(claimPath, { force: true });
7312
7365
  await rmdir(lockPath).catch(() => void 0);
7366
+ if (!sameInstance) return false;
7367
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
7313
7368
  return true;
7314
7369
  }
7315
- async function waitForTestStaleRecoveryBarrier(profile) {
7370
+ async function waitForTestBarrier(environmentName, profile) {
7316
7371
  if (process.env.NODE_ENV !== "test") return;
7317
- const barrierDirectory = process.env[TEST_LOCK_RECOVERY_BARRIER_DIR];
7372
+ const barrierDirectory = process.env[environmentName];
7318
7373
  if (!barrierDirectory) return;
7319
7374
  await mkdir(barrierDirectory, { recursive: true });
7320
7375
  const readyPath = join(barrierDirectory, `ready-${process.pid}-${profile}`);
@@ -7338,9 +7393,6 @@ function isStaleOwner(owner, staleAfterMs) {
7338
7393
  }
7339
7394
  return !isProcessAlive(owner.pid);
7340
7395
  }
7341
- function isLockAlreadyHeld(error) {
7342
- return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
7343
- }
7344
7396
  function isProcessAlive(pid) {
7345
7397
  try {
7346
7398
  process.kill(pid, 0);
@@ -7361,7 +7413,7 @@ function isErrno(error, code) {
7361
7413
  function sleep(milliseconds) {
7362
7414
  return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
7363
7415
  }
7364
- var DEFAULT_PROFILE, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_LOCK_RETRY_MS, DEFAULT_STALE_LOCK_MS, TEST_LOCK_CONTENTION_SIGNAL_PATH, TEST_LOCK_RECOVERY_BARRIER_DIR, invocationStateRoot, ProfileLockTimeoutError;
7416
+ var DEFAULT_PROFILE, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_LOCK_RETRY_MS, DEFAULT_STALE_LOCK_MS, TEST_LOCK_CONTENTION_SIGNAL_PATH, PRIVATE_FILE_MODE, PRIVATE_DIR_MODE, TEST_LOCK_RECOVERY_BARRIER_DIR, TEST_LOCK_OWNERLESS_BARRIER_DIR, TEST_LOCK_PUBLISH_BARRIER_DIR, TEST_LOCK_RELEASE_BARRIER_DIR, TEST_LOCK_CLAIM_BARRIER_DIR, TEST_LOCK_CLAIMED_BARRIER_DIR, TEST_LOCK_FENCED_BARRIER_DIR, TEST_LOCK_VERIFIED_BARRIER_DIR, invocationStateRoot, heldProfileLocks, ProfileLockTimeoutError, ABANDONED_CLAIM, STAGED_OWNER;
7365
7417
  var init_state = __esm({
7366
7418
  "src/state.ts"() {
7367
7419
  "use strict";
@@ -7370,8 +7422,18 @@ var init_state = __esm({
7370
7422
  DEFAULT_LOCK_RETRY_MS = 25;
7371
7423
  DEFAULT_STALE_LOCK_MS = 3e4;
7372
7424
  TEST_LOCK_CONTENTION_SIGNAL_PATH = "TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH";
7425
+ PRIVATE_FILE_MODE = 384;
7426
+ PRIVATE_DIR_MODE = 448;
7373
7427
  TEST_LOCK_RECOVERY_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR";
7428
+ TEST_LOCK_OWNERLESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR";
7429
+ TEST_LOCK_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR";
7430
+ TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
7431
+ TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
7432
+ TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
7433
+ TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
7434
+ TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
7374
7435
  invocationStateRoot = new AsyncLocalStorage();
7436
+ heldProfileLocks = new AsyncLocalStorage();
7375
7437
  ProfileLockTimeoutError = class extends Error {
7376
7438
  constructor(profile, timeoutMs) {
7377
7439
  super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
@@ -7379,6 +7441,8 @@ var init_state = __esm({
7379
7441
  this.name = "ProfileLockTimeoutError";
7380
7442
  }
7381
7443
  };
7444
+ ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
7445
+ STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
7382
7446
  }
7383
7447
  });
7384
7448
 
@@ -10061,7 +10125,7 @@ var init_profile = __esm({
10061
10125
  "src/profile.ts"() {
10062
10126
  "use strict";
10063
10127
  init_state();
10064
- DEFAULT_HOST = "https://node.tinycloud.xyz";
10128
+ DEFAULT_HOST = "https://tee.node.tinycloud.xyz";
10065
10129
  }
10066
10130
  });
10067
10131