@tinycloud/operations 0.3.3-beta.9 → 0.3.4-beta.0

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.
@@ -3620,16 +3620,17 @@ var init_dist2 = __esm({
3620
3620
  ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
3621
3621
  }
3622
3622
  );
3623
+ if (response.status === 401 || response.status === 403) {
3624
+ const errorText = await response.text();
3625
+ const message = errorText.trim().length > 0 ? errorText : `Failed to put key "${key}": ${response.status} - ${response.statusText || "authorization failed"}`;
3626
+ const { resource, action: requiredAction } = parseAuthError(errorText);
3627
+ return err(authUnauthorizedError("kv", message, {
3628
+ status: response.status,
3629
+ ...requiredAction && { requiredAction },
3630
+ ...resource && { resource }
3631
+ }));
3632
+ }
3623
3633
  if (!response.ok) {
3624
- if (response.status === 401) {
3625
- const errorText2 = await response.text();
3626
- const { resource, action } = parseAuthError(errorText2);
3627
- return err(authUnauthorizedError("kv", errorText2, {
3628
- status: response.status,
3629
- ...action && { requiredAction: action },
3630
- ...resource && { resource }
3631
- }));
3632
- }
3633
3634
  const errorText = await response.text();
3634
3635
  if (response.status === 412) {
3635
3636
  return err(serviceError(
@@ -7126,9 +7127,6 @@ function profileStoreMetadataPath(profile, store) {
7126
7127
  function profileLockPath(profile) {
7127
7128
  return (0, import_node_path.join)(profilePath(profile), ".lock");
7128
7129
  }
7129
- function profileLockMetadataPath(profile) {
7130
- return (0, import_node_path.join)(profileLockPath(profile), "owner.json");
7131
- }
7132
7130
  async function readJson(filePath) {
7133
7131
  try {
7134
7132
  return JSON.parse(await (0, import_promises.readFile)(filePath, "utf8"));
@@ -7145,9 +7143,9 @@ async function writeJsonAtomic(filePath, value) {
7145
7143
  );
7146
7144
  const contents = `${JSON.stringify(value, null, 2)}
7147
7145
  `;
7148
- await (0, import_promises.mkdir)(directory, { recursive: true });
7146
+ await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
7149
7147
  try {
7150
- await (0, import_promises.writeFile)(temporaryPath, contents, "utf8");
7148
+ await (0, import_promises.writeFile)(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
7151
7149
  await (0, import_promises.rename)(temporaryPath, filePath);
7152
7150
  } catch (error) {
7153
7151
  await (0, import_promises.rm)(temporaryPath, { force: true }).catch(() => void 0);
@@ -7187,10 +7185,15 @@ async function readProfileStore(profile, store) {
7187
7185
  }
7188
7186
  async function withProfileLock(profile, action, options = {}) {
7189
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();
7190
7191
  const release = await acquireProfileLock(normalizedProfile, options);
7192
+ const ownership = { lockPath, active: true };
7191
7193
  try {
7192
- return await action();
7194
+ return await heldProfileLocks.run([...held, ownership], action);
7193
7195
  } finally {
7196
+ ownership.active = false;
7194
7197
  await release();
7195
7198
  }
7196
7199
  }
@@ -7217,28 +7220,18 @@ async function acquireProfileLock(profile, options) {
7217
7220
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
7218
7221
  const startedAt = Date.now();
7219
7222
  const lockPath = profileLockPath(profile);
7220
- 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
+ }
7221
7227
  while (true) {
7222
- try {
7223
- await (0, import_promises.mkdir)(lockPath);
7224
- const token = (0, import_node_crypto.randomUUID)();
7225
- try {
7226
- await writeJsonAtomic(profileLockMetadataPath(profile), {
7227
- pid: process.pid,
7228
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7229
- token
7230
- });
7231
- } catch (error) {
7232
- await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
7233
- throw error;
7234
- }
7228
+ const token = (0, import_node_crypto.randomUUID)();
7229
+ if (await publishProfileLock(profile, lockPath, token)) {
7235
7230
  return async () => {
7236
- await releaseProfileLock(lockPath, token);
7231
+ await releaseProfileLock(profile, lockPath, token);
7237
7232
  };
7238
- } catch (error) {
7239
- if (!isLockAlreadyHeld(error)) throw error;
7240
- await signalTestLockContention(profile);
7241
7233
  }
7234
+ await signalTestLockContention(profile);
7242
7235
  if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
7243
7236
  const elapsedMs = Date.now() - startedAt;
7244
7237
  if (elapsedMs >= timeoutMs) {
@@ -7247,21 +7240,35 @@ async function acquireProfileLock(profile, options) {
7247
7240
  await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
7248
7241
  }
7249
7242
  }
7250
- async function releaseProfileLock(lockPath, token) {
7251
- const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
7252
- const claimPath = (0, import_node_path.join)(lockPath, `.release-${token}.json`);
7243
+ async function publishProfileLock(profile, lockPath, token) {
7253
7244
  try {
7254
- await (0, import_promises.rename)(ownerPath, claimPath);
7245
+ await (0, import_promises.mkdir)(lockPath, { mode: PRIVATE_DIR_MODE });
7255
7246
  } catch (error) {
7256
- if (isErrno(error, "ENOENT")) return;
7247
+ if (isErrno(error, "EEXIST")) return false;
7257
7248
  throw error;
7258
7249
  }
7259
- const claimed = await readJson(claimPath).catch(() => null);
7260
- if (claimed?.token !== token) {
7261
- await (0, import_promises.rename)(claimPath, ownerPath).catch(() => void 0);
7262
- 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);
7263
7264
  }
7264
- 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);
7265
7272
  await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
7266
7273
  }
7267
7274
  async function signalTestLockContention(profile) {
@@ -7278,32 +7285,77 @@ async function signalTestLockContention(profile) {
7278
7285
  await (0, import_promises.writeFile)(signalPath, `${profile}
7279
7286
  `, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
7280
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
+ }
7281
7323
  async function recoverStaleLock(profile, lockPath, staleAfterMs) {
7282
7324
  const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
7283
7325
  const owner = await readJson(ownerPath).catch(() => null);
7326
+ if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
7284
7327
  if (!isStaleOwner(owner, staleAfterMs)) return false;
7285
7328
  const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
7286
- await waitForTestStaleRecoveryBarrier(profile);
7329
+ await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
7287
7330
  const claimPath = (0, import_node_path.join)(lockPath, `.stale-${(0, import_node_crypto.randomUUID)()}.json`);
7288
7331
  try {
7289
- await (0, import_promises.rename)(ownerPath, claimPath);
7290
- } catch (error) {
7291
- if (isErrno(error, "ENOENT")) return false;
7332
+ await (0, import_promises.link)(ownerPath, claimPath);
7333
+ } catch {
7292
7334
  return false;
7293
7335
  }
7336
+ const claimedAt = performance.now();
7337
+ await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
7294
7338
  const claimed = await readJson(claimPath).catch(() => null);
7295
7339
  const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
7296
- if (!sameInstance) {
7297
- 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);
7298
7344
  return false;
7299
7345
  }
7346
+ if (sameInstance) {
7347
+ await (0, import_promises.rm)(ownerPath, { force: true });
7348
+ await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
7349
+ }
7300
7350
  await (0, import_promises.rm)(claimPath, { force: true });
7301
7351
  await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
7352
+ if (!sameInstance) return false;
7353
+ await removeAgedOwnerFiles(lockPath, staleAfterMs);
7302
7354
  return true;
7303
7355
  }
7304
- async function waitForTestStaleRecoveryBarrier(profile) {
7356
+ async function waitForTestBarrier(environmentName, profile) {
7305
7357
  if (process.env.NODE_ENV !== "test") return;
7306
- const barrierDirectory = process.env[TEST_LOCK_RECOVERY_BARRIER_DIR];
7358
+ const barrierDirectory = process.env[environmentName];
7307
7359
  if (!barrierDirectory) return;
7308
7360
  await (0, import_promises.mkdir)(barrierDirectory, { recursive: true });
7309
7361
  const readyPath = (0, import_node_path.join)(barrierDirectory, `ready-${process.pid}-${profile}`);
@@ -7327,9 +7379,6 @@ function isStaleOwner(owner, staleAfterMs) {
7327
7379
  }
7328
7380
  return !isProcessAlive(owner.pid);
7329
7381
  }
7330
- function isLockAlreadyHeld(error) {
7331
- return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
7332
- }
7333
7382
  function isProcessAlive(pid) {
7334
7383
  try {
7335
7384
  process.kill(pid, 0);
@@ -7350,7 +7399,7 @@ function isErrno(error, code) {
7350
7399
  function sleep(milliseconds) {
7351
7400
  return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
7352
7401
  }
7353
- 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;
7354
7403
  var init_state = __esm({
7355
7404
  "src/state.ts"() {
7356
7405
  "use strict";
@@ -7364,8 +7413,18 @@ var init_state = __esm({
7364
7413
  DEFAULT_LOCK_RETRY_MS = 25;
7365
7414
  DEFAULT_STALE_LOCK_MS = 3e4;
7366
7415
  TEST_LOCK_CONTENTION_SIGNAL_PATH = "TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH";
7416
+ PRIVATE_FILE_MODE = 384;
7417
+ PRIVATE_DIR_MODE = 448;
7367
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";
7368
7426
  invocationStateRoot = new import_node_async_hooks.AsyncLocalStorage();
7427
+ heldProfileLocks = new import_node_async_hooks.AsyncLocalStorage();
7369
7428
  ProfileLockTimeoutError = class extends Error {
7370
7429
  constructor(profile, timeoutMs) {
7371
7430
  super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
@@ -7373,6 +7432,8 @@ var init_state = __esm({
7373
7432
  this.name = "ProfileLockTimeoutError";
7374
7433
  }
7375
7434
  };
7435
+ ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
7436
+ STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
7376
7437
  }
7377
7438
  });
7378
7439
 
@@ -10050,7 +10111,7 @@ var init_profile = __esm({
10050
10111
  "src/profile.ts"() {
10051
10112
  "use strict";
10052
10113
  init_state();
10053
- DEFAULT_HOST = "https://node.tinycloud.xyz";
10114
+ DEFAULT_HOST = "https://tee.node.tinycloud.xyz";
10054
10115
  }
10055
10116
  });
10056
10117