@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.cjs
CHANGED
|
@@ -7127,9 +7127,6 @@ function profileStoreMetadataPath(profile, store) {
|
|
|
7127
7127
|
function profileLockPath(profile) {
|
|
7128
7128
|
return (0, import_node_path.join)(profilePath(profile), ".lock");
|
|
7129
7129
|
}
|
|
7130
|
-
function profileLockMetadataPath(profile) {
|
|
7131
|
-
return (0, import_node_path.join)(profileLockPath(profile), "owner.json");
|
|
7132
|
-
}
|
|
7133
7130
|
async function readJson(filePath) {
|
|
7134
7131
|
try {
|
|
7135
7132
|
return JSON.parse(await (0, import_promises.readFile)(filePath, "utf8"));
|
|
@@ -7146,9 +7143,9 @@ async function writeJsonAtomic(filePath, value) {
|
|
|
7146
7143
|
);
|
|
7147
7144
|
const contents = `${JSON.stringify(value, null, 2)}
|
|
7148
7145
|
`;
|
|
7149
|
-
await (0, import_promises.mkdir)(directory, { recursive: true });
|
|
7146
|
+
await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7150
7147
|
try {
|
|
7151
|
-
await (0, import_promises.writeFile)(temporaryPath, contents, "utf8");
|
|
7148
|
+
await (0, import_promises.writeFile)(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
7152
7149
|
await (0, import_promises.rename)(temporaryPath, filePath);
|
|
7153
7150
|
} catch (error) {
|
|
7154
7151
|
await (0, import_promises.rm)(temporaryPath, { force: true }).catch(() => void 0);
|
|
@@ -7188,10 +7185,15 @@ async function readProfileStore(profile, store) {
|
|
|
7188
7185
|
}
|
|
7189
7186
|
async function withProfileLock(profile, action, options = {}) {
|
|
7190
7187
|
const normalizedProfile = validateProfileName(profile);
|
|
7188
|
+
const lockPath = profileLockPath(normalizedProfile);
|
|
7189
|
+
const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
|
|
7190
|
+
if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
|
|
7191
7191
|
const release = await acquireProfileLock(normalizedProfile, options);
|
|
7192
|
+
const ownership = { lockPath, active: true };
|
|
7192
7193
|
try {
|
|
7193
|
-
return await action
|
|
7194
|
+
return await heldProfileLocks.run([...held, ownership], action);
|
|
7194
7195
|
} finally {
|
|
7196
|
+
ownership.active = false;
|
|
7195
7197
|
await release();
|
|
7196
7198
|
}
|
|
7197
7199
|
}
|
|
@@ -7218,28 +7220,18 @@ async function acquireProfileLock(profile, options) {
|
|
|
7218
7220
|
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
|
|
7219
7221
|
const startedAt = Date.now();
|
|
7220
7222
|
const lockPath = profileLockPath(profile);
|
|
7221
|
-
await (0, import_promises.mkdir)(profilePath(profile), { recursive: true });
|
|
7223
|
+
await (0, import_promises.mkdir)(profilePath(profile), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7224
|
+
for (const directory of [tinycloudHomePath(), profilesPath(), profilePath(profile)]) {
|
|
7225
|
+
await (0, import_promises.chmod)(directory, PRIVATE_DIR_MODE);
|
|
7226
|
+
}
|
|
7222
7227
|
while (true) {
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
const token = (0, import_node_crypto.randomUUID)();
|
|
7226
|
-
try {
|
|
7227
|
-
await writeJsonAtomic(profileLockMetadataPath(profile), {
|
|
7228
|
-
pid: process.pid,
|
|
7229
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7230
|
-
token
|
|
7231
|
-
});
|
|
7232
|
-
} catch (error) {
|
|
7233
|
-
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7234
|
-
throw error;
|
|
7235
|
-
}
|
|
7228
|
+
const token = (0, import_node_crypto.randomUUID)();
|
|
7229
|
+
if (await publishProfileLock(profile, lockPath, token)) {
|
|
7236
7230
|
return async () => {
|
|
7237
|
-
await releaseProfileLock(lockPath, token);
|
|
7231
|
+
await releaseProfileLock(profile, lockPath, token);
|
|
7238
7232
|
};
|
|
7239
|
-
} catch (error) {
|
|
7240
|
-
if (!isLockAlreadyHeld(error)) throw error;
|
|
7241
|
-
await signalTestLockContention(profile);
|
|
7242
7233
|
}
|
|
7234
|
+
await signalTestLockContention(profile);
|
|
7243
7235
|
if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
|
|
7244
7236
|
const elapsedMs = Date.now() - startedAt;
|
|
7245
7237
|
if (elapsedMs >= timeoutMs) {
|
|
@@ -7248,21 +7240,35 @@ async function acquireProfileLock(profile, options) {
|
|
|
7248
7240
|
await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
|
|
7249
7241
|
}
|
|
7250
7242
|
}
|
|
7251
|
-
async function
|
|
7252
|
-
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
7253
|
-
const claimPath = (0, import_node_path.join)(lockPath, `.release-${token}.json`);
|
|
7243
|
+
async function publishProfileLock(profile, lockPath, token) {
|
|
7254
7244
|
try {
|
|
7255
|
-
await (0, import_promises.
|
|
7245
|
+
await (0, import_promises.mkdir)(lockPath, { mode: PRIVATE_DIR_MODE });
|
|
7256
7246
|
} catch (error) {
|
|
7257
|
-
if (isErrno(error, "
|
|
7247
|
+
if (isErrno(error, "EEXIST")) return false;
|
|
7258
7248
|
throw error;
|
|
7259
7249
|
}
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7250
|
+
await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
|
|
7251
|
+
const staged = (0, import_node_path.join)((0, import_node_path.dirname)(lockPath), `.lock-owner-${token}.tmp`);
|
|
7252
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token };
|
|
7253
|
+
try {
|
|
7254
|
+
await (0, import_promises.writeFile)(staged, `${JSON.stringify(owner, null, 2)}
|
|
7255
|
+
`, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
7256
|
+
await (0, import_promises.link)(staged, (0, import_node_path.join)(lockPath, "owner.json"));
|
|
7257
|
+
return true;
|
|
7258
|
+
} catch (error) {
|
|
7259
|
+
if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
|
|
7260
|
+
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7261
|
+
throw error;
|
|
7262
|
+
} finally {
|
|
7263
|
+
await (0, import_promises.rm)(staged, { force: true }).catch(() => void 0);
|
|
7264
7264
|
}
|
|
7265
|
-
|
|
7265
|
+
}
|
|
7266
|
+
async function releaseProfileLock(profile, lockPath, token) {
|
|
7267
|
+
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
7268
|
+
const owner = await readJson(ownerPath).catch(() => null);
|
|
7269
|
+
if (owner?.token !== token) return;
|
|
7270
|
+
await (0, import_promises.rm)(ownerPath, { force: true });
|
|
7271
|
+
await waitForTestBarrier(TEST_LOCK_RELEASE_BARRIER_DIR, profile);
|
|
7266
7272
|
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7267
7273
|
}
|
|
7268
7274
|
async function signalTestLockContention(profile) {
|
|
@@ -7279,32 +7285,77 @@ async function signalTestLockContention(profile) {
|
|
|
7279
7285
|
await (0, import_promises.writeFile)(signalPath, `${profile}
|
|
7280
7286
|
`, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
7281
7287
|
}
|
|
7288
|
+
async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
7289
|
+
try {
|
|
7290
|
+
const { mtimeMs } = await (0, import_promises.stat)(lockPath);
|
|
7291
|
+
if (Date.now() - mtimeMs < staleAfterMs) return false;
|
|
7292
|
+
} catch {
|
|
7293
|
+
return false;
|
|
7294
|
+
}
|
|
7295
|
+
const checkedAt = performance.now();
|
|
7296
|
+
await waitForTestBarrier(TEST_LOCK_OWNERLESS_BARRIER_DIR, profile);
|
|
7297
|
+
try {
|
|
7298
|
+
const entries = await (0, import_promises.readdir)(lockPath);
|
|
7299
|
+
if (entries.includes("owner.json")) return false;
|
|
7300
|
+
if (recoveryFenced(checkedAt, staleAfterMs)) return false;
|
|
7301
|
+
for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
|
|
7302
|
+
await (0, import_promises.rm)((0, import_node_path.join)(lockPath, name), { force: true });
|
|
7303
|
+
}
|
|
7304
|
+
await (0, import_promises.rmdir)(lockPath);
|
|
7305
|
+
} catch {
|
|
7306
|
+
return false;
|
|
7307
|
+
}
|
|
7308
|
+
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7309
|
+
return true;
|
|
7310
|
+
}
|
|
7311
|
+
function recoveryFenced(since, staleAfterMs) {
|
|
7312
|
+
return performance.now() - since >= staleAfterMs / 2;
|
|
7313
|
+
}
|
|
7314
|
+
async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
|
|
7315
|
+
const directory = (0, import_node_path.dirname)(lockPath);
|
|
7316
|
+
const names = await (0, import_promises.readdir)(directory).catch(() => []);
|
|
7317
|
+
for (const name of names.filter((entry) => STAGED_OWNER.test(entry))) {
|
|
7318
|
+
const path = (0, import_node_path.join)(directory, name);
|
|
7319
|
+
const aged = await (0, import_promises.stat)(path).then(({ mtimeMs }) => Date.now() - mtimeMs >= staleAfterMs, () => false);
|
|
7320
|
+
if (aged) await (0, import_promises.rm)(path, { force: true }).catch(() => void 0);
|
|
7321
|
+
}
|
|
7322
|
+
}
|
|
7282
7323
|
async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
7283
7324
|
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
7284
7325
|
const owner = await readJson(ownerPath).catch(() => null);
|
|
7326
|
+
if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
|
|
7285
7327
|
if (!isStaleOwner(owner, staleAfterMs)) return false;
|
|
7286
7328
|
const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
|
|
7287
|
-
await
|
|
7329
|
+
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
7288
7330
|
const claimPath = (0, import_node_path.join)(lockPath, `.stale-${(0, import_node_crypto.randomUUID)()}.json`);
|
|
7289
7331
|
try {
|
|
7290
|
-
await (0, import_promises.
|
|
7291
|
-
} catch
|
|
7292
|
-
if (isErrno(error, "ENOENT")) return false;
|
|
7332
|
+
await (0, import_promises.link)(ownerPath, claimPath);
|
|
7333
|
+
} catch {
|
|
7293
7334
|
return false;
|
|
7294
7335
|
}
|
|
7336
|
+
const claimedAt = performance.now();
|
|
7337
|
+
await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
|
|
7295
7338
|
const claimed = await readJson(claimPath).catch(() => null);
|
|
7296
7339
|
const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
|
|
7297
|
-
|
|
7298
|
-
|
|
7340
|
+
await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
|
|
7341
|
+
if (sameInstance && recoveryFenced(claimedAt, staleAfterMs)) {
|
|
7342
|
+
await (0, import_promises.rm)(claimPath, { force: true });
|
|
7343
|
+
await waitForTestBarrier(TEST_LOCK_FENCED_BARRIER_DIR, profile);
|
|
7299
7344
|
return false;
|
|
7300
7345
|
}
|
|
7346
|
+
if (sameInstance) {
|
|
7347
|
+
await (0, import_promises.rm)(ownerPath, { force: true });
|
|
7348
|
+
await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
|
|
7349
|
+
}
|
|
7301
7350
|
await (0, import_promises.rm)(claimPath, { force: true });
|
|
7302
7351
|
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7352
|
+
if (!sameInstance) return false;
|
|
7353
|
+
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7303
7354
|
return true;
|
|
7304
7355
|
}
|
|
7305
|
-
async function
|
|
7356
|
+
async function waitForTestBarrier(environmentName, profile) {
|
|
7306
7357
|
if (process.env.NODE_ENV !== "test") return;
|
|
7307
|
-
const barrierDirectory = process.env[
|
|
7358
|
+
const barrierDirectory = process.env[environmentName];
|
|
7308
7359
|
if (!barrierDirectory) return;
|
|
7309
7360
|
await (0, import_promises.mkdir)(barrierDirectory, { recursive: true });
|
|
7310
7361
|
const readyPath = (0, import_node_path.join)(barrierDirectory, `ready-${process.pid}-${profile}`);
|
|
@@ -7328,9 +7379,6 @@ function isStaleOwner(owner, staleAfterMs) {
|
|
|
7328
7379
|
}
|
|
7329
7380
|
return !isProcessAlive(owner.pid);
|
|
7330
7381
|
}
|
|
7331
|
-
function isLockAlreadyHeld(error) {
|
|
7332
|
-
return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
|
|
7333
|
-
}
|
|
7334
7382
|
function isProcessAlive(pid) {
|
|
7335
7383
|
try {
|
|
7336
7384
|
process.kill(pid, 0);
|
|
@@ -7351,7 +7399,7 @@ function isErrno(error, code) {
|
|
|
7351
7399
|
function sleep(milliseconds) {
|
|
7352
7400
|
return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
|
|
7353
7401
|
}
|
|
7354
|
-
var import_node_crypto, import_node_async_hooks, import_promises, import_node_os, import_node_path, 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;
|
|
7402
|
+
var import_node_crypto, import_node_async_hooks, import_promises, import_node_os, import_node_path, 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;
|
|
7355
7403
|
var init_state = __esm({
|
|
7356
7404
|
"src/state.ts"() {
|
|
7357
7405
|
"use strict";
|
|
@@ -7365,8 +7413,18 @@ var init_state = __esm({
|
|
|
7365
7413
|
DEFAULT_LOCK_RETRY_MS = 25;
|
|
7366
7414
|
DEFAULT_STALE_LOCK_MS = 3e4;
|
|
7367
7415
|
TEST_LOCK_CONTENTION_SIGNAL_PATH = "TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH";
|
|
7416
|
+
PRIVATE_FILE_MODE = 384;
|
|
7417
|
+
PRIVATE_DIR_MODE = 448;
|
|
7368
7418
|
TEST_LOCK_RECOVERY_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RECOVERY_BARRIER_DIR";
|
|
7419
|
+
TEST_LOCK_OWNERLESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_OWNERLESS_BARRIER_DIR";
|
|
7420
|
+
TEST_LOCK_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR";
|
|
7421
|
+
TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
|
|
7422
|
+
TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
|
|
7423
|
+
TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
|
|
7424
|
+
TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
|
|
7425
|
+
TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
|
|
7369
7426
|
invocationStateRoot = new import_node_async_hooks.AsyncLocalStorage();
|
|
7427
|
+
heldProfileLocks = new import_node_async_hooks.AsyncLocalStorage();
|
|
7370
7428
|
ProfileLockTimeoutError = class extends Error {
|
|
7371
7429
|
constructor(profile, timeoutMs) {
|
|
7372
7430
|
super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
|
|
@@ -7374,6 +7432,8 @@ var init_state = __esm({
|
|
|
7374
7432
|
this.name = "ProfileLockTimeoutError";
|
|
7375
7433
|
}
|
|
7376
7434
|
};
|
|
7435
|
+
ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
|
|
7436
|
+
STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
|
|
7377
7437
|
}
|
|
7378
7438
|
});
|
|
7379
7439
|
|
|
@@ -10051,7 +10111,7 @@ var init_profile = __esm({
|
|
|
10051
10111
|
"src/profile.ts"() {
|
|
10052
10112
|
"use strict";
|
|
10053
10113
|
init_state();
|
|
10054
|
-
DEFAULT_HOST = "https://node.tinycloud.xyz";
|
|
10114
|
+
DEFAULT_HOST = "https://tee.node.tinycloud.xyz";
|
|
10055
10115
|
}
|
|
10056
10116
|
});
|
|
10057
10117
|
|