@tinycloud/operations 0.3.3-beta.9 → 0.3.4-beta.0
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 +115 -54
- package/dist/artifacts.cjs.map +1 -1
- package/dist/artifacts.d.cts +30 -30
- package/dist/artifacts.d.ts +30 -30
- package/dist/artifacts.js +119 -54
- package/dist/artifacts.js.map +1 -1
- package/dist/cli-runtime.cjs +117 -56
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.js +121 -56
- package/dist/cli-runtime.js.map +1 -1
- package/dist/index.cjs +117 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +121 -56
- 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 +3 -3
package/dist/cli-runtime.cjs
CHANGED
|
@@ -3620,16 +3620,17 @@ var init_dist2 = __esm({
|
|
|
3620
3620
|
...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
|
|
3621
3621
|
}
|
|
3622
3622
|
);
|
|
3623
|
+
if (response.status === 401 || response.status === 403) {
|
|
3624
|
+
const errorText = await response.text();
|
|
3625
|
+
const message = errorText.trim().length > 0 ? errorText : `Failed to put key "${key}": ${response.status} - ${response.statusText || "authorization failed"}`;
|
|
3626
|
+
const { resource, action: requiredAction } = parseAuthError(errorText);
|
|
3627
|
+
return err(authUnauthorizedError("kv", message, {
|
|
3628
|
+
status: response.status,
|
|
3629
|
+
...requiredAction && { requiredAction },
|
|
3630
|
+
...resource && { resource }
|
|
3631
|
+
}));
|
|
3632
|
+
}
|
|
3623
3633
|
if (!response.ok) {
|
|
3624
|
-
if (response.status === 401) {
|
|
3625
|
-
const errorText2 = await response.text();
|
|
3626
|
-
const { resource, action } = parseAuthError(errorText2);
|
|
3627
|
-
return err(authUnauthorizedError("kv", errorText2, {
|
|
3628
|
-
status: response.status,
|
|
3629
|
-
...action && { requiredAction: action },
|
|
3630
|
-
...resource && { resource }
|
|
3631
|
-
}));
|
|
3632
|
-
}
|
|
3633
3634
|
const errorText = await response.text();
|
|
3634
3635
|
if (response.status === 412) {
|
|
3635
3636
|
return err(serviceError(
|
|
@@ -7126,9 +7127,6 @@ function profileStoreMetadataPath(profile, store) {
|
|
|
7126
7127
|
function profileLockPath(profile) {
|
|
7127
7128
|
return (0, import_node_path.join)(profilePath(profile), ".lock");
|
|
7128
7129
|
}
|
|
7129
|
-
function profileLockMetadataPath(profile) {
|
|
7130
|
-
return (0, import_node_path.join)(profileLockPath(profile), "owner.json");
|
|
7131
|
-
}
|
|
7132
7130
|
async function readJson(filePath) {
|
|
7133
7131
|
try {
|
|
7134
7132
|
return JSON.parse(await (0, import_promises.readFile)(filePath, "utf8"));
|
|
@@ -7145,9 +7143,9 @@ async function writeJsonAtomic(filePath, value) {
|
|
|
7145
7143
|
);
|
|
7146
7144
|
const contents = `${JSON.stringify(value, null, 2)}
|
|
7147
7145
|
`;
|
|
7148
|
-
await (0, import_promises.mkdir)(directory, { recursive: true });
|
|
7146
|
+
await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7149
7147
|
try {
|
|
7150
|
-
await (0, import_promises.writeFile)(temporaryPath, contents, "utf8");
|
|
7148
|
+
await (0, import_promises.writeFile)(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
7151
7149
|
await (0, import_promises.rename)(temporaryPath, filePath);
|
|
7152
7150
|
} catch (error) {
|
|
7153
7151
|
await (0, import_promises.rm)(temporaryPath, { force: true }).catch(() => void 0);
|
|
@@ -7187,10 +7185,15 @@ async function readProfileStore(profile, store) {
|
|
|
7187
7185
|
}
|
|
7188
7186
|
async function withProfileLock(profile, action, options = {}) {
|
|
7189
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();
|
|
7190
7191
|
const release = await acquireProfileLock(normalizedProfile, options);
|
|
7192
|
+
const ownership = { lockPath, active: true };
|
|
7191
7193
|
try {
|
|
7192
|
-
return await action
|
|
7194
|
+
return await heldProfileLocks.run([...held, ownership], action);
|
|
7193
7195
|
} finally {
|
|
7196
|
+
ownership.active = false;
|
|
7194
7197
|
await release();
|
|
7195
7198
|
}
|
|
7196
7199
|
}
|
|
@@ -7217,28 +7220,18 @@ async function acquireProfileLock(profile, options) {
|
|
|
7217
7220
|
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
|
|
7218
7221
|
const startedAt = Date.now();
|
|
7219
7222
|
const lockPath = profileLockPath(profile);
|
|
7220
|
-
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
|
+
}
|
|
7221
7227
|
while (true) {
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
const token = (0, import_node_crypto.randomUUID)();
|
|
7225
|
-
try {
|
|
7226
|
-
await writeJsonAtomic(profileLockMetadataPath(profile), {
|
|
7227
|
-
pid: process.pid,
|
|
7228
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7229
|
-
token
|
|
7230
|
-
});
|
|
7231
|
-
} catch (error) {
|
|
7232
|
-
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7233
|
-
throw error;
|
|
7234
|
-
}
|
|
7228
|
+
const token = (0, import_node_crypto.randomUUID)();
|
|
7229
|
+
if (await publishProfileLock(profile, lockPath, token)) {
|
|
7235
7230
|
return async () => {
|
|
7236
|
-
await releaseProfileLock(lockPath, token);
|
|
7231
|
+
await releaseProfileLock(profile, lockPath, token);
|
|
7237
7232
|
};
|
|
7238
|
-
} catch (error) {
|
|
7239
|
-
if (!isLockAlreadyHeld(error)) throw error;
|
|
7240
|
-
await signalTestLockContention(profile);
|
|
7241
7233
|
}
|
|
7234
|
+
await signalTestLockContention(profile);
|
|
7242
7235
|
if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
|
|
7243
7236
|
const elapsedMs = Date.now() - startedAt;
|
|
7244
7237
|
if (elapsedMs >= timeoutMs) {
|
|
@@ -7247,21 +7240,35 @@ async function acquireProfileLock(profile, options) {
|
|
|
7247
7240
|
await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
|
|
7248
7241
|
}
|
|
7249
7242
|
}
|
|
7250
|
-
async function
|
|
7251
|
-
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
7252
|
-
const claimPath = (0, import_node_path.join)(lockPath, `.release-${token}.json`);
|
|
7243
|
+
async function publishProfileLock(profile, lockPath, token) {
|
|
7253
7244
|
try {
|
|
7254
|
-
await (0, import_promises.
|
|
7245
|
+
await (0, import_promises.mkdir)(lockPath, { mode: PRIVATE_DIR_MODE });
|
|
7255
7246
|
} catch (error) {
|
|
7256
|
-
if (isErrno(error, "
|
|
7247
|
+
if (isErrno(error, "EEXIST")) return false;
|
|
7257
7248
|
throw error;
|
|
7258
7249
|
}
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
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);
|
|
7263
7264
|
}
|
|
7264
|
-
|
|
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);
|
|
7265
7272
|
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7266
7273
|
}
|
|
7267
7274
|
async function signalTestLockContention(profile) {
|
|
@@ -7278,32 +7285,77 @@ async function signalTestLockContention(profile) {
|
|
|
7278
7285
|
await (0, import_promises.writeFile)(signalPath, `${profile}
|
|
7279
7286
|
`, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
7280
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
|
+
}
|
|
7281
7323
|
async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
7282
7324
|
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
7283
7325
|
const owner = await readJson(ownerPath).catch(() => null);
|
|
7326
|
+
if (owner === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
|
|
7284
7327
|
if (!isStaleOwner(owner, staleAfterMs)) return false;
|
|
7285
7328
|
const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
|
|
7286
|
-
await
|
|
7329
|
+
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
7287
7330
|
const claimPath = (0, import_node_path.join)(lockPath, `.stale-${(0, import_node_crypto.randomUUID)()}.json`);
|
|
7288
7331
|
try {
|
|
7289
|
-
await (0, import_promises.
|
|
7290
|
-
} catch
|
|
7291
|
-
if (isErrno(error, "ENOENT")) return false;
|
|
7332
|
+
await (0, import_promises.link)(ownerPath, claimPath);
|
|
7333
|
+
} catch {
|
|
7292
7334
|
return false;
|
|
7293
7335
|
}
|
|
7336
|
+
const claimedAt = performance.now();
|
|
7337
|
+
await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
|
|
7294
7338
|
const claimed = await readJson(claimPath).catch(() => null);
|
|
7295
7339
|
const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
|
|
7296
|
-
|
|
7297
|
-
|
|
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);
|
|
7298
7344
|
return false;
|
|
7299
7345
|
}
|
|
7346
|
+
if (sameInstance) {
|
|
7347
|
+
await (0, import_promises.rm)(ownerPath, { force: true });
|
|
7348
|
+
await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
|
|
7349
|
+
}
|
|
7300
7350
|
await (0, import_promises.rm)(claimPath, { force: true });
|
|
7301
7351
|
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7352
|
+
if (!sameInstance) return false;
|
|
7353
|
+
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7302
7354
|
return true;
|
|
7303
7355
|
}
|
|
7304
|
-
async function
|
|
7356
|
+
async function waitForTestBarrier(environmentName, profile) {
|
|
7305
7357
|
if (process.env.NODE_ENV !== "test") return;
|
|
7306
|
-
const barrierDirectory = process.env[
|
|
7358
|
+
const barrierDirectory = process.env[environmentName];
|
|
7307
7359
|
if (!barrierDirectory) return;
|
|
7308
7360
|
await (0, import_promises.mkdir)(barrierDirectory, { recursive: true });
|
|
7309
7361
|
const readyPath = (0, import_node_path.join)(barrierDirectory, `ready-${process.pid}-${profile}`);
|
|
@@ -7327,9 +7379,6 @@ function isStaleOwner(owner, staleAfterMs) {
|
|
|
7327
7379
|
}
|
|
7328
7380
|
return !isProcessAlive(owner.pid);
|
|
7329
7381
|
}
|
|
7330
|
-
function isLockAlreadyHeld(error) {
|
|
7331
|
-
return isErrno(error, "EEXIST") || isErrno(error, "ENOTEMPTY");
|
|
7332
|
-
}
|
|
7333
7382
|
function isProcessAlive(pid) {
|
|
7334
7383
|
try {
|
|
7335
7384
|
process.kill(pid, 0);
|
|
@@ -7350,7 +7399,7 @@ function isErrno(error, code) {
|
|
|
7350
7399
|
function sleep(milliseconds) {
|
|
7351
7400
|
return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
|
|
7352
7401
|
}
|
|
7353
|
-
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;
|
|
7354
7403
|
var init_state = __esm({
|
|
7355
7404
|
"src/state.ts"() {
|
|
7356
7405
|
"use strict";
|
|
@@ -7364,8 +7413,18 @@ var init_state = __esm({
|
|
|
7364
7413
|
DEFAULT_LOCK_RETRY_MS = 25;
|
|
7365
7414
|
DEFAULT_STALE_LOCK_MS = 3e4;
|
|
7366
7415
|
TEST_LOCK_CONTENTION_SIGNAL_PATH = "TC_TEST_PROFILE_LOCK_CONTENTION_SIGNAL_PATH";
|
|
7416
|
+
PRIVATE_FILE_MODE = 384;
|
|
7417
|
+
PRIVATE_DIR_MODE = 448;
|
|
7367
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";
|
|
7368
7426
|
invocationStateRoot = new import_node_async_hooks.AsyncLocalStorage();
|
|
7427
|
+
heldProfileLocks = new import_node_async_hooks.AsyncLocalStorage();
|
|
7369
7428
|
ProfileLockTimeoutError = class extends Error {
|
|
7370
7429
|
constructor(profile, timeoutMs) {
|
|
7371
7430
|
super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.`);
|
|
@@ -7373,6 +7432,8 @@ var init_state = __esm({
|
|
|
7373
7432
|
this.name = "ProfileLockTimeoutError";
|
|
7374
7433
|
}
|
|
7375
7434
|
};
|
|
7435
|
+
ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
|
|
7436
|
+
STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
|
|
7376
7437
|
}
|
|
7377
7438
|
});
|
|
7378
7439
|
|
|
@@ -10050,7 +10111,7 @@ var init_profile = __esm({
|
|
|
10050
10111
|
"src/profile.ts"() {
|
|
10051
10112
|
"use strict";
|
|
10052
10113
|
init_state();
|
|
10053
|
-
DEFAULT_HOST = "https://node.tinycloud.xyz";
|
|
10114
|
+
DEFAULT_HOST = "https://tee.node.tinycloud.xyz";
|
|
10054
10115
|
}
|
|
10055
10116
|
});
|
|
10056
10117
|
|