@tinycloud/operations 0.6.1-beta.1 → 0.6.1-beta.11
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 +1 -0
- package/dist/artifacts.cjs +263 -56
- package/dist/artifacts.cjs.map +1 -1
- package/dist/artifacts.d.cts +54 -54
- package/dist/artifacts.d.ts +54 -54
- package/dist/artifacts.js +266 -58
- package/dist/artifacts.js.map +1 -1
- package/dist/cli-runtime.cjs +284 -69
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.js +287 -71
- package/dist/cli-runtime.js.map +1 -1
- package/dist/delegation-binding.cjs +268 -58
- package/dist/delegation-binding.cjs.map +1 -1
- package/dist/delegation-binding.js +271 -60
- package/dist/delegation-binding.js.map +1 -1
- package/dist/index.cjs +284 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +287 -71
- package/dist/index.js.map +1 -1
- package/dist/profile.cjs +160 -10
- package/dist/profile.cjs.map +1 -1
- package/dist/profile.js +163 -12
- package/dist/profile.js.map +1 -1
- package/dist/state.cjs +277 -58
- package/dist/state.cjs.map +1 -1
- package/dist/state.d.cts +12 -1
- package/dist/state.d.ts +12 -1
- package/dist/state.js +276 -60
- package/dist/state.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -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
|
}
|
|
@@ -4246,9 +4258,9 @@ var init_authority = __esm({
|
|
|
4246
4258
|
|
|
4247
4259
|
// src/state.ts
|
|
4248
4260
|
function processWideContext(key) {
|
|
4249
|
-
var
|
|
4261
|
+
var _a5;
|
|
4250
4262
|
const registry = globalThis;
|
|
4251
|
-
return registry[
|
|
4263
|
+
return registry[_a5 = Symbol.for(key)] ?? (registry[_a5] = new import_node_async_hooks.AsyncLocalStorage());
|
|
4252
4264
|
}
|
|
4253
4265
|
function processOrigin() {
|
|
4254
4266
|
if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
|
|
@@ -4419,9 +4431,14 @@ function tinycloudConfigPath() {
|
|
|
4419
4431
|
function profilesPath() {
|
|
4420
4432
|
return (0, import_node_path2.join)(tinycloudHomePath(), "profiles");
|
|
4421
4433
|
}
|
|
4422
|
-
function
|
|
4434
|
+
function namedProfilePath(profile) {
|
|
4423
4435
|
return (0, import_node_path2.join)(profilesPath(), validateProfileName(profile));
|
|
4424
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
|
+
}
|
|
4425
4442
|
function profileConfigPath(profile) {
|
|
4426
4443
|
return (0, import_node_path2.join)(profilePath(profile), "profile.json");
|
|
4427
4444
|
}
|
|
@@ -4438,10 +4455,12 @@ function profileStoreMetadataPath(profile, store) {
|
|
|
4438
4455
|
return `${profileStorePath(profile, store)}.metadata.json`;
|
|
4439
4456
|
}
|
|
4440
4457
|
function profileLockPath(profile) {
|
|
4441
|
-
return (0, import_node_path2.join)(
|
|
4458
|
+
return (0, import_node_path2.join)(namedProfilePath(profile), ".lock");
|
|
4442
4459
|
}
|
|
4443
4460
|
function profileTurnLockPath(profile) {
|
|
4444
|
-
|
|
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));
|
|
4445
4464
|
}
|
|
4446
4465
|
async function readJson(filePath) {
|
|
4447
4466
|
try {
|
|
@@ -4451,6 +4470,44 @@ async function readJson(filePath) {
|
|
|
4451
4470
|
throw error;
|
|
4452
4471
|
}
|
|
4453
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
|
+
}
|
|
4454
4511
|
async function writeJsonAtomic(filePath, value) {
|
|
4455
4512
|
await replaceJson(filePath, value, false);
|
|
4456
4513
|
}
|
|
@@ -4484,11 +4541,14 @@ async function writeFileDurably(path, contents) {
|
|
|
4484
4541
|
}
|
|
4485
4542
|
}
|
|
4486
4543
|
async function readProfile(profile) {
|
|
4487
|
-
return
|
|
4544
|
+
return withPinnedProfileDirectory(profile, (directory) => directory ? readProfileJson(profile, (0, import_node_path2.join)(directory, "profile.json")) : Promise.resolve(null));
|
|
4488
4545
|
}
|
|
4489
4546
|
async function readSession(profile) {
|
|
4490
|
-
|
|
4491
|
-
|
|
4547
|
+
return withPinnedProfileDirectory(profile, async (directory) => {
|
|
4548
|
+
if (!directory) return null;
|
|
4549
|
+
await readStoreMetadata(profile, "session");
|
|
4550
|
+
return readProfileJson(profile, sessionPath(profile));
|
|
4551
|
+
});
|
|
4492
4552
|
}
|
|
4493
4553
|
async function readAdditionalDelegations(profile) {
|
|
4494
4554
|
return (await readProfileStore(profile, "additional-delegations")).records;
|
|
@@ -4497,43 +4557,187 @@ async function readAuthRequests(profile) {
|
|
|
4497
4557
|
return (await readProfileStore(profile, "auth-requests")).records;
|
|
4498
4558
|
}
|
|
4499
4559
|
async function readStoreMetadata(profile, store) {
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
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
|
+
});
|
|
4506
4568
|
}
|
|
4507
4569
|
async function readProfileStore(profile, store) {
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
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
|
+
});
|
|
4516
4581
|
}
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
}
|
|
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;
|
|
4529
4617
|
try {
|
|
4530
|
-
|
|
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
|
+
}
|
|
4531
4681
|
} finally {
|
|
4532
|
-
|
|
4533
|
-
await acquired.release();
|
|
4534
|
-
if (acquired.deletedWhileWaiting) await (0, import_promises.rmdir)(profilePath(normalizedProfile)).catch(() => void 0);
|
|
4682
|
+
for (const handle of handles.reverse()) await handle.close();
|
|
4535
4683
|
}
|
|
4536
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
|
+
}
|
|
4537
4741
|
async function refuseWriteToDeletedProfile(profile) {
|
|
4538
4742
|
const lockPath = profileLockPath(profile);
|
|
4539
4743
|
const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);
|
|
@@ -4566,17 +4770,15 @@ async function acquireProfileLock(profile, options) {
|
|
|
4566
4770
|
retryMs: options.retryMs ?? DEFAULT_LOCK_RETRY_MS
|
|
4567
4771
|
};
|
|
4568
4772
|
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
|
|
4569
|
-
const lockPath =
|
|
4773
|
+
const lockPath = (0, import_node_path2.join)(profilePath(profile), ".lock");
|
|
4570
4774
|
const turnDirectory = profileTurnLockPath(profile);
|
|
4571
4775
|
const directory = profilePath(profile);
|
|
4776
|
+
await assertRealProfileDirectory(profile);
|
|
4572
4777
|
if (options.requireProfile && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
|
|
4573
4778
|
if (!options.requireProfile) await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
4779
|
+
await assertRealProfileDirectory(profile);
|
|
4574
4780
|
await (0, import_promises.mkdir)((0, import_node_path2.dirname)(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
4575
|
-
|
|
4576
|
-
await (0, import_promises.chmod)(path, PRIVATE_DIR_MODE).catch((error) => {
|
|
4577
|
-
if (path !== directory || !isErrno(error, "ENOENT")) throw error;
|
|
4578
|
-
});
|
|
4579
|
-
}
|
|
4781
|
+
await (0, import_promises.chmod)(directory, PRIVATE_DIR_MODE);
|
|
4580
4782
|
const liveness = { directory: turnDirectory, held: void 0 };
|
|
4581
4783
|
const closeAcquisitionLiveness = async () => {
|
|
4582
4784
|
if (liveness.held) await closeLiveness(liveness.held);
|
|
@@ -4596,6 +4798,7 @@ async function acquireProfileLock(profile, options) {
|
|
|
4596
4798
|
}
|
|
4597
4799
|
const deletion = await readJson((0, import_node_path2.join)(turnDirectory, "deleted.json")).catch(() => null);
|
|
4598
4800
|
const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
|
|
4801
|
+
if (deletedWhileWaiting && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
|
|
4599
4802
|
while (true) {
|
|
4600
4803
|
const token = (0, import_node_crypto.randomUUID)();
|
|
4601
4804
|
if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
|
|
@@ -4741,7 +4944,7 @@ async function takeTurn(profile, directory, latest, liveness) {
|
|
|
4741
4944
|
if (published) await markTurn(path, token, "done");
|
|
4742
4945
|
return null;
|
|
4743
4946
|
} catch (error) {
|
|
4744
|
-
if (published) settleOwnTurn(directory, slot, token);
|
|
4947
|
+
if (published) await settleOwnTurn(directory, slot, token);
|
|
4745
4948
|
throw error;
|
|
4746
4949
|
}
|
|
4747
4950
|
}
|
|
@@ -4752,17 +4955,25 @@ async function settleAbandonedTurn(directory, abandoned) {
|
|
|
4752
4955
|
}
|
|
4753
4956
|
await markTurn(path, abandoned.token, "done");
|
|
4754
4957
|
}
|
|
4755
|
-
function settleOwnTurn(directory, slot, token
|
|
4958
|
+
async function settleOwnTurn(directory, slot, token) {
|
|
4756
4959
|
relinquishedTurns.add(token);
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
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();
|
|
4766
4977
|
}
|
|
4767
4978
|
async function markTurn(path, token, kind) {
|
|
4768
4979
|
for (let attempt = 0; ; attempt++) {
|
|
@@ -4807,7 +5018,7 @@ async function releaseTurn(profile, turn, liveness) {
|
|
|
4807
5018
|
try {
|
|
4808
5019
|
await markTurn((0, import_node_path2.join)(turn.directory, String(turn.slot)), turn.token, "done");
|
|
4809
5020
|
} catch {
|
|
4810
|
-
settleOwnTurn(turn.directory, turn.slot, turn.token);
|
|
5021
|
+
await settleOwnTurn(turn.directory, turn.slot, turn.token);
|
|
4811
5022
|
return;
|
|
4812
5023
|
}
|
|
4813
5024
|
await collectTurns(profile, turn, liveness).catch(() => void 0);
|
|
@@ -5091,7 +5302,7 @@ function isErrno(error, code) {
|
|
|
5091
5302
|
function sleep(milliseconds) {
|
|
5092
5303
|
return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
|
|
5093
5304
|
}
|
|
5094
|
-
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,
|
|
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;
|
|
5095
5306
|
var init_state = __esm({
|
|
5096
5307
|
"src/state.ts"() {
|
|
5097
5308
|
"use strict";
|
|
@@ -5123,7 +5334,10 @@ var init_state = __esm({
|
|
|
5123
5334
|
TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
|
|
5124
5335
|
invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
|
|
5125
5336
|
heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
|
|
5126
|
-
|
|
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)());
|
|
5127
5341
|
LIVENESS_SOCKET = /^\.live-([0-9a-f]+)-([0-9a-f]+)-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(\.new)?$/;
|
|
5128
5342
|
LIVENESS_PROBE_TIMEOUT_MS = 1e3;
|
|
5129
5343
|
ProfileLockTimeoutError = class extends Error {
|
|
@@ -5141,8 +5355,15 @@ var init_state = __esm({
|
|
|
5141
5355
|
this.name = "ProfileDeletedError";
|
|
5142
5356
|
}
|
|
5143
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
|
+
};
|
|
5144
5365
|
TURN = /^(?:0|[1-9][0-9]*)$/;
|
|
5145
|
-
relinquishedTurns = globalThis[
|
|
5366
|
+
relinquishedTurns = globalThis[_a4 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a4] = /* @__PURE__ */ new Set());
|
|
5146
5367
|
MARKER_RETRY_DELAYS_MS = [10, 50, 250];
|
|
5147
5368
|
SETTLE_RETRY_MS = 1e3;
|
|
5148
5369
|
SETTLE_RETRIES = 300;
|
|
@@ -6326,6 +6547,8 @@ function appsFailure(error) {
|
|
|
6326
6547
|
if (auth?.status === "error") return auth.error;
|
|
6327
6548
|
const storage = storageFullOperationError(error);
|
|
6328
6549
|
if (storage) return storage;
|
|
6550
|
+
const nodeRetry = nodeRetryOperationError(error);
|
|
6551
|
+
if (nodeRetry) return nodeRetry;
|
|
6329
6552
|
const code = typeof error === "object" && error !== null ? error.code : void 0;
|
|
6330
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" } });
|
|
6331
6554
|
if (isOperationErrorCode(code)) return operationError(code, "The node could not complete this personal-record operation.");
|
|
@@ -7799,16 +8022,8 @@ function kvFailure(error, action) {
|
|
|
7799
8022
|
if (error.code === "KV_PRECONDITION_FAILED") {
|
|
7800
8023
|
return { status: "error", error: operationError("KV_PRECONDITION_FAILED", "The TinyCloud KV object changed or already exists.") };
|
|
7801
8024
|
}
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
status: "error",
|
|
7805
|
-
error: operationError(
|
|
7806
|
-
"KV_CONFLICT",
|
|
7807
|
-
"A database serialization conflict prevented the conditional KV mutation; re-read the key state, which may still be absent, before deciding whether to retry.",
|
|
7808
|
-
{ retryable: true }
|
|
7809
|
-
)
|
|
7810
|
-
};
|
|
7811
|
-
}
|
|
8025
|
+
const nodeRetry = nodeRetryOperationError(error);
|
|
8026
|
+
if (nodeRetry) return { status: "error", error: nodeRetry };
|
|
7812
8027
|
if (error.code === "KV_RESPONSE_TOO_LARGE") {
|
|
7813
8028
|
return { status: "error", error: operationError("KV_RESPONSE_TOO_LARGE", `The TinyCloud KV value exceeds ${MAX_KV_VALUE_BYTES} bytes.`) };
|
|
7814
8029
|
}
|