@prized/sdk 0.1.0 → 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 +76 -21
- package/dist/cjs/commands.d.ts +47 -14
- package/dist/cjs/commands.js +94 -26
- package/dist/cjs/errors.d.ts +43 -3
- package/dist/cjs/errors.js +54 -6
- package/dist/cjs/files.d.ts +21 -5
- package/dist/cjs/files.js +38 -14
- package/dist/cjs/http.d.ts +74 -15
- package/dist/cjs/http.js +208 -32
- package/dist/cjs/index.d.ts +3 -3
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/outcome.d.ts +11 -1
- package/dist/cjs/outcome.js +28 -5
- package/dist/cjs/output.d.ts +19 -8
- package/dist/cjs/output.js +152 -56
- package/dist/cjs/sandbox.d.ts +3 -2
- package/dist/cjs/sandbox.js +40 -5
- 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 +4 -1
- package/dist/cjs/types.d.ts +13 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/commands.d.ts +47 -14
- package/dist/esm/commands.js +95 -27
- package/dist/esm/errors.d.ts +43 -3
- package/dist/esm/errors.js +52 -5
- package/dist/esm/files.d.ts +21 -5
- package/dist/esm/files.js +39 -15
- package/dist/esm/http.d.ts +74 -15
- package/dist/esm/http.js +207 -31
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +1 -1
- package/dist/esm/outcome.d.ts +11 -1
- package/dist/esm/outcome.js +27 -5
- package/dist/esm/output.d.ts +19 -8
- package/dist/esm/output.js +153 -58
- package/dist/esm/sandbox.d.ts +3 -2
- package/dist/esm/sandbox.js +40 -5
- 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 +4 -1
- package/dist/esm/types.d.ts +13 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
package/dist/cjs/http.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Polls = exports.LAST_LOOK_MAX_MS = exports.LAST_LOOK_MS = exports.MAX_RETRY_AFTER_MS = exports.MAX_BACKOFF_MS = exports.BACKOFF_FIRST_MS = exports.DEFAULT_POLL_OUTAGE_MS = exports.DEFAULT_EDGE_URL = exports.DEFAULT_BASE_URL = void 0;
|
|
3
|
+
exports.UPLOAD_CHUNK = exports.timing = exports.Polls = exports.LAST_LOOK_MAX_MS = exports.LAST_LOOK_MS = exports.MAX_RETRY_AFTER_MS = exports.MAX_BACKOFF_MS = exports.BACKOFF_FIRST_MS = exports.DEFAULT_POLL_OUTAGE_MS = exports.DEFAULT_EDGE_URL = exports.DEFAULT_BASE_URL = void 0;
|
|
4
4
|
exports.abortableSleep = abortableSleep;
|
|
5
5
|
exports.validOutage = validOutage;
|
|
6
6
|
exports.resolveConfig = resolveConfig;
|
|
@@ -21,6 +21,11 @@ const outcome_js_1 = require("./outcome.js");
|
|
|
21
21
|
const version_js_1 = require("./version.js");
|
|
22
22
|
exports.DEFAULT_BASE_URL = 'https://api.prized.dev';
|
|
23
23
|
exports.DEFAULT_EDGE_URL = 'https://term.prized.dev';
|
|
24
|
+
/** Node's own fetch (not Bun's or Deno's, which also say they are Node), with web streams. */
|
|
25
|
+
function nodeFetch() {
|
|
26
|
+
const versions = globalThis.process?.versions;
|
|
27
|
+
return typeof versions?.node === 'string' && versions.bun === undefined && versions.deno === undefined && typeof ReadableStream === 'function';
|
|
28
|
+
}
|
|
24
29
|
function envApiKey() {
|
|
25
30
|
try {
|
|
26
31
|
const env = globalThis.process?.env;
|
|
@@ -33,7 +38,15 @@ function envApiKey() {
|
|
|
33
38
|
function isLoopback(hostname) {
|
|
34
39
|
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
|
|
35
40
|
}
|
|
36
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* https anywhere, plain http only to this machine: the API key rides in
|
|
43
|
+
* every request's Authorization header. The same rule for the options and
|
|
44
|
+
* for an edge the API names.
|
|
45
|
+
*/
|
|
46
|
+
function secureEndpoint(url) {
|
|
47
|
+
return url.protocol === 'https:' || (url.protocol === 'http:' && isLoopback(url.hostname));
|
|
48
|
+
}
|
|
49
|
+
/** An https (or loopback http) origin, trailing slashes and a trailing /api/v1 dropped. */
|
|
37
50
|
function normalizeOrigin(name, raw) {
|
|
38
51
|
let url;
|
|
39
52
|
try {
|
|
@@ -45,6 +58,9 @@ function normalizeOrigin(name, raw) {
|
|
|
45
58
|
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
46
59
|
throw new errors_js_1.PrizedError('invalid_argument', `${name} must be an http(s) URL`);
|
|
47
60
|
}
|
|
61
|
+
if (!secureEndpoint(url)) {
|
|
62
|
+
throw new errors_js_1.PrizedError('invalid_argument', `${name} must use https (plain http only for localhost, 127.0.0.1 or [::1])`);
|
|
63
|
+
}
|
|
48
64
|
if (url.username || url.password || url.search || url.hash) {
|
|
49
65
|
throw new errors_js_1.PrizedError('invalid_argument', `${name} must not carry credentials, a query or a fragment`);
|
|
50
66
|
}
|
|
@@ -69,10 +85,11 @@ function abortableSleep(ms, signal) {
|
|
|
69
85
|
});
|
|
70
86
|
}
|
|
71
87
|
/**
|
|
72
|
-
* How long a streamed or long exec
|
|
73
|
-
* before it
|
|
74
|
-
*
|
|
75
|
-
*
|
|
88
|
+
* How long a wait (a streamed or long exec, a handle's wait()) keeps polling
|
|
89
|
+
* while its polls fail before it gives up (pollOutageMs), and a streamed or
|
|
90
|
+
* long exec stops its command, and how long a start is sent again: long
|
|
91
|
+
* enough to ride out a dropped connection or an edge that restarts (about
|
|
92
|
+
* 70 s, the last time it crashed). The Python SDK's DEFAULT_POLL_OUTAGE.
|
|
76
93
|
*/
|
|
77
94
|
exports.DEFAULT_POLL_OUTAGE_MS = 120_000;
|
|
78
95
|
/** A pollOutageMs given (a number of ms ≥ 0), or undefined. */
|
|
@@ -114,22 +131,54 @@ function resolveConfig(options = {}) {
|
|
|
114
131
|
sleep: options.sleep ?? abortableSleep,
|
|
115
132
|
now: options.now ?? Date.now,
|
|
116
133
|
random: options.random ?? Math.random,
|
|
134
|
+
streamBodies: options.fetch === undefined && nodeFetch(),
|
|
117
135
|
};
|
|
118
136
|
}
|
|
137
|
+
/** Prized's own domain: its API and its edges are hosts under it. */
|
|
138
|
+
const PRIZED_DOMAIN = 'prized.dev';
|
|
139
|
+
function under(host, domain) {
|
|
140
|
+
return host === domain || host.endsWith(`.${domain}`);
|
|
141
|
+
}
|
|
142
|
+
/** A URL's hostname (lowercase, as URL gives it), or '' if it does not parse. */
|
|
143
|
+
function hostOf(raw) {
|
|
144
|
+
try {
|
|
145
|
+
return new URL(raw).hostname;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Where a toolboxBase may point, given the API's host: the API host itself;
|
|
153
|
+
* prized.dev's own hosts when the API is one of them; loopback when the API
|
|
154
|
+
* is. Never a domain guessed from the API host's name (a public suffix such
|
|
155
|
+
* as co.uk would trust everyone under it). The Python SDK's _trusted_edge.
|
|
156
|
+
*/
|
|
157
|
+
function trustedEdge(edgeHost, apiHost) {
|
|
158
|
+
if (edgeHost === '' || apiHost === '')
|
|
159
|
+
return false;
|
|
160
|
+
if (isLoopback(apiHost) || isLoopback(edgeHost))
|
|
161
|
+
return isLoopback(apiHost) && isLoopback(edgeHost);
|
|
162
|
+
return edgeHost === apiHost || (under(apiHost, PRIZED_DOMAIN) && under(edgeHost, PRIZED_DOMAIN));
|
|
163
|
+
}
|
|
119
164
|
/**
|
|
120
165
|
* Where exec and files go for one sandbox. An explicit edgeUrl wins; else the
|
|
121
166
|
* API's own `toolboxBase` when it has the expected shape (https, or plain
|
|
122
|
-
* http on loopback for local development)
|
|
167
|
+
* http on loopback for local development, ending /v1/sandbox/<id>) on a host
|
|
168
|
+
* trusted beside the API's (term.prized.dev beside api.prized.dev; see
|
|
169
|
+
* trustedEdge); else the default edge, for Prized's own API only. The API key
|
|
170
|
+
* is never sent anywhere else: discovery is routing, not authority.
|
|
123
171
|
*/
|
|
124
172
|
function toolboxBaseFor(config, sandboxId, toolboxBase) {
|
|
125
173
|
const path = `/v1/sandbox/${encodeURIComponent(sandboxId)}`;
|
|
126
174
|
if (config.edgeUrl !== null)
|
|
127
175
|
return `${config.edgeUrl}${path}`;
|
|
176
|
+
const apiHost = hostOf(config.baseUrl);
|
|
128
177
|
if (toolboxBase) {
|
|
129
178
|
try {
|
|
130
179
|
const url = new URL(toolboxBase);
|
|
131
|
-
|
|
132
|
-
|
|
180
|
+
if (secureEndpoint(url) &&
|
|
181
|
+
trustedEdge(url.hostname, apiHost) &&
|
|
133
182
|
!url.username &&
|
|
134
183
|
!url.password &&
|
|
135
184
|
!url.search &&
|
|
@@ -142,7 +191,11 @@ function toolboxBaseFor(config, sandboxId, toolboxBase) {
|
|
|
142
191
|
// fall through to the default
|
|
143
192
|
}
|
|
144
193
|
}
|
|
145
|
-
|
|
194
|
+
// Prized's own edge only for Prized's own API: a key for another
|
|
195
|
+
// deployment is never sent to it. That deployment names its edge.
|
|
196
|
+
if (under(apiHost, PRIZED_DOMAIN))
|
|
197
|
+
return `${exports.DEFAULT_EDGE_URL}${path}`;
|
|
198
|
+
throw new errors_js_1.PrizedError('edge_unknown', `The API at ${config.baseUrl} named no command endpoint this SDK trusts for ${sandboxId}; pass edgeUrl for that deployment.`, { sandboxId });
|
|
146
199
|
}
|
|
147
200
|
/**
|
|
148
201
|
* The edge this config's sandboxes answer on, before the API has named one:
|
|
@@ -239,7 +292,7 @@ function retryable(error, mode, method) {
|
|
|
239
292
|
// have done a write's work, so a write is left to its caller (a create
|
|
240
293
|
// that may have made a sandbox finds and deletes it). A GET did nothing:
|
|
241
294
|
// a poll that meets one is sent again rather than failing its create.
|
|
242
|
-
if (error.status === 500 && method === 'GET')
|
|
295
|
+
if (mode === 'safe' && error.status === 500 && method === 'GET')
|
|
243
296
|
return true;
|
|
244
297
|
return error.status === 429 || error.status === 502 || error.status === 503 || error.status === 504;
|
|
245
298
|
}
|
|
@@ -326,6 +379,84 @@ function backoffMs(config, attempt) {
|
|
|
326
379
|
const base = Math.min(exports.MAX_BACKOFF_MS, exports.BACKOFF_FIRST_MS * 2 ** attempt);
|
|
327
380
|
return Math.round(base / 2 + (config.random() * base) / 2);
|
|
328
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* How long a file transfer may move no bytes before the attempt is given up
|
|
384
|
+
* (sdk/retry-spec.json transferStallMs; the Python SDK's TRANSFER_STALL).
|
|
385
|
+
* The edge holds a request about 20 s at most before it reads the body (its
|
|
386
|
+
* admission queue, then waking a paused sandbox), and ends a body that brings
|
|
387
|
+
* nothing for 60 s itself: kept above that, a stalled copy is over at the
|
|
388
|
+
* edge before the next one goes to the same staged name, so the two never
|
|
389
|
+
* race. Once an upload's last byte is handed over, its answer is waited for
|
|
390
|
+
* that and a second more for every uploadAnswerBytesPerSecond it sent: what
|
|
391
|
+
* the socket's buffers, or a proxy that takes a whole body before it sends it
|
|
392
|
+
* on, still hold moves unseen, and must not read as a stall (the Python SDK's
|
|
393
|
+
* UPLOAD_ANSWER_RATE). Mutable for tests only.
|
|
394
|
+
*/
|
|
395
|
+
exports.timing = { transferStallMs: 120_000, uploadAnswerBytesPerSecond: 128 * 1024 };
|
|
396
|
+
/** A streamed upload goes out this many bytes at a time. */
|
|
397
|
+
exports.UPLOAD_CHUNK = 64 * 1024;
|
|
398
|
+
/**
|
|
399
|
+
* A body's bytes, to stream; null for one to send as it is. By shape, not
|
|
400
|
+
* instanceof: a Uint8Array or Buffer from another realm (a test runner's vm
|
|
401
|
+
* context, say) is one too.
|
|
402
|
+
*/
|
|
403
|
+
async function bodyBytes(body) {
|
|
404
|
+
if (typeof body === 'string')
|
|
405
|
+
return new TextEncoder().encode(body);
|
|
406
|
+
if (ArrayBuffer.isView(body))
|
|
407
|
+
return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
408
|
+
if (Object.prototype.toString.call(body) === '[object ArrayBuffer]')
|
|
409
|
+
return new Uint8Array(body);
|
|
410
|
+
if (typeof body.arrayBuffer === 'function')
|
|
411
|
+
return new Uint8Array(await body.arrayBuffer());
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* `bytes` as a stream fed UPLOAD_CHUNK at a time, `moved` told each time
|
|
416
|
+
* fetch asks for more, and at the end given the answer's allowance
|
|
417
|
+
* (timing.uploadAnswerBytesPerSecond).
|
|
418
|
+
*/
|
|
419
|
+
function inPieces(bytes, moved) {
|
|
420
|
+
let at = 0;
|
|
421
|
+
return new ReadableStream({
|
|
422
|
+
pull(controller) {
|
|
423
|
+
if (at >= bytes.byteLength) {
|
|
424
|
+
moved((bytes.byteLength / exports.timing.uploadAnswerBytesPerSecond) * 1000);
|
|
425
|
+
controller.close();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
moved();
|
|
429
|
+
// A copy: a runtime may take the piece's buffer as it sends it.
|
|
430
|
+
controller.enqueue(bytes.slice(at, at + exports.UPLOAD_CHUNK));
|
|
431
|
+
at += exports.UPLOAD_CHUNK;
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
// Nothing read ahead: a pull is the socket taking the last piece.
|
|
435
|
+
{ highWaterMark: 0 });
|
|
436
|
+
}
|
|
437
|
+
/** A body read whole, `moved` told of every piece. */
|
|
438
|
+
async function readAll(response, moved) {
|
|
439
|
+
if (response.body === null)
|
|
440
|
+
return new Uint8Array(0);
|
|
441
|
+
const reader = response.body.getReader();
|
|
442
|
+
const parts = [];
|
|
443
|
+
let size = 0;
|
|
444
|
+
for (;;) {
|
|
445
|
+
const { done, value } = await reader.read();
|
|
446
|
+
if (done)
|
|
447
|
+
break;
|
|
448
|
+
moved();
|
|
449
|
+
parts.push(value);
|
|
450
|
+
size += value.byteLength;
|
|
451
|
+
}
|
|
452
|
+
const whole = new Uint8Array(size);
|
|
453
|
+
let at = 0;
|
|
454
|
+
for (const part of parts) {
|
|
455
|
+
whole.set(part, at);
|
|
456
|
+
at += part.byteLength;
|
|
457
|
+
}
|
|
458
|
+
return whole;
|
|
459
|
+
}
|
|
329
460
|
/**
|
|
330
461
|
* One fetch, body included, under one timeout and the caller's signal. A
|
|
331
462
|
* body that stalls or breaks after the headers fails the attempt like a
|
|
@@ -334,10 +465,24 @@ function backoffMs(config, attempt) {
|
|
|
334
465
|
async function attemptOnce(config, spec, url, timeoutMs) {
|
|
335
466
|
const controller = new AbortController();
|
|
336
467
|
let timedOut = false;
|
|
468
|
+
let stalled = false;
|
|
337
469
|
const timer = setTimeout(() => {
|
|
338
470
|
timedOut = true;
|
|
339
471
|
controller.abort();
|
|
340
472
|
}, timeoutMs);
|
|
473
|
+
// The stall bound starts over whenever bytes move (stallMs), until the attempt is over.
|
|
474
|
+
let stall;
|
|
475
|
+
let over = false;
|
|
476
|
+
const moved = (extraMs = 0) => {
|
|
477
|
+
if (spec.stallMs === undefined || over)
|
|
478
|
+
return;
|
|
479
|
+
clearTimeout(stall);
|
|
480
|
+
stall = setTimeout(() => {
|
|
481
|
+
stalled = true;
|
|
482
|
+
timedOut = true;
|
|
483
|
+
controller.abort();
|
|
484
|
+
}, spec.stallMs + extraMs);
|
|
485
|
+
};
|
|
341
486
|
const onAbort = () => controller.abort();
|
|
342
487
|
spec.signal?.addEventListener('abort', onAbort, { once: true });
|
|
343
488
|
const headers = {
|
|
@@ -347,30 +492,52 @@ async function attemptOnce(config, spec, url, timeoutMs) {
|
|
|
347
492
|
...spec.headers,
|
|
348
493
|
};
|
|
349
494
|
let body;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
else if (spec.body !== undefined) {
|
|
355
|
-
headers['content-type'] = spec.contentType ?? 'application/octet-stream';
|
|
356
|
-
body = spec.body;
|
|
357
|
-
}
|
|
495
|
+
// Watched from the start unless an upload goes out unwatched (Config.streamBodies).
|
|
496
|
+
let watched = spec.stallMs !== undefined && spec.body === undefined;
|
|
497
|
+
let streamed = false;
|
|
358
498
|
let status = null;
|
|
359
499
|
try {
|
|
500
|
+
if (spec.json !== undefined) {
|
|
501
|
+
headers['content-type'] = 'application/json';
|
|
502
|
+
body = JSON.stringify(spec.json);
|
|
503
|
+
}
|
|
504
|
+
else if (spec.body !== undefined) {
|
|
505
|
+
headers['content-type'] = spec.contentType ?? 'application/octet-stream';
|
|
506
|
+
body = spec.body;
|
|
507
|
+
const bytes = spec.stallMs !== undefined && config.streamBodies ? await bodyBytes(spec.body) : null;
|
|
508
|
+
if (bytes !== null) {
|
|
509
|
+
headers['content-length'] = String(bytes.byteLength);
|
|
510
|
+
body = inPieces(bytes, moved);
|
|
511
|
+
streamed = true;
|
|
512
|
+
watched = true;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (watched)
|
|
516
|
+
moved();
|
|
360
517
|
const response = await config.fetch(url, {
|
|
361
518
|
method: spec.method,
|
|
362
519
|
headers,
|
|
363
520
|
body: body,
|
|
521
|
+
// A streamed body (Node's fetch asks for it): sent before the answer is read.
|
|
522
|
+
...(streamed ? { duplex: 'half' } : {}),
|
|
364
523
|
// Never follow a redirect with the key attached.
|
|
365
524
|
redirect: 'manual',
|
|
366
525
|
signal: controller.signal,
|
|
367
526
|
});
|
|
368
527
|
status = response.status;
|
|
528
|
+
if (spec.stallMs !== undefined)
|
|
529
|
+
moved();
|
|
369
530
|
const ok = response.status >= 200 && response.status < 300;
|
|
370
531
|
if (response.status >= 300 && response.status < 400) {
|
|
371
532
|
await response.body?.cancel().catch(() => undefined);
|
|
372
533
|
return { answer: { status: response.status, headers: response.headers, text: '', bytes: null } };
|
|
373
534
|
}
|
|
535
|
+
if (spec.stallMs !== undefined) {
|
|
536
|
+
const whole = await readAll(response, moved);
|
|
537
|
+
if (ok && spec.read === 'bytes')
|
|
538
|
+
return { answer: { status: response.status, headers: response.headers, text: '', bytes: whole } };
|
|
539
|
+
return { answer: { status: response.status, headers: response.headers, text: new TextDecoder('utf-8').decode(whole), bytes: null } };
|
|
540
|
+
}
|
|
374
541
|
if (ok && spec.read === 'bytes') {
|
|
375
542
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
376
543
|
return { answer: { status: response.status, headers: response.headers, text: '', bytes } };
|
|
@@ -378,10 +545,12 @@ async function attemptOnce(config, spec, url, timeoutMs) {
|
|
|
378
545
|
return { answer: { status: response.status, headers: response.headers, text: await response.text(), bytes: null } };
|
|
379
546
|
}
|
|
380
547
|
catch (error) {
|
|
381
|
-
return { error, timedOut, status };
|
|
548
|
+
return { error, timedOut, stalled, status };
|
|
382
549
|
}
|
|
383
550
|
finally {
|
|
551
|
+
over = true;
|
|
384
552
|
clearTimeout(timer);
|
|
553
|
+
clearTimeout(stall);
|
|
385
554
|
spec.signal?.removeEventListener('abort', onAbort);
|
|
386
555
|
}
|
|
387
556
|
}
|
|
@@ -427,8 +596,9 @@ function outcomeOf(error) {
|
|
|
427
596
|
/**
|
|
428
597
|
* The error a request throws. With an `effect`, it says whether the sandbox
|
|
429
598
|
* acted on the request: by this attempt, or by an earlier one that was sent
|
|
430
|
-
* again (`acted`: only a keyed request, the background start,
|
|
431
|
-
* after
|
|
599
|
+
* again (`acted`: only a keyed request, the background start, and a staged
|
|
600
|
+
* upload are sent again after they may have arrived). `cutStatus`: the 2xx
|
|
601
|
+
* whose body broke off.
|
|
432
602
|
*/
|
|
433
603
|
function raised(failure, outcome, acted, effect, cutStatus = null) {
|
|
434
604
|
if (effect === undefined)
|
|
@@ -443,7 +613,8 @@ async function request(config, spec) {
|
|
|
443
613
|
const timeoutMs = spec.timeoutMs ?? config.requestTimeoutMs;
|
|
444
614
|
const sandboxId = spec.sandboxId ?? null;
|
|
445
615
|
const effect = spec.effect ?? (spec.retry === 'exec' ? (0, outcome_js_1.commandEffect)() : undefined);
|
|
446
|
-
|
|
616
|
+
// Whether an attempt that was sent again (or an earlier copy) may have reached the sandbox.
|
|
617
|
+
let acted = spec.acted ?? false;
|
|
447
618
|
// Names the last attempt's failure, if any, so a call cut short by its
|
|
448
619
|
// deadline still says what it was waiting out (a 503, a rate limit).
|
|
449
620
|
const pastDeadline = (last) => {
|
|
@@ -451,9 +622,11 @@ async function request(config, spec) {
|
|
|
451
622
|
const tail = what === null ? '' : ` The last attempt failed with ${what}.`;
|
|
452
623
|
let error = new errors_js_1.PrizedError('timeout', `The time allowed for this call ran out.${tail}`, { sandboxId, cause: last });
|
|
453
624
|
// A command is sent again only when it provably never arrived, so every
|
|
454
|
-
// attempt of one cut short this way never reached the sandbox.
|
|
455
|
-
|
|
456
|
-
|
|
625
|
+
// attempt of one cut short this way never reached the sandbox. A staged
|
|
626
|
+
// upload's copies may have, unless each provably never arrived, and so
|
|
627
|
+
// may a keyed start's (the one `safe` request that names an effect).
|
|
628
|
+
if (effect !== undefined)
|
|
629
|
+
error = (0, outcome_js_1.withOutcome)(error, effect, spec.retry !== 'exec' && acted ? 'maybe' : 'never');
|
|
457
630
|
cutShort.add(error);
|
|
458
631
|
return error;
|
|
459
632
|
};
|
|
@@ -462,8 +635,6 @@ async function request(config, spec) {
|
|
|
462
635
|
return error;
|
|
463
636
|
};
|
|
464
637
|
let last;
|
|
465
|
-
// Whether an attempt that was sent again may have reached the sandbox.
|
|
466
|
-
let acted = false;
|
|
467
638
|
// An abort with an `effect` says whether the sandbox may have acted:
|
|
468
639
|
// before an attempt, or while waiting to send one again, nothing was sent
|
|
469
640
|
// that might have, unless an earlier attempt of a keyed request (`acted`)
|
|
@@ -493,15 +664,19 @@ async function request(config, spec) {
|
|
|
493
664
|
}
|
|
494
665
|
const unsentAttempt = !outcome.timedOut && neverSent(outcome.error);
|
|
495
666
|
failure = failed(outcome.timedOut
|
|
496
|
-
? new errors_js_1.PrizedError('timeout', `No answer within ${attemptMs} ms`, {
|
|
667
|
+
? new errors_js_1.PrizedError('timeout', outcome.stalled ? `No bytes moved for ${spec.stallMs} ms` : `No answer within ${attemptMs} ms`, {
|
|
668
|
+
sandboxId,
|
|
669
|
+
cause: outcome.error,
|
|
670
|
+
})
|
|
497
671
|
: unsentAttempt
|
|
498
672
|
? new errors_js_1.PrizedError('network_error', null, { sandboxId, cause: outcome.error })
|
|
499
673
|
: new errors_js_1.PrizedError('network_error', lostMessage(spec.method, url, outcome.error), { sandboxId, cause: outcome.error }));
|
|
500
674
|
// The status came and the body did not: the sandbox acted on it.
|
|
501
675
|
const cut = outcome.status !== null && outcome.status >= 200 && outcome.status < 300;
|
|
502
676
|
const said = cut ? 'did' : unsentAttempt ? 'never' : 'maybe';
|
|
503
|
-
// A command
|
|
504
|
-
|
|
677
|
+
// A command is sent again only if it never left; a staged upload
|
|
678
|
+
// unless its 2xx came (its bytes are there).
|
|
679
|
+
const resend = spec.retry === 'safe' || (spec.retry === 'exec' && unsentAttempt) || (spec.retry === 'staged' && !cut);
|
|
505
680
|
if (!resend || attempt >= config.maxRetries)
|
|
506
681
|
throw raised(failure, said, acted, effect, cut ? outcome.status : null);
|
|
507
682
|
acted = acted || said !== 'never';
|
|
@@ -517,7 +692,8 @@ async function request(config, spec) {
|
|
|
517
692
|
}
|
|
518
693
|
}
|
|
519
694
|
if (answer.status >= 300 && answer.status < 400) {
|
|
520
|
-
|
|
695
|
+
// A refusal; worded only when an earlier copy of a keyed start may have arrived.
|
|
696
|
+
throw raised(failed(new errors_js_1.PrizedError('unexpected_redirect', null, { status: answer.status, sandboxId })), null, acted, effect);
|
|
521
697
|
}
|
|
522
698
|
const retryAfter = (0, errors_js_1.parseRetryAfter)(answer.headers.get('retry-after'), config.now());
|
|
523
699
|
failure = failed((0, errors_js_1.errorFromResponse)(answer.status, answer.text, retryAfter, sandboxId, urlPath(url)));
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -4,9 +4,9 @@ export { CommandHandle, DEFAULT_EXEC_TIMEOUT_MS, MAX_BLOCKING_EXEC_MS } from './
|
|
|
4
4
|
export type { CommandStatus, ExecOptions, WaitOptions } from './commands.js';
|
|
5
5
|
export { Files } from './files.js';
|
|
6
6
|
export type { FileData, WriteOptions } from './files.js';
|
|
7
|
-
export { CommandTimeoutError, PrizedError } from './errors.js';
|
|
8
|
-
export type { PrizedErrorInit } from './errors.js';
|
|
7
|
+
export { CommandTimeoutError, OutputReadError, PrizedError } from './errors.js';
|
|
8
|
+
export type { OutputReadErrorInit, PrizedErrorInit } from './errors.js';
|
|
9
9
|
export { DEFAULT_BASE_URL, DEFAULT_EDGE_URL } from './http.js';
|
|
10
10
|
export type { ClientOptions } from './http.js';
|
|
11
|
-
export type { ExecResult, FileInfo, Resources, ResourcesInput, SandboxInfo, SandboxState, Template, WriteInfo, } from './types.js';
|
|
11
|
+
export type { ExecResult, FileInfo, Resources, ResourcesInput, SandboxInfo, SandboxState, Template, TruncatedReason, WriteInfo, } from './types.js';
|
|
12
12
|
export { VERSION } from './version.js';
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VERSION = exports.DEFAULT_EDGE_URL = exports.DEFAULT_BASE_URL = exports.PrizedError = exports.CommandTimeoutError = exports.Files = exports.MAX_BLOCKING_EXEC_MS = exports.DEFAULT_EXEC_TIMEOUT_MS = exports.CommandHandle = exports.PREVIEW_KEY_HEADER = exports.DEFAULT_TEMPLATE = exports.Sandbox = void 0;
|
|
3
|
+
exports.VERSION = exports.DEFAULT_EDGE_URL = exports.DEFAULT_BASE_URL = exports.PrizedError = exports.OutputReadError = exports.CommandTimeoutError = exports.Files = exports.MAX_BLOCKING_EXEC_MS = exports.DEFAULT_EXEC_TIMEOUT_MS = exports.CommandHandle = exports.PREVIEW_KEY_HEADER = exports.DEFAULT_TEMPLATE = exports.Sandbox = void 0;
|
|
4
4
|
// Prized Sandboxes (beta) from TypeScript: the public surface.
|
|
5
5
|
var sandbox_js_1 = require("./sandbox.js");
|
|
6
6
|
Object.defineProperty(exports, "Sandbox", { enumerable: true, get: function () { return sandbox_js_1.Sandbox; } });
|
|
@@ -14,6 +14,7 @@ var files_js_1 = require("./files.js");
|
|
|
14
14
|
Object.defineProperty(exports, "Files", { enumerable: true, get: function () { return files_js_1.Files; } });
|
|
15
15
|
var errors_js_1 = require("./errors.js");
|
|
16
16
|
Object.defineProperty(exports, "CommandTimeoutError", { enumerable: true, get: function () { return errors_js_1.CommandTimeoutError; } });
|
|
17
|
+
Object.defineProperty(exports, "OutputReadError", { enumerable: true, get: function () { return errors_js_1.OutputReadError; } });
|
|
17
18
|
Object.defineProperty(exports, "PrizedError", { enumerable: true, get: function () { return errors_js_1.PrizedError; } });
|
|
18
19
|
var http_js_1 = require("./http.js");
|
|
19
20
|
Object.defineProperty(exports, "DEFAULT_BASE_URL", { enumerable: true, get: function () { return http_js_1.DEFAULT_BASE_URL; } });
|
package/dist/cjs/outcome.d.ts
CHANGED
|
@@ -30,11 +30,12 @@ export declare function helperEffect(): Effect;
|
|
|
30
30
|
export type Outcome = 'never' | 'maybe' | 'did' | null;
|
|
31
31
|
/** Whether withOutcome said the sandbox may have acted on the request. */
|
|
32
32
|
export declare function mayHaveActed(error: unknown): error is PrizedError;
|
|
33
|
-
/** `error` with another message (and, given, another status, code, sandbox or cause), every other field kept. */
|
|
33
|
+
/** `error` with another message (and, given, another status, code, sandbox, exit code or cause), every other field kept. */
|
|
34
34
|
export declare function restate(error: PrizedError, message: string, init?: {
|
|
35
35
|
status?: number;
|
|
36
36
|
code?: string;
|
|
37
37
|
sandboxId?: string;
|
|
38
|
+
exitCode?: number;
|
|
38
39
|
cause?: unknown;
|
|
39
40
|
}): PrizedError;
|
|
40
41
|
/** `error` (same code, status and body), its message saying what it means for `effect`. */
|
|
@@ -48,6 +49,15 @@ export declare function withOutcome(error: PrizedError, effect: Effect, outcome:
|
|
|
48
49
|
* connection closes. TypeScript only: the Python SDK takes no signal.
|
|
49
50
|
*/
|
|
50
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;
|
|
51
61
|
/**
|
|
52
62
|
* A background start that may have started (`error`, from withOutcome or
|
|
53
63
|
* withAbort) and that the SDK then stopped by its run id (`stopped`: the
|
package/dist/cjs/outcome.js
CHANGED
|
@@ -10,13 +10,16 @@ exports.mayHaveActed = mayHaveActed;
|
|
|
10
10
|
exports.restate = restate;
|
|
11
11
|
exports.withOutcome = withOutcome;
|
|
12
12
|
exports.withAbort = withAbort;
|
|
13
|
+
exports.startGaveUp = startGaveUp;
|
|
13
14
|
exports.startAbandoned = startAbandoned;
|
|
14
15
|
exports.runAborted = runAborted;
|
|
15
16
|
exports.pollFailed = pollFailed;
|
|
16
17
|
// What an error says about a command that may have run. A command, a file
|
|
17
|
-
// write, a remove or a signal (`exec` retry mode) is never sent again
|
|
18
|
-
// may have reached the sandbox, so its error has to say what the
|
|
19
|
-
// see: whether it happened.
|
|
18
|
+
// write's rename, a remove or a signal (`exec` retry mode) is never sent again
|
|
19
|
+
// once it may have reached the sandbox, so its error has to say what the
|
|
20
|
+
// caller cannot see: whether it happened. A write's staged upload (`staged`)
|
|
21
|
+
// may have been sent again, and its error says whether a copy may have
|
|
22
|
+
// reached the sandbox. The Python SDK words these the same (_core.py).
|
|
20
23
|
const errors_js_1 = require("./errors.js");
|
|
21
24
|
const NOT_SENT_AGAIN = 'It was not sent again';
|
|
22
25
|
function commandEffect() {
|
|
@@ -65,7 +68,7 @@ const acted = new WeakSet();
|
|
|
65
68
|
function mayHaveActed(error) {
|
|
66
69
|
return error instanceof errors_js_1.PrizedError && acted.has(error);
|
|
67
70
|
}
|
|
68
|
-
/** `error` with another message (and, given, another status, code, sandbox or cause), every other field kept. */
|
|
71
|
+
/** `error` with another message (and, given, another status, code, sandbox, exit code or cause), every other field kept. */
|
|
69
72
|
function restate(error, message, init = {}) {
|
|
70
73
|
return new errors_js_1.PrizedError(init.code ?? error.code, message, {
|
|
71
74
|
status: init.status ?? error.status,
|
|
@@ -73,6 +76,7 @@ function restate(error, message, init = {}) {
|
|
|
73
76
|
sandboxId: error.sandboxId ?? init.sandboxId ?? null,
|
|
74
77
|
body: error.body,
|
|
75
78
|
retryAfter: error.retryAfter,
|
|
79
|
+
exitCode: init.exitCode ?? error.exitCode,
|
|
76
80
|
cause: 'cause' in init ? init.cause : error.cause,
|
|
77
81
|
});
|
|
78
82
|
}
|
|
@@ -102,12 +106,14 @@ function maybeTail(effect, closed) {
|
|
|
102
106
|
tail += ` ${effect.check}`;
|
|
103
107
|
return tail;
|
|
104
108
|
}
|
|
109
|
+
/** How an error says its request never reached the sandbox (withOutcome). */
|
|
110
|
+
const NEVER_TAIL = ' It never reached the sandbox, so it had no effect.';
|
|
105
111
|
/** `error` (same code, status and body), its message saying what it means for `effect`. */
|
|
106
112
|
function withOutcome(error, effect, outcome) {
|
|
107
113
|
if (outcome === null)
|
|
108
114
|
return error;
|
|
109
115
|
if (outcome === 'never')
|
|
110
|
-
return restate(error, `${trimmed(error.message)}
|
|
116
|
+
return restate(error, `${trimmed(error.message)}.${NEVER_TAIL}`);
|
|
111
117
|
if (outcome === 'did')
|
|
112
118
|
return acting(error, `${effect.did}, but the answer could not be read (${detail(error)}).`);
|
|
113
119
|
return acting(error, `No clear answer came back (${detail(error)}), so ${maybeTail(effect, false)}`);
|
|
@@ -131,6 +137,23 @@ function stopTail(runId, stopped) {
|
|
|
131
137
|
? ` The SDK stopped ${runId}, or it had already ended.`
|
|
132
138
|
: ` The SDK could not confirm it stopped: it may still be running as ${runId} (exec('ps -ef | grep ${runId}') finds it).`;
|
|
133
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* What a start that gave up throws when the failure it gives up with
|
|
142
|
+
* (`failure`, Outage's pick) is not the last copy's own: an earlier copy's,
|
|
143
|
+
* worded before a later copy may have reached the sandbox, or what a copy
|
|
144
|
+
* cut short by the window met, not yet worded. Worded for the start: as one
|
|
145
|
+
* that may have started when any copy may have reached the sandbox
|
|
146
|
+
* (`anyActed`), else as one that never did. The Python SDK's start_gave_up.
|
|
147
|
+
*/
|
|
148
|
+
function startGaveUp(failure, anyActed) {
|
|
149
|
+
if (acted.has(failure))
|
|
150
|
+
return failure;
|
|
151
|
+
const never = failure.message.endsWith(NEVER_TAIL);
|
|
152
|
+
if (never && !anyActed)
|
|
153
|
+
return failure;
|
|
154
|
+
const plain = never ? restate(failure, failure.message.slice(0, -NEVER_TAIL.length)) : failure;
|
|
155
|
+
return withOutcome(plain, startEffect(), anyActed ? 'maybe' : 'never');
|
|
156
|
+
}
|
|
134
157
|
/**
|
|
135
158
|
* A background start that may have started (`error`, from withOutcome or
|
|
136
159
|
* withAbort) and that the SDK then stopped by its run id (`stopped`: the
|
package/dist/cjs/output.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Toolbox } from './toolbox.js';
|
|
2
|
-
import type { ExecResult } from './types.js';
|
|
2
|
+
import type { ExecResult, TruncatedReason } from './types.js';
|
|
3
3
|
export declare const OUTPUT_DIR = "/home/prized/.prized/exec";
|
|
4
4
|
export declare const OUTPUT_ID_RE: RegExp;
|
|
5
5
|
/** Each stream of a fullOutput command keeps at most this many bytes: one download. */
|
|
@@ -22,17 +22,22 @@ export declare const RUN_SCRIPT = "eval \"${PRIZED_EXEC_SCRIPT:?}\"";
|
|
|
22
22
|
* Nothing is assigned before it runs but the positional parameters, so every
|
|
23
23
|
* variable the caller's env sets reaches it unchanged, PATH included; and
|
|
24
24
|
* every step holds under an errexit the env may switch on (SHELLOPTS). As in
|
|
25
|
-
* a plain exec, the
|
|
25
|
+
* a plain exec, the relays stop 5 s after the command ends if a process it
|
|
26
26
|
* left running still holds its output, keeping what came through. A command
|
|
27
|
-
* a signal ends exits 128+n, as a background command does.
|
|
28
|
-
*
|
|
27
|
+
* a signal ends exits 128+n, as a background command does. A reader that
|
|
28
|
+
* could not write its file (failedLines) sends the output to the files API
|
|
29
|
+
* whatever its size, with the marker saying which. Without /bin/sh, setsid,
|
|
30
|
+
* cat or a directory to write to, the command runs as a plain exec.
|
|
29
31
|
*/
|
|
30
32
|
export declare function captureScript(id: string): string;
|
|
31
33
|
/**
|
|
32
34
|
* What a fullOutput command left when the script could not print it (its
|
|
33
35
|
* time limit, or a signal, ended the script): the marker line, or `gone`,
|
|
34
36
|
* with the directory removed, when there is nothing to read (it was never
|
|
35
|
-
* made, or its output was already printed back).
|
|
37
|
+
* made, or its output was already printed back). A failed write is read from
|
|
38
|
+
* the reader's flag, before the size, which is final once the flag is there
|
|
39
|
+
* (the script's wait for the reader's exit status went with the script); a
|
|
40
|
+
* reader killed, or one that could not make its flag, is not seen here.
|
|
36
41
|
*/
|
|
37
42
|
export declare function collectScript(id: string): string;
|
|
38
43
|
/**
|
|
@@ -41,11 +46,17 @@ export declare function collectScript(id: string): string;
|
|
|
41
46
|
* answer to remove its own.
|
|
42
47
|
*/
|
|
43
48
|
export declare function removeOutputScript(id: string): string;
|
|
44
|
-
/**
|
|
45
|
-
export
|
|
49
|
+
/** What the marker line says: each stream's byte count (one cut at FULL_OUTPUT_BYTES reads more), and whether its reader failed to write it. */
|
|
50
|
+
export interface Marker {
|
|
46
51
|
out: number;
|
|
47
52
|
err: number;
|
|
48
|
-
|
|
53
|
+
outFailed: boolean;
|
|
54
|
+
errFailed: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** The marker (one without failure flags, as an earlier SDK's collect printed, has none), `gone`, or null for no marker. */
|
|
57
|
+
export declare function parseMarker(stdout: string, id: string): Marker | 'gone' | null;
|
|
58
|
+
/** Why the read-back output is cut, if it is: a failed write first, since it is the one the caller cannot foresee. */
|
|
59
|
+
export declare function markerTruncation(marker: Marker): TruncatedReason | null;
|
|
49
60
|
/**
|
|
50
61
|
* A blocking exec that keeps the whole output (up to FULL_OUTPUT_BYTES of
|
|
51
62
|
* each stream). The result is a plain exec's, timeouts included: the caller
|