@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.cjs CHANGED
@@ -7127,9 +7127,6 @@ function profileStoreMetadataPath(profile, store) {
7127
7127
  function profileLockPath(profile) {
7128
7128
  return (0, import_node_path.join)(profilePath(profile), ".lock");
7129
7129
  }
7130
- function profileLockMetadataPath(profile) {
7131
- return (0, import_node_path.join)(profileLockPath(profile), "owner.json");
7132
- }
7133
7130
  async function readJson(filePath) {
7134
7131
  try {
7135
7132
  return JSON.parse(await (0, import_promises.readFile)(filePath, "utf8"));
@@ -7146,9 +7143,9 @@ async function writeJsonAtomic(filePath, value) {
7146
7143
  );
7147
7144
  const contents = `${JSON.stringify(value, null, 2)}
7148
7145
  `;
7149
- await (0, import_promises.mkdir)(directory, { recursive: true });
7146
+ await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
7150
7147
  try {
7151
- await (0, import_promises.writeFile)(temporaryPath, contents, "utf8");
7148
+ await (0, import_promises.writeFile)(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
7152
7149
  await (0, import_promises.rename)(temporaryPath, filePath);
7153
7150
  } catch (error) {
7154
7151
  await (0, import_promises.rm)(temporaryPath, { force: true }).catch(() => void 0);
@@ -7188,10 +7185,15 @@ async function readProfileStore(profile, store) {
7188
7185
  }
7189
7186
  async function withProfileLock(profile, action, options = {}) {
7190
7187
  const normalizedProfile = validateProfileName(profile);
7188
+ const lockPath = profileLockPath(normalizedProfile);
7189
+ const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
7190
+ if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
7191
7191
  const release = await acquireProfileLock(normalizedProfile, options);
7192
+ const ownership = { lockPath, active: true };
7192
7193
  try {
7193
- return await action();
7194
+ return await heldProfileLocks.run([...held, ownership], action);
7194
7195
  } finally {
7196
+ ownership.active = false;
7195
7197
  await release();
7196
7198
  }
7197
7199
  }
@@ -7218,28 +7220,18 @@ async function acquireProfileLock(profile, options) {
7218
7220
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
7219
7221
  const startedAt = Date.now();
7220
7222
  const lockPath = profileLockPath(profile);
7221
- await (0, import_promises.mkdir)(profilePath(profile), { recursive: true });
7223
+ await (0, import_promises.mkdir)(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });
7224
+ for (const directory of [tinycloudHomePath(), profilesPath(), profilePath(profile)]) {
7225
+ await (0, import_promises.chmod)(directory, PRIVATE_DIR_MODE);
7226
+ }
7222
7227
  while (true) {
7223
- try {
7224
- await (0, import_promises.mkdir)(lockPath);
7225
- const token = (0, import_node_crypto.randomUUID)();
7226
- try {
7227
- await writeJsonAtomic(profileLockMetadataPath(profile), {
7228
- pid: process.pid,
7229
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7230
- token
7231
- });
7232
- } catch (error) {
7233
- await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
7234
- throw error;
7235
- }
7228
+ const token = (0, import_node_crypto.randomUUID)();
7229
+ if (await publishProfileLock(profile, lockPath, token)) {
7236
7230
  return async () => {
7237
- await releaseProfileLock(lockPath, token);
7231
+ await releaseProfileLock(profile, lockPath, token);
7238
7232
  };
7239
- } catch (error) {
7240
- if (!isLockAlreadyHeld(error)) throw error;
7241
- await signalTestLockContention(profile);
7242
7233
  }
7234
+ await signalTestLockContention(profile);
7243
7235
  if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
7244
7236
  const elapsedMs = Date.now() - startedAt;
7245
7237
  if (elapsedMs >= timeoutMs) {
@@ -7248,21 +7240,35 @@ async function acquireProfileLock(profile, options) {
7248
7240
  await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
7249
7241
  }
7250
7242
  }
7251
- async function releaseProfileLock(lockPath, token) {
7252
- const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
7253
- const claimPath = (0, import_node_path.join)(lockPath, `.release-${token}.json`);
7243
+ async function publishProfileLock(profile, lockPath, token) {
7254
7244
  try {
7255
- await (0, import_promises.rename)(ownerPath, claimPath);
7245
+ await (0, import_promises.mkdir)(lockPath, { mode: PRIVATE_DIR_MODE });
7256
7246
  } catch (error) {
7257
- if (isErrno(error, "ENOENT")) return;
7247
+ if (isErrno(error, "EEXIST")) return false;
7258
7248
  throw error;
7259
7249
  }
7260
- const claimed = await readJson(claimPath).catch(() => null);
7261
- if (claimed?.token !== token) {
7262
- await (0, import_promises.rename)(claimPath, ownerPath).catch(() => void 0);
7263
- return;
7250
+ await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
7251
+ const staged = (0, import_node_path.join)((0, import_node_path.dirname)(lockPath), `.lock-owner-${token}.tmp`);
7252
+ const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token };
7253
+ try {
7254
+ await (0, import_promises.writeFile)(staged, `${JSON.stringify(owner, null, 2)}
7255
+ `, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
7256
+ await (0, import_promises.link)(staged, (0, import_node_path.join)(lockPath, "owner.json"));
7257
+ return true;
7258
+ } catch (error) {
7259
+ if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
7260
+ await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
7261
+ throw error;
7262
+ } finally {
7263
+ await (0, import_promises.rm)(staged, { force: true }).catch(() => void 0);
7264
7264
  }
7265
- await (0, import_promises.rm)(claimPath, { force: true });
7265
+ }
7266
+ async function releaseProfileLock(profile, lockPath, token) {
7267
+ const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
7268
+ const owner = await readJson(ownerPath).catch(() => null);
7269
+ if (owner?.token !== token) return;
7270
+ await (0, import_promises.rm)(ownerPath, { force: true });
7271
+ await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);
7266
7272
  await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
7267
7273
  }
7268
7274
  async function signalTestLockContention(profile) {
@@ -7279,32 +7285,77 @@ async function signalTestLockContention(profile) {
7279
7285
  await (0, import_promises.writeFile)(signalPath, `${profile}
7280
7286
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
7281
7287
  }
7288
+ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
7289
+ try {
7290
+ const { mtimeMs } = await (0, import_promises.stat)(lockPath);
7291
+ if (Date.now() - mtimeMs < staleAfterMs) return false;
7292
+ } catch {
7293
+ return false;
7294
+ }
7295
+ const checkedAt = performance.now();
7296
+ await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);
7297
+ try {
7298
+ const entries = await (0, import_promises.readdir)(lockPath);
7299
+ if (entries.includes("owner.json")) return false;
7300
+ if (recoveryFenced(checkedAt, staleAfterMs)) return false;
7301
+ for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
7302
+ await (0, import_promises.rm)((0, import_node_path.join)(lockPath, name), { force: true });
7303
+ }
7304
+ await (0, import_promises.rmdir)(lockPath);
7305
+ } catch {
7306
+ return false;
7307
+ }
7308
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
7309
+ return true;
7310
+ }
7311
+ function recoveryFenced(since, staleAfterMs) {
7312
+ return performance.now() - since >= staleAfterMs / 2;
7313
+ }
7314
+ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
7315
+ const directory = (0, import_node_path.dirname)(lockPath);
7316
+ const names = await (0, import_promises.readdir)(directory).catch(() => []);
7317
+ for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {
7318
+ const path = (0, import_node_path.join)(directory, name);
7319
+ const aged = await (0, import_promises.stat)(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);
7320
+ if (aged) await (0, import_promises.rm)(path, { force: true }).catch(() => void 0);
7321
+ }
7322
+ }
7282
7323
  async function recoverStaleLock(profile, lockPath, staleAfterMs) {
7283
7324
  const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
7284
7325
  const owner = await readJson(ownerPath).catch(() => null);
7326
+ if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
7285
7327
  if (!isStaleOwner(owner, staleAfterMs)) return false;
7286
7328
  const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
7287
- await waitForTestStaleRecoveryBarrier(profile);
7329
+ await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
7288
7330
  const claimPath = (0, import_node_path.join)(lockPath, `.stale-${(0, import_node_crypto.randomUUID)()}.json`);
7289
7331
  try {
7290
- await (0, import_promises.rename)(ownerPath, claimPath);
7291
- } catch (error) {
7292
- if (isErrno(error, "ENOENT")) return false;
7332
+ await (0, import_promises.link)(ownerPath, claimPath);
7333
+ } catch {
7293
7334
  return false;
7294
7335
  }
7336
+ const claimedAt = performance.now();
7337
+ await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
7295
7338
  const claimed = await readJson(claimPath).catch(() => null);
7296
7339
  const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
7297
- if (!sameInstance) {
7298
- await (0, import_promises.rename)(claimPath, ownerPath).catch(() => void 0);
7340
+ await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
7341
+ if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {
7342
+ await (0, import_promises.rm)(claimPath, { force: true });
7343
+ await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);
7299
7344
  return false;
7300
7345
  }
7346
+ if (sameInstance) {
7347
+ await (0, import_promises.rm)(ownerPath, { force: true });
7348
+ await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
7349
+ }
7301
7350
  await (0, import_promises.rm)(claimPath, { force: true });
7302
7351
  await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
7352
+ if (!sameInstance) return false;
7353
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
7303
7354
  return true;
7304
7355
  }
7305
- async function waitForTestStaleRecoveryBarrier(profile) {
7356
+ async function waitForTestBarrier(environmentName, profile) {
7306
7357
  if (process.env.NODE_ENV !== "test") return;
7307
- const barrierDirectory = process.env[TEST_LOCK_RECOVERY_BARRIER_DIR];
7358
+ const barrierDirectory = process.env[environmentName];
7308
7359
  if (!barrierDirectory) return;
7309
7360
  await (0, import_promises.mkdir)(barrierDirectory, { recursive: true });
7310
7361
  const readyPath = (0, import_node_path.join)(barrierDirectory, `ready-${process.pid}-${profile}`);
@@ -7328,9 +7379,6 @@ function isStaleOwner(owner, staleAfterMs) {
7328
7379
  }
7329
7380
  return !isProcessAlive(owner.pid);
7330
7381
  }
7331
- function isLockAlreadyHeld(error) {
7332
- return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
7333
- }
7334
7382
  function isProcessAlive(pid) {
7335
7383
  try {
7336
7384
  process.kill(pid, 0);
@@ -7351,7 +7399,7 @@ function isErrno(error, code) {
7351
7399
  function sleep(milliseconds) {
7352
7400
  return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
7353
7401
  }
7354
- var import_node_crypto, import_node_async_hooks, import_promises, import_node_os, import_node_path, 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;
7402
+ var import_node_crypto, import_node_async_hooks, import_promises, import_node_os, import_node_path, 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;
7355
7403
  var init_state = __esm({
7356
7404
  "src/state.ts"() {
7357
7405
  "use strict";
@@ -7365,8 +7413,18 @@ var init_state = __esm({
7365
7413
  DEFAULT_LOCK_RETRY_MS = 25;
7366
7414
  DEFAULT_STALE_LOCK_MS = 3e4;
7367
7415
  TEST_LOCK_CONTENTION_SIGNAL_PATH = "TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH";
7416
+ PRIVATE_FILE_MODE = 384;
7417
+ PRIVATE_DIR_MODE = 448;
7368
7418
  TEST_LOCK_RECOVERY_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR";
7419
+ TEST_LOCK_OWNERLESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR";
7420
+ TEST_LOCK_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR";
7421
+ TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
7422
+ TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
7423
+ TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
7424
+ TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
7425
+ TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
7369
7426
  invocationStateRoot = new import_node_async_hooks.AsyncLocalStorage();
7427
+ heldProfileLocks = new import_node_async_hooks.AsyncLocalStorage();
7370
7428
  ProfileLockTimeoutError = class extends Error {
7371
7429
  constructor(profile, timeoutMs) {
7372
7430
  super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
@@ -7374,6 +7432,8 @@ var init_state = __esm({
7374
7432
  this.name = "ProfileLockTimeoutError";
7375
7433
  }
7376
7434
  };
7435
+ ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
7436
+ STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
7377
7437
  }
7378
7438
  });
7379
7439
 
@@ -10051,7 +10111,7 @@ var init_profile = __esm({
10051
10111
  "src/profile.ts"() {
10052
10112
  "use strict";
10053
10113
  init_state();
10054
- DEFAULT_HOST = "https://node.tinycloud.xyz";
10114
+ DEFAULT_HOST = "https://tee.node.tinycloud.xyz";
10055
10115
  }
10056
10116
  });
10057
10117