@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 +1 -0
- package/dist/artifacts.cjs +264 -56
- package/dist/artifacts.cjs.map +1 -1
- package/dist/artifacts.js +267 -58
- package/dist/artifacts.js.map +1 -1
- package/dist/cli-runtime.cjs +285 -69
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.js +288 -71
- package/dist/cli-runtime.js.map +1 -1
- package/dist/delegation-binding.cjs +269 -58
- package/dist/delegation-binding.cjs.map +1 -1
- package/dist/delegation-binding.js +272 -60
- package/dist/delegation-binding.js.map +1 -1
- package/dist/index.cjs +285 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +288 -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
|
}
|
|
@@ -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
|
|
4261
|
+
var _a5;
|
|
4249
4262
|
const registry = globalThis;
|
|
4250
|
-
return registry[
|
|
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
|
|
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)(
|
|
4458
|
+
return (0, import_node_path2.join)(namedProfilePath(profile), ".lock");
|
|
4441
4459
|
}
|
|
4442
4460
|
function profileTurnLockPath(profile) {
|
|
4443
|
-
|
|
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
|
|
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
|
-
|
|
4490
|
-
|
|
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
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
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
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
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
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
4958
|
+
async function settleOwnTurn(directory, slot, token) {
|
|
4755
4959
|
relinquishedTurns.add(token);
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
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,
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
7802
|
-
|
|
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
|
}
|