@prized/sdk 0.1.1 → 0.1.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 +25 -11
- package/dist/cjs/commands.d.ts +3 -1
- package/dist/cjs/commands.js +45 -7
- package/dist/cjs/files.d.ts +6 -5
- package/dist/cjs/files.js +8 -5
- package/dist/cjs/http.d.ts +48 -5
- package/dist/cjs/http.js +147 -19
- package/dist/cjs/outcome.d.ts +9 -0
- package/dist/cjs/outcome.js +21 -1
- package/dist/cjs/output.js +1 -0
- package/dist/cjs/sandbox.d.ts +3 -2
- package/dist/cjs/sandbox.js +12 -3
- package/dist/cjs/shell.d.ts +2 -1
- package/dist/cjs/shell.js +2 -1
- package/dist/cjs/toolbox.d.ts +2 -0
- package/dist/cjs/toolbox.js +1 -0
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/commands.d.ts +3 -1
- package/dist/esm/commands.js +46 -8
- package/dist/esm/files.d.ts +6 -5
- package/dist/esm/files.js +9 -6
- package/dist/esm/http.d.ts +48 -5
- package/dist/esm/http.js +146 -18
- package/dist/esm/outcome.d.ts +9 -0
- package/dist/esm/outcome.js +20 -1
- package/dist/esm/output.js +2 -1
- package/dist/esm/sandbox.d.ts +3 -2
- package/dist/esm/sandbox.js +12 -3
- package/dist/esm/shell.d.ts +2 -1
- package/dist/esm/shell.js +2 -1
- package/dist/esm/toolbox.d.ts +2 -0
- package/dist/esm/toolbox.js +1 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
package/dist/esm/http.js
CHANGED
|
@@ -5,6 +5,11 @@ import { commandEffect, restate, withAbort, withOutcome } from './outcome.js';
|
|
|
5
5
|
import { VERSION } from './version.js';
|
|
6
6
|
export const DEFAULT_BASE_URL = 'https://api.prized.dev';
|
|
7
7
|
export const DEFAULT_EDGE_URL = 'https://term.prized.dev';
|
|
8
|
+
/** Node's own fetch (not Bun's or Deno's, which also say they are Node), with web streams. */
|
|
9
|
+
function nodeFetch() {
|
|
10
|
+
const versions = globalThis.process?.versions;
|
|
11
|
+
return typeof versions?.node === 'string' && versions.bun === undefined && versions.deno === undefined && typeof ReadableStream === 'function';
|
|
12
|
+
}
|
|
8
13
|
function envApiKey() {
|
|
9
14
|
try {
|
|
10
15
|
const env = globalThis.process?.env;
|
|
@@ -66,8 +71,9 @@ export function abortableSleep(ms, signal) {
|
|
|
66
71
|
/**
|
|
67
72
|
* How long a wait (a streamed or long exec, a handle's wait()) keeps polling
|
|
68
73
|
* while its polls fail before it gives up (pollOutageMs), and a streamed or
|
|
69
|
-
* long exec stops its command
|
|
70
|
-
* or an edge that restarts
|
|
74
|
+
* long exec stops its command, and how long a start is sent again: long
|
|
75
|
+
* enough to ride out a dropped connection or an edge that restarts (about
|
|
76
|
+
* 70 s, the last time it crashed). The Python SDK's DEFAULT_POLL_OUTAGE.
|
|
71
77
|
*/
|
|
72
78
|
export const DEFAULT_POLL_OUTAGE_MS = 120_000;
|
|
73
79
|
/** A pollOutageMs given (a number of ms ≥ 0), or undefined. */
|
|
@@ -109,6 +115,7 @@ export function resolveConfig(options = {}) {
|
|
|
109
115
|
sleep: options.sleep ?? abortableSleep,
|
|
110
116
|
now: options.now ?? Date.now,
|
|
111
117
|
random: options.random ?? Math.random,
|
|
118
|
+
streamBodies: options.fetch === undefined && nodeFetch(),
|
|
112
119
|
};
|
|
113
120
|
}
|
|
114
121
|
/** Prized's own domain: its API and its edges are hosts under it. */
|
|
@@ -355,6 +362,84 @@ export function backoffMs(config, attempt) {
|
|
|
355
362
|
const base = Math.min(MAX_BACKOFF_MS, BACKOFF_FIRST_MS * 2 ** attempt);
|
|
356
363
|
return Math.round(base / 2 + (config.random() * base) / 2);
|
|
357
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* How long a file transfer may move no bytes before the attempt is given up
|
|
367
|
+
* (sdk/retry-spec.json transferStallMs; the Python SDK's TRANSFER_STALL).
|
|
368
|
+
* The edge holds a request about 20 s at most before it reads the body (its
|
|
369
|
+
* admission queue, then waking a paused sandbox), and ends a body that brings
|
|
370
|
+
* nothing for 60 s itself: kept above that, a stalled copy is over at the
|
|
371
|
+
* edge before the next one goes to the same staged name, so the two never
|
|
372
|
+
* race. Once an upload's last byte is handed over, its answer is waited for
|
|
373
|
+
* that and a second more for every uploadAnswerBytesPerSecond it sent: what
|
|
374
|
+
* the socket's buffers, or a proxy that takes a whole body before it sends it
|
|
375
|
+
* on, still hold moves unseen, and must not read as a stall (the Python SDK's
|
|
376
|
+
* UPLOAD_ANSWER_RATE). Mutable for tests only.
|
|
377
|
+
*/
|
|
378
|
+
export const timing = { transferStallMs: 120_000, uploadAnswerBytesPerSecond: 128 * 1024 };
|
|
379
|
+
/** A streamed upload goes out this many bytes at a time. */
|
|
380
|
+
export const UPLOAD_CHUNK = 64 * 1024;
|
|
381
|
+
/**
|
|
382
|
+
* A body's bytes, to stream; null for one to send as it is. By shape, not
|
|
383
|
+
* instanceof: a Uint8Array or Buffer from another realm (a test runner's vm
|
|
384
|
+
* context, say) is one too.
|
|
385
|
+
*/
|
|
386
|
+
async function bodyBytes(body) {
|
|
387
|
+
if (typeof body === 'string')
|
|
388
|
+
return new TextEncoder().encode(body);
|
|
389
|
+
if (ArrayBuffer.isView(body))
|
|
390
|
+
return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
391
|
+
if (Object.prototype.toString.call(body) === '[object ArrayBuffer]')
|
|
392
|
+
return new Uint8Array(body);
|
|
393
|
+
if (typeof body.arrayBuffer === 'function')
|
|
394
|
+
return new Uint8Array(await body.arrayBuffer());
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* `bytes` as a stream fed UPLOAD_CHUNK at a time, `moved` told each time
|
|
399
|
+
* fetch asks for more, and at the end given the answer's allowance
|
|
400
|
+
* (timing.uploadAnswerBytesPerSecond).
|
|
401
|
+
*/
|
|
402
|
+
function inPieces(bytes, moved) {
|
|
403
|
+
let at = 0;
|
|
404
|
+
return new ReadableStream({
|
|
405
|
+
pull(controller) {
|
|
406
|
+
if (at >= bytes.byteLength) {
|
|
407
|
+
moved((bytes.byteLength / timing.uploadAnswerBytesPerSecond) * 1000);
|
|
408
|
+
controller.close();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
moved();
|
|
412
|
+
// A copy: a runtime may take the piece's buffer as it sends it.
|
|
413
|
+
controller.enqueue(bytes.slice(at, at + UPLOAD_CHUNK));
|
|
414
|
+
at += UPLOAD_CHUNK;
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
// Nothing read ahead: a pull is the socket taking the last piece.
|
|
418
|
+
{ highWaterMark: 0 });
|
|
419
|
+
}
|
|
420
|
+
/** A body read whole, `moved` told of every piece. */
|
|
421
|
+
async function readAll(response, moved) {
|
|
422
|
+
if (response.body === null)
|
|
423
|
+
return new Uint8Array(0);
|
|
424
|
+
const reader = response.body.getReader();
|
|
425
|
+
const parts = [];
|
|
426
|
+
let size = 0;
|
|
427
|
+
for (;;) {
|
|
428
|
+
const { done, value } = await reader.read();
|
|
429
|
+
if (done)
|
|
430
|
+
break;
|
|
431
|
+
moved();
|
|
432
|
+
parts.push(value);
|
|
433
|
+
size += value.byteLength;
|
|
434
|
+
}
|
|
435
|
+
const whole = new Uint8Array(size);
|
|
436
|
+
let at = 0;
|
|
437
|
+
for (const part of parts) {
|
|
438
|
+
whole.set(part, at);
|
|
439
|
+
at += part.byteLength;
|
|
440
|
+
}
|
|
441
|
+
return whole;
|
|
442
|
+
}
|
|
358
443
|
/**
|
|
359
444
|
* One fetch, body included, under one timeout and the caller's signal. A
|
|
360
445
|
* body that stalls or breaks after the headers fails the attempt like a
|
|
@@ -363,10 +448,24 @@ export function backoffMs(config, attempt) {
|
|
|
363
448
|
async function attemptOnce(config, spec, url, timeoutMs) {
|
|
364
449
|
const controller = new AbortController();
|
|
365
450
|
let timedOut = false;
|
|
451
|
+
let stalled = false;
|
|
366
452
|
const timer = setTimeout(() => {
|
|
367
453
|
timedOut = true;
|
|
368
454
|
controller.abort();
|
|
369
455
|
}, timeoutMs);
|
|
456
|
+
// The stall bound starts over whenever bytes move (stallMs), until the attempt is over.
|
|
457
|
+
let stall;
|
|
458
|
+
let over = false;
|
|
459
|
+
const moved = (extraMs = 0) => {
|
|
460
|
+
if (spec.stallMs === undefined || over)
|
|
461
|
+
return;
|
|
462
|
+
clearTimeout(stall);
|
|
463
|
+
stall = setTimeout(() => {
|
|
464
|
+
stalled = true;
|
|
465
|
+
timedOut = true;
|
|
466
|
+
controller.abort();
|
|
467
|
+
}, spec.stallMs + extraMs);
|
|
468
|
+
};
|
|
370
469
|
const onAbort = () => controller.abort();
|
|
371
470
|
spec.signal?.addEventListener('abort', onAbort, { once: true });
|
|
372
471
|
const headers = {
|
|
@@ -376,30 +475,52 @@ async function attemptOnce(config, spec, url, timeoutMs) {
|
|
|
376
475
|
...spec.headers,
|
|
377
476
|
};
|
|
378
477
|
let body;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
else if (spec.body !== undefined) {
|
|
384
|
-
headers['content-type'] = spec.contentType ?? 'application/octet-stream';
|
|
385
|
-
body = spec.body;
|
|
386
|
-
}
|
|
478
|
+
// Watched from the start unless an upload goes out unwatched (Config.streamBodies).
|
|
479
|
+
let watched = spec.stallMs !== undefined && spec.body === undefined;
|
|
480
|
+
let streamed = false;
|
|
387
481
|
let status = null;
|
|
388
482
|
try {
|
|
483
|
+
if (spec.json !== undefined) {
|
|
484
|
+
headers['content-type'] = 'application/json';
|
|
485
|
+
body = JSON.stringify(spec.json);
|
|
486
|
+
}
|
|
487
|
+
else if (spec.body !== undefined) {
|
|
488
|
+
headers['content-type'] = spec.contentType ?? 'application/octet-stream';
|
|
489
|
+
body = spec.body;
|
|
490
|
+
const bytes = spec.stallMs !== undefined && config.streamBodies ? await bodyBytes(spec.body) : null;
|
|
491
|
+
if (bytes !== null) {
|
|
492
|
+
headers['content-length'] = String(bytes.byteLength);
|
|
493
|
+
body = inPieces(bytes, moved);
|
|
494
|
+
streamed = true;
|
|
495
|
+
watched = true;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (watched)
|
|
499
|
+
moved();
|
|
389
500
|
const response = await config.fetch(url, {
|
|
390
501
|
method: spec.method,
|
|
391
502
|
headers,
|
|
392
503
|
body: body,
|
|
504
|
+
// A streamed body (Node's fetch asks for it): sent before the answer is read.
|
|
505
|
+
...(streamed ? { duplex: 'half' } : {}),
|
|
393
506
|
// Never follow a redirect with the key attached.
|
|
394
507
|
redirect: 'manual',
|
|
395
508
|
signal: controller.signal,
|
|
396
509
|
});
|
|
397
510
|
status = response.status;
|
|
511
|
+
if (spec.stallMs !== undefined)
|
|
512
|
+
moved();
|
|
398
513
|
const ok = response.status >= 200 && response.status < 300;
|
|
399
514
|
if (response.status >= 300 && response.status < 400) {
|
|
400
515
|
await response.body?.cancel().catch(() => undefined);
|
|
401
516
|
return { answer: { status: response.status, headers: response.headers, text: '', bytes: null } };
|
|
402
517
|
}
|
|
518
|
+
if (spec.stallMs !== undefined) {
|
|
519
|
+
const whole = await readAll(response, moved);
|
|
520
|
+
if (ok && spec.read === 'bytes')
|
|
521
|
+
return { answer: { status: response.status, headers: response.headers, text: '', bytes: whole } };
|
|
522
|
+
return { answer: { status: response.status, headers: response.headers, text: new TextDecoder('utf-8').decode(whole), bytes: null } };
|
|
523
|
+
}
|
|
403
524
|
if (ok && spec.read === 'bytes') {
|
|
404
525
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
405
526
|
return { answer: { status: response.status, headers: response.headers, text: '', bytes } };
|
|
@@ -407,10 +528,12 @@ async function attemptOnce(config, spec, url, timeoutMs) {
|
|
|
407
528
|
return { answer: { status: response.status, headers: response.headers, text: await response.text(), bytes: null } };
|
|
408
529
|
}
|
|
409
530
|
catch (error) {
|
|
410
|
-
return { error, timedOut, status };
|
|
531
|
+
return { error, timedOut, stalled, status };
|
|
411
532
|
}
|
|
412
533
|
finally {
|
|
534
|
+
over = true;
|
|
413
535
|
clearTimeout(timer);
|
|
536
|
+
clearTimeout(stall);
|
|
414
537
|
spec.signal?.removeEventListener('abort', onAbort);
|
|
415
538
|
}
|
|
416
539
|
}
|
|
@@ -473,8 +596,8 @@ export async function request(config, spec) {
|
|
|
473
596
|
const timeoutMs = spec.timeoutMs ?? config.requestTimeoutMs;
|
|
474
597
|
const sandboxId = spec.sandboxId ?? null;
|
|
475
598
|
const effect = spec.effect ?? (spec.retry === 'exec' ? commandEffect() : undefined);
|
|
476
|
-
// Whether an attempt that was sent again may have reached the sandbox.
|
|
477
|
-
let acted = false;
|
|
599
|
+
// Whether an attempt that was sent again (or an earlier copy) may have reached the sandbox.
|
|
600
|
+
let acted = spec.acted ?? false;
|
|
478
601
|
// Names the last attempt's failure, if any, so a call cut short by its
|
|
479
602
|
// deadline still says what it was waiting out (a 503, a rate limit).
|
|
480
603
|
const pastDeadline = (last) => {
|
|
@@ -483,9 +606,10 @@ export async function request(config, spec) {
|
|
|
483
606
|
let error = new PrizedError('timeout', `The time allowed for this call ran out.${tail}`, { sandboxId, cause: last });
|
|
484
607
|
// A command is sent again only when it provably never arrived, so every
|
|
485
608
|
// attempt of one cut short this way never reached the sandbox. A staged
|
|
486
|
-
// upload's copies may have, unless each provably never arrived
|
|
487
|
-
|
|
488
|
-
|
|
609
|
+
// upload's copies may have, unless each provably never arrived, and so
|
|
610
|
+
// may a keyed start's (the one `safe` request that names an effect).
|
|
611
|
+
if (effect !== undefined)
|
|
612
|
+
error = withOutcome(error, effect, spec.retry !== 'exec' && acted ? 'maybe' : 'never');
|
|
489
613
|
cutShort.add(error);
|
|
490
614
|
return error;
|
|
491
615
|
};
|
|
@@ -523,7 +647,10 @@ export async function request(config, spec) {
|
|
|
523
647
|
}
|
|
524
648
|
const unsentAttempt = !outcome.timedOut && neverSent(outcome.error);
|
|
525
649
|
failure = failed(outcome.timedOut
|
|
526
|
-
? new PrizedError('timeout', `No answer within ${attemptMs} ms`, {
|
|
650
|
+
? new PrizedError('timeout', outcome.stalled ? `No bytes moved for ${spec.stallMs} ms` : `No answer within ${attemptMs} ms`, {
|
|
651
|
+
sandboxId,
|
|
652
|
+
cause: outcome.error,
|
|
653
|
+
})
|
|
527
654
|
: unsentAttempt
|
|
528
655
|
? new PrizedError('network_error', null, { sandboxId, cause: outcome.error })
|
|
529
656
|
: new PrizedError('network_error', lostMessage(spec.method, url, outcome.error), { sandboxId, cause: outcome.error }));
|
|
@@ -548,7 +675,8 @@ export async function request(config, spec) {
|
|
|
548
675
|
}
|
|
549
676
|
}
|
|
550
677
|
if (answer.status >= 300 && answer.status < 400) {
|
|
551
|
-
|
|
678
|
+
// A refusal; worded only when an earlier copy of a keyed start may have arrived.
|
|
679
|
+
throw raised(failed(new PrizedError('unexpected_redirect', null, { status: answer.status, sandboxId })), null, acted, effect);
|
|
552
680
|
}
|
|
553
681
|
const retryAfter = parseRetryAfter(answer.headers.get('retry-after'), config.now());
|
|
554
682
|
failure = failed(errorFromResponse(answer.status, answer.text, retryAfter, sandboxId, urlPath(url)));
|
package/dist/esm/outcome.d.ts
CHANGED
|
@@ -49,6 +49,15 @@ export declare function withOutcome(error: PrizedError, effect: Effect, outcome:
|
|
|
49
49
|
* connection closes. TypeScript only: the Python SDK takes no signal.
|
|
50
50
|
*/
|
|
51
51
|
export declare function withAbort(error: PrizedError, effect: Effect, outcome: 'never' | 'maybe' | 'did'): PrizedError;
|
|
52
|
+
/**
|
|
53
|
+
* What a start that gave up throws when the failure it gives up with
|
|
54
|
+
* (`failure`, Outage's pick) is not the last copy's own: an earlier copy's,
|
|
55
|
+
* worded before a later copy may have reached the sandbox, or what a copy
|
|
56
|
+
* cut short by the window met, not yet worded. Worded for the start: as one
|
|
57
|
+
* that may have started when any copy may have reached the sandbox
|
|
58
|
+
* (`anyActed`), else as one that never did. The Python SDK's start_gave_up.
|
|
59
|
+
*/
|
|
60
|
+
export declare function startGaveUp(failure: PrizedError, anyActed: boolean): PrizedError;
|
|
52
61
|
/**
|
|
53
62
|
* A background start that may have started (`error`, from withOutcome or
|
|
54
63
|
* withAbort) and that the SDK then stopped by its run id (`stopped`: the
|
package/dist/esm/outcome.js
CHANGED
|
@@ -90,12 +90,14 @@ function maybeTail(effect, closed) {
|
|
|
90
90
|
tail += ` ${effect.check}`;
|
|
91
91
|
return tail;
|
|
92
92
|
}
|
|
93
|
+
/** How an error says its request never reached the sandbox (withOutcome). */
|
|
94
|
+
const NEVER_TAIL = ' It never reached the sandbox, so it had no effect.';
|
|
93
95
|
/** `error` (same code, status and body), its message saying what it means for `effect`. */
|
|
94
96
|
export function withOutcome(error, effect, outcome) {
|
|
95
97
|
if (outcome === null)
|
|
96
98
|
return error;
|
|
97
99
|
if (outcome === 'never')
|
|
98
|
-
return restate(error, `${trimmed(error.message)}
|
|
100
|
+
return restate(error, `${trimmed(error.message)}.${NEVER_TAIL}`);
|
|
99
101
|
if (outcome === 'did')
|
|
100
102
|
return acting(error, `${effect.did}, but the answer could not be read (${detail(error)}).`);
|
|
101
103
|
return acting(error, `No clear answer came back (${detail(error)}), so ${maybeTail(effect, false)}`);
|
|
@@ -119,6 +121,23 @@ function stopTail(runId, stopped) {
|
|
|
119
121
|
? ` The SDK stopped ${runId}, or it had already ended.`
|
|
120
122
|
: ` The SDK could not confirm it stopped: it may still be running as ${runId} (exec('ps -ef | grep ${runId}') finds it).`;
|
|
121
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* What a start that gave up throws when the failure it gives up with
|
|
126
|
+
* (`failure`, Outage's pick) is not the last copy's own: an earlier copy's,
|
|
127
|
+
* worded before a later copy may have reached the sandbox, or what a copy
|
|
128
|
+
* cut short by the window met, not yet worded. Worded for the start: as one
|
|
129
|
+
* that may have started when any copy may have reached the sandbox
|
|
130
|
+
* (`anyActed`), else as one that never did. The Python SDK's start_gave_up.
|
|
131
|
+
*/
|
|
132
|
+
export function startGaveUp(failure, anyActed) {
|
|
133
|
+
if (acted.has(failure))
|
|
134
|
+
return failure;
|
|
135
|
+
const never = failure.message.endsWith(NEVER_TAIL);
|
|
136
|
+
if (never && !anyActed)
|
|
137
|
+
return failure;
|
|
138
|
+
const plain = never ? restate(failure, failure.message.slice(0, -NEVER_TAIL.length)) : failure;
|
|
139
|
+
return withOutcome(plain, startEffect(), anyActed ? 'maybe' : 'never');
|
|
140
|
+
}
|
|
122
141
|
/**
|
|
123
142
|
* A background start that may have started (`error`, from withOutcome or
|
|
124
143
|
* withAbort) and that the SDK then stopped by its run id (`stopped`: the
|
package/dist/esm/output.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// (sdk/python/src/prized/_output.py), rendered the same; both SDKs check
|
|
19
19
|
// them against sdk/output-scripts/.
|
|
20
20
|
import { OutputReadError, PrizedError } from './errors.js';
|
|
21
|
-
import { request } from './http.js';
|
|
21
|
+
import { request, timing } from './http.js';
|
|
22
22
|
import { restate } from './outcome.js';
|
|
23
23
|
import { LAUNCH_PATH } from './shell.js';
|
|
24
24
|
import { helperExec, MAX_FILE_BYTES, MAX_OUTPUT_BYTES, rawExec } from './toolbox.js';
|
|
@@ -270,6 +270,7 @@ async function download(toolbox, path, signal) {
|
|
|
270
270
|
retry: 'safe',
|
|
271
271
|
read: 'bytes',
|
|
272
272
|
timeoutMs: READ_MS,
|
|
273
|
+
stallMs: timing.transferStallMs,
|
|
273
274
|
deadline: toolbox.config.now() + READ_MS,
|
|
274
275
|
signal,
|
|
275
276
|
sandboxId: toolbox.sandboxId,
|
package/dist/esm/sandbox.d.ts
CHANGED
|
@@ -128,8 +128,9 @@ export declare class Sandbox {
|
|
|
128
128
|
/**
|
|
129
129
|
* Delete every sandbox carrying all of `labels` (at least one) and resolve
|
|
130
130
|
* with their ids (the API takes up to 250 a request; this repeats it until
|
|
131
|
-
* none are left).
|
|
132
|
-
*
|
|
131
|
+
* none are left). Each request selects the current matches, so a repeat
|
|
132
|
+
* can delete sandboxes created since the first request. An uncertain
|
|
133
|
+
* answer is not retried; list the matches before deleting again. Each
|
|
133
134
|
* counts toward the workspace's quotas until it is gone;
|
|
134
135
|
* `Sandbox.list({ labels })` coming back empty means they all are.
|
|
135
136
|
*/
|
package/dist/esm/sandbox.js
CHANGED
|
@@ -437,8 +437,9 @@ export class Sandbox {
|
|
|
437
437
|
/**
|
|
438
438
|
* Delete every sandbox carrying all of `labels` (at least one) and resolve
|
|
439
439
|
* with their ids (the API takes up to 250 a request; this repeats it until
|
|
440
|
-
* none are left).
|
|
441
|
-
*
|
|
440
|
+
* none are left). Each request selects the current matches, so a repeat
|
|
441
|
+
* can delete sandboxes created since the first request. An uncertain
|
|
442
|
+
* answer is not retried; list the matches before deleting again. Each
|
|
442
443
|
* counts toward the workspace's quotas until it is gone;
|
|
443
444
|
* `Sandbox.list({ labels })` coming back empty means they all are.
|
|
444
445
|
*/
|
|
@@ -452,7 +453,15 @@ export class Sandbox {
|
|
|
452
453
|
const deleted = new Set();
|
|
453
454
|
const stuck = (calls) => new PrizedError('delete_incomplete', `Deleted ${deleted.size} sandboxes in ${calls} requests and some are left; call again to delete the rest.`);
|
|
454
455
|
for (let call = 1; call <= MAX_BULK_DELETE_CALLS; call += 1) {
|
|
455
|
-
|
|
456
|
+
// A label is a live selector, not a stable resource or idempotency key.
|
|
457
|
+
const result = await request(config, {
|
|
458
|
+
method: 'DELETE', url: apiUrl(config, `/sandboxes${query}`), retry: 'exec', deadline,
|
|
459
|
+
effect: {
|
|
460
|
+
maybe: 'some matching sandboxes may already be deleting',
|
|
461
|
+
did: 'The matching sandboxes were marked for deletion',
|
|
462
|
+
check: 'It was not sent again: list the matching sandboxes before deleting again, because the matches may have changed.',
|
|
463
|
+
},
|
|
464
|
+
});
|
|
456
465
|
const answer = result.json;
|
|
457
466
|
const ids = answer?.ids;
|
|
458
467
|
if (!Array.isArray(ids) || !ids.every((id) => typeof id === 'string')) {
|
package/dist/esm/shell.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ export declare const RUN_ID_RE: RegExp;
|
|
|
11
11
|
/**
|
|
12
12
|
* Minutes (by the sandbox's clock) a cleaned or abandoned run's directory
|
|
13
13
|
* stays: what keeps a resend of its start from running the command again.
|
|
14
|
-
* The SDK stops resending a start
|
|
14
|
+
* The SDK stops resending a start once pollOutageMs (2 minutes by default)
|
|
15
|
+
* has run out after its first copy's retries, so an hour is ample; each
|
|
15
16
|
* cleanup then removes the ones older than that, so they never pile up.
|
|
16
17
|
*/
|
|
17
18
|
export declare const CLEANED_KEEP_MIN = 60;
|
package/dist/esm/shell.js
CHANGED
|
@@ -50,7 +50,8 @@ export const RUN_ID_RE = /^run_[0-9a-f]{16}$/;
|
|
|
50
50
|
/**
|
|
51
51
|
* Minutes (by the sandbox's clock) a cleaned or abandoned run's directory
|
|
52
52
|
* stays: what keeps a resend of its start from running the command again.
|
|
53
|
-
* The SDK stops resending a start
|
|
53
|
+
* The SDK stops resending a start once pollOutageMs (2 minutes by default)
|
|
54
|
+
* has run out after its first copy's retries, so an hour is ample; each
|
|
54
55
|
* cleanup then removes the ones older than that, so they never pile up.
|
|
55
56
|
*/
|
|
56
57
|
export const CLEANED_KEEP_MIN = 60;
|
package/dist/esm/toolbox.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export declare function rawExec(toolbox: Toolbox, req: RawExecRequest, options:
|
|
|
30
30
|
deadline?: number | undefined;
|
|
31
31
|
/** What the command does, for an error that says whether it happened (`exec`: a command unless given). */
|
|
32
32
|
effect?: Effect | undefined;
|
|
33
|
+
/** An earlier copy of a keyed start may have reached the sandbox. */
|
|
34
|
+
acted?: boolean | undefined;
|
|
33
35
|
}): Promise<ExecResult>;
|
|
34
36
|
/** A helper command the SDK sends itself; a non-zero exit is the caller's to judge. */
|
|
35
37
|
export declare function helperExec(toolbox: Toolbox, command: string, options?: {
|
package/dist/esm/toolbox.js
CHANGED
|
@@ -46,6 +46,7 @@ export async function rawExec(toolbox, req, options) {
|
|
|
46
46
|
deadline: options.deadline,
|
|
47
47
|
sandboxId: toolbox.sandboxId,
|
|
48
48
|
...(effect === undefined ? {} : { effect }),
|
|
49
|
+
...(options.acted ? { acted: true } : {}),
|
|
49
50
|
});
|
|
50
51
|
const doc = result.json;
|
|
51
52
|
if (doc === null || typeof doc !== 'object' || typeof doc.exitCode !== 'number') {
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** This package's version; test/version.test.ts holds it equal to package.json. */
|
|
2
|
-
export declare const VERSION = "0.1.
|
|
2
|
+
export declare const VERSION = "0.1.2";
|
package/dist/esm/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** This package's version; test/version.test.ts holds it equal to package.json. */
|
|
2
|
-
export const VERSION = '0.1.
|
|
2
|
+
export const VERSION = '0.1.2';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prized/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Prized Sandboxes (beta): create microVM sandboxes, run commands, move files, pause, resume and fork them from TypeScript.",
|
|
5
5
|
"homepage": "https://prized.dev/docs/sandboxes",
|
|
6
6
|
"license": "Apache-2.0",
|