@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.
package/coverage.json CHANGED
@@ -65,6 +65,7 @@
65
65
  { "command": "share list", "status": "legacy" },
66
66
  { "command": "share revoke <id>", "status": "legacy" },
67
67
  { "command": "share show <id>", "status": "legacy" },
68
+ { "command": "share versions <artifact-id>", "status": "legacy" },
68
69
  { "command": "node", "status": "legacy" },
69
70
  { "command": "node health", "status": "legacy" },
70
71
  { "command": "node version", "status": "legacy" },
@@ -176,6 +176,7 @@ var import_sdk_services = require("@tinycloud/sdk-services");
176
176
  var DEFAULT_MANIFEST_SPACE = "applications";
177
177
  var SERVICE_SHORT_TO_LONG = Object.freeze({
178
178
  kv: "tinycloud.kv",
179
+ space: "tinycloud.space",
179
180
  sql: "tinycloud.sql",
180
181
  duckdb: "tinycloud.duckdb",
181
182
  capabilities: "tinycloud.capabilities",
@@ -430,14 +431,18 @@ var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIE
430
431
  var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
431
432
  var TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
432
433
  function processWideContext(key) {
433
- var _a3;
434
+ var _a4;
434
435
  const registry = globalThis;
435
- return registry[_a3 = Symbol.for(key)] ?? (registry[_a3] = new import_node_async_hooks.AsyncLocalStorage());
436
+ return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new import_node_async_hooks.AsyncLocalStorage());
436
437
  }
437
438
  var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
438
439
  var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
440
+ var pinnedProfiles = processWideContext("tinycloud.operations.pinnedProfiles.v2");
441
+ var pinnedHomes = processWideContext("tinycloud.operations.pinnedHomes.v2");
439
442
  var _a;
440
- var processInstanceId = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a] = (0, import_node_crypto.randomUUID)());
443
+ var canonicalHomes = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.canonicalHomes.v1")] ?? (globalThis[_a] = /* @__PURE__ */ new Map());
444
+ var _a2;
445
+ var processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = (0, import_node_crypto.randomUUID)());
441
446
  var cachedProcessOrigin;
442
447
  function processOrigin() {
443
448
  if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
@@ -615,9 +620,14 @@ function tinycloudHomePath() {
615
620
  function profilesPath() {
616
621
  return (0, import_node_path.join)(tinycloudHomePath(), "profiles");
617
622
  }
618
- function profilePath(profile) {
623
+ function namedProfilePath(profile) {
619
624
  return (0, import_node_path.join)(profilesPath(), validateProfileName(profile));
620
625
  }
626
+ function profilePath(profile) {
627
+ const named = namedProfilePath(profile);
628
+ const pinned = pinnedProfiles.getStore()?.get(named);
629
+ return pinned ? livePinnedPath(profile, pinned) : named;
630
+ }
621
631
  function profileConfigPath(profile) {
622
632
  return (0, import_node_path.join)(profilePath(profile), "profile.json");
623
633
  }
@@ -628,10 +638,12 @@ function profileStoreMetadataPath(profile, store) {
628
638
  return `${profileStorePath(profile, store)}.metadata.json`;
629
639
  }
630
640
  function profileLockPath(profile) {
631
- return (0, import_node_path.join)(profilePath(profile), ".lock");
641
+ return (0, import_node_path.join)(namedProfilePath(profile), ".lock");
632
642
  }
633
643
  function profileTurnLockPath(profile) {
634
- return (0, import_node_path.join)(tinycloudHomePath(), "profile-locks", validateProfileName(profile));
644
+ const home = tinycloudHomePath();
645
+ const pinned = pinnedHomes.getStore()?.get(home);
646
+ return (0, import_node_path.join)(pinned ? livePinnedPath(profile, pinned) : home, "profile-locks", validateProfileName(profile));
635
647
  }
636
648
  async function readJson(filePath) {
637
649
  try {
@@ -641,6 +653,44 @@ async function readJson(filePath) {
641
653
  throw error;
642
654
  }
643
655
  }
656
+ async function readProfileJson(profile, filePath) {
657
+ const before = process.platform !== "linux" ? await (0, import_promises.lstat)(filePath, { bigint: true }).catch((error) => {
658
+ if (error.code === "ENOENT") return null;
659
+ throw error;
660
+ }) : null;
661
+ if (process.platform !== "linux" && !before) return null;
662
+ if (before && !before.isFile()) throw new ProfileSymlinkError(profile);
663
+ const dev = before && exactFileId(before.dev);
664
+ const ino = before && exactFileId(before.ino);
665
+ try {
666
+ const handle = await (0, import_promises.open)(filePath, import_node_fs.constants.O_RDONLY | (import_node_fs.constants.O_NOFOLLOW ?? 0));
667
+ try {
668
+ if (before) {
669
+ const opened = await handle.stat({ bigint: true });
670
+ if (!opened.isFile() || dev === null || ino === null || dev !== exactFileId(opened.dev) || ino !== exactFileId(opened.ino)) {
671
+ throw new ProfileSymlinkError(profile, "changed while opening its file");
672
+ }
673
+ }
674
+ const contents = await handle.readFile("utf8");
675
+ if (before) {
676
+ const named = await (0, import_promises.lstat)(filePath, { bigint: true }).catch((error) => {
677
+ if (error.code === "ENOENT") return null;
678
+ throw error;
679
+ });
680
+ if (!named?.isFile() || dev !== exactFileId(named.dev) || ino !== exactFileId(named.ino)) {
681
+ throw new ProfileSymlinkError(profile, "changed while reading its file");
682
+ }
683
+ }
684
+ return JSON.parse(contents);
685
+ } finally {
686
+ await handle.close();
687
+ }
688
+ } catch (error) {
689
+ if (isErrno(error, "ENOENT")) return null;
690
+ if (isErrno(error, "ELOOP") || isErrno(error, "ENOTDIR")) throw new ProfileSymlinkError(profile);
691
+ throw error;
692
+ }
693
+ }
644
694
  async function writeJsonAtomic(filePath, value) {
645
695
  await replaceJson(filePath, value, false);
646
696
  }
@@ -677,43 +727,194 @@ async function readAuthRequests(profile) {
677
727
  return (await readProfileStore(profile, "auth-requests")).records;
678
728
  }
679
729
  async function readStoreMetadata(profile, store) {
680
- const metadata = await readJson(profileStoreMetadataPath(profile, store));
681
- if (metadata === null) return { formatVersion: 1 };
682
- if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
683
- throw new TypeError(`Unsupported store format for "${store}".`);
684
- }
685
- return metadata;
730
+ return withPinnedProfileDirectory(profile, async (directory) => {
731
+ const metadata = directory ? await readProfileJson(profile, profileStoreMetadataPath(profile, store)) : null;
732
+ if (metadata === null) return { formatVersion: 1 };
733
+ if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
734
+ throw new TypeError(`Unsupported store format for "${store}".`);
735
+ }
736
+ return metadata;
737
+ });
686
738
  }
687
739
  async function readProfileStore(profile, store) {
688
- const [metadata, raw] = await Promise.all([
689
- readStoreMetadata(profile, store),
690
- readJson(profileStorePath(profile, store))
691
- ]);
692
- return {
693
- formatVersion: metadata.formatVersion,
694
- records: Array.isArray(raw) ? raw : []
695
- };
740
+ return withPinnedProfileDirectory(profile, async (directory) => {
741
+ if (!directory) return { formatVersion: 1, records: [] };
742
+ const [metadata, raw] = await Promise.all([
743
+ readStoreMetadata(profile, store),
744
+ readProfileJson(profile, profileStorePath(profile, store))
745
+ ]);
746
+ return {
747
+ formatVersion: metadata.formatVersion,
748
+ records: Array.isArray(raw) ? raw : []
749
+ };
750
+ });
696
751
  }
697
- async function withProfileLock(profile, action, options = {}) {
698
- const normalizedProfile = validateProfileName(profile);
699
- const lockPath = profileLockPath(normalizedProfile);
700
- const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
701
- if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
702
- const acquired = await acquireProfileLock(normalizedProfile, options);
703
- const ownership = {
704
- lockPath,
705
- turnSlot: acquired.turnSlot,
706
- deletedWhileWaiting: acquired.deletedWhileWaiting,
707
- active: true
708
- };
752
+ var ProfileSymlinkError = class extends Error {
753
+ constructor(profile, detail = "must be a real directory, not a symlink or another file type") {
754
+ super(`Profile "${profile}" ${detail}.`);
755
+ this.code = "PROFILE_SYMLINK";
756
+ this.name = "ProfileSymlinkError";
757
+ }
758
+ };
759
+ function exactFileId(value) {
760
+ if (typeof value === "bigint") return value >= 0n ? value : null;
761
+ return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
762
+ }
763
+ function isPinnedLive(pinned) {
764
+ if (!pinned.active) return false;
709
765
  try {
710
- return await heldProfileLocks.run([...held, ownership], action);
766
+ const stats = (0, import_node_fs.fstatSync)(pinned.fd, { bigint: true });
767
+ return exactFileId(stats.dev) === pinned.dev && exactFileId(stats.ino) === pinned.ino;
768
+ } catch {
769
+ return false;
770
+ }
771
+ }
772
+ function livePinnedPath(profile, pinned) {
773
+ if (isPinnedLive(pinned)) return pinned.path;
774
+ throw new ProfileSymlinkError(profile, "its pinned directory is no longer active or changed during use");
775
+ }
776
+ async function withPinnedProfileDirectory(profile, action, options = {}) {
777
+ const named = namedProfilePath(profile);
778
+ const existing = pinnedProfiles.getStore()?.get(named);
779
+ if (existing) return action(livePinnedPath(profile, existing));
780
+ if (process.platform !== "linux") {
781
+ if (options.create) await (0, import_promises.mkdir)(named, { recursive: true, mode: PRIVATE_DIR_MODE });
782
+ const stats = await (0, import_promises.lstat)(named).catch((error) => {
783
+ if (error.code === "ENOENT") return null;
784
+ throw error;
785
+ });
786
+ if (!stats) return action(null);
787
+ if (!stats.isDirectory() || stats.isSymbolicLink()) throw new ProfileSymlinkError(profile);
788
+ return action(named);
789
+ }
790
+ const home = tinycloudHomePath();
791
+ const handles = [];
792
+ const fdPath = (fd) => `/proc/self/fd/${fd}`;
793
+ const flags = import_node_fs.constants.O_RDONLY | import_node_fs.constants.O_DIRECTORY | import_node_fs.constants.O_NOFOLLOW;
794
+ try {
795
+ if (options.create) await (0, import_promises.mkdir)(home, { recursive: true, mode: PRIVATE_DIR_MODE });
796
+ const resolved = canonicalHomes.get(home) ?? await (0, import_promises.realpath)(home).catch((error) => {
797
+ if (error.code === "ENOENT" && !options.create) return null;
798
+ if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
799
+ throw error;
800
+ });
801
+ if (!resolved) return await action(null);
802
+ const root = canonicalHomes.get(home) ?? resolved;
803
+ canonicalHomes.set(home, root);
804
+ const segments = [root, "profiles", validateProfileName(profile)];
805
+ for (let index = 0; index < segments.length; index++) {
806
+ const path = handles.length ? (0, import_node_path.join)(fdPath(handles[handles.length - 1].fd), segments[index]) : segments[index];
807
+ if (options.create && handles.length) {
808
+ await (0, import_promises.mkdir)(path, { mode: PRIVATE_DIR_MODE }).catch((error) => {
809
+ if (error.code !== "EEXIST") throw error;
810
+ });
811
+ }
812
+ const handle = await (0, import_promises.open)(path, index === 1 ? flags & ~import_node_fs.constants.O_NOFOLLOW : flags).catch((error) => {
813
+ if (error.code === "ENOENT" && !options.create) return null;
814
+ if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
815
+ throw error;
816
+ });
817
+ if (!handle) return await action(null);
818
+ handles.push(handle);
819
+ if (options.create) await handle.chmod(PRIVATE_DIR_MODE);
820
+ }
821
+ const opened = handles[2];
822
+ const [actual, namedStats] = await Promise.all([opened.stat({ bigint: true }), (0, import_promises.lstat)(named, { bigint: true })]);
823
+ const dev = exactFileId(actual.dev);
824
+ const ino = exactFileId(actual.ino);
825
+ if (!namedStats.isDirectory() || dev === null || ino === null || dev !== exactFileId(namedStats.dev) || ino !== exactFileId(namedStats.ino)) {
826
+ throw new ProfileSymlinkError(profile, "changed while opening its directory");
827
+ }
828
+ const directory = fdPath(opened.fd);
829
+ const pinned = {
830
+ path: directory,
831
+ fd: opened.fd,
832
+ dev,
833
+ ino,
834
+ active: true
835
+ };
836
+ const homeHandle = handles[0];
837
+ const homeStats = await homeHandle.stat({ bigint: true });
838
+ const homeDev = exactFileId(homeStats.dev);
839
+ const homeIno = exactFileId(homeStats.ino);
840
+ if (homeDev === null || homeIno === null) throw new ProfileSymlinkError(profile, "has an unverified home directory identity");
841
+ const homePinned = {
842
+ path: fdPath(homeHandle.fd),
843
+ fd: homeHandle.fd,
844
+ dev: homeDev,
845
+ ino: homeIno,
846
+ active: true
847
+ };
848
+ const context = new Map(pinnedProfiles.getStore());
849
+ context.set(named, pinned);
850
+ const homes = new Map(pinnedHomes.getStore());
851
+ homes.set(home, homePinned);
852
+ try {
853
+ return await pinnedHomes.run(homes, () => pinnedProfiles.run(context, () => action(directory)));
854
+ } finally {
855
+ pinned.active = false;
856
+ homePinned.active = false;
857
+ }
711
858
  } finally {
712
- ownership.active = false;
713
- await acquired.release();
714
- if (acquired.deletedWhileWaiting) await (0, import_promises.rmdir)(profilePath(normalizedProfile)).catch(() => void 0);
859
+ for (const handle of handles.reverse()) await handle.close();
715
860
  }
716
861
  }
862
+ async function assertRealProfileDirectory(profile) {
863
+ const named = namedProfilePath(profile);
864
+ const pinned = pinnedProfiles.getStore()?.get(named);
865
+ if (pinned) {
866
+ livePinnedPath(profile, pinned);
867
+ return;
868
+ }
869
+ const stats = await (0, import_promises.lstat)(named).catch((error) => {
870
+ if (error.code === "ENOENT") return null;
871
+ throw error;
872
+ });
873
+ if (stats && (!stats.isDirectory() || stats.isSymbolicLink())) throw new ProfileSymlinkError(profile);
874
+ }
875
+ async function withProfileLock(profile, action, options = {}) {
876
+ const normalizedProfile = validateProfileName(profile);
877
+ const lockPath = profileLockPath(normalizedProfile);
878
+ const held = (heldProfileLocks.getStore() ?? []).filter((ownership) => ownership.active);
879
+ if (held.some((ownership) => ownership.lockPath === lockPath)) {
880
+ await assertRealProfileDirectory(normalizedProfile);
881
+ return action();
882
+ }
883
+ const named = namedProfilePath(normalizedProfile);
884
+ const inherited = pinnedProfiles.getStore()?.get(named);
885
+ if (inherited && !isPinnedLive(inherited)) {
886
+ const profiles = new Map(pinnedProfiles.getStore());
887
+ profiles.delete(named);
888
+ const homes = new Map(pinnedHomes.getStore());
889
+ homes.delete(tinycloudHomePath());
890
+ return pinnedHomes.run(homes, () => pinnedProfiles.run(profiles, () => withProfileLock(normalizedProfile, action, options)));
891
+ }
892
+ return withPinnedProfileDirectory(normalizedProfile, async (directory) => {
893
+ if (!directory) throw new ProfileDeletedError(normalizedProfile);
894
+ const acquired = await acquireProfileLock(normalizedProfile, options);
895
+ const ownership = {
896
+ lockPath,
897
+ turnSlot: acquired.turnSlot,
898
+ deletedWhileWaiting: acquired.deletedWhileWaiting,
899
+ active: true
900
+ };
901
+ try {
902
+ return await heldProfileLocks.run([...held, ownership], action);
903
+ } finally {
904
+ ownership.active = false;
905
+ await acquired.release();
906
+ if (ownership.deletedProfile || acquired.deletedWhileWaiting) {
907
+ const current = await (0, import_promises.lstat)(namedProfilePath(normalizedProfile), { bigint: true }).catch(() => null);
908
+ const original = await (0, import_promises.stat)(directory, { bigint: true }).catch(() => null);
909
+ const currentDev = current && exactFileId(current.dev);
910
+ const currentIno = current && exactFileId(current.ino);
911
+ if (currentDev !== null && currentIno !== null && original && currentDev === exactFileId(original.dev) && currentIno === exactFileId(original.ino)) {
912
+ await (0, import_promises.rmdir)(namedProfilePath(normalizedProfile)).catch(() => void 0);
913
+ }
914
+ }
915
+ }
916
+ }, { create: !options.requireProfile });
917
+ }
717
918
  async function refuseWriteToDeletedProfile(profile) {
718
919
  const lockPath = profileLockPath(profile);
719
920
  const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);
@@ -746,17 +947,15 @@ async function acquireProfileLock(profile, options) {
746
947
  retryMs: options.retryMs ?? DEFAULT_LOCK_RETRY_MS
747
948
  };
748
949
  const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
749
- const lockPath = profileLockPath(profile);
950
+ const lockPath = (0, import_node_path.join)(profilePath(profile), ".lock");
750
951
  const turnDirectory = profileTurnLockPath(profile);
751
952
  const directory = profilePath(profile);
953
+ await assertRealProfileDirectory(profile);
752
954
  if (options.requireProfile && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
753
955
  if (!options.requireProfile) await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
956
+ await assertRealProfileDirectory(profile);
754
957
  await (0, import_promises.mkdir)((0, import_node_path.dirname)(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
755
- for (const path of [tinycloudHomePath(), profilesPath(), directory]) {
756
- await (0, import_promises.chmod)(path, PRIVATE_DIR_MODE).catch((error) => {
757
- if (path !== directory || !isErrno(error, "ENOENT")) throw error;
758
- });
759
- }
958
+ await (0, import_promises.chmod)(directory, PRIVATE_DIR_MODE);
760
959
  const liveness = { directory: turnDirectory, held: void 0 };
761
960
  const closeAcquisitionLiveness = async () => {
762
961
  if (liveness.held) await closeLiveness(liveness.held);
@@ -776,6 +975,7 @@ async function acquireProfileLock(profile, options) {
776
975
  }
777
976
  const deletion = await readJson((0, import_node_path.join)(turnDirectory, "deleted.json")).catch(() => null);
778
977
  const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
978
+ if (deletedWhileWaiting && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
779
979
  while (true) {
780
980
  const token = (0, import_node_crypto.randomUUID)();
781
981
  if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
@@ -812,8 +1012,8 @@ async function waitOrTimeOut(profile, deadline, detail) {
812
1012
  await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
813
1013
  }
814
1014
  var TURN = /^(?:0|[1-9][0-9]*)$/;
815
- var _a2;
816
- var relinquishedTurns = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a2] = /* @__PURE__ */ new Set());
1015
+ var _a3;
1016
+ var relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
817
1017
  var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
818
1018
  var SETTLE_RETRY_MS = 1e3;
819
1019
  var SETTLE_RETRIES = 300;
@@ -927,7 +1127,7 @@ async function takeTurn(profile, directory, latest, liveness) {
927
1127
  if (published) await markTurn(path, token, "done");
928
1128
  return null;
929
1129
  } catch (error) {
930
- if (published) settleOwnTurn(directory, slot, token);
1130
+ if (published) await settleOwnTurn(directory, slot, token);
931
1131
  throw error;
932
1132
  }
933
1133
  }
@@ -938,17 +1138,25 @@ async function settleAbandonedTurn(directory, abandoned) {
938
1138
  }
939
1139
  await markTurn(path, abandoned.token, "done");
940
1140
  }
941
- function settleOwnTurn(directory, slot, token, retries = SETTLE_RETRIES) {
1141
+ async function settleOwnTurn(directory, slot, token) {
942
1142
  relinquishedTurns.add(token);
943
- void (async () => {
944
- try {
945
- const turn = await readTurn(directory, slot);
946
- if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(directory, turn);
947
- relinquishedTurns.delete(token);
948
- } catch {
949
- if (retries > 0) setTimeout(() => settleOwnTurn(directory, slot, token, retries - 1), SETTLE_RETRY_MS).unref();
950
- }
951
- })();
1143
+ const pinned = await pinDirectory(directory).catch(() => null);
1144
+ const path = pinned?.path(".") ?? directory;
1145
+ let retries = SETTLE_RETRIES;
1146
+ const attempt = () => {
1147
+ void (async () => {
1148
+ try {
1149
+ const turn = await readTurn(path, slot);
1150
+ if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(path, turn);
1151
+ relinquishedTurns.delete(token);
1152
+ await pinned?.close();
1153
+ } catch {
1154
+ if (retries-- > 0) setTimeout(attempt, SETTLE_RETRY_MS).unref();
1155
+ else await pinned?.close();
1156
+ }
1157
+ })();
1158
+ };
1159
+ attempt();
952
1160
  }
953
1161
  async function markTurn(path, token, kind) {
954
1162
  for (let attempt = 0; ; attempt++) {
@@ -993,7 +1201,7 @@ async function releaseTurn(profile, turn, liveness) {
993
1201
  try {
994
1202
  await markTurn((0, import_node_path.join)(turn.directory, String(turn.slot)), turn.token, "done");
995
1203
  } catch {
996
- settleOwnTurn(turn.directory, turn.slot, turn.token);
1204
+ await settleOwnTurn(turn.directory, turn.slot, turn.token);
997
1205
  return;
998
1206
  }
999
1207
  await collectTurns(profile, turn, liveness).catch(() => void 0);