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