@kubb/studio 5.4.0 → 5.4.2
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/README.md +6 -3
- package/dist/index.cjs +187 -238
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +12 -94
- package/dist/index.js +186 -237
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -2,17 +2,17 @@ import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
|
|
|
2
2
|
import { AGENT_INSTANCE_HEADER, AgentCloseCode, GENERATION_GONE_MESSAGE, generationEventTypes } from "./protocol.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { createHash, hash, randomBytes, randomUUID } from "node:crypto";
|
|
5
|
-
import {
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
|
-
import path, { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
8
6
|
import process$1 from "node:process";
|
|
9
7
|
import { spawn } from "node:child_process";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
8
|
+
import { glob, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
9
|
+
import path, { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
12
10
|
import { FetchError, ofetch } from "ofetch";
|
|
13
11
|
import { promisify, styleText } from "node:util";
|
|
14
12
|
import { createStorage } from "unstorage";
|
|
15
13
|
import fsDriver from "unstorage/drivers/fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, memoryStorage, resolveCacheDir } from "@kubb/core";
|
|
16
16
|
import { builders, detectCodeFormat, generateCode, parseModule } from "magicast";
|
|
17
17
|
import { existsSync } from "node:fs";
|
|
18
18
|
import { pathToFileURL } from "node:url";
|
|
@@ -344,9 +344,6 @@ function getElapsedMs(hrStart) {
|
|
|
344
344
|
return Math.round(ms * 100) / 100;
|
|
345
345
|
}
|
|
346
346
|
//#endregion
|
|
347
|
-
//#region package.json
|
|
348
|
-
var version = "5.4.0";
|
|
349
|
-
//#endregion
|
|
350
347
|
//#region src/operations/machine.ts
|
|
351
348
|
/**
|
|
352
349
|
* Key-value storage the runtime uses for its machine secret and the last Studio config.
|
|
@@ -423,10 +420,6 @@ function responseMessage(data) {
|
|
|
423
420
|
]) if (typeof value === "string" && value) return value;
|
|
424
421
|
}
|
|
425
422
|
/**
|
|
426
|
-
* Retries after the first registration attempt, each backing off twice as far as the last.
|
|
427
|
-
*/
|
|
428
|
-
const REGISTER_RETRIES = 3;
|
|
429
|
-
/**
|
|
430
423
|
* Thrown when Studio rejects the agent token itself (401). Retrying cannot help: the token was
|
|
431
424
|
* revoked, or the agent it belonged to was deleted in the Studio UI. Hosts catch this to forget
|
|
432
425
|
* the stored credential and pair again.
|
|
@@ -463,11 +456,11 @@ function registrationError(cause) {
|
|
|
463
456
|
* identity to the token, reports what the process can take on, and gets back the URL of the one
|
|
464
457
|
* socket it keeps open.
|
|
465
458
|
*
|
|
466
|
-
*
|
|
459
|
+
* Makes one bounded attempt; `runConnection` owns retries. A rejected token (401) throws
|
|
467
460
|
* {@link InvalidAgentTokenError} and an unsupported agent version (426) throws
|
|
468
461
|
* {@link IncompatibleAgentError}, since retrying either cannot help.
|
|
469
462
|
*/
|
|
470
|
-
async function registerAgent({ token, studioUrl, instanceId, capacity }) {
|
|
463
|
+
async function registerAgent({ token, studioUrl, instanceId, capacity, signal }) {
|
|
471
464
|
const body = {
|
|
472
465
|
machineToken: await getMachineToken(),
|
|
473
466
|
instanceId,
|
|
@@ -478,8 +471,9 @@ async function registerAgent({ token, studioUrl, instanceId, capacity }) {
|
|
|
478
471
|
method: "POST",
|
|
479
472
|
headers: { Authorization: `Bearer ${token}` },
|
|
480
473
|
body,
|
|
481
|
-
retry:
|
|
482
|
-
|
|
474
|
+
retry: 0,
|
|
475
|
+
timeout: 1e4,
|
|
476
|
+
signal
|
|
483
477
|
});
|
|
484
478
|
} catch (error) {
|
|
485
479
|
if (rejectedWith(error, 401)) throw new InvalidAgentTokenError(studioUrl, { cause: error });
|
|
@@ -649,6 +643,60 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
|
|
|
649
643
|
}
|
|
650
644
|
}
|
|
651
645
|
//#endregion
|
|
646
|
+
//#region src/operations/constants.ts
|
|
647
|
+
/**
|
|
648
|
+
* Hosted Kubb Studio URL. Exported so credential stores can bind tokens to the resolved instance,
|
|
649
|
+
* not whatever default the client would pick on its own.
|
|
650
|
+
*/
|
|
651
|
+
const defaultStudioUrl = "https://kubb.studio";
|
|
652
|
+
/**
|
|
653
|
+
* Defaults the Studio client uses when a host passes nothing.
|
|
654
|
+
* Config path is left out on purpose: each host discovers that itself.
|
|
655
|
+
*/
|
|
656
|
+
const agentDefaults = {
|
|
657
|
+
studioUrl: defaultStudioUrl,
|
|
658
|
+
retryIntervalMs: 3e4,
|
|
659
|
+
heartbeatIntervalMs: 3e4,
|
|
660
|
+
/**
|
|
661
|
+
* Slowest heartbeat a host may ask for. Studio drops an agent from the active list once its
|
|
662
|
+
* stored ping is older than its liveness window, and it stores a ping at most once a minute, so
|
|
663
|
+
* a slower cadence would make a healthy agent look dead after a single missed ping.
|
|
664
|
+
*/
|
|
665
|
+
maxHeartbeatIntervalMs: 6e4,
|
|
666
|
+
/** How long a heartbeat ping may take before the session is treated as dead. */
|
|
667
|
+
heartbeatTimeoutMs: 1e4,
|
|
668
|
+
maxConcurrent: 1,
|
|
669
|
+
maxGenerations: 8,
|
|
670
|
+
maxGenerationsMb: 100,
|
|
671
|
+
maxSnapshotMb: 50
|
|
672
|
+
};
|
|
673
|
+
function positiveNumber(value) {
|
|
674
|
+
const parsed = Number(value);
|
|
675
|
+
return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* How many generations an agent keeps and how large they may get, read from
|
|
679
|
+
* `KUBB_AGENT_MAX_GENERATIONS`, `KUBB_AGENT_MAX_GENERATIONS_MB` and `KUBB_AGENT_MAX_SNAPSHOT_MB`.
|
|
680
|
+
* An unset or invalid value keeps the default.
|
|
681
|
+
*/
|
|
682
|
+
function resolveGenerationLimits(env = process.env) {
|
|
683
|
+
return {
|
|
684
|
+
maxCount: Math.max(1, Math.floor(positiveNumber(env.KUBB_AGENT_MAX_GENERATIONS) ?? agentDefaults.maxGenerations)),
|
|
685
|
+
maxMb: positiveNumber(env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? agentDefaults.maxGenerationsMb,
|
|
686
|
+
maxSnapshotMb: positiveNumber(env.KUBB_AGENT_MAX_SNAPSHOT_MB) ?? agentDefaults.maxSnapshotMb
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* An agent's capacity read from `KUBB_AGENT_MAX_CONCURRENT`. An unset or invalid value keeps the
|
|
691
|
+
* default: one job at a time.
|
|
692
|
+
*/
|
|
693
|
+
function resolveAgentCapacity(env = process.env) {
|
|
694
|
+
return { maxConcurrent: Math.max(1, Math.floor(positiveNumber(env.KUBB_AGENT_MAX_CONCURRENT) ?? agentDefaults.maxConcurrent)) };
|
|
695
|
+
}
|
|
696
|
+
//#endregion
|
|
697
|
+
//#region package.json
|
|
698
|
+
var version = "5.4.2";
|
|
699
|
+
//#endregion
|
|
652
700
|
//#region src/operations/resolveConfig.ts
|
|
653
701
|
/**
|
|
654
702
|
* Imports a package, falling back to how the user's project would resolve it.
|
|
@@ -1384,57 +1432,6 @@ function withTrailingNewline(code, hadTrailingNewline) {
|
|
|
1384
1432
|
return `${code}\n`;
|
|
1385
1433
|
}
|
|
1386
1434
|
//#endregion
|
|
1387
|
-
//#region src/operations/constants.ts
|
|
1388
|
-
/**
|
|
1389
|
-
* Hosted Kubb Studio URL. Exported so credential stores can bind tokens to the resolved instance,
|
|
1390
|
-
* not whatever default the client would pick on its own.
|
|
1391
|
-
*/
|
|
1392
|
-
const defaultStudioUrl = "https://kubb.studio";
|
|
1393
|
-
/**
|
|
1394
|
-
* Defaults the Studio client uses when a host passes nothing.
|
|
1395
|
-
* Config path is left out on purpose: each host discovers that itself.
|
|
1396
|
-
*/
|
|
1397
|
-
const agentDefaults = {
|
|
1398
|
-
studioUrl: defaultStudioUrl,
|
|
1399
|
-
retryIntervalMs: 3e4,
|
|
1400
|
-
heartbeatIntervalMs: 3e4,
|
|
1401
|
-
/**
|
|
1402
|
-
* Slowest heartbeat a host may ask for. Studio drops an agent from the active list once its
|
|
1403
|
-
* stored ping is older than its liveness window, and it stores a ping at most once a minute, so
|
|
1404
|
-
* a slower cadence would make a healthy agent look dead after a single missed ping.
|
|
1405
|
-
*/
|
|
1406
|
-
maxHeartbeatIntervalMs: 6e4,
|
|
1407
|
-
/** How long a heartbeat ping may take before the session is treated as dead. */
|
|
1408
|
-
heartbeatTimeoutMs: 1e4,
|
|
1409
|
-
maxConcurrent: 1,
|
|
1410
|
-
maxGenerations: 8,
|
|
1411
|
-
maxGenerationsMb: 100,
|
|
1412
|
-
maxSnapshotMb: 50
|
|
1413
|
-
};
|
|
1414
|
-
function positiveNumber(value) {
|
|
1415
|
-
const parsed = Number(value);
|
|
1416
|
-
return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
1417
|
-
}
|
|
1418
|
-
/**
|
|
1419
|
-
* How many generations an agent keeps and how large they may get, read from
|
|
1420
|
-
* `KUBB_AGENT_MAX_GENERATIONS`, `KUBB_AGENT_MAX_GENERATIONS_MB` and `KUBB_AGENT_MAX_SNAPSHOT_MB`.
|
|
1421
|
-
* An unset or invalid value keeps the default.
|
|
1422
|
-
*/
|
|
1423
|
-
function resolveGenerationLimits(env = process.env) {
|
|
1424
|
-
return {
|
|
1425
|
-
maxCount: Math.max(1, Math.floor(positiveNumber(env.KUBB_AGENT_MAX_GENERATIONS) ?? agentDefaults.maxGenerations)),
|
|
1426
|
-
maxMb: positiveNumber(env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? agentDefaults.maxGenerationsMb,
|
|
1427
|
-
maxSnapshotMb: positiveNumber(env.KUBB_AGENT_MAX_SNAPSHOT_MB) ?? agentDefaults.maxSnapshotMb
|
|
1428
|
-
};
|
|
1429
|
-
}
|
|
1430
|
-
/**
|
|
1431
|
-
* An agent's capacity read from `KUBB_AGENT_MAX_CONCURRENT`. An unset or invalid value keeps the
|
|
1432
|
-
* default: one job at a time.
|
|
1433
|
-
*/
|
|
1434
|
-
function resolveAgentCapacity(env = process.env) {
|
|
1435
|
-
return { maxConcurrent: Math.max(1, Math.floor(positiveNumber(env.KUBB_AGENT_MAX_CONCURRENT) ?? agentDefaults.maxConcurrent)) };
|
|
1436
|
-
}
|
|
1437
|
-
//#endregion
|
|
1438
1435
|
//#region src/operations/snapshotPackage.ts
|
|
1439
1436
|
const gzipAsync = promisify(gzip);
|
|
1440
1437
|
const UPLOAD_TIMEOUT_MS = 12e4;
|
|
@@ -2347,39 +2344,13 @@ function applyStudioDefaults(options) {
|
|
|
2347
2344
|
}
|
|
2348
2345
|
/**
|
|
2349
2346
|
* Jobs one agent process can run at once today. Two runs would share this session's hook emitter,
|
|
2350
|
-
* and with it each other's events,
|
|
2347
|
+
* and with it each other's events, so the connection keeps this limit.
|
|
2351
2348
|
*/
|
|
2352
2349
|
const RUNTIME_MAX_CONCURRENT = 1;
|
|
2353
2350
|
const MB = 1048576;
|
|
2354
2351
|
function rssMb() {
|
|
2355
2352
|
return process$1.memoryUsage().rss / MB;
|
|
2356
2353
|
}
|
|
2357
|
-
function backoffDelayMs(attempt, maxMs) {
|
|
2358
|
-
const cap = Math.min(1e3 * 2 ** (attempt - 1), maxMs);
|
|
2359
|
-
return Math.random() * cap;
|
|
2360
|
-
}
|
|
2361
|
-
function reconnect(options, delayMs, attempt) {
|
|
2362
|
-
const { signal, onTokenRejected } = options;
|
|
2363
|
-
if (signal?.aborted) return;
|
|
2364
|
-
const cancel = () => clearTimeout(timer);
|
|
2365
|
-
const timer = setTimeout(() => {
|
|
2366
|
-
signal?.removeEventListener("abort", cancel);
|
|
2367
|
-
if (signal?.aborted) return;
|
|
2368
|
-
new StudioSession({
|
|
2369
|
-
...options,
|
|
2370
|
-
reconnectAttempt: attempt
|
|
2371
|
-
}).start().catch((error) => {
|
|
2372
|
-
if (error instanceof InvalidAgentTokenError) {
|
|
2373
|
-
onTokenRejected?.(error);
|
|
2374
|
-
return;
|
|
2375
|
-
}
|
|
2376
|
-
if (error instanceof IncompatibleAgentError) return;
|
|
2377
|
-
const nextAttempt = attempt + 1;
|
|
2378
|
-
reconnect(options, backoffDelayMs(nextAttempt, options.retryInterval), nextAttempt);
|
|
2379
|
-
});
|
|
2380
|
-
}, delayMs);
|
|
2381
|
-
signal?.addEventListener("abort", cancel, { once: true });
|
|
2382
|
-
}
|
|
2383
2354
|
/**
|
|
2384
2355
|
* Reads what Studio meant by closing the connection. A code Studio did not send on purpose is an
|
|
2385
2356
|
* ordinary drop, and the agent reconnects as it always has.
|
|
@@ -2406,7 +2377,7 @@ function planEnd(close) {
|
|
|
2406
2377
|
}
|
|
2407
2378
|
/**
|
|
2408
2379
|
* One agent-to-Studio RPC transport: opening it, keeping it alive, and serving remote methods.
|
|
2409
|
-
* `
|
|
2380
|
+
* `runConnection` owns the lifetime and retry of each attempt.
|
|
2410
2381
|
*/
|
|
2411
2382
|
var StudioSession = class {
|
|
2412
2383
|
#options;
|
|
@@ -2435,10 +2406,11 @@ var StudioSession = class {
|
|
|
2435
2406
|
* host does not queue jobs before the agent session is registered.
|
|
2436
2407
|
*/
|
|
2437
2408
|
#connectAck = Promise.withResolvers();
|
|
2438
|
-
#
|
|
2439
|
-
|
|
2409
|
+
#ended = Promise.withResolvers();
|
|
2410
|
+
closed = this.#ended.promise;
|
|
2411
|
+
#generationResult;
|
|
2412
|
+
constructor(options) {
|
|
2440
2413
|
this.#options = applyStudioDefaults(options);
|
|
2441
|
-
this.#reconnectAttempt = reconnectAttempt ?? 0;
|
|
2442
2414
|
this.#connectAck.promise.catch(() => {});
|
|
2443
2415
|
}
|
|
2444
2416
|
/**
|
|
@@ -2475,10 +2447,14 @@ var StudioSession = class {
|
|
|
2475
2447
|
async start() {
|
|
2476
2448
|
const { token, studioUrl, signal, heartbeatInterval, installLogger, instanceId, capacity } = this.#options;
|
|
2477
2449
|
await installLogger?.(this.#hooks);
|
|
2450
|
+
signal?.addEventListener("abort", this.#onAbort, { once: true });
|
|
2451
|
+
this.#unhooks.push(() => signal?.removeEventListener("abort", this.#onAbort));
|
|
2478
2452
|
try {
|
|
2453
|
+
signal?.throwIfAborted();
|
|
2479
2454
|
await this.#hooks.callHook("studio:connecting", { url: studioUrl });
|
|
2480
|
-
if (
|
|
2455
|
+
if (capacity.maxConcurrent > RUNTIME_MAX_CONCURRENT) await this.#warn(`Running ${RUNTIME_MAX_CONCURRENT} job at a time: KUBB_AGENT_MAX_CONCURRENT=${capacity.maxConcurrent} needs per-job workers, which this agent does not have yet`);
|
|
2481
2456
|
const registration = await registerAgent({
|
|
2457
|
+
signal,
|
|
2482
2458
|
token,
|
|
2483
2459
|
studioUrl,
|
|
2484
2460
|
instanceId,
|
|
@@ -2487,6 +2463,7 @@ var StudioSession = class {
|
|
|
2487
2463
|
maxConcurrent: Math.min(capacity.maxConcurrent, RUNTIME_MAX_CONCURRENT)
|
|
2488
2464
|
}
|
|
2489
2465
|
});
|
|
2466
|
+
signal?.throwIfAborted();
|
|
2490
2467
|
this.#registration = registration;
|
|
2491
2468
|
this.#studioVersion = registration.version;
|
|
2492
2469
|
const rpc = await (this.#options.connector ?? connectWebSocketRpc)({
|
|
@@ -2497,8 +2474,10 @@ var StudioSession = class {
|
|
|
2497
2474
|
});
|
|
2498
2475
|
this.#rpc = rpc;
|
|
2499
2476
|
rpc.closed.then(this.#onClose);
|
|
2500
|
-
signal?.
|
|
2501
|
-
|
|
2477
|
+
if (signal?.aborted || this.#disposed) {
|
|
2478
|
+
rpc.close();
|
|
2479
|
+
throw new Error("Connection stopped during startup");
|
|
2480
|
+
}
|
|
2502
2481
|
this.#scheduleHeartbeat(heartbeatInterval);
|
|
2503
2482
|
await this.#hooks.callHook("studio:connected", {
|
|
2504
2483
|
url: studioUrl,
|
|
@@ -2511,26 +2490,15 @@ var StudioSession = class {
|
|
|
2511
2490
|
organizationSlug: registration.organizationSlug
|
|
2512
2491
|
});
|
|
2513
2492
|
await this.#connectAck.promise;
|
|
2514
|
-
this.#reconnectAttempt = 0;
|
|
2515
2493
|
await this.#hooks.callHook("studio:ready", {});
|
|
2516
2494
|
} catch (error) {
|
|
2517
|
-
this
|
|
2518
|
-
this.
|
|
2519
|
-
|
|
2520
|
-
if (error instanceof InvalidAgentTokenError || error instanceof IncompatibleAgentError) throw error;
|
|
2521
|
-
await this.#reconnect();
|
|
2495
|
+
await this.dispose();
|
|
2496
|
+
if (!signal?.aborted) await this.#hooks.callHook("studio:error", { error: toError(error) });
|
|
2497
|
+
throw error;
|
|
2522
2498
|
}
|
|
2523
2499
|
}
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
* runtime has no output of its own.
|
|
2527
|
-
*/
|
|
2528
|
-
async #reconnect() {
|
|
2529
|
-
if (this.#options.signal?.aborted) return;
|
|
2530
|
-
const attempt = this.#reconnectAttempt + 1;
|
|
2531
|
-
const delayMs = backoffDelayMs(attempt, this.#options.retryInterval);
|
|
2532
|
-
await this.#hooks.callHook("studio:reconnecting", { delayMs });
|
|
2533
|
-
reconnect(this.#options, delayMs, attempt);
|
|
2500
|
+
reportRetry(delayMs) {
|
|
2501
|
+
return this.#hooks.callHook("studio:reconnecting", { delayMs });
|
|
2534
2502
|
}
|
|
2535
2503
|
#warn(message, permission) {
|
|
2536
2504
|
return this.#hooks.callHook("studio:warn", {
|
|
@@ -2625,34 +2593,35 @@ var StudioSession = class {
|
|
|
2625
2593
|
#onAbort = () => void this.#end({
|
|
2626
2594
|
reason: "shutdown",
|
|
2627
2595
|
retry: false
|
|
2628
|
-
});
|
|
2629
|
-
#onClose = (close) => void this.#end(planEnd(close));
|
|
2630
|
-
/**
|
|
2631
|
-
* Drops the socket and detaches every listener and timer this session added. Idempotent, and
|
|
2632
|
-
* safe before `connect` opened anything.
|
|
2633
|
-
*
|
|
2634
|
-
* @internal
|
|
2635
|
-
*/
|
|
2596
|
+
}).catch(() => {});
|
|
2597
|
+
#onClose = (close) => void this.#end(planEnd(close)).catch(() => {});
|
|
2598
|
+
/** Closes this attempt and waits for its generation to stop before another can start. */
|
|
2636
2599
|
dispose() {
|
|
2600
|
+
return this.#end({
|
|
2601
|
+
reason: "shutdown",
|
|
2602
|
+
retry: false
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
async #end(plan) {
|
|
2606
|
+
if (this.#disposed) {
|
|
2607
|
+
await this.closed;
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
this.#disposed = true;
|
|
2637
2611
|
clearTimeout(this.#heartbeatTimer);
|
|
2638
|
-
this.#heartbeatTimer = void 0;
|
|
2639
2612
|
this.#rpc?.close();
|
|
2640
2613
|
this.#rpc = void 0;
|
|
2641
2614
|
this.#connectAck.reject(/* @__PURE__ */ new Error("Session ended before Studio called connect()"));
|
|
2642
2615
|
for (const unhook of this.#unhooks) unhook();
|
|
2643
2616
|
this.#unhooks.length = 0;
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
this.dispose();
|
|
2653
|
-
await this.#hooks.callHook("studio:disconnected", { reason });
|
|
2654
|
-
if (error) await this.#hooks.callHook("studio:error", { error });
|
|
2655
|
-
if (retry) await this.#reconnect();
|
|
2617
|
+
try {
|
|
2618
|
+
await this.#activeJob?.cancel();
|
|
2619
|
+
await this.#generationResult?.catch(() => {});
|
|
2620
|
+
await this.#hooks.callHook("studio:disconnected", { reason: plan.reason });
|
|
2621
|
+
if (plan.error) await this.#hooks.callHook("studio:error", { error: plan.error });
|
|
2622
|
+
} finally {
|
|
2623
|
+
this.#ended.resolve(plan);
|
|
2624
|
+
}
|
|
2656
2625
|
}
|
|
2657
2626
|
startGeneration(data) {
|
|
2658
2627
|
const generationStream = createGenerationStream(this.#hooks, data.jobId, { onGenerationEnd: (result) => {
|
|
@@ -2671,6 +2640,7 @@ var StudioSession = class {
|
|
|
2671
2640
|
}).finally(() => {
|
|
2672
2641
|
if (this.#activeJob?.cancel === cancelRun) this.#activeJob = void 0;
|
|
2673
2642
|
});
|
|
2643
|
+
if (this.#activeJob?.cancel === cancelRun) this.#generationResult = result;
|
|
2674
2644
|
result.catch(() => {});
|
|
2675
2645
|
return new GenerationRunTarget(generationStream.stream, result, cancelRun, () => {
|
|
2676
2646
|
controller.abort(/* @__PURE__ */ new Error("Generation canceled"));
|
|
@@ -2684,6 +2654,7 @@ var StudioSession = class {
|
|
|
2684
2654
|
if (this.#activeJob?.jobId === jobId) await this.#activeJob.cancel();
|
|
2685
2655
|
}
|
|
2686
2656
|
async #runGeneration(data, controller, cancelRun) {
|
|
2657
|
+
if (this.#disposed) throw new Error("Connection is closed");
|
|
2687
2658
|
if (this.#isGenerating) return this.#refuse("Ignored generate: a generation is already in progress", "A generation is already in progress, please wait for it to finish");
|
|
2688
2659
|
this.#isGenerating = true;
|
|
2689
2660
|
this.#activeJob = {
|
|
@@ -2879,114 +2850,92 @@ var StudioSession = class {
|
|
|
2879
2850
|
}
|
|
2880
2851
|
};
|
|
2881
2852
|
//#endregion
|
|
2882
|
-
//#region src/runtime/client.ts
|
|
2883
|
-
/**
|
|
2884
|
-
* Creates the Kubb Studio client: the connection, the command loop, and the generation event
|
|
2885
|
-
* stream shared by the `kubb studio` CLI command and the Docker agent.
|
|
2886
|
-
*
|
|
2887
|
-
* Every permission is off by default. A host that wants more grants it explicitly. The machine
|
|
2888
|
-
* identity comes from the storage the host installed with `setStorage`, before connecting.
|
|
2889
|
-
*
|
|
2890
|
-
* @example
|
|
2891
|
-
* ```ts
|
|
2892
|
-
* const studio = createClient({ token, configPath, version, loadConfig: () => loadMyConfig() })
|
|
2893
|
-
* await studio.connect()
|
|
2894
|
-
* ```
|
|
2895
|
-
*/
|
|
2896
|
-
function createClient({ onAuthRequired, ...options }) {
|
|
2897
|
-
const controller = new AbortController();
|
|
2898
|
-
const instanceId = options.instanceId ?? randomUUID();
|
|
2899
|
-
function notifyAuthRequired(error) {
|
|
2900
|
-
if (controller.signal.aborted) return;
|
|
2901
|
-
controller.abort();
|
|
2902
|
-
onAuthRequired?.(error);
|
|
2903
|
-
}
|
|
2904
|
-
return {
|
|
2905
|
-
async connect() {
|
|
2906
|
-
await new StudioSession({
|
|
2907
|
-
...options,
|
|
2908
|
-
instanceId,
|
|
2909
|
-
signal: controller.signal,
|
|
2910
|
-
onTokenRejected: notifyAuthRequired
|
|
2911
|
-
}).start();
|
|
2912
|
-
},
|
|
2913
|
-
disconnect() {
|
|
2914
|
-
controller.abort();
|
|
2915
|
-
}
|
|
2916
|
-
};
|
|
2917
|
-
}
|
|
2918
|
-
//#endregion
|
|
2919
2853
|
//#region src/runtime/runConnection.ts
|
|
2920
|
-
/**
|
|
2921
|
-
|
|
2922
|
-
* background reconnect. Resolves with the rejection, or nothing when the run is being shut down.
|
|
2923
|
-
*/
|
|
2924
|
-
function waitForRejection(authRequired, signal) {
|
|
2925
|
-
if (!signal) return authRequired;
|
|
2926
|
-
const settled = new AbortController();
|
|
2927
|
-
const shutdown = new Promise((resolve) => {
|
|
2928
|
-
if (signal.aborted) {
|
|
2929
|
-
resolve(void 0);
|
|
2930
|
-
return;
|
|
2931
|
-
}
|
|
2932
|
-
signal.addEventListener("abort", () => resolve(void 0), {
|
|
2933
|
-
once: true,
|
|
2934
|
-
signal: settled.signal
|
|
2935
|
-
});
|
|
2936
|
-
});
|
|
2937
|
-
return Promise.race([shutdown, authRequired]).finally(() => settled.abort());
|
|
2938
|
-
}
|
|
2939
|
-
/**
|
|
2940
|
-
* Keeps a host connected to Studio across token changes: it opens a client, waits until the run
|
|
2941
|
-
* ends or Studio rejects the token, and reconnects with whatever credential the host hands back.
|
|
2942
|
-
*
|
|
2943
|
-
* The host owns everything around that. Where credentials live, whether a rejected token may be
|
|
2944
|
-
* replaced, and how any of it is reported are all decisions `onTokenRejected` makes.
|
|
2945
|
-
*
|
|
2946
|
-
* @example
|
|
2947
|
-
* ```ts
|
|
2948
|
-
* const outcome = await runConnection({
|
|
2949
|
-
* credentials,
|
|
2950
|
-
* clientOptions: () => ({ studioUrl, configPath, version, loadConfig }),
|
|
2951
|
-
* signal: shutdown.signal,
|
|
2952
|
-
* onTokenRejected: ({ error, live }) => pairAgain(error, live),
|
|
2953
|
-
* })
|
|
2954
|
-
* ```
|
|
2955
|
-
*/
|
|
2956
|
-
async function runConnection({ credentials, clientOptions, onTokenRejected, signal }) {
|
|
2854
|
+
/** Runs the process's one connection loop, including retries and credential replacement. */
|
|
2855
|
+
async function runConnection({ credentials, clientOptions, onTokenRejected, signal, onStarted }) {
|
|
2957
2856
|
let current = credentials;
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2857
|
+
const initialOptions = clientOptions(current);
|
|
2858
|
+
const instanceId = initialOptions.instanceId ?? randomUUID();
|
|
2859
|
+
let options = initialOptions;
|
|
2860
|
+
let attempt = 0;
|
|
2861
|
+
let live = false;
|
|
2862
|
+
while (!signal?.aborted) {
|
|
2863
|
+
const session = new StudioSession({
|
|
2864
|
+
...options,
|
|
2865
|
+
instanceId,
|
|
2963
2866
|
token: current.token,
|
|
2964
|
-
|
|
2867
|
+
signal
|
|
2965
2868
|
});
|
|
2966
|
-
let rejection;
|
|
2967
2869
|
try {
|
|
2968
|
-
await
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
credentials: current,
|
|
2974
|
-
live: true
|
|
2975
|
-
};
|
|
2870
|
+
await session.start();
|
|
2871
|
+
live = true;
|
|
2872
|
+
attempt = 0;
|
|
2873
|
+
onStarted?.();
|
|
2874
|
+
if (!(await session.closed).retry) return signal?.aborted ? "shutdown" : "stopped";
|
|
2976
2875
|
} catch (error) {
|
|
2977
|
-
if (
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2876
|
+
if (signal?.aborted) return "shutdown";
|
|
2877
|
+
if (error instanceof IncompatibleAgentError) throw error;
|
|
2878
|
+
await session.dispose();
|
|
2879
|
+
const end = await session.closed;
|
|
2880
|
+
if (!end.retry && end.reason !== "shutdown") return "stopped";
|
|
2881
|
+
if (error instanceof InvalidAgentTokenError) {
|
|
2882
|
+
const next = await onTokenRejected({
|
|
2883
|
+
error,
|
|
2884
|
+
credentials: current,
|
|
2885
|
+
live
|
|
2886
|
+
});
|
|
2887
|
+
if (!next) return "stopped";
|
|
2888
|
+
current = next;
|
|
2889
|
+
options = clientOptions(current);
|
|
2890
|
+
live = false;
|
|
2891
|
+
attempt = 0;
|
|
2892
|
+
continue;
|
|
2893
|
+
}
|
|
2983
2894
|
} finally {
|
|
2984
|
-
|
|
2895
|
+
await session.dispose();
|
|
2896
|
+
}
|
|
2897
|
+
onStarted?.();
|
|
2898
|
+
const cap = Math.min(1e3 * 2 ** Math.min(attempt++, 16), options.retryInterval ?? agentDefaults.retryIntervalMs);
|
|
2899
|
+
const delayMs = Math.random() * cap;
|
|
2900
|
+
await session.reportRetry(delayMs);
|
|
2901
|
+
try {
|
|
2902
|
+
await setTimeout$1(delayMs, void 0, { signal });
|
|
2903
|
+
} catch (error) {
|
|
2904
|
+
if (!signal?.aborted) throw error;
|
|
2985
2905
|
}
|
|
2986
|
-
const next = await onTokenRejected(rejection);
|
|
2987
|
-
if (!next) return "stopped";
|
|
2988
|
-
current = next;
|
|
2989
2906
|
}
|
|
2907
|
+
return "shutdown";
|
|
2908
|
+
}
|
|
2909
|
+
//#endregion
|
|
2910
|
+
//#region src/runtime/client.ts
|
|
2911
|
+
/** Compatibility facade over the shared connection loop. */
|
|
2912
|
+
function createClient({ onAuthRequired, ...options }) {
|
|
2913
|
+
const shutdown = new AbortController();
|
|
2914
|
+
const started = Promise.withResolvers();
|
|
2915
|
+
let resolved = false;
|
|
2916
|
+
let running;
|
|
2917
|
+
return {
|
|
2918
|
+
connect() {
|
|
2919
|
+
running ??= runConnection({
|
|
2920
|
+
credentials: { token: options.token },
|
|
2921
|
+
clientOptions: () => options,
|
|
2922
|
+
signal: shutdown.signal,
|
|
2923
|
+
onStarted: () => {
|
|
2924
|
+
resolved = true;
|
|
2925
|
+
started.resolve();
|
|
2926
|
+
},
|
|
2927
|
+
onTokenRejected: async ({ error }) => {
|
|
2928
|
+
if (!resolved) throw error;
|
|
2929
|
+
onAuthRequired?.(error);
|
|
2930
|
+
return null;
|
|
2931
|
+
}
|
|
2932
|
+
}).then(() => started.resolve(), (error) => started.reject(error));
|
|
2933
|
+
return started.promise;
|
|
2934
|
+
},
|
|
2935
|
+
disconnect() {
|
|
2936
|
+
shutdown.abort();
|
|
2937
|
+
}
|
|
2938
|
+
};
|
|
2990
2939
|
}
|
|
2991
2940
|
//#endregion
|
|
2992
2941
|
//#region src/operations/pair.ts
|