@tinycloud/operations 0.3.3-beta.13 → 0.3.3-beta.14
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 +105 -45
- package/dist/artifacts.cjs.map +1 -1
- package/dist/artifacts.js +109 -45
- package/dist/artifacts.js.map +1 -1
- package/dist/cli-runtime.cjs +107 -47
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.js +111 -47
- package/dist/cli-runtime.js.map +1 -1
- package/dist/index.cjs +107 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +111 -47
- package/dist/index.js.map +1 -1
- package/dist/profile.cjs +2 -1
- package/dist/profile.cjs.map +1 -1
- package/dist/profile.js +6 -1
- package/dist/profile.js.map +1 -1
- package/dist/state.cjs +105 -42
- package/dist/state.cjs.map +1 -1
- package/dist/state.d.cts +8 -0
- package/dist/state.d.ts +8 -0
- package/dist/state.js +109 -42
- package/dist/state.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -7090,11 +7090,15 @@ var init_authority = __esm({
|
|
|
7090
7090
|
import { randomUUID } from "crypto";
|
|
7091
7091
|
import { AsyncLocalStorage } from "async_hooks";
|
|
7092
7092
|
import {
|
|
7093
|
+
chmod,
|
|
7094
|
+
link,
|
|
7093
7095
|
mkdir,
|
|
7094
7096
|
readFile,
|
|
7097
|
+
readdir,
|
|
7095
7098
|
rename,
|
|
7096
7099
|
rmdir,
|
|
7097
7100
|
rm,
|
|
7101
|
+
stat,
|
|
7098
7102
|
writeFile
|
|
7099
7103
|
} from "fs/promises";
|
|
7100
7104
|
import { homedir } from "os";
|
|
@@ -7137,9 +7141,6 @@ function profileStoreMetadataPath(profile, store) {
|
|
|
7137
7141
|
function profileLockPath(profile) {
|
|
7138
7142
|
return join(profilePath(profile), ".lock");
|
|
7139
7143
|
}
|
|
7140
|
-
function profileLockMetadataPath(profile) {
|
|
7141
|
-
return join(profileLockPath(profile), "owner.json");
|
|
7142
|
-
}
|
|
7143
7144
|
async function readJson(filePath) {
|
|
7144
7145
|
try {
|
|
7145
7146
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
@@ -7156,9 +7157,9 @@ async function writeJsonAtomic(filePath, value) {
|
|
|
7156
7157
|
);
|
|
7157
7158
|
const contents = `${JSON.stringify(value, null, 2)}
|
|
7158
7159
|
`;
|
|
7159
|
-
await mkdir(directory, { recursive: true });
|
|
7160
|
+
await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7160
7161
|
try {
|
|
7161
|
-
await writeFile(temporaryPath, contents, "utf8");
|
|
7162
|
+
await writeFile(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
7162
7163
|
await rename(temporaryPath, filePath);
|
|
7163
7164
|
} catch (error) {
|
|
7164
7165
|
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
@@ -7198,10 +7199,15 @@ async function readProfileStore(profile, store) {
|
|
|
7198
7199
|
}
|
|
7199
7200
|
async function withProfileLock(profile, action, options = {}) {
|
|
7200
7201
|
const normalizedProfile = validateProfileName(profile);
|
|
7202
|
+
const lockPath = profileLockPath(normalizedProfile);
|
|
7203
|
+
const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
|
|
7204
|
+
if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
|
|
7201
7205
|
const release = await acquireProfileLock(normalizedProfile, options);
|
|
7206
|
+
const ownership = { lockPath, active: true };
|
|
7202
7207
|
try {
|
|
7203
|
-
return await action
|
|
7208
|
+
return await heldProfileLocks.run([...held, ownership], action);
|
|
7204
7209
|
} finally {
|
|
7210
|
+
ownership.active = false;
|
|
7205
7211
|
await release();
|
|
7206
7212
|
}
|
|
7207
7213
|
}
|
|
@@ -7228,28 +7234,18 @@ async function acquireProfileLock(profile, options) {
|
|
|
7228
7234
|
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
|
|
7229
7235
|
const startedAt = Date.now();
|
|
7230
7236
|
const lockPath = profileLockPath(profile);
|
|
7231
|
-
await mkdir(profilePath(profile), { recursive: true });
|
|
7237
|
+
await mkdir(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7238
|
+
for (const directory of [tinycloudHomePath(), profilesPath(), profilePath(profile)]) {
|
|
7239
|
+
await chmod(directory, PRIVATE_DIR_MODE);
|
|
7240
|
+
}
|
|
7232
7241
|
while (true) {
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
const token = randomUUID();
|
|
7236
|
-
try {
|
|
7237
|
-
await writeJsonAtomic(profileLockMetadataPath(profile), {
|
|
7238
|
-
pid: process.pid,
|
|
7239
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7240
|
-
token
|
|
7241
|
-
});
|
|
7242
|
-
} catch (error) {
|
|
7243
|
-
await rmdir(lockPath).catch(() => void 0);
|
|
7244
|
-
throw error;
|
|
7245
|
-
}
|
|
7242
|
+
const token = randomUUID();
|
|
7243
|
+
if (await publishProfileLock(profile, lockPath, token)) {
|
|
7246
7244
|
return async () => {
|
|
7247
|
-
await releaseProfileLock(lockPath, token);
|
|
7245
|
+
await releaseProfileLock(profile, lockPath, token);
|
|
7248
7246
|
};
|
|
7249
|
-
} catch (error) {
|
|
7250
|
-
if (!isLockAlreadyHeld(error)) throw error;
|
|
7251
|
-
await signalTestLockContention(profile);
|
|
7252
7247
|
}
|
|
7248
|
+
await signalTestLockContention(profile);
|
|
7253
7249
|
if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
|
|
7254
7250
|
const elapsedMs = Date.now() - startedAt;
|
|
7255
7251
|
if (elapsedMs >= timeoutMs) {
|
|
@@ -7258,21 +7254,35 @@ async function acquireProfileLock(profile, options) {
|
|
|
7258
7254
|
await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
|
|
7259
7255
|
}
|
|
7260
7256
|
}
|
|
7261
|
-
async function
|
|
7262
|
-
const ownerPath = join(lockPath, "owner.json");
|
|
7263
|
-
const claimPath = join(lockPath, `.release-${token}.json`);
|
|
7257
|
+
async function publishProfileLock(profile, lockPath, token) {
|
|
7264
7258
|
try {
|
|
7265
|
-
await
|
|
7259
|
+
await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
|
|
7266
7260
|
} catch (error) {
|
|
7267
|
-
if (isErrno(error, "
|
|
7261
|
+
if (isErrno(error, "EEXIST")) return false;
|
|
7268
7262
|
throw error;
|
|
7269
7263
|
}
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7264
|
+
await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
|
|
7265
|
+
const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
|
|
7266
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token };
|
|
7267
|
+
try {
|
|
7268
|
+
await writeFile(staged, `${JSON.stringify(owner, null, 2)}
|
|
7269
|
+
`, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
7270
|
+
await link(staged, join(lockPath, "owner.json"));
|
|
7271
|
+
return true;
|
|
7272
|
+
} catch (error) {
|
|
7273
|
+
if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
|
|
7274
|
+
await rmdir(lockPath).catch(() => void 0);
|
|
7275
|
+
throw error;
|
|
7276
|
+
} finally {
|
|
7277
|
+
await rm(staged, { force: true }).catch(() => void 0);
|
|
7274
7278
|
}
|
|
7275
|
-
|
|
7279
|
+
}
|
|
7280
|
+
async function releaseProfileLock(profile, lockPath, token) {
|
|
7281
|
+
const ownerPath = join(lockPath, "owner.json");
|
|
7282
|
+
const owner = await readJson(ownerPath).catch(() => null);
|
|
7283
|
+
if (owner?.token !== token) return;
|
|
7284
|
+
await rm(ownerPath, { force: true });
|
|
7285
|
+
await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);
|
|
7276
7286
|
await rmdir(lockPath).catch(() => void 0);
|
|
7277
7287
|
}
|
|
7278
7288
|
async function signalTestLockContention(profile) {
|
|
@@ -7289,32 +7299,77 @@ async function signalTestLockContention(profile) {
|
|
|
7289
7299
|
await writeFile(signalPath, `${profile}
|
|
7290
7300
|
`, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
7291
7301
|
}
|
|
7302
|
+
async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
7303
|
+
try {
|
|
7304
|
+
const { mtimeMs } = await stat(lockPath);
|
|
7305
|
+
if (Date.now() - mtimeMs < staleAfterMs) return false;
|
|
7306
|
+
} catch {
|
|
7307
|
+
return false;
|
|
7308
|
+
}
|
|
7309
|
+
const checkedAt = performance.now();
|
|
7310
|
+
await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);
|
|
7311
|
+
try {
|
|
7312
|
+
const entries = await readdir(lockPath);
|
|
7313
|
+
if (entries.includes("owner.json")) return false;
|
|
7314
|
+
if (recoveryFenced(checkedAt, staleAfterMs)) return false;
|
|
7315
|
+
for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
|
|
7316
|
+
await rm(join(lockPath, name), { force: true });
|
|
7317
|
+
}
|
|
7318
|
+
await rmdir(lockPath);
|
|
7319
|
+
} catch {
|
|
7320
|
+
return false;
|
|
7321
|
+
}
|
|
7322
|
+
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7323
|
+
return true;
|
|
7324
|
+
}
|
|
7325
|
+
function recoveryFenced(since, staleAfterMs) {
|
|
7326
|
+
return performance.now() - since >= staleAfterMs / 2;
|
|
7327
|
+
}
|
|
7328
|
+
async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
|
|
7329
|
+
const directory = dirname(lockPath);
|
|
7330
|
+
const names = await readdir(directory).catch(() => []);
|
|
7331
|
+
for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {
|
|
7332
|
+
const path = join(directory, name);
|
|
7333
|
+
const aged = await stat(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);
|
|
7334
|
+
if (aged) await rm(path, { force: true }).catch(() => void 0);
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7292
7337
|
async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
7293
7338
|
const ownerPath = join(lockPath, "owner.json");
|
|
7294
7339
|
const owner = await readJson(ownerPath).catch(() => null);
|
|
7340
|
+
if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
|
|
7295
7341
|
if (!isStaleOwner(owner, staleAfterMs)) return false;
|
|
7296
7342
|
const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
|
|
7297
|
-
await
|
|
7343
|
+
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
7298
7344
|
const claimPath = join(lockPath, `.stale-${randomUUID()}.json`);
|
|
7299
7345
|
try {
|
|
7300
|
-
await
|
|
7301
|
-
} catch
|
|
7302
|
-
if (isErrno(error, "ENOENT")) return false;
|
|
7346
|
+
await link(ownerPath, claimPath);
|
|
7347
|
+
} catch {
|
|
7303
7348
|
return false;
|
|
7304
7349
|
}
|
|
7350
|
+
const claimedAt = performance.now();
|
|
7351
|
+
await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
|
|
7305
7352
|
const claimed = await readJson(claimPath).catch(() => null);
|
|
7306
7353
|
const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
|
|
7307
|
-
|
|
7308
|
-
|
|
7354
|
+
await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
|
|
7355
|
+
if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {
|
|
7356
|
+
await rm(claimPath, { force: true });
|
|
7357
|
+
await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);
|
|
7309
7358
|
return false;
|
|
7310
7359
|
}
|
|
7360
|
+
if (sameInstance) {
|
|
7361
|
+
await rm(ownerPath, { force: true });
|
|
7362
|
+
await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
|
|
7363
|
+
}
|
|
7311
7364
|
await rm(claimPath, { force: true });
|
|
7312
7365
|
await rmdir(lockPath).catch(() => void 0);
|
|
7366
|
+
if (!sameInstance) return false;
|
|
7367
|
+
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7313
7368
|
return true;
|
|
7314
7369
|
}
|
|
7315
|
-
async function
|
|
7370
|
+
async function waitForTestBarrier(environmentName, profile) {
|
|
7316
7371
|
if (process.env.NODE_ENV !== "test") return;
|
|
7317
|
-
const barrierDirectory = process.env[
|
|
7372
|
+
const barrierDirectory = process.env[environmentName];
|
|
7318
7373
|
if (!barrierDirectory) return;
|
|
7319
7374
|
await mkdir(barrierDirectory, { recursive: true });
|
|
7320
7375
|
const readyPath = join(barrierDirectory, `ready-${process.pid}-${profile}`);
|
|
@@ -7338,9 +7393,6 @@ function isStaleOwner(owner, staleAfterMs) {
|
|
|
7338
7393
|
}
|
|
7339
7394
|
return !isProcessAlive(owner.pid);
|
|
7340
7395
|
}
|
|
7341
|
-
function isLockAlreadyHeld(error) {
|
|
7342
|
-
return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
|
|
7343
|
-
}
|
|
7344
7396
|
function isProcessAlive(pid) {
|
|
7345
7397
|
try {
|
|
7346
7398
|
process.kill(pid, 0);
|
|
@@ -7361,7 +7413,7 @@ function isErrno(error, code) {
|
|
|
7361
7413
|
function sleep(milliseconds) {
|
|
7362
7414
|
return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
|
|
7363
7415
|
}
|
|
7364
|
-
var DEFAULT_PROFILE, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_LOCK_RETRY_MS, DEFAULT_STALE_LOCK_MS, TEST_LOCK_CONTENTION_SIGNAL_PATH, TEST_LOCK_RECOVERY_BARRIER_DIR, invocationStateRoot, ProfileLockTimeoutError;
|
|
7416
|
+
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_FENCED_BARRIER_DIR, TEST_LOCK_VERIFIED_BARRIER_DIR, invocationStateRoot, heldProfileLocks, ProfileLockTimeoutError, ABANDONED_CLAIM, STAGED_OWNER;
|
|
7365
7417
|
var init_state = __esm({
|
|
7366
7418
|
"src/state.ts"() {
|
|
7367
7419
|
"use strict";
|
|
@@ -7370,8 +7422,18 @@ var init_state = __esm({
|
|
|
7370
7422
|
DEFAULT_LOCK_RETRY_MS = 25;
|
|
7371
7423
|
DEFAULT_STALE_LOCK_MS = 3e4;
|
|
7372
7424
|
TEST_LOCK_CONTENTION_SIGNAL_PATH = "TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH";
|
|
7425
|
+
PRIVATE_FILE_MODE = 384;
|
|
7426
|
+
PRIVATE_DIR_MODE = 448;
|
|
7373
7427
|
TEST_LOCK_RECOVERY_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR";
|
|
7428
|
+
TEST_LOCK_OWNERLESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR";
|
|
7429
|
+
TEST_LOCK_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR";
|
|
7430
|
+
TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
|
|
7431
|
+
TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
|
|
7432
|
+
TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
|
|
7433
|
+
TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
|
|
7434
|
+
TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
|
|
7374
7435
|
invocationStateRoot = new AsyncLocalStorage();
|
|
7436
|
+
heldProfileLocks = new AsyncLocalStorage();
|
|
7375
7437
|
ProfileLockTimeoutError = class extends Error {
|
|
7376
7438
|
constructor(profile, timeoutMs) {
|
|
7377
7439
|
super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
|
|
@@ -7379,6 +7441,8 @@ var init_state = __esm({
|
|
|
7379
7441
|
this.name = "ProfileLockTimeoutError";
|
|
7380
7442
|
}
|
|
7381
7443
|
};
|
|
7444
|
+
ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
|
|
7445
|
+
STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
|
|
7382
7446
|
}
|
|
7383
7447
|
});
|
|
7384
7448
|
|
|
@@ -10061,7 +10125,7 @@ var init_profile = __esm({
|
|
|
10061
10125
|
"src/profile.ts"() {
|
|
10062
10126
|
"use strict";
|
|
10063
10127
|
init_state();
|
|
10064
|
-
DEFAULT_HOST = "https://node.tinycloud.xyz";
|
|
10128
|
+
DEFAULT_HOST = "https://tee.node.tinycloud.xyz";
|
|
10065
10129
|
}
|
|
10066
10130
|
});
|
|
10067
10131
|
|