@miosa/sdk 3.2.2 → 3.2.4
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 +18 -0
- package/dist/index.d.ts +42 -15
- package/dist/index.js +191 -73
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -268,6 +268,23 @@ await sbx.pause();
|
|
|
268
268
|
await sbx.resume();
|
|
269
269
|
```
|
|
270
270
|
|
|
271
|
+
### Cancelling sandbox commands
|
|
272
|
+
|
|
273
|
+
Pass an `AbortSignal` to `sandbox.exec.run(command, { signal, timeoutSec })` or `sandbox.exec.stream(command, { signal, timeoutSec })`.
|
|
274
|
+
Aborting rejects with the signal's abort reason (an `AbortError` by default); it does not return a successful exit code or replay the command.
|
|
275
|
+
Signal-bearing `exec.run` uses the request-owned stream and aggregates its exact stdout, stderr, and terminal exit code.
|
|
276
|
+
If that stream ends without an exit event, `exec.run` rejects with `EXEC_STREAM_INCOMPLETE` (502, not retryable).
|
|
277
|
+
Ordinary execution without a signal keeps the standard exec endpoint.
|
|
278
|
+
An explicit `timeoutSec` sets the guest deadline and allows five additional seconds for its response.
|
|
279
|
+
A guest that overruns that deadline rejects with `TimeoutError`, never with `AbortError`.
|
|
280
|
+
Cancellation requires the server's request-owned streaming implementation and the guest's process-group cancellation support.
|
|
281
|
+
|
|
282
|
+
### Confirming sandbox deletion
|
|
283
|
+
|
|
284
|
+
`sandbox.destroy()` acknowledges deletion and updates the handle to the server's state.
|
|
285
|
+
If deletion is still in progress, the handle reports `destroying` and exposes `sandbox.data.operation_id` when supplied by the server.
|
|
286
|
+
Use `sandbox.refresh()` to verify completion; an acknowledgement alone does not prove resource cleanup.
|
|
287
|
+
|
|
271
288
|
## Publish from sandbox
|
|
272
289
|
|
|
273
290
|
Preview is mutable; publish creates a durable deployment version. Static output
|
|
@@ -414,6 +431,7 @@ try {
|
|
|
414
431
|
```
|
|
415
432
|
|
|
416
433
|
The SDK retries `429` and `5xx` automatically (3 retries, exponential backoff + jitter). Set `maxRetries: 0` to disable.
|
|
434
|
+
Sandbox `exec` requests are never retried, because a replayed command would run twice; see [Cancelling sandbox commands](#cancelling-sandbox-commands).
|
|
417
435
|
Every `MiosaError` carries `retryable` (server-supplied when present, otherwise `true` for 408/429/5xx and transport failures) and `requestId` for support correlation.
|
|
418
436
|
|
|
419
437
|
## Configuration
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ interface RequestOptions {
|
|
|
6
6
|
headers?: Record<string, string>;
|
|
7
7
|
body?: unknown;
|
|
8
8
|
timeout?: number;
|
|
9
|
+
/** Cancel this request and its response stream without retrying. */
|
|
10
|
+
signal?: AbortSignal | undefined;
|
|
11
|
+
/** Override retries for operations which cannot safely be replayed. */
|
|
12
|
+
maxRetries?: number;
|
|
9
13
|
/** Return raw Response instead of parsing JSON */
|
|
10
14
|
rawResponse?: boolean;
|
|
11
15
|
/** Send body as multipart/form-data */
|
|
@@ -6728,7 +6732,7 @@ type SandboxSize = "xs" | "small" | "medium" | "large" | "xl";
|
|
|
6728
6732
|
type SandboxId = string & {
|
|
6729
6733
|
readonly __brand: "SandboxId";
|
|
6730
6734
|
};
|
|
6731
|
-
type SandboxState = "provisioning" | "running" | "paused" | "destroyed" | "error";
|
|
6735
|
+
type SandboxState = "provisioning" | "running" | "paused" | "destroying" | "destroyed" | "error";
|
|
6732
6736
|
interface SandboxCreateParams {
|
|
6733
6737
|
templateId?: string;
|
|
6734
6738
|
template_id?: string;
|
|
@@ -6848,6 +6852,8 @@ interface SandboxListParams {
|
|
|
6848
6852
|
external_project_id?: string;
|
|
6849
6853
|
}
|
|
6850
6854
|
interface SandboxExecOptions {
|
|
6855
|
+
/** Abort the request-scoped execution, including its response stream. */
|
|
6856
|
+
signal?: AbortSignal;
|
|
6851
6857
|
cwd?: string;
|
|
6852
6858
|
workingDir?: string;
|
|
6853
6859
|
working_dir?: string;
|
|
@@ -6917,6 +6923,8 @@ interface SandboxExecRunner {
|
|
|
6917
6923
|
stream(command: string, options?: SandboxExecOptions): AsyncIterableIterator<SandboxExecEvent>;
|
|
6918
6924
|
}
|
|
6919
6925
|
interface SandboxData {
|
|
6926
|
+
operation_id?: string | null;
|
|
6927
|
+
request_id?: string | null;
|
|
6920
6928
|
id: SandboxId;
|
|
6921
6929
|
state: SandboxState;
|
|
6922
6930
|
slug?: string;
|
|
@@ -7372,6 +7380,7 @@ declare class Sandbox {
|
|
|
7372
7380
|
*/
|
|
7373
7381
|
prompt(prompt: string, options?: SandboxPromptOptions): Promise<AgentRun>;
|
|
7374
7382
|
private runExec;
|
|
7383
|
+
private runCancellableExec;
|
|
7375
7384
|
private execStream;
|
|
7376
7385
|
writeFile(path: string, content: string | Uint8Array): Promise<void>;
|
|
7377
7386
|
download(path: string): Promise<Uint8Array>;
|
|
@@ -7459,30 +7468,39 @@ declare class Sandbox {
|
|
|
7459
7468
|
/** Check readiness of the sandbox (GET /sandboxes/:id/readiness). */
|
|
7460
7469
|
readiness(): Promise<Record<string, unknown>>;
|
|
7461
7470
|
/**
|
|
7462
|
-
* Block until the sandbox reports ready, or *timeout* seconds
|
|
7471
|
+
* Block until the sandbox reports ready, or the caller's *timeout* seconds
|
|
7472
|
+
* (the full deadline budget) elapse.
|
|
7463
7473
|
*
|
|
7464
|
-
* When `stream` is `true` (the default) this opens an SSE connection
|
|
7465
|
-
*
|
|
7466
|
-
* `event: ready` frame.
|
|
7467
|
-
*
|
|
7468
|
-
*
|
|
7474
|
+
* When `stream` is `true` (the default) this opens an SSE connection to
|
|
7475
|
+
* `GET /sandboxes/:id/readiness/stream?timeout=<window>` and waits for an
|
|
7476
|
+
* `event: ready` frame. `<window>` is the caller's remaining budget clamped
|
|
7477
|
+
* to {@link READINESS_STREAM_MAX_TIMEOUT_SEC}, the server's own maximum
|
|
7478
|
+
* lease for a single stream connection — a long-restoring persistent
|
|
7479
|
+
* sandbox (e.g. Docker restore, 60-90 s) legitimately outlives one lease.
|
|
7480
|
+
* The server emits `ready` immediately if the sandbox is already ready,
|
|
7481
|
+
* `event: timeout` when *that connection's* window elapses without a
|
|
7482
|
+
* verdict, and `event: error` on a terminal boot failure.
|
|
7469
7483
|
*
|
|
7470
|
-
*
|
|
7471
|
-
*
|
|
7484
|
+
* An `event: timeout` (or the stream closing without a verdict) is NOT
|
|
7485
|
+
* treated as the final answer while the caller's overall deadline still has
|
|
7486
|
+
* budget left — that would silently truncate a 180 s wait down to the
|
|
7487
|
+
* server's lease window and return `false` while the sandbox was still
|
|
7488
|
+
* mid-restore. Instead this reconnects the stream (or falls back to
|
|
7489
|
+
* polling) for the remaining budget. `false` is only returned once the
|
|
7490
|
+
* caller's full deadline has elapsed.
|
|
7472
7491
|
*
|
|
7473
7492
|
* Throws {@link MiosaError} with code `SANDBOX_BOOT_FAILED` as soon as the
|
|
7474
7493
|
* sandbox is observed in a terminal failure state (`error` or `destroyed`)
|
|
7475
7494
|
* while waiting, instead of silently exhausting the timeout and returning
|
|
7476
7495
|
* `false` — a permanent boot failure and "still booting, keep waiting" must
|
|
7477
|
-
* never look the same to the caller.
|
|
7478
|
-
*
|
|
7479
|
-
*
|
|
7480
|
-
* authoritative {@link readiness} check before giving up, to catch a
|
|
7481
|
-
* failure that landed right at the boundary.
|
|
7496
|
+
* never look the same to the caller. On each `event: timeout` boundary this
|
|
7497
|
+
* also makes one authoritative {@link readiness} check before deciding to
|
|
7498
|
+
* reconnect or stop, to catch a failure that landed right at the boundary.
|
|
7482
7499
|
*
|
|
7483
7500
|
* If the SSE endpoint returns 404 (server pre-dates the streaming
|
|
7484
7501
|
* endpoint) this transparently falls back to polling {@link readiness}
|
|
7485
|
-
* every 10 ms until ready,
|
|
7502
|
+
* every 10 ms until ready, the caller's deadline elapses, or terminal
|
|
7503
|
+
* failure.
|
|
7486
7504
|
*/
|
|
7487
7505
|
waitUntilReady(options?: {
|
|
7488
7506
|
timeout?: number;
|
|
@@ -7513,6 +7531,15 @@ declare class Sandbox {
|
|
|
7513
7531
|
* stream endpoint is unavailable (404 or transport error) so callers
|
|
7514
7532
|
* can fall back to polling.
|
|
7515
7533
|
*/
|
|
7534
|
+
/**
|
|
7535
|
+
* Opens one readiness SSE connection for at most *windowSec* seconds (this
|
|
7536
|
+
* is a single lease, not the caller's overall {@link waitUntilReady}
|
|
7537
|
+
* budget — see there for how leases are chained). *windowSec* is sent to
|
|
7538
|
+
* the server as `?timeout=<seconds>` so the server's own stream deadline
|
|
7539
|
+
* matches what was actually requested instead of always falling back to
|
|
7540
|
+
* its 30 s default, which is what let a 180 s `waitUntilReady` call get
|
|
7541
|
+
* cut short at 30 s.
|
|
7542
|
+
*/
|
|
7516
7543
|
private tryReadinessStream;
|
|
7517
7544
|
destroy(): Promise<void>;
|
|
7518
7545
|
delete(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -174,7 +174,7 @@ var TokenRefreshFailedError = class extends MiosaError {
|
|
|
174
174
|
};
|
|
175
175
|
|
|
176
176
|
// src/version.ts
|
|
177
|
-
var SDK_VERSION = "3.2.
|
|
177
|
+
var SDK_VERSION = "3.2.4";
|
|
178
178
|
var SDK_USER_AGENT = `@miosa/sdk/${SDK_VERSION}`;
|
|
179
179
|
|
|
180
180
|
// src/http.ts
|
|
@@ -271,8 +271,12 @@ var HttpClient = class {
|
|
|
271
271
|
fetchBody = JSON.stringify(body5);
|
|
272
272
|
}
|
|
273
273
|
let lastError;
|
|
274
|
-
|
|
274
|
+
const maxRetries = options.maxRetries ?? this.maxRetries;
|
|
275
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
276
|
+
options.signal?.throwIfAborted();
|
|
275
277
|
const controller = new AbortController();
|
|
278
|
+
const cancel = () => controller.abort(options.signal?.reason);
|
|
279
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
276
280
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
277
281
|
try {
|
|
278
282
|
const response = await fetch(this.buildUrl(path), {
|
|
@@ -296,7 +300,7 @@ var HttpClient = class {
|
|
|
296
300
|
errorBody,
|
|
297
301
|
requestId
|
|
298
302
|
);
|
|
299
|
-
if (RETRY_STATUS.has(response.status) && attempt <
|
|
303
|
+
if (RETRY_STATUS.has(response.status) && attempt < maxRetries) {
|
|
300
304
|
lastError = error;
|
|
301
305
|
await sleep(retryDelay(attempt, retryAfter));
|
|
302
306
|
continue;
|
|
@@ -313,7 +317,7 @@ var HttpClient = class {
|
|
|
313
317
|
if (options.rawResponse) return response;
|
|
314
318
|
return await response.json();
|
|
315
319
|
} catch (err) {
|
|
316
|
-
|
|
320
|
+
options.signal?.throwIfAborted();
|
|
317
321
|
if (err instanceof MiosaError) {
|
|
318
322
|
throw err;
|
|
319
323
|
}
|
|
@@ -327,7 +331,7 @@ var HttpClient = class {
|
|
|
327
331
|
`Network error reaching ${path}: ${err.message}`,
|
|
328
332
|
err
|
|
329
333
|
);
|
|
330
|
-
if (attempt <
|
|
334
|
+
if (attempt < maxRetries) {
|
|
331
335
|
lastError = networkErr;
|
|
332
336
|
await sleep(retryDelay(attempt, null));
|
|
333
337
|
continue;
|
|
@@ -335,6 +339,9 @@ var HttpClient = class {
|
|
|
335
339
|
throw networkErr;
|
|
336
340
|
}
|
|
337
341
|
throw err;
|
|
342
|
+
} finally {
|
|
343
|
+
clearTimeout(timer);
|
|
344
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
338
345
|
}
|
|
339
346
|
}
|
|
340
347
|
throw lastError ?? new MiosaError(
|
|
@@ -394,44 +401,52 @@ var HttpClient = class {
|
|
|
394
401
|
headers = { ...headers, "Content-Type": "application/json" };
|
|
395
402
|
body5 = JSON.stringify(options.body);
|
|
396
403
|
}
|
|
404
|
+
options.signal?.throwIfAborted();
|
|
397
405
|
const controller = new AbortController();
|
|
398
|
-
|
|
406
|
+
const cancel = () => controller.abort(options.signal?.reason);
|
|
407
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
408
|
+
const deadline = options.timeout === void 0 ? void 0 : new TimeoutError(
|
|
409
|
+
`Stream ${path} timed out after ${options.timeout}ms`
|
|
410
|
+
);
|
|
411
|
+
const timer = deadline === void 0 ? void 0 : setTimeout(() => controller.abort(deadline), options.timeout);
|
|
412
|
+
let reader;
|
|
399
413
|
try {
|
|
400
|
-
response
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
414
|
+
let response;
|
|
415
|
+
try {
|
|
416
|
+
response = await fetch(this.buildUrl(path), {
|
|
417
|
+
method,
|
|
418
|
+
headers,
|
|
419
|
+
body: body5,
|
|
420
|
+
signal: controller.signal
|
|
421
|
+
});
|
|
422
|
+
} catch (err) {
|
|
423
|
+
if (err instanceof TypeError) {
|
|
424
|
+
throw new NetworkError(
|
|
425
|
+
`Network error opening SSE stream: ${err.message}`,
|
|
426
|
+
err
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
throw err;
|
|
430
|
+
}
|
|
431
|
+
if (!response.ok) {
|
|
432
|
+
let errorBody = {};
|
|
433
|
+
try {
|
|
434
|
+
errorBody = await response.json();
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
throw MiosaError.fromResponse(
|
|
438
|
+
response.status,
|
|
439
|
+
errorBody,
|
|
440
|
+
response.headers.get("x-request-id") ?? void 0
|
|
411
441
|
);
|
|
412
442
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (!response.ok) {
|
|
416
|
-
let errorBody = {};
|
|
417
|
-
try {
|
|
418
|
-
errorBody = await response.json();
|
|
419
|
-
} catch {
|
|
443
|
+
if (!response.body) {
|
|
444
|
+
throw new MiosaError("SSE stream has no body", 0, "NO_STREAM_BODY");
|
|
420
445
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
if (!response.body) {
|
|
428
|
-
throw new MiosaError("SSE stream has no body", 0, "NO_STREAM_BODY");
|
|
429
|
-
}
|
|
430
|
-
const reader = response.body.getReader();
|
|
431
|
-
const decoder = new TextDecoder();
|
|
432
|
-
let buffer = "";
|
|
433
|
-
let event = null;
|
|
434
|
-
try {
|
|
446
|
+
reader = response.body.getReader();
|
|
447
|
+
const decoder = new TextDecoder();
|
|
448
|
+
let buffer = "";
|
|
449
|
+
let event = null;
|
|
435
450
|
while (true) {
|
|
436
451
|
const { done, value } = await reader.read();
|
|
437
452
|
if (done) break;
|
|
@@ -459,9 +474,17 @@ var HttpClient = class {
|
|
|
459
474
|
}
|
|
460
475
|
}
|
|
461
476
|
}
|
|
477
|
+
} catch (err) {
|
|
478
|
+
options.signal?.throwIfAborted();
|
|
479
|
+
if (deadline !== void 0 && controller.signal.reason === deadline) {
|
|
480
|
+
throw deadline;
|
|
481
|
+
}
|
|
482
|
+
throw err;
|
|
462
483
|
} finally {
|
|
484
|
+
clearTimeout(timer);
|
|
485
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
463
486
|
controller.abort();
|
|
464
|
-
reader
|
|
487
|
+
reader?.releaseLock();
|
|
465
488
|
}
|
|
466
489
|
}
|
|
467
490
|
/**
|
|
@@ -8494,6 +8517,7 @@ function isCommandReady(payload) {
|
|
|
8494
8517
|
return status === "ready";
|
|
8495
8518
|
});
|
|
8496
8519
|
}
|
|
8520
|
+
var READINESS_STREAM_MAX_TIMEOUT_SEC = 120;
|
|
8497
8521
|
var SANDBOX_TEMPLATE = "miosa-sandbox";
|
|
8498
8522
|
var SANDBOX_TIER_BY_CPU = {
|
|
8499
8523
|
1: { size: "xs", memoryMb: 2048 },
|
|
@@ -8628,6 +8652,16 @@ function raiseUnresolvableShape(cpuCount, memoryMb, tier) {
|
|
|
8628
8652
|
`Unsupported cpu/memory combination (${supplied}). Supported tiers: xs (1/2048), small (2/4096), medium (4/8192), large (8/16384), xl (16/32768). Pass a matching cpuCount + memoryMb (any diskSizeMb is allowed), use size, or supply cpuCount, memoryMb, and diskSizeMb together for a custom shape.`
|
|
8629
8653
|
);
|
|
8630
8654
|
}
|
|
8655
|
+
function execDeadline(options) {
|
|
8656
|
+
const seconds = options?.timeout ?? options?.timeoutSec ?? options?.timeout_sec;
|
|
8657
|
+
if (seconds === void 0) return {};
|
|
8658
|
+
if (!Number.isFinite(seconds) || seconds <= 0 || seconds > 86400) {
|
|
8659
|
+
throw new RangeError(
|
|
8660
|
+
"Exec timeout must be greater than zero and at most 86400 seconds"
|
|
8661
|
+
);
|
|
8662
|
+
}
|
|
8663
|
+
return { timeout: seconds * 1e3 + 5e3 };
|
|
8664
|
+
}
|
|
8631
8665
|
function execBody(command, options = {}) {
|
|
8632
8666
|
return stripUndefined26({
|
|
8633
8667
|
command,
|
|
@@ -9087,10 +9121,17 @@ var Sandbox = class _Sandbox {
|
|
|
9087
9121
|
}
|
|
9088
9122
|
async runExec(command, options) {
|
|
9089
9123
|
this.assertRunning("exec");
|
|
9124
|
+
if (options?.signal) return this.runCancellableExec(command, options);
|
|
9090
9125
|
const response = unwrap48(
|
|
9091
|
-
await this.http.
|
|
9126
|
+
await this.http.request(
|
|
9092
9127
|
`/sandboxes/${this.id}/exec`,
|
|
9093
|
-
|
|
9128
|
+
{
|
|
9129
|
+
method: "POST",
|
|
9130
|
+
body: execBody(command, options),
|
|
9131
|
+
signal: options?.signal,
|
|
9132
|
+
maxRetries: 0,
|
|
9133
|
+
...execDeadline(options)
|
|
9134
|
+
}
|
|
9094
9135
|
)
|
|
9095
9136
|
);
|
|
9096
9137
|
const exitCode = Number(response.exit_code ?? response.exitCode ?? 0);
|
|
@@ -9106,13 +9147,42 @@ var Sandbox = class _Sandbox {
|
|
|
9106
9147
|
}
|
|
9107
9148
|
return result;
|
|
9108
9149
|
}
|
|
9150
|
+
async runCancellableExec(command, options) {
|
|
9151
|
+
let stdout = "";
|
|
9152
|
+
let stderr = "";
|
|
9153
|
+
let exitCode;
|
|
9154
|
+
for await (const event of this.execStream(command, options)) {
|
|
9155
|
+
if (event.type === "stdout" && typeof event.line === "string")
|
|
9156
|
+
stdout += event.line;
|
|
9157
|
+
if (event.type === "stderr" && typeof event.line === "string")
|
|
9158
|
+
stderr += event.line;
|
|
9159
|
+
if (event.type === "exit" && typeof event.exit_code === "number") {
|
|
9160
|
+
exitCode = event.exit_code;
|
|
9161
|
+
break;
|
|
9162
|
+
}
|
|
9163
|
+
}
|
|
9164
|
+
options.signal?.throwIfAborted();
|
|
9165
|
+
if (exitCode === void 0) {
|
|
9166
|
+
throw new MiosaError(
|
|
9167
|
+
"Execution stream ended without a terminal result",
|
|
9168
|
+
502,
|
|
9169
|
+
"EXEC_STREAM_INCOMPLETE",
|
|
9170
|
+
void 0,
|
|
9171
|
+
void 0,
|
|
9172
|
+
false
|
|
9173
|
+
);
|
|
9174
|
+
}
|
|
9175
|
+
return { stdout, stderr, exitCode, exit_code: exitCode };
|
|
9176
|
+
}
|
|
9109
9177
|
execStream(command, options) {
|
|
9110
9178
|
this.assertRunning("exec.stream");
|
|
9111
9179
|
const frames = this.http.streamFrames(
|
|
9112
9180
|
`/sandboxes/${this.id}/exec/stream`,
|
|
9113
9181
|
{
|
|
9114
9182
|
method: "POST",
|
|
9115
|
-
body: execBody(command, options)
|
|
9183
|
+
body: execBody(command, options),
|
|
9184
|
+
signal: options?.signal,
|
|
9185
|
+
...execDeadline(options)
|
|
9116
9186
|
}
|
|
9117
9187
|
);
|
|
9118
9188
|
return (async function* () {
|
|
@@ -9496,47 +9566,66 @@ var Sandbox = class _Sandbox {
|
|
|
9496
9566
|
);
|
|
9497
9567
|
}
|
|
9498
9568
|
/**
|
|
9499
|
-
* Block until the sandbox reports ready, or *timeout* seconds
|
|
9569
|
+
* Block until the sandbox reports ready, or the caller's *timeout* seconds
|
|
9570
|
+
* (the full deadline budget) elapse.
|
|
9500
9571
|
*
|
|
9501
|
-
* When `stream` is `true` (the default) this opens an SSE connection
|
|
9502
|
-
*
|
|
9503
|
-
* `event: ready` frame.
|
|
9504
|
-
*
|
|
9505
|
-
*
|
|
9572
|
+
* When `stream` is `true` (the default) this opens an SSE connection to
|
|
9573
|
+
* `GET /sandboxes/:id/readiness/stream?timeout=<window>` and waits for an
|
|
9574
|
+
* `event: ready` frame. `<window>` is the caller's remaining budget clamped
|
|
9575
|
+
* to {@link READINESS_STREAM_MAX_TIMEOUT_SEC}, the server's own maximum
|
|
9576
|
+
* lease for a single stream connection — a long-restoring persistent
|
|
9577
|
+
* sandbox (e.g. Docker restore, 60-90 s) legitimately outlives one lease.
|
|
9578
|
+
* The server emits `ready` immediately if the sandbox is already ready,
|
|
9579
|
+
* `event: timeout` when *that connection's* window elapses without a
|
|
9580
|
+
* verdict, and `event: error` on a terminal boot failure.
|
|
9506
9581
|
*
|
|
9507
|
-
*
|
|
9508
|
-
*
|
|
9582
|
+
* An `event: timeout` (or the stream closing without a verdict) is NOT
|
|
9583
|
+
* treated as the final answer while the caller's overall deadline still has
|
|
9584
|
+
* budget left — that would silently truncate a 180 s wait down to the
|
|
9585
|
+
* server's lease window and return `false` while the sandbox was still
|
|
9586
|
+
* mid-restore. Instead this reconnects the stream (or falls back to
|
|
9587
|
+
* polling) for the remaining budget. `false` is only returned once the
|
|
9588
|
+
* caller's full deadline has elapsed.
|
|
9509
9589
|
*
|
|
9510
9590
|
* Throws {@link MiosaError} with code `SANDBOX_BOOT_FAILED` as soon as the
|
|
9511
9591
|
* sandbox is observed in a terminal failure state (`error` or `destroyed`)
|
|
9512
9592
|
* while waiting, instead of silently exhausting the timeout and returning
|
|
9513
9593
|
* `false` — a permanent boot failure and "still booting, keep waiting" must
|
|
9514
|
-
* never look the same to the caller.
|
|
9515
|
-
*
|
|
9516
|
-
*
|
|
9517
|
-
* authoritative {@link readiness} check before giving up, to catch a
|
|
9518
|
-
* failure that landed right at the boundary.
|
|
9594
|
+
* never look the same to the caller. On each `event: timeout` boundary this
|
|
9595
|
+
* also makes one authoritative {@link readiness} check before deciding to
|
|
9596
|
+
* reconnect or stop, to catch a failure that landed right at the boundary.
|
|
9519
9597
|
*
|
|
9520
9598
|
* If the SSE endpoint returns 404 (server pre-dates the streaming
|
|
9521
9599
|
* endpoint) this transparently falls back to polling {@link readiness}
|
|
9522
|
-
* every 10 ms until ready,
|
|
9600
|
+
* every 10 ms until ready, the caller's deadline elapses, or terminal
|
|
9601
|
+
* failure.
|
|
9523
9602
|
*/
|
|
9524
9603
|
async waitUntilReady(options = {}) {
|
|
9525
9604
|
const timeout = options.timeout ?? 30;
|
|
9526
9605
|
const stream = options.stream ?? true;
|
|
9527
9606
|
const requireCommandReady = options.requireCommandReady ?? true;
|
|
9607
|
+
const deadlineMs = Date.now() + timeout * 1e3;
|
|
9528
9608
|
if (stream) {
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
await this.
|
|
9536
|
-
|
|
9609
|
+
while (true) {
|
|
9610
|
+
const remainingMs = deadlineMs - Date.now();
|
|
9611
|
+
if (remainingMs <= 0) return false;
|
|
9612
|
+
const remainingSec = remainingMs / 1e3;
|
|
9613
|
+
const clamped = remainingSec > READINESS_STREAM_MAX_TIMEOUT_SEC;
|
|
9614
|
+
const windowSec = clamped ? READINESS_STREAM_MAX_TIMEOUT_SEC : remainingSec;
|
|
9615
|
+
const sseResult = await this.tryReadinessStream(windowSec);
|
|
9616
|
+
if (sseResult === true) {
|
|
9617
|
+
await this.adoptReadyState();
|
|
9618
|
+
return true;
|
|
9619
|
+
}
|
|
9620
|
+
if (sseResult === false) {
|
|
9621
|
+
await this.throwIfTerminallyFailed();
|
|
9622
|
+
if (!clamped || Date.now() >= deadlineMs) return false;
|
|
9623
|
+
continue;
|
|
9624
|
+
}
|
|
9625
|
+
break;
|
|
9537
9626
|
}
|
|
9627
|
+
if (Date.now() >= deadlineMs) return false;
|
|
9538
9628
|
}
|
|
9539
|
-
const deadlineMs = Date.now() + timeout * 1e3;
|
|
9540
9629
|
while (Date.now() < deadlineMs) {
|
|
9541
9630
|
try {
|
|
9542
9631
|
const data = await this.readiness();
|
|
@@ -9600,9 +9689,25 @@ var Sandbox = class _Sandbox {
|
|
|
9600
9689
|
* stream endpoint is unavailable (404 or transport error) so callers
|
|
9601
9690
|
* can fall back to polling.
|
|
9602
9691
|
*/
|
|
9603
|
-
|
|
9692
|
+
/**
|
|
9693
|
+
* Opens one readiness SSE connection for at most *windowSec* seconds (this
|
|
9694
|
+
* is a single lease, not the caller's overall {@link waitUntilReady}
|
|
9695
|
+
* budget — see there for how leases are chained). *windowSec* is sent to
|
|
9696
|
+
* the server as `?timeout=<seconds>` so the server's own stream deadline
|
|
9697
|
+
* matches what was actually requested instead of always falling back to
|
|
9698
|
+
* its 30 s default, which is what let a 180 s `waitUntilReady` call get
|
|
9699
|
+
* cut short at 30 s.
|
|
9700
|
+
*/
|
|
9701
|
+
async tryReadinessStream(windowSec) {
|
|
9702
|
+
const clampedWindowSec = Math.max(
|
|
9703
|
+
1,
|
|
9704
|
+
Math.min(windowSec, READINESS_STREAM_MAX_TIMEOUT_SEC)
|
|
9705
|
+
);
|
|
9604
9706
|
const abort = new AbortController();
|
|
9605
|
-
const timer = setTimeout(
|
|
9707
|
+
const timer = setTimeout(
|
|
9708
|
+
() => abort.abort(),
|
|
9709
|
+
(clampedWindowSec + 5) * 1e3
|
|
9710
|
+
);
|
|
9606
9711
|
try {
|
|
9607
9712
|
const headers = {
|
|
9608
9713
|
Authorization: `Bearer ${this.http.apiKey}`,
|
|
@@ -9614,7 +9719,7 @@ var Sandbox = class _Sandbox {
|
|
|
9614
9719
|
"User-Agent": SDK_USER_AGENT
|
|
9615
9720
|
};
|
|
9616
9721
|
const response = await fetch(
|
|
9617
|
-
`${this.http.baseUrl}/sandboxes/${this.id}/readiness/stream`,
|
|
9722
|
+
`${this.http.baseUrl}/sandboxes/${this.id}/readiness/stream?timeout=${clampedWindowSec}`,
|
|
9618
9723
|
{ method: "GET", headers, signal: abort.signal }
|
|
9619
9724
|
);
|
|
9620
9725
|
if (response.status === 404) return null;
|
|
@@ -9635,6 +9740,7 @@ var Sandbox = class _Sandbox {
|
|
|
9635
9740
|
const evt = line.slice(6).trim();
|
|
9636
9741
|
if (evt === "ready") return true;
|
|
9637
9742
|
if (evt === "timeout") return false;
|
|
9743
|
+
if (evt === "error") throw this.bootFailedError("error");
|
|
9638
9744
|
}
|
|
9639
9745
|
}
|
|
9640
9746
|
}
|
|
@@ -9645,7 +9751,10 @@ var Sandbox = class _Sandbox {
|
|
|
9645
9751
|
}
|
|
9646
9752
|
}
|
|
9647
9753
|
return null;
|
|
9648
|
-
} catch {
|
|
9754
|
+
} catch (err) {
|
|
9755
|
+
if (err instanceof MiosaError && err.code === "SANDBOX_BOOT_FAILED") {
|
|
9756
|
+
throw err;
|
|
9757
|
+
}
|
|
9649
9758
|
return null;
|
|
9650
9759
|
} finally {
|
|
9651
9760
|
clearTimeout(timer);
|
|
@@ -9656,12 +9765,21 @@ var Sandbox = class _Sandbox {
|
|
|
9656
9765
|
}
|
|
9657
9766
|
}
|
|
9658
9767
|
async destroy() {
|
|
9659
|
-
if (this.data.state === "destroyed")
|
|
9660
|
-
|
|
9768
|
+
if (this.data.state === "destroyed" || this.data.state === "destroying") {
|
|
9769
|
+
return;
|
|
9770
|
+
}
|
|
9771
|
+
const receipt = unwrap48(
|
|
9772
|
+
await this.http.delete(
|
|
9773
|
+
`/sandboxes/${this.id}`
|
|
9774
|
+
)
|
|
9775
|
+
) ?? {};
|
|
9776
|
+
const settled = receipt.state === "destroyed" || receipt.state === void 0 && receipt.operation_id == null;
|
|
9661
9777
|
this.data = {
|
|
9662
9778
|
...this.data,
|
|
9663
|
-
state: "destroyed",
|
|
9664
|
-
|
|
9779
|
+
state: settled ? "destroyed" : "destroying",
|
|
9780
|
+
ready: false,
|
|
9781
|
+
operation_id: receipt.operation_id ?? this.data.operation_id ?? null,
|
|
9782
|
+
destroyed_at: receipt.destroyed_at ?? this.data.destroyed_at ?? null
|
|
9665
9783
|
};
|
|
9666
9784
|
}
|
|
9667
9785
|
delete() {
|