@tinycloud/operations 0.6.1-beta.0 → 0.6.1-beta.10

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.
@@ -274,6 +274,18 @@ function storageFullOperationError(error) {
274
274
  { retryable: false, ...account === void 0 ? {} : { details: { account } } }
275
275
  );
276
276
  }
277
+ function nodeRetryOperationError(error) {
278
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
279
+ if (code === "OUTCOME_UNKNOWN") {
280
+ return operationError(
281
+ "NODE_ERROR",
282
+ "The TinyCloud node may have applied the write; read the data back before retrying.",
283
+ { retryable: false }
284
+ );
285
+ }
286
+ if (code !== "NODE_BUSY" && code !== "REQUEST_TIMEOUT") return void 0;
287
+ return operationError("NODE_ERROR", "The TinyCloud node was busy and applied nothing; try again shortly.", { retryable: true });
288
+ }
277
289
  function isOperationErrorCode(value) {
278
290
  return typeof value === "string" && OPERATION_ERROR_CODES.includes(value);
279
291
  }
@@ -1045,6 +1057,7 @@ var init_manifest = __esm({
1045
1057
  DEFAULT_MANIFEST_SPACE = "applications";
1046
1058
  SERVICE_SHORT_TO_LONG = Object.freeze({
1047
1059
  kv: "tinycloud.kv",
1060
+ space: "tinycloud.space",
1048
1061
  sql: "tinycloud.sql",
1049
1062
  duckdb: "tinycloud.duckdb",
1050
1063
  capabilities: "tinycloud.capabilities",
@@ -4245,9 +4258,9 @@ var init_authority = __esm({
4245
4258
 
4246
4259
  // src/state.ts
4247
4260
  function processWideContext(key) {
4248
- var _a4;
4261
+ var _a5;
4249
4262
  const registry = globalThis;
4250
- return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new import_node_async_hooks.AsyncLocalStorage());
4263
+ return registry[_a5 = Symbol.for(key)] ?? (registry[_a5] = new import_node_async_hooks.AsyncLocalStorage());
4251
4264
  }
4252
4265
  function processOrigin() {
4253
4266
  if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
@@ -4418,9 +4431,14 @@ function tinycloudConfigPath() {
4418
4431
  function profilesPath() {
4419
4432
  return (0, import_node_path2.join)(tinycloudHomePath(), "profiles");
4420
4433
  }
4421
- function profilePath(profile) {
4434
+ function namedProfilePath(profile) {
4422
4435
  return (0, import_node_path2.join)(profilesPath(), validateProfileName(profile));
4423
4436
  }
4437
+ function profilePath(profile) {
4438
+ const named = namedProfilePath(profile);
4439
+ const pinned = pinnedProfiles.getStore()?.get(named);
4440
+ return pinned ? livePinnedPath(profile, pinned) : named;
4441
+ }
4424
4442
  function profileConfigPath(profile) {
4425
4443
  return (0, import_node_path2.join)(profilePath(profile), "profile.json");
4426
4444
  }
@@ -4437,10 +4455,12 @@ function profileStoreMetadataPath(profile, store) {
4437
4455
  return `${profileStorePath(profile, store)}.metadata.json`;
4438
4456
  }
4439
4457
  function profileLockPath(profile) {
4440
- return (0, import_node_path2.join)(profilePath(profile), ".lock");
4458
+ return (0, import_node_path2.join)(namedProfilePath(profile), ".lock");
4441
4459
  }
4442
4460
  function profileTurnLockPath(profile) {
4443
- return (0, import_node_path2.join)(tinycloudHomePath(), "profile-locks", validateProfileName(profile));
4461
+ const home = tinycloudHomePath();
4462
+ const pinned = pinnedHomes.getStore()?.get(home);
4463
+ return (0, import_node_path2.join)(pinned ? livePinnedPath(profile, pinned) : home, "profile-locks", validateProfileName(profile));
4444
4464
  }
4445
4465
  async function readJson(filePath) {
4446
4466
  try {
@@ -4450,6 +4470,44 @@ async function readJson(filePath) {
4450
4470
  throw error;
4451
4471
  }
4452
4472
  }
4473
+ async function readProfileJson(profile, filePath) {
4474
+ const before = process.platform !== "linux" ? await (0, import_promises.lstat)(filePath, { bigint: true }).catch((error) => {
4475
+ if (error.code === "ENOENT") return null;
4476
+ throw error;
4477
+ }) : null;
4478
+ if (process.platform !== "linux" && !before) return null;
4479
+ if (before && !before.isFile()) throw new ProfileSymlinkError(profile);
4480
+ const dev = before && exactFileId(before.dev);
4481
+ const ino = before && exactFileId(before.ino);
4482
+ try {
4483
+ const handle = await (0, import_promises.open)(filePath, import_node_fs.constants.O_RDONLY | (import_node_fs.constants.O_NOFOLLOW ?? 0));
4484
+ try {
4485
+ if (before) {
4486
+ const opened = await handle.stat({ bigint: true });
4487
+ if (!opened.isFile() || dev === null || ino === null || dev !== exactFileId(opened.dev) || ino !== exactFileId(opened.ino)) {
4488
+ throw new ProfileSymlinkError(profile, "changed while opening its file");
4489
+ }
4490
+ }
4491
+ const contents = await handle.readFile("utf8");
4492
+ if (before) {
4493
+ const named = await (0, import_promises.lstat)(filePath, { bigint: true }).catch((error) => {
4494
+ if (error.code === "ENOENT") return null;
4495
+ throw error;
4496
+ });
4497
+ if (!named?.isFile() || dev !== exactFileId(named.dev) || ino !== exactFileId(named.ino)) {
4498
+ throw new ProfileSymlinkError(profile, "changed while reading its file");
4499
+ }
4500
+ }
4501
+ return JSON.parse(contents);
4502
+ } finally {
4503
+ await handle.close();
4504
+ }
4505
+ } catch (error) {
4506
+ if (isErrno(error, "ENOENT")) return null;
4507
+ if (isErrno(error, "ELOOP") || isErrno(error, "ENOTDIR")) throw new ProfileSymlinkError(profile);
4508
+ throw error;
4509
+ }
4510
+ }
4453
4511
  async function writeJsonAtomic(filePath, value) {
4454
4512
  await replaceJson(filePath, value, false);
4455
4513
  }
@@ -4483,11 +4541,14 @@ async function writeFileDurably(path, contents) {
4483
4541
  }
4484
4542
  }
4485
4543
  async function readProfile(profile) {
4486
- return readJson(profileConfigPath(profile));
4544
+ return withPinnedProfileDirectory(profile, (directory) => directory ? readProfileJson(profile, (0, import_node_path2.join)(directory, "profile.json")) : Promise.resolve(null));
4487
4545
  }
4488
4546
  async function readSession(profile) {
4489
- await readStoreMetadata(profile, "session");
4490
- return readJson(sessionPath(profile));
4547
+ return withPinnedProfileDirectory(profile, async (directory) => {
4548
+ if (!directory) return null;
4549
+ await readStoreMetadata(profile, "session");
4550
+ return readProfileJson(profile, sessionPath(profile));
4551
+ });
4491
4552
  }
4492
4553
  async function readAdditionalDelegations(profile) {
4493
4554
  return (await readProfileStore(profile, "additional-delegations")).records;
@@ -4496,43 +4557,187 @@ async function readAuthRequests(profile) {
4496
4557
  return (await readProfileStore(profile, "auth-requests")).records;
4497
4558
  }
4498
4559
  async function readStoreMetadata(profile, store) {
4499
- const metadata = await readJson(profileStoreMetadataPath(profile, store));
4500
- if (metadata === null) return { formatVersion: 1 };
4501
- if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
4502
- throw new TypeError(`Unsupported store format for "${store}".`);
4503
- }
4504
- return metadata;
4560
+ return withPinnedProfileDirectory(profile, async (directory) => {
4561
+ const metadata = directory ? await readProfileJson(profile, profileStoreMetadataPath(profile, store)) : null;
4562
+ if (metadata === null) return { formatVersion: 1 };
4563
+ if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
4564
+ throw new TypeError(`Unsupported store format for "${store}".`);
4565
+ }
4566
+ return metadata;
4567
+ });
4505
4568
  }
4506
4569
  async function readProfileStore(profile, store) {
4507
- const [metadata, raw] = await Promise.all([
4508
- readStoreMetadata(profile, store),
4509
- readJson(profileStorePath(profile, store))
4510
- ]);
4511
- return {
4512
- formatVersion: metadata.formatVersion,
4513
- records: Array.isArray(raw) ? raw : []
4514
- };
4570
+ return withPinnedProfileDirectory(profile, async (directory) => {
4571
+ if (!directory) return { formatVersion: 1, records: [] };
4572
+ const [metadata, raw] = await Promise.all([
4573
+ readStoreMetadata(profile, store),
4574
+ readProfileJson(profile, profileStorePath(profile, store))
4575
+ ]);
4576
+ return {
4577
+ formatVersion: metadata.formatVersion,
4578
+ records: Array.isArray(raw) ? raw : []
4579
+ };
4580
+ });
4515
4581
  }
4516
- async function withProfileLock(profile, action, options = {}) {
4517
- const normalizedProfile = validateProfileName(profile);
4518
- const lockPath = profileLockPath(normalizedProfile);
4519
- const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
4520
- if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
4521
- const acquired = await acquireProfileLock(normalizedProfile, options);
4522
- const ownership = {
4523
- lockPath,
4524
- turnSlot: acquired.turnSlot,
4525
- deletedWhileWaiting: acquired.deletedWhileWaiting,
4526
- active: true
4527
- };
4582
+ function exactFileId(value) {
4583
+ if (typeof value === "bigint") return value >= 0n ? value : null;
4584
+ return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
4585
+ }
4586
+ function isPinnedLive(pinned) {
4587
+ if (!pinned.active) return false;
4588
+ try {
4589
+ const stats = (0, import_node_fs.fstatSync)(pinned.fd, { bigint: true });
4590
+ return exactFileId(stats.dev) === pinned.dev && exactFileId(stats.ino) === pinned.ino;
4591
+ } catch {
4592
+ return false;
4593
+ }
4594
+ }
4595
+ function livePinnedPath(profile, pinned) {
4596
+ if (isPinnedLive(pinned)) return pinned.path;
4597
+ throw new ProfileSymlinkError(profile, "its pinned directory is no longer active or changed during use");
4598
+ }
4599
+ async function withPinnedProfileDirectory(profile, action, options = {}) {
4600
+ const named = namedProfilePath(profile);
4601
+ const existing = pinnedProfiles.getStore()?.get(named);
4602
+ if (existing) return action(livePinnedPath(profile, existing));
4603
+ if (process.platform !== "linux") {
4604
+ if (options.create) await (0, import_promises.mkdir)(named, { recursive: true, mode: PRIVATE_DIR_MODE });
4605
+ const stats = await (0, import_promises.lstat)(named).catch((error) => {
4606
+ if (error.code === "ENOENT") return null;
4607
+ throw error;
4608
+ });
4609
+ if (!stats) return action(null);
4610
+ if (!stats.isDirectory() || stats.isSymbolicLink()) throw new ProfileSymlinkError(profile);
4611
+ return action(named);
4612
+ }
4613
+ const home = tinycloudHomePath();
4614
+ const handles = [];
4615
+ const fdPath = (fd) => `/proc/self/fd/${fd}`;
4616
+ const flags = import_node_fs.constants.O_RDONLY | import_node_fs.constants.O_DIRECTORY | import_node_fs.constants.O_NOFOLLOW;
4528
4617
  try {
4529
- return await heldProfileLocks.run([...held, ownership], action);
4618
+ if (options.create) await (0, import_promises.mkdir)(home, { recursive: true, mode: PRIVATE_DIR_MODE });
4619
+ const resolved = canonicalHomes.get(home) ?? await (0, import_promises.realpath)(home).catch((error) => {
4620
+ if (error.code === "ENOENT" && !options.create) return null;
4621
+ if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
4622
+ throw error;
4623
+ });
4624
+ if (!resolved) return await action(null);
4625
+ const root = canonicalHomes.get(home) ?? resolved;
4626
+ canonicalHomes.set(home, root);
4627
+ const segments = [root, "profiles", validateProfileName(profile)];
4628
+ for (let index = 0; index < segments.length; index++) {
4629
+ const path = handles.length ? (0, import_node_path2.join)(fdPath(handles[handles.length - 1].fd), segments[index]) : segments[index];
4630
+ if (options.create && handles.length) {
4631
+ await (0, import_promises.mkdir)(path, { mode: PRIVATE_DIR_MODE }).catch((error) => {
4632
+ if (error.code !== "EEXIST") throw error;
4633
+ });
4634
+ }
4635
+ const handle = await (0, import_promises.open)(path, index === 1 ? flags & ~import_node_fs.constants.O_NOFOLLOW : flags).catch((error) => {
4636
+ if (error.code === "ENOENT" && !options.create) return null;
4637
+ if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
4638
+ throw error;
4639
+ });
4640
+ if (!handle) return await action(null);
4641
+ handles.push(handle);
4642
+ if (options.create) await handle.chmod(PRIVATE_DIR_MODE);
4643
+ }
4644
+ const opened = handles[2];
4645
+ const [actual, namedStats] = await Promise.all([opened.stat({ bigint: true }), (0, import_promises.lstat)(named, { bigint: true })]);
4646
+ const dev = exactFileId(actual.dev);
4647
+ const ino = exactFileId(actual.ino);
4648
+ if (!namedStats.isDirectory() || dev === null || ino === null || dev !== exactFileId(namedStats.dev) || ino !== exactFileId(namedStats.ino)) {
4649
+ throw new ProfileSymlinkError(profile, "changed while opening its directory");
4650
+ }
4651
+ const directory = fdPath(opened.fd);
4652
+ const pinned = {
4653
+ path: directory,
4654
+ fd: opened.fd,
4655
+ dev,
4656
+ ino,
4657
+ active: true
4658
+ };
4659
+ const homeHandle = handles[0];
4660
+ const homeStats = await homeHandle.stat({ bigint: true });
4661
+ const homeDev = exactFileId(homeStats.dev);
4662
+ const homeIno = exactFileId(homeStats.ino);
4663
+ if (homeDev === null || homeIno === null) throw new ProfileSymlinkError(profile, "has an unverified home directory identity");
4664
+ const homePinned = {
4665
+ path: fdPath(homeHandle.fd),
4666
+ fd: homeHandle.fd,
4667
+ dev: homeDev,
4668
+ ino: homeIno,
4669
+ active: true
4670
+ };
4671
+ const context = new Map(pinnedProfiles.getStore());
4672
+ context.set(named, pinned);
4673
+ const homes = new Map(pinnedHomes.getStore());
4674
+ homes.set(home, homePinned);
4675
+ try {
4676
+ return await pinnedHomes.run(homes, () => pinnedProfiles.run(context, () => action(directory)));
4677
+ } finally {
4678
+ pinned.active = false;
4679
+ homePinned.active = false;
4680
+ }
4530
4681
  } finally {
4531
- ownership.active = false;
4532
- await acquired.release();
4533
- if (acquired.deletedWhileWaiting) await (0, import_promises.rmdir)(profilePath(normalizedProfile)).catch(() => void 0);
4682
+ for (const handle of handles.reverse()) await handle.close();
4534
4683
  }
4535
4684
  }
4685
+ async function assertRealProfileDirectory(profile) {
4686
+ const named = namedProfilePath(profile);
4687
+ const pinned = pinnedProfiles.getStore()?.get(named);
4688
+ if (pinned) {
4689
+ livePinnedPath(profile, pinned);
4690
+ return;
4691
+ }
4692
+ const stats = await (0, import_promises.lstat)(named).catch((error) => {
4693
+ if (error.code === "ENOENT") return null;
4694
+ throw error;
4695
+ });
4696
+ if (stats && (!stats.isDirectory() || stats.isSymbolicLink())) throw new ProfileSymlinkError(profile);
4697
+ }
4698
+ async function withProfileLock(profile, action, options = {}) {
4699
+ const normalizedProfile = validateProfileName(profile);
4700
+ const lockPath = profileLockPath(normalizedProfile);
4701
+ const held = (heldProfileLocks.getStore() ?? []).filter((ownership) => ownership.active);
4702
+ if (held.some((ownership) => ownership.lockPath === lockPath)) {
4703
+ await assertRealProfileDirectory(normalizedProfile);
4704
+ return action();
4705
+ }
4706
+ const named = namedProfilePath(normalizedProfile);
4707
+ const inherited = pinnedProfiles.getStore()?.get(named);
4708
+ if (inherited && !isPinnedLive(inherited)) {
4709
+ const profiles = new Map(pinnedProfiles.getStore());
4710
+ profiles.delete(named);
4711
+ const homes = new Map(pinnedHomes.getStore());
4712
+ homes.delete(tinycloudHomePath());
4713
+ return pinnedHomes.run(homes, () => pinnedProfiles.run(profiles, () => withProfileLock(normalizedProfile, action, options)));
4714
+ }
4715
+ return withPinnedProfileDirectory(normalizedProfile, async (directory) => {
4716
+ if (!directory) throw new ProfileDeletedError(normalizedProfile);
4717
+ const acquired = await acquireProfileLock(normalizedProfile, options);
4718
+ const ownership = {
4719
+ lockPath,
4720
+ turnSlot: acquired.turnSlot,
4721
+ deletedWhileWaiting: acquired.deletedWhileWaiting,
4722
+ active: true
4723
+ };
4724
+ try {
4725
+ return await heldProfileLocks.run([...held, ownership], action);
4726
+ } finally {
4727
+ ownership.active = false;
4728
+ await acquired.release();
4729
+ if (ownership.deletedProfile || acquired.deletedWhileWaiting) {
4730
+ const current = await (0, import_promises.lstat)(namedProfilePath(normalizedProfile), { bigint: true }).catch(() => null);
4731
+ const original = await (0, import_promises.stat)(directory, { bigint: true }).catch(() => null);
4732
+ const currentDev = current && exactFileId(current.dev);
4733
+ const currentIno = current && exactFileId(current.ino);
4734
+ if (currentDev !== null && currentIno !== null && original && currentDev === exactFileId(original.dev) && currentIno === exactFileId(original.ino)) {
4735
+ await (0, import_promises.rmdir)(namedProfilePath(normalizedProfile)).catch(() => void 0);
4736
+ }
4737
+ }
4738
+ }
4739
+ }, { create: !options.requireProfile });
4740
+ }
4536
4741
  async function refuseWriteToDeletedProfile(profile) {
4537
4742
  const lockPath = profileLockPath(profile);
4538
4743
  const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);
@@ -4565,17 +4770,15 @@ async function acquireProfileLock(profile, options) {
4565
4770
  retryMs: options.retryMs ?? DEFAULT_LOCK_RETRY_MS
4566
4771
  };
4567
4772
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
4568
- const lockPath = profileLockPath(profile);
4773
+ const lockPath = (0, import_node_path2.join)(profilePath(profile), ".lock");
4569
4774
  const turnDirectory = profileTurnLockPath(profile);
4570
4775
  const directory = profilePath(profile);
4776
+ await assertRealProfileDirectory(profile);
4571
4777
  if (options.requireProfile && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
4572
4778
  if (!options.requireProfile) await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
4779
+ await assertRealProfileDirectory(profile);
4573
4780
  await (0, import_promises.mkdir)((0, import_node_path2.dirname)(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
4574
- for (const path of [tinycloudHomePath(), profilesPath(), directory]) {
4575
- await (0, import_promises.chmod)(path, PRIVATE_DIR_MODE).catch((error) => {
4576
- if (path !== directory || !isErrno(error, "ENOENT")) throw error;
4577
- });
4578
- }
4781
+ await (0, import_promises.chmod)(directory, PRIVATE_DIR_MODE);
4579
4782
  const liveness = { directory: turnDirectory, held: void 0 };
4580
4783
  const closeAcquisitionLiveness = async () => {
4581
4784
  if (liveness.held) await closeLiveness(liveness.held);
@@ -4595,6 +4798,7 @@ async function acquireProfileLock(profile, options) {
4595
4798
  }
4596
4799
  const deletion = await readJson((0, import_node_path2.join)(turnDirectory, "deleted.json")).catch(() => null);
4597
4800
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
4801
+ if (deletedWhileWaiting && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
4598
4802
  while (true) {
4599
4803
  const token = (0, import_node_crypto.randomUUID)();
4600
4804
  if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
@@ -4740,7 +4944,7 @@ async function takeTurn(profile, directory, latest, liveness) {
4740
4944
  if (published) await markTurn(path, token, "done");
4741
4945
  return null;
4742
4946
  } catch (error) {
4743
- if (published) settleOwnTurn(directory, slot, token);
4947
+ if (published) await settleOwnTurn(directory, slot, token);
4744
4948
  throw error;
4745
4949
  }
4746
4950
  }
@@ -4751,17 +4955,25 @@ async function settleAbandonedTurn(directory, abandoned) {
4751
4955
  }
4752
4956
  await markTurn(path, abandoned.token, "done");
4753
4957
  }
4754
- function settleOwnTurn(directory, slot, token, retries = SETTLE_RETRIES) {
4958
+ async function settleOwnTurn(directory, slot, token) {
4755
4959
  relinquishedTurns.add(token);
4756
- void (async () => {
4757
- try {
4758
- const turn = await readTurn(directory, slot);
4759
- if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(directory, turn);
4760
- relinquishedTurns.delete(token);
4761
- } catch {
4762
- if (retries > 0) setTimeout(() => settleOwnTurn(directory, slot, token, retries - 1), SETTLE_RETRY_MS).unref();
4763
- }
4764
- })();
4960
+ const pinned = await pinDirectory(directory).catch(() => null);
4961
+ const path = pinned?.path(".") ?? directory;
4962
+ let retries = SETTLE_RETRIES;
4963
+ const attempt = () => {
4964
+ void (async () => {
4965
+ try {
4966
+ const turn = await readTurn(path, slot);
4967
+ if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(path, turn);
4968
+ relinquishedTurns.delete(token);
4969
+ await pinned?.close();
4970
+ } catch {
4971
+ if (retries-- > 0) setTimeout(attempt, SETTLE_RETRY_MS).unref();
4972
+ else await pinned?.close();
4973
+ }
4974
+ })();
4975
+ };
4976
+ attempt();
4765
4977
  }
4766
4978
  async function markTurn(path, token, kind) {
4767
4979
  for (let attempt = 0; ; attempt++) {
@@ -4806,7 +5018,7 @@ async function releaseTurn(profile, turn, liveness) {
4806
5018
  try {
4807
5019
  await markTurn((0, import_node_path2.join)(turn.directory, String(turn.slot)), turn.token, "done");
4808
5020
  } catch {
4809
- settleOwnTurn(turn.directory, turn.slot, turn.token);
5021
+ await settleOwnTurn(turn.directory, turn.slot, turn.token);
4810
5022
  return;
4811
5023
  }
4812
5024
  await collectTurns(profile, turn, liveness).catch(() => void 0);
@@ -5090,7 +5302,7 @@ function isErrno(error, code) {
5090
5302
  function sleep(milliseconds) {
5091
5303
  return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
5092
5304
  }
5093
- var import_node_crypto, import_node_async_hooks, import_node_fs, import_promises, import_node_net, import_node_os, import_node_path2, 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;
5305
+ var import_node_crypto, import_node_async_hooks, import_node_fs, import_promises, import_node_net, import_node_os, import_node_path2, 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;
5094
5306
  var init_state = __esm({
5095
5307
  "src/state.ts"() {
5096
5308
  "use strict";
@@ -5122,7 +5334,10 @@ var init_state = __esm({
5122
5334
  TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
5123
5335
  invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
5124
5336
  heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
5125
- processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = (0, import_node_crypto.randomUUID)());
5337
+ pinnedProfiles = processWideContext("tinycloud.operations.pinnedProfiles.v2");
5338
+ pinnedHomes = processWideContext("tinycloud.operations.pinnedHomes.v2");
5339
+ canonicalHomes = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.canonicalHomes.v1")] ?? (globalThis[_a2] = /* @__PURE__ */ new Map());
5340
+ processInstanceId = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a3] = (0, import_node_crypto.randomUUID)());
5126
5341
  LIVENESS_SOCKET = /^\.live-([0-9a-f]+)-([0-9a-f]+)-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(\.new)?$/;
5127
5342
  LIVENESS_PROBE_TIMEOUT_MS = 1e3;
5128
5343
  ProfileLockTimeoutError = class extends Error {
@@ -5140,8 +5355,15 @@ var init_state = __esm({
5140
5355
  this.name = "ProfileDeletedError";
5141
5356
  }
5142
5357
  };
5358
+ ProfileSymlinkError = class extends Error {
5359
+ constructor(profile, detail = "must be a real directory, not a symlink or another file type") {
5360
+ super(`Profile "${profile}" ${detail}.`);
5361
+ this.code = "PROFILE_SYMLINK";
5362
+ this.name = "ProfileSymlinkError";
5363
+ }
5364
+ };
5143
5365
  TURN = /^(?:0|[1-9][0-9]*)$/;
5144
- relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
5366
+ relinquishedTurns = globalThis[_a4 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a4] = /* @__PURE__ */ new Set());
5145
5367
  MARKER_RETRY_DELAYS_MS = [10, 50, 250];
5146
5368
  SETTLE_RETRY_MS = 1e3;
5147
5369
  SETTLE_RETRIES = 300;
@@ -6325,6 +6547,8 @@ function appsFailure(error) {
6325
6547
  if (auth?.status === "error") return auth.error;
6326
6548
  const storage = storageFullOperationError(error);
6327
6549
  if (storage) return storage;
6550
+ const nodeRetry = nodeRetryOperationError(error);
6551
+ if (nodeRetry) return nodeRetry;
6328
6552
  const code = typeof error === "object" && error !== null ? error.code : void 0;
6329
6553
  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" } });
6330
6554
  if (isOperationErrorCode(code)) return operationError(code, "The node could not complete this personal-record operation.");
@@ -7798,16 +8022,8 @@ function kvFailure(error, action) {
7798
8022
  if (error.code === "KV_PRECONDITION_FAILED") {
7799
8023
  return { status: "error", error: operationError("KV_PRECONDITION_FAILED", "The TinyCloud KV object changed or already exists.") };
7800
8024
  }
7801
- if (error.code === "KV_CONFLICT") {
7802
- return {
7803
- status: "error",
7804
- error: operationError(
7805
- "KV_CONFLICT",
7806
- "A database serialization conflict prevented the conditional KV mutation; re-read the key state, which may still be absent, before deciding whether to retry.",
7807
- { retryable: true }
7808
- )
7809
- };
7810
- }
8025
+ const nodeRetry = nodeRetryOperationError(error);
8026
+ if (nodeRetry) return { status: "error", error: nodeRetry };
7811
8027
  if (error.code === "KV_RESPONSE_TOO_LARGE") {
7812
8028
  return { status: "error", error: operationError("KV_RESPONSE_TOO_LARGE", `The TinyCloud KV value exceeds ${MAX_KV_VALUE_BYTES} bytes.`) };
7813
8029
  }