@tinycloud/operations 0.6.1-beta.2 → 0.6.1-beta.21

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
@@ -272,6 +272,18 @@ function storageFullOperationError(error) {
272
272
  { retryable: false, ...account === void 0 ? {} : { details: { account } } }
273
273
  );
274
274
  }
275
+ function nodeRetryOperationError(error) {
276
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
277
+ if (code === "OUTCOME_UNKNOWN") {
278
+ return operationError(
279
+ "NODE_ERROR",
280
+ "The TinyCloud node may have applied the write; read the data back before retrying.",
281
+ { retryable: false }
282
+ );
283
+ }
284
+ if (code !== "NODE_BUSY" && code !== "REQUEST_TIMEOUT") return void 0;
285
+ return operationError("NODE_ERROR", "The TinyCloud node was busy and applied nothing; try again shortly.", { retryable: true });
286
+ }
275
287
  function isOperationErrorCode(value) {
276
288
  return typeof value === "string" && OPERATION_ERROR_CODES.includes(value);
277
289
  }
@@ -4245,15 +4257,16 @@ var init_authority = __esm({
4245
4257
  // src/state.ts
4246
4258
  import { randomUUID } from "crypto";
4247
4259
  import { AsyncLocalStorage } from "async_hooks";
4248
- import { readFileSync, readlinkSync } from "fs";
4260
+ import { constants as fsConstants, fstatSync, readFileSync, readlinkSync } from "fs";
4249
4261
  import {
4250
4262
  chmod,
4251
4263
  link,
4252
- lstat,
4253
4264
  mkdir,
4265
+ lstat,
4254
4266
  open,
4255
4267
  readFile,
4256
4268
  readdir,
4269
+ realpath,
4257
4270
  rename,
4258
4271
  rmdir,
4259
4272
  rm,
@@ -4265,9 +4278,9 @@ import { connect, createServer } from "net";
4265
4278
  import { homedir } from "os";
4266
4279
  import { basename as basename2, dirname, isAbsolute, join, relative, resolve } from "path";
4267
4280
  function processWideContext(key) {
4268
- var _a4;
4281
+ var _a5;
4269
4282
  const registry = globalThis;
4270
- return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new AsyncLocalStorage());
4283
+ return registry[_a5 = Symbol.for(key)] ?? (registry[_a5] = new AsyncLocalStorage());
4271
4284
  }
4272
4285
  function processOrigin() {
4273
4286
  if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
@@ -4438,9 +4451,14 @@ function tinycloudConfigPath() {
4438
4451
  function profilesPath() {
4439
4452
  return join(tinycloudHomePath(), "profiles");
4440
4453
  }
4441
- function profilePath(profile) {
4454
+ function namedProfilePath(profile) {
4442
4455
  return join(profilesPath(), validateProfileName(profile));
4443
4456
  }
4457
+ function profilePath(profile) {
4458
+ const named = namedProfilePath(profile);
4459
+ const pinned = pinnedProfiles.getStore()?.get(named);
4460
+ return pinned ? livePinnedPath(profile, pinned) : named;
4461
+ }
4444
4462
  function profileConfigPath(profile) {
4445
4463
  return join(profilePath(profile), "profile.json");
4446
4464
  }
@@ -4457,10 +4475,12 @@ function profileStoreMetadataPath(profile, store) {
4457
4475
  return `${profileStorePath(profile, store)}.metadata.json`;
4458
4476
  }
4459
4477
  function profileLockPath(profile) {
4460
- return join(profilePath(profile), ".lock");
4478
+ return join(namedProfilePath(profile), ".lock");
4461
4479
  }
4462
4480
  function profileTurnLockPath(profile) {
4463
- return join(tinycloudHomePath(), "profile-locks", validateProfileName(profile));
4481
+ const home = tinycloudHomePath();
4482
+ const pinned = pinnedHomes.getStore()?.get(home);
4483
+ return join(pinned ? livePinnedPath(profile, pinned) : home, "profile-locks", validateProfileName(profile));
4464
4484
  }
4465
4485
  async function readJson(filePath) {
4466
4486
  try {
@@ -4470,6 +4490,44 @@ async function readJson(filePath) {
4470
4490
  throw error;
4471
4491
  }
4472
4492
  }
4493
+ async function readProfileJson(profile, filePath) {
4494
+ const before = process.platform !== "linux" ? await lstat(filePath, { bigint: true }).catch((error) => {
4495
+ if (error.code === "ENOENT") return null;
4496
+ throw error;
4497
+ }) : null;
4498
+ if (process.platform !== "linux" && !before) return null;
4499
+ if (before && !before.isFile()) throw new ProfileSymlinkError(profile);
4500
+ const dev = before && exactFileId(before.dev);
4501
+ const ino = before && exactFileId(before.ino);
4502
+ try {
4503
+ const handle = await open(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
4504
+ try {
4505
+ if (before) {
4506
+ const opened = await handle.stat({ bigint: true });
4507
+ if (!opened.isFile() || dev === null || ino === null || dev !== exactFileId(opened.dev) || ino !== exactFileId(opened.ino)) {
4508
+ throw new ProfileSymlinkError(profile, "changed while opening its file");
4509
+ }
4510
+ }
4511
+ const contents = await handle.readFile("utf8");
4512
+ if (before) {
4513
+ const named = await lstat(filePath, { bigint: true }).catch((error) => {
4514
+ if (error.code === "ENOENT") return null;
4515
+ throw error;
4516
+ });
4517
+ if (!named?.isFile() || dev !== exactFileId(named.dev) || ino !== exactFileId(named.ino)) {
4518
+ throw new ProfileSymlinkError(profile, "changed while reading its file");
4519
+ }
4520
+ }
4521
+ return JSON.parse(contents);
4522
+ } finally {
4523
+ await handle.close();
4524
+ }
4525
+ } catch (error) {
4526
+ if (isErrno(error, "ENOENT")) return null;
4527
+ if (isErrno(error, "ELOOP") || isErrno(error, "ENOTDIR")) throw new ProfileSymlinkError(profile);
4528
+ throw error;
4529
+ }
4530
+ }
4473
4531
  async function writeJsonAtomic(filePath, value) {
4474
4532
  await replaceJson(filePath, value, false);
4475
4533
  }
@@ -4503,11 +4561,14 @@ async function writeFileDurably(path, contents) {
4503
4561
  }
4504
4562
  }
4505
4563
  async function readProfile(profile) {
4506
- return readJson(profileConfigPath(profile));
4564
+ return withPinnedProfileDirectory(profile, (directory) => directory ? readProfileJson(profile, join(directory, "profile.json")) : Promise.resolve(null));
4507
4565
  }
4508
4566
  async function readSession(profile) {
4509
- await readStoreMetadata(profile, "session");
4510
- return readJson(sessionPath(profile));
4567
+ return withPinnedProfileDirectory(profile, async (directory) => {
4568
+ if (!directory) return null;
4569
+ await readStoreMetadata(profile, "session");
4570
+ return readProfileJson(profile, sessionPath(profile));
4571
+ });
4511
4572
  }
4512
4573
  async function readAdditionalDelegations(profile) {
4513
4574
  return (await readProfileStore(profile, "additional-delegations")).records;
@@ -4516,43 +4577,187 @@ async function readAuthRequests(profile) {
4516
4577
  return (await readProfileStore(profile, "auth-requests")).records;
4517
4578
  }
4518
4579
  async function readStoreMetadata(profile, store) {
4519
- const metadata = await readJson(profileStoreMetadataPath(profile, store));
4520
- if (metadata === null) return { formatVersion: 1 };
4521
- if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
4522
- throw new TypeError(`Unsupported store format for "${store}".`);
4523
- }
4524
- return metadata;
4580
+ return withPinnedProfileDirectory(profile, async (directory) => {
4581
+ const metadata = directory ? await readProfileJson(profile, profileStoreMetadataPath(profile, store)) : null;
4582
+ if (metadata === null) return { formatVersion: 1 };
4583
+ if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
4584
+ throw new TypeError(`Unsupported store format for "${store}".`);
4585
+ }
4586
+ return metadata;
4587
+ });
4525
4588
  }
4526
4589
  async function readProfileStore(profile, store) {
4527
- const [metadata, raw] = await Promise.all([
4528
- readStoreMetadata(profile, store),
4529
- readJson(profileStorePath(profile, store))
4530
- ]);
4531
- return {
4532
- formatVersion: metadata.formatVersion,
4533
- records: Array.isArray(raw) ? raw : []
4534
- };
4590
+ return withPinnedProfileDirectory(profile, async (directory) => {
4591
+ if (!directory) return { formatVersion: 1, records: [] };
4592
+ const [metadata, raw] = await Promise.all([
4593
+ readStoreMetadata(profile, store),
4594
+ readProfileJson(profile, profileStorePath(profile, store))
4595
+ ]);
4596
+ return {
4597
+ formatVersion: metadata.formatVersion,
4598
+ records: Array.isArray(raw) ? raw : []
4599
+ };
4600
+ });
4535
4601
  }
4536
- async function withProfileLock(profile, action, options = {}) {
4537
- const normalizedProfile = validateProfileName(profile);
4538
- const lockPath = profileLockPath(normalizedProfile);
4539
- const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
4540
- if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
4541
- const acquired = await acquireProfileLock(normalizedProfile, options);
4542
- const ownership = {
4543
- lockPath,
4544
- turnSlot: acquired.turnSlot,
4545
- deletedWhileWaiting: acquired.deletedWhileWaiting,
4546
- active: true
4547
- };
4602
+ function exactFileId(value) {
4603
+ if (typeof value === "bigint") return value >= 0n ? value : null;
4604
+ return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
4605
+ }
4606
+ function isPinnedLive(pinned) {
4607
+ if (!pinned.active) return false;
4608
+ try {
4609
+ const stats = fstatSync(pinned.fd, { bigint: true });
4610
+ return exactFileId(stats.dev) === pinned.dev && exactFileId(stats.ino) === pinned.ino;
4611
+ } catch {
4612
+ return false;
4613
+ }
4614
+ }
4615
+ function livePinnedPath(profile, pinned) {
4616
+ if (isPinnedLive(pinned)) return pinned.path;
4617
+ throw new ProfileSymlinkError(profile, "its pinned directory is no longer active or changed during use");
4618
+ }
4619
+ async function withPinnedProfileDirectory(profile, action, options = {}) {
4620
+ const named = namedProfilePath(profile);
4621
+ const existing = pinnedProfiles.getStore()?.get(named);
4622
+ if (existing) return action(livePinnedPath(profile, existing));
4623
+ if (process.platform !== "linux") {
4624
+ if (options.create) await mkdir(named, { recursive: true, mode: PRIVATE_DIR_MODE });
4625
+ const stats = await lstat(named).catch((error) => {
4626
+ if (error.code === "ENOENT") return null;
4627
+ throw error;
4628
+ });
4629
+ if (!stats) return action(null);
4630
+ if (!stats.isDirectory() || stats.isSymbolicLink()) throw new ProfileSymlinkError(profile);
4631
+ return action(named);
4632
+ }
4633
+ const home = tinycloudHomePath();
4634
+ const handles = [];
4635
+ const fdPath = (fd) => `/proc/self/fd/${fd}`;
4636
+ const flags = fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW;
4548
4637
  try {
4549
- return await heldProfileLocks.run([...held, ownership], action);
4638
+ if (options.create) await mkdir(home, { recursive: true, mode: PRIVATE_DIR_MODE });
4639
+ const resolved = canonicalHomes.get(home) ?? await realpath(home).catch((error) => {
4640
+ if (error.code === "ENOENT" && !options.create) return null;
4641
+ if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
4642
+ throw error;
4643
+ });
4644
+ if (!resolved) return await action(null);
4645
+ const root = canonicalHomes.get(home) ?? resolved;
4646
+ canonicalHomes.set(home, root);
4647
+ const segments = [root, "profiles", validateProfileName(profile)];
4648
+ for (let index = 0; index < segments.length; index++) {
4649
+ const path = handles.length ? join(fdPath(handles[handles.length - 1].fd), segments[index]) : segments[index];
4650
+ if (options.create && handles.length) {
4651
+ await mkdir(path, { mode: PRIVATE_DIR_MODE }).catch((error) => {
4652
+ if (error.code !== "EEXIST") throw error;
4653
+ });
4654
+ }
4655
+ const handle = await open(path, index === 1 ? flags & ~fsConstants.O_NOFOLLOW : flags).catch((error) => {
4656
+ if (error.code === "ENOENT" && !options.create) return null;
4657
+ if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
4658
+ throw error;
4659
+ });
4660
+ if (!handle) return await action(null);
4661
+ handles.push(handle);
4662
+ if (options.create) await handle.chmod(PRIVATE_DIR_MODE);
4663
+ }
4664
+ const opened = handles[2];
4665
+ const [actual, namedStats] = await Promise.all([opened.stat({ bigint: true }), lstat(named, { bigint: true })]);
4666
+ const dev = exactFileId(actual.dev);
4667
+ const ino = exactFileId(actual.ino);
4668
+ if (!namedStats.isDirectory() || dev === null || ino === null || dev !== exactFileId(namedStats.dev) || ino !== exactFileId(namedStats.ino)) {
4669
+ throw new ProfileSymlinkError(profile, "changed while opening its directory");
4670
+ }
4671
+ const directory = fdPath(opened.fd);
4672
+ const pinned = {
4673
+ path: directory,
4674
+ fd: opened.fd,
4675
+ dev,
4676
+ ino,
4677
+ active: true
4678
+ };
4679
+ const homeHandle = handles[0];
4680
+ const homeStats = await homeHandle.stat({ bigint: true });
4681
+ const homeDev = exactFileId(homeStats.dev);
4682
+ const homeIno = exactFileId(homeStats.ino);
4683
+ if (homeDev === null || homeIno === null) throw new ProfileSymlinkError(profile, "has an unverified home directory identity");
4684
+ const homePinned = {
4685
+ path: fdPath(homeHandle.fd),
4686
+ fd: homeHandle.fd,
4687
+ dev: homeDev,
4688
+ ino: homeIno,
4689
+ active: true
4690
+ };
4691
+ const context = new Map(pinnedProfiles.getStore());
4692
+ context.set(named, pinned);
4693
+ const homes = new Map(pinnedHomes.getStore());
4694
+ homes.set(home, homePinned);
4695
+ try {
4696
+ return await pinnedHomes.run(homes, () => pinnedProfiles.run(context, () => action(directory)));
4697
+ } finally {
4698
+ pinned.active = false;
4699
+ homePinned.active = false;
4700
+ }
4550
4701
  } finally {
4551
- ownership.active = false;
4552
- await acquired.release();
4553
- if (acquired.deletedWhileWaiting) await rmdir(profilePath(normalizedProfile)).catch(() => void 0);
4702
+ for (const handle of handles.reverse()) await handle.close();
4554
4703
  }
4555
4704
  }
4705
+ async function assertRealProfileDirectory(profile) {
4706
+ const named = namedProfilePath(profile);
4707
+ const pinned = pinnedProfiles.getStore()?.get(named);
4708
+ if (pinned) {
4709
+ livePinnedPath(profile, pinned);
4710
+ return;
4711
+ }
4712
+ const stats = await lstat(named).catch((error) => {
4713
+ if (error.code === "ENOENT") return null;
4714
+ throw error;
4715
+ });
4716
+ if (stats && (!stats.isDirectory() || stats.isSymbolicLink())) throw new ProfileSymlinkError(profile);
4717
+ }
4718
+ async function withProfileLock(profile, action, options = {}) {
4719
+ const normalizedProfile = validateProfileName(profile);
4720
+ const lockPath = profileLockPath(normalizedProfile);
4721
+ const held = (heldProfileLocks.getStore() ?? []).filter((ownership) => ownership.active);
4722
+ if (held.some((ownership) => ownership.lockPath === lockPath)) {
4723
+ await assertRealProfileDirectory(normalizedProfile);
4724
+ return action();
4725
+ }
4726
+ const named = namedProfilePath(normalizedProfile);
4727
+ const inherited = pinnedProfiles.getStore()?.get(named);
4728
+ if (inherited && !isPinnedLive(inherited)) {
4729
+ const profiles = new Map(pinnedProfiles.getStore());
4730
+ profiles.delete(named);
4731
+ const homes = new Map(pinnedHomes.getStore());
4732
+ homes.delete(tinycloudHomePath());
4733
+ return pinnedHomes.run(homes, () => pinnedProfiles.run(profiles, () => withProfileLock(normalizedProfile, action, options)));
4734
+ }
4735
+ return withPinnedProfileDirectory(normalizedProfile, async (directory) => {
4736
+ if (!directory) throw new ProfileDeletedError(normalizedProfile);
4737
+ const acquired = await acquireProfileLock(normalizedProfile, options);
4738
+ const ownership = {
4739
+ lockPath,
4740
+ turnSlot: acquired.turnSlot,
4741
+ deletedWhileWaiting: acquired.deletedWhileWaiting,
4742
+ active: true
4743
+ };
4744
+ try {
4745
+ return await heldProfileLocks.run([...held, ownership], action);
4746
+ } finally {
4747
+ ownership.active = false;
4748
+ await acquired.release();
4749
+ if (ownership.deletedProfile || acquired.deletedWhileWaiting) {
4750
+ const current = await lstat(namedProfilePath(normalizedProfile), { bigint: true }).catch(() => null);
4751
+ const original = await stat(directory, { bigint: true }).catch(() => null);
4752
+ const currentDev = current && exactFileId(current.dev);
4753
+ const currentIno = current && exactFileId(current.ino);
4754
+ if (currentDev !== null && currentIno !== null && original && currentDev === exactFileId(original.dev) && currentIno === exactFileId(original.ino)) {
4755
+ await rmdir(namedProfilePath(normalizedProfile)).catch(() => void 0);
4756
+ }
4757
+ }
4758
+ }
4759
+ }, { create: !options.requireProfile });
4760
+ }
4556
4761
  async function refuseWriteToDeletedProfile(profile) {
4557
4762
  const lockPath = profileLockPath(profile);
4558
4763
  const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);
@@ -4585,17 +4790,15 @@ async function acquireProfileLock(profile, options) {
4585
4790
  retryMs: options.retryMs ?? DEFAULT_LOCK_RETRY_MS
4586
4791
  };
4587
4792
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
4588
- const lockPath = profileLockPath(profile);
4793
+ const lockPath = join(profilePath(profile), ".lock");
4589
4794
  const turnDirectory = profileTurnLockPath(profile);
4590
4795
  const directory = profilePath(profile);
4796
+ await assertRealProfileDirectory(profile);
4591
4797
  if (options.requireProfile && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
4592
4798
  if (!options.requireProfile) await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
4799
+ await assertRealProfileDirectory(profile);
4593
4800
  await mkdir(dirname(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
4594
- for (const path of [tinycloudHomePath(), profilesPath(), directory]) {
4595
- await chmod(path, PRIVATE_DIR_MODE).catch((error) => {
4596
- if (path !== directory || !isErrno(error, "ENOENT")) throw error;
4597
- });
4598
- }
4801
+ await chmod(directory, PRIVATE_DIR_MODE);
4599
4802
  const liveness = { directory: turnDirectory, held: void 0 };
4600
4803
  const closeAcquisitionLiveness = async () => {
4601
4804
  if (liveness.held) await closeLiveness(liveness.held);
@@ -4615,6 +4818,7 @@ async function acquireProfileLock(profile, options) {
4615
4818
  }
4616
4819
  const deletion = await readJson(join(turnDirectory, "deleted.json")).catch(() => null);
4617
4820
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
4821
+ if (deletedWhileWaiting && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
4618
4822
  while (true) {
4619
4823
  const token = randomUUID();
4620
4824
  if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
@@ -4760,7 +4964,7 @@ async function takeTurn(profile, directory, latest, liveness) {
4760
4964
  if (published) await markTurn(path, token, "done");
4761
4965
  return null;
4762
4966
  } catch (error) {
4763
- if (published) settleOwnTurn(directory, slot, token);
4967
+ if (published) await settleOwnTurn(directory, slot, token);
4764
4968
  throw error;
4765
4969
  }
4766
4970
  }
@@ -4771,17 +4975,25 @@ async function settleAbandonedTurn(directory, abandoned) {
4771
4975
  }
4772
4976
  await markTurn(path, abandoned.token, "done");
4773
4977
  }
4774
- function settleOwnTurn(directory, slot, token, retries = SETTLE_RETRIES) {
4978
+ async function settleOwnTurn(directory, slot, token) {
4775
4979
  relinquishedTurns.add(token);
4776
- void (async () => {
4777
- try {
4778
- const turn = await readTurn(directory, slot);
4779
- if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(directory, turn);
4780
- relinquishedTurns.delete(token);
4781
- } catch {
4782
- if (retries > 0) setTimeout(() => settleOwnTurn(directory, slot, token, retries - 1), SETTLE_RETRY_MS).unref();
4783
- }
4784
- })();
4980
+ const pinned = await pinDirectory(directory).catch(() => null);
4981
+ const path = pinned?.path(".") ?? directory;
4982
+ let retries = SETTLE_RETRIES;
4983
+ const attempt = () => {
4984
+ void (async () => {
4985
+ try {
4986
+ const turn = await readTurn(path, slot);
4987
+ if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(path, turn);
4988
+ relinquishedTurns.delete(token);
4989
+ await pinned?.close();
4990
+ } catch {
4991
+ if (retries-- > 0) setTimeout(attempt, SETTLE_RETRY_MS).unref();
4992
+ else await pinned?.close();
4993
+ }
4994
+ })();
4995
+ };
4996
+ attempt();
4785
4997
  }
4786
4998
  async function markTurn(path, token, kind) {
4787
4999
  for (let attempt = 0; ; attempt++) {
@@ -4826,7 +5038,7 @@ async function releaseTurn(profile, turn, liveness) {
4826
5038
  try {
4827
5039
  await markTurn(join(turn.directory, String(turn.slot)), turn.token, "done");
4828
5040
  } catch {
4829
- settleOwnTurn(turn.directory, turn.slot, turn.token);
5041
+ await settleOwnTurn(turn.directory, turn.slot, turn.token);
4830
5042
  return;
4831
5043
  }
4832
5044
  await collectTurns(profile, turn, liveness).catch(() => void 0);
@@ -5110,7 +5322,7 @@ function isErrno(error, code) {
5110
5322
  function sleep(milliseconds) {
5111
5323
  return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
5112
5324
  }
5113
- 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_VERIFIED_BARRIER_DIR, TEST_LOCK_MOVED_BARRIER_DIR, TEST_LOCK_TURN_PUBLISH_BARRIER_DIR, TEST_LOCK_TURN_SETTLE_BARRIER_DIR, TEST_LOCK_TURN_COLLECT_BARRIER_DIR, TEST_LOCK_LIVENESS_BARRIER_DIR, invocationStateRoot, heldProfileLocks, _a2, processInstanceId, cachedProcessOrigin, LIVENESS_SOCKET, LIVENESS_PROBE_TIMEOUT_MS, ProfileLockTimeoutError, ProfileDeletedError, TURN, _a3, relinquishedTurns, MARKER_RETRY_DELAYS_MS, SETTLE_RETRY_MS, SETTLE_RETRIES, NO_HARD_LINK_ERRORS, ABANDONED_CLAIM, RECOVERY_CLAIM, STAGED_OWNER;
5325
+ 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_VERIFIED_BARRIER_DIR, TEST_LOCK_MOVED_BARRIER_DIR, TEST_LOCK_TURN_PUBLISH_BARRIER_DIR, TEST_LOCK_TURN_SETTLE_BARRIER_DIR, TEST_LOCK_TURN_COLLECT_BARRIER_DIR, TEST_LOCK_LIVENESS_BARRIER_DIR, invocationStateRoot, heldProfileLocks, pinnedProfiles, pinnedHomes, _a2, canonicalHomes, _a3, processInstanceId, cachedProcessOrigin, LIVENESS_SOCKET, LIVENESS_PROBE_TIMEOUT_MS, ProfileLockTimeoutError, ProfileDeletedError, ProfileSymlinkError, TURN, _a4, relinquishedTurns, MARKER_RETRY_DELAYS_MS, SETTLE_RETRY_MS, SETTLE_RETRIES, NO_HARD_LINK_ERRORS, ABANDONED_CLAIM, RECOVERY_CLAIM, STAGED_OWNER;
5114
5326
  var init_state = __esm({
5115
5327
  "src/state.ts"() {
5116
5328
  "use strict";
@@ -5135,7 +5347,10 @@ var init_state = __esm({
5135
5347
  TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
5136
5348
  invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
5137
5349
  heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
5138
- processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = randomUUID());
5350
+ pinnedProfiles = processWideContext("tinycloud.operations.pinnedProfiles.v2");
5351
+ pinnedHomes = processWideContext("tinycloud.operations.pinnedHomes.v2");
5352
+ canonicalHomes = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.canonicalHomes.v1")] ?? (globalThis[_a2] = /* @__PURE__ */ new Map());
5353
+ processInstanceId = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a3] = randomUUID());
5139
5354
  LIVENESS_SOCKET = /^\.live-([0-9a-f]+)-([0-9a-f]+)-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(\.new)?$/;
5140
5355
  LIVENESS_PROBE_TIMEOUT_MS = 1e3;
5141
5356
  ProfileLockTimeoutError = class extends Error {
@@ -5153,8 +5368,15 @@ var init_state = __esm({
5153
5368
  this.name = "ProfileDeletedError";
5154
5369
  }
5155
5370
  };
5371
+ ProfileSymlinkError = class extends Error {
5372
+ constructor(profile, detail = "must be a real directory, not a symlink or another file type") {
5373
+ super(`Profile "${profile}" ${detail}.`);
5374
+ this.code = "PROFILE_SYMLINK";
5375
+ this.name = "ProfileSymlinkError";
5376
+ }
5377
+ };
5156
5378
  TURN = /^(?:0|[1-9][0-9]*)$/;
5157
- relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
5379
+ relinquishedTurns = globalThis[_a4 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a4] = /* @__PURE__ */ new Set());
5158
5380
  MARKER_RETRY_DELAYS_MS = [10, 50, 250];
5159
5381
  SETTLE_RETRY_MS = 1e3;
5160
5382
  SETTLE_RETRIES = 300;
@@ -6337,6 +6559,8 @@ function appsFailure(error) {
6337
6559
  if (auth?.status === "error") return auth.error;
6338
6560
  const storage = storageFullOperationError(error);
6339
6561
  if (storage) return storage;
6562
+ const nodeRetry = nodeRetryOperationError(error);
6563
+ if (nodeRetry) return nodeRetry;
6340
6564
  const code = typeof error === "object" && error !== null ? error.code : void 0;
6341
6565
  if (code === "SPACE_NOT_HOSTED") return operationError("NODE_ERROR", "Your TinyCloud default space is not set up. Try again in a minute; if it still fails, ask for help setting up the space.", { details: { reason: "space_not_hosted" } });
6342
6566
  if (isOperationErrorCode(code)) return operationError(code, "The node could not complete this personal-record operation.");
@@ -7810,16 +8034,8 @@ function kvFailure(error, action) {
7810
8034
  if (error.code === "KV_PRECONDITION_FAILED") {
7811
8035
  return { status: "error", error: operationError("KV_PRECONDITION_FAILED", "The TinyCloud KV object changed or already exists.") };
7812
8036
  }
7813
- if (error.code === "KV_CONFLICT") {
7814
- return {
7815
- status: "error",
7816
- error: operationError(
7817
- "KV_CONFLICT",
7818
- "A database serialization conflict prevented the conditional KV mutation; re-read the key state, which may still be absent, before deciding whether to retry.",
7819
- { retryable: true }
7820
- )
7821
- };
7822
- }
8037
+ const nodeRetry = nodeRetryOperationError(error);
8038
+ if (nodeRetry) return { status: "error", error: nodeRetry };
7823
8039
  if (error.code === "KV_RESPONSE_TOO_LARGE") {
7824
8040
  return { status: "error", error: operationError("KV_RESPONSE_TOO_LARGE", `The TinyCloud KV value exceeds ${MAX_KV_VALUE_BYTES} bytes.`) };
7825
8041
  }