@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/errors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CommandTimeoutError = exports.PrizedError = exports.FREE_TIER_URL = exports.SIGN_UP_URL = exports.API_KEYS_URL = void 0;
|
|
3
|
+
exports.OutputReadError = exports.CommandTimeoutError = exports.PrizedError = exports.FREE_TIER_URL = exports.AGENT_SIGN_UP_URL = exports.SIGN_UP_URL = exports.API_KEYS_URL = void 0;
|
|
4
4
|
exports.defaultMessage = defaultMessage;
|
|
5
5
|
exports.parseRetryAfter = parseRetryAfter;
|
|
6
6
|
exports.errorFromResponse = errorFromResponse;
|
|
@@ -8,15 +8,22 @@ exports.errorFromResponse = errorFromResponse;
|
|
|
8
8
|
exports.API_KEYS_URL = 'https://prized.dev/dashboard/workspace/api-keys';
|
|
9
9
|
/** Where a newcomer signs up (free credit, no card) and gets one. */
|
|
10
10
|
exports.SIGN_UP_URL = 'https://prized.dev/onboarding/sandboxes';
|
|
11
|
+
/** What a coding agent follows to sign its person up and get a key itself:
|
|
12
|
+
* their email address, then the 6-digit code Prized emails them. The SDK's
|
|
13
|
+
* own key errors name it; a refused key's 401 carries the API's own line
|
|
14
|
+
* (`signUp`), which follows the deployment. */
|
|
15
|
+
exports.AGENT_SIGN_UP_URL = 'https://prized.dev/auth.md';
|
|
16
|
+
const AGENT_SIGN_UP = `No Prized account yet? If you are an agent, you can sign your person up with their email and a 6-digit code: follow ${exports.AGENT_SIGN_UP_URL}`;
|
|
11
17
|
/** The free tier's limits, and what lifts them. */
|
|
12
18
|
exports.FREE_TIER_URL = 'https://prized.dev/docs/sandboxes#free-tier';
|
|
13
19
|
/** A sentence for the codes a caller is most likely to meet, used when the
|
|
14
20
|
* answer carries no `message` of its own. */
|
|
15
21
|
const MESSAGES = {
|
|
16
22
|
// The SDK's own codes.
|
|
17
|
-
missing_api_key: `No API key: pass apiKey or set PRIZED_API_KEY. New to Prized? ${exports.SIGN_UP_URL} signs you up and makes one
|
|
18
|
-
invalid_api_key: `The API key has characters no key has (the docs' placeholder, or a stray character?); copy it again from ${exports.API_KEYS_URL}
|
|
23
|
+
missing_api_key: `No API key: pass apiKey or set PRIZED_API_KEY. New to Prized? ${exports.SIGN_UP_URL} signs you up and makes one, or, if you are an agent, you can sign your person up with their email and a 6-digit code: follow ${exports.AGENT_SIGN_UP_URL}, keep the key file it writes, and pass that key as apiKey at run time, never in source. Otherwise make one at ${exports.API_KEYS_URL}.`,
|
|
24
|
+
invalid_api_key: `The API key has characters no key has (the docs' placeholder, or a stray character?); copy it again from ${exports.API_KEYS_URL}. ${AGENT_SIGN_UP}`,
|
|
19
25
|
invalid_argument: 'An argument was not valid.',
|
|
26
|
+
edge_unknown: 'The API named no command endpoint this SDK trusts; pass edgeUrl for that deployment.',
|
|
20
27
|
network_error: 'Could not reach the Prized API.',
|
|
21
28
|
timeout: 'Timed out.',
|
|
22
29
|
aborted: 'The operation was aborted.',
|
|
@@ -27,6 +34,7 @@ const MESSAGES = {
|
|
|
27
34
|
sandbox_deleted: 'The sandbox was deleted.',
|
|
28
35
|
command_failed: 'The command failed in the sandbox.',
|
|
29
36
|
command_timeout: 'The command ran past its timeout and was killed.',
|
|
37
|
+
output_read_failed: 'The command ran, but its output could not be read back from the sandbox.',
|
|
30
38
|
not_directory: 'Not a directory.',
|
|
31
39
|
// The API's and the exec edge's codes.
|
|
32
40
|
unauthorized: `The API key was refused. Check that it is a live key from ${exports.API_KEYS_URL}.`,
|
|
@@ -83,13 +91,21 @@ function defaultMessage(code) {
|
|
|
83
91
|
* - `code`: the API's error string, e.g. `paid_balance_required`,
|
|
84
92
|
* `insufficient_balance`, `sandbox_quota_exceeded`, `not_found`,
|
|
85
93
|
* `rate_limited`; or one of the SDK's own: `missing_api_key`,
|
|
86
|
-
* `invalid_api_key`, `invalid_argument`, `
|
|
94
|
+
* `invalid_api_key`, `invalid_argument`, `edge_unknown` (another
|
|
95
|
+
* deployment's API named no command endpoint this SDK sends the key to),
|
|
96
|
+
* `network_error`, `timeout`, `aborted`,
|
|
87
97
|
* `bad_response`, `http_error`, `sandbox_error`, `sandbox_deleted`,
|
|
88
|
-
* `command_failed`, `command_timeout` (thrown as CommandTimeoutError)
|
|
98
|
+
* `command_failed`, `command_timeout` (thrown as CommandTimeoutError),
|
|
99
|
+
* `output_read_failed` (thrown as OutputReadError).
|
|
89
100
|
* - `reason`: the finer-grained reason, when there is one (for
|
|
90
101
|
* `sandbox_quota_exceeded` it names the quota: `sandbox_limit`,
|
|
91
102
|
* `paused_limit`, `running_vcpu`, `running_memory`, `retained_disk`,
|
|
92
|
-
* `max_runtime`
|
|
103
|
+
* `max_runtime`; for `output_read_failed`, the code of the failure that
|
|
104
|
+
* stopped the read).
|
|
105
|
+
* - `exitCode`: the command's exit code when the error came after it ran: a
|
|
106
|
+
* `fullOutput` command whose output could not be read back
|
|
107
|
+
* (`output_read_failed`, or `sandbox_deleted` when the sandbox was deleted
|
|
108
|
+
* first). Don't run such a command again blindly. Null otherwise.
|
|
93
109
|
*/
|
|
94
110
|
class PrizedError extends Error {
|
|
95
111
|
status;
|
|
@@ -98,6 +114,7 @@ class PrizedError extends Error {
|
|
|
98
114
|
sandboxId;
|
|
99
115
|
body;
|
|
100
116
|
retryAfter;
|
|
117
|
+
exitCode;
|
|
101
118
|
constructor(code, message, init = {}) {
|
|
102
119
|
super(message || defaultMessage(code), init.cause === undefined ? undefined : { cause: init.cause });
|
|
103
120
|
this.name = 'PrizedError';
|
|
@@ -107,6 +124,7 @@ class PrizedError extends Error {
|
|
|
107
124
|
this.sandboxId = init.sandboxId ?? null;
|
|
108
125
|
this.body = init.body ?? null;
|
|
109
126
|
this.retryAfter = init.retryAfter ?? null;
|
|
127
|
+
this.exitCode = init.exitCode ?? null;
|
|
110
128
|
}
|
|
111
129
|
toString() {
|
|
112
130
|
const head = this.status === null ? this.code : `${this.status} ${this.code}`;
|
|
@@ -143,6 +161,31 @@ class CommandTimeoutError extends PrizedError {
|
|
|
143
161
|
}
|
|
144
162
|
}
|
|
145
163
|
exports.CommandTimeoutError = CommandTimeoutError;
|
|
164
|
+
/**
|
|
165
|
+
* A `fullOutput` command ran, but its output could not be read back from the
|
|
166
|
+
* files it was copied to (`code` is `output_read_failed`). The command did
|
|
167
|
+
* run (`exitCode` is its exit code), so do not run it again blindly; and the
|
|
168
|
+
* sandbox is not known to be gone (a deleted one throws `sandbox_deleted`
|
|
169
|
+
* instead). `reason` is the code of the failure that stopped the read (say
|
|
170
|
+
* `not_found`, `network_error`), and `cause` is that error, with its HTTP
|
|
171
|
+
* status; `status` is null, as the command's own call succeeded. The output
|
|
172
|
+
* stays in the sandbox for an hour at `stdoutPath` and `stderrPath`: read it
|
|
173
|
+
* with files.read(), then remove their directory.
|
|
174
|
+
*/
|
|
175
|
+
class OutputReadError extends PrizedError {
|
|
176
|
+
exitCode;
|
|
177
|
+
stdoutPath;
|
|
178
|
+
stderrPath;
|
|
179
|
+
constructor(message, init) {
|
|
180
|
+
const { cause } = init;
|
|
181
|
+
super('output_read_failed', message, { reason: cause.code, sandboxId: cause.sandboxId, cause, exitCode: init.exitCode });
|
|
182
|
+
this.name = 'OutputReadError';
|
|
183
|
+
this.exitCode = init.exitCode;
|
|
184
|
+
this.stdoutPath = init.stdoutPath;
|
|
185
|
+
this.stderrPath = init.stderrPath;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
exports.OutputReadError = OutputReadError;
|
|
146
189
|
function stringField(record, key) {
|
|
147
190
|
const value = record[key];
|
|
148
191
|
return typeof value === 'string' && value !== '' ? value : null;
|
|
@@ -254,5 +297,10 @@ function errorFromResponse(status, text, retryAfter, sandboxId, path = '') {
|
|
|
254
297
|
const reason = doc ? stringField(doc, 'reason') ?? stringField(doc, 'field') ?? stringField(doc, 'detail') : null;
|
|
255
298
|
if (doc && doc.tier === 'free' && !(message ?? '').includes('free tier'))
|
|
256
299
|
message = freeTierMessage(code, message, reason, doc.limit);
|
|
300
|
+
// A refused key's 401 may say how an agent signs its person up (the API
|
|
301
|
+
// sends it where agent sign-in answers, with that deployment's address).
|
|
302
|
+
const signUp = code === 'unauthorized' && doc ? stringField(doc, 'signUp') : null;
|
|
303
|
+
if (signUp !== null)
|
|
304
|
+
message = `${message ?? defaultMessage(code)} ${signUp.slice(0, 300)}`;
|
|
257
305
|
return new PrizedError(code, message, { status, reason, sandboxId, body: doc, retryAfter });
|
|
258
306
|
}
|
package/dist/cjs/files.d.ts
CHANGED
|
@@ -11,7 +11,13 @@ export interface WriteOptions {
|
|
|
11
11
|
*/
|
|
12
12
|
mode?: number;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The sandbox's file transfers take at most this long, retries included:
|
|
16
|
+
* the edge's own limit on one transfer. An attempt that moves no bytes for
|
|
17
|
+
* timing.transferStallMs (2 minutes) is given up; a download, or a write's
|
|
18
|
+
* staged upload, is then sent again (as after a lost answer or a 502, 503 or
|
|
19
|
+
* 504) while time is left.
|
|
20
|
+
*/
|
|
15
21
|
export declare const FILE_TIMEOUT_MS: number;
|
|
16
22
|
/** The publish script's own exits (the Python SDK's too). */
|
|
17
23
|
export declare const EXIT_TARGET_IS_LINK = 47;
|
|
@@ -54,12 +60,22 @@ export declare class Files {
|
|
|
54
60
|
* A file that is replaced keeps its mode, so a script made executable
|
|
55
61
|
* stays executable; a new file gets the sandbox's default (0600 in
|
|
56
62
|
* prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
|
|
57
|
-
*
|
|
58
|
-
* lost
|
|
59
|
-
*
|
|
63
|
+
* The bytes go up under a temporary name beside the file, sent again if
|
|
64
|
+
* need be (a lost answer, a 502, 503 or 504, 2 minutes with no bytes
|
|
65
|
+
* moving; all copies within the transfer's 10 minutes),
|
|
66
|
+
* and one command that is never sent again puts them in place: a write
|
|
67
|
+
* whose answer to that command was lost throws, since it may have landed
|
|
68
|
+
* and the sandbox written the file since (read it back, or write again,
|
|
69
|
+
* as suits the file).
|
|
60
70
|
*/
|
|
61
71
|
write(path: string, data: FileData, options?: WriteOptions): Promise<WriteInfo>;
|
|
62
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* One upload to `path`, as the files API takes it; `target` is the file the
|
|
74
|
+
* caller asked for, and `effect` what the upload does to it. `exec` for an
|
|
75
|
+
* upload straight to the file: like a command, a resend after the answer
|
|
76
|
+
* was lost could overwrite what the sandbox wrote to the path since.
|
|
77
|
+
* `staged` for one to a fresh name: sent again like a read.
|
|
78
|
+
*/
|
|
63
79
|
private send;
|
|
64
80
|
/**
|
|
65
81
|
* Remove a staged upload that was not put in place (best effort: the
|
package/dist/cjs/files.js
CHANGED
|
@@ -9,7 +9,13 @@ const http_js_1 = require("./http.js");
|
|
|
9
9
|
const outcome_js_1 = require("./outcome.js");
|
|
10
10
|
const shell_js_1 = require("./shell.js");
|
|
11
11
|
const toolbox_js_1 = require("./toolbox.js");
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* The sandbox's file transfers take at most this long, retries included:
|
|
14
|
+
* the edge's own limit on one transfer. An attempt that moves no bytes for
|
|
15
|
+
* timing.transferStallMs (2 minutes) is given up; a download, or a write's
|
|
16
|
+
* staged upload, is then sent again (as after a lost answer or a 502, 503 or
|
|
17
|
+
* 504) while time is left.
|
|
18
|
+
*/
|
|
13
19
|
exports.FILE_TIMEOUT_MS = 10 * 60_000;
|
|
14
20
|
function byteSize(data) {
|
|
15
21
|
if (typeof data === 'string')
|
|
@@ -45,12 +51,16 @@ exports.EXIT_TARGET_NOT_REGULAR = 48;
|
|
|
45
51
|
const DISCARD_MS = 10_000;
|
|
46
52
|
/** A write's own commands: a rename, a chmod or an rm. */
|
|
47
53
|
const WRITE_COMMAND_MS = 60_000;
|
|
48
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* For the staged upload: the target is changed only by the command that
|
|
56
|
+
* follows it. The upload may have been sent again, so it says only what to
|
|
57
|
+
* do (langchain-prized's staged_upload_effect words it the same).
|
|
58
|
+
*/
|
|
49
59
|
function stagedEffect(path) {
|
|
50
60
|
return {
|
|
51
61
|
maybe: `${path} was not changed (its new bytes may have reached the sandbox, but were not put in place)`,
|
|
52
62
|
did: `${path} was not changed yet`,
|
|
53
|
-
check: '
|
|
63
|
+
check: 'Write it again.',
|
|
54
64
|
};
|
|
55
65
|
}
|
|
56
66
|
/** For the command that puts the staged upload in place: a 2xx proves it ran, not how it ended. */
|
|
@@ -159,6 +169,7 @@ class Files {
|
|
|
159
169
|
retry: 'safe',
|
|
160
170
|
read: 'bytes',
|
|
161
171
|
timeoutMs: exports.FILE_TIMEOUT_MS,
|
|
172
|
+
stallMs: http_js_1.timing.transferStallMs,
|
|
162
173
|
deadline: this.toolbox.config.now() + exports.FILE_TIMEOUT_MS,
|
|
163
174
|
sandboxId: this.toolbox.sandboxId,
|
|
164
175
|
});
|
|
@@ -170,9 +181,13 @@ class Files {
|
|
|
170
181
|
* A file that is replaced keeps its mode, so a script made executable
|
|
171
182
|
* stays executable; a new file gets the sandbox's default (0600 in
|
|
172
183
|
* prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
|
|
173
|
-
*
|
|
174
|
-
* lost
|
|
175
|
-
*
|
|
184
|
+
* The bytes go up under a temporary name beside the file, sent again if
|
|
185
|
+
* need be (a lost answer, a 502, 503 or 504, 2 minutes with no bytes
|
|
186
|
+
* moving; all copies within the transfer's 10 minutes),
|
|
187
|
+
* and one command that is never sent again puts them in place: a write
|
|
188
|
+
* whose answer to that command was lost throws, since it may have landed
|
|
189
|
+
* and the sandbox written the file since (read it back, or write again,
|
|
190
|
+
* as suits the file).
|
|
176
191
|
*/
|
|
177
192
|
async write(path, data, options = {}) {
|
|
178
193
|
const sandboxId = this.toolbox.sandboxId;
|
|
@@ -184,14 +199,18 @@ class Files {
|
|
|
184
199
|
const target = (0, shell_js_1.sandboxPath)(path);
|
|
185
200
|
const plan = stagingFor(target);
|
|
186
201
|
if (plan === null)
|
|
187
|
-
return this.send(target, data, size, target, (0, outcome_js_1.writeEffect)(target));
|
|
202
|
+
return this.send(target, data, size, target, (0, outcome_js_1.writeEffect)(target), 'exec');
|
|
188
203
|
const { clean, staging } = plan;
|
|
189
204
|
try {
|
|
190
|
-
|
|
205
|
+
// Every copy to the same fresh name, which only the command below puts in place.
|
|
206
|
+
await this.send(staging, data, size, clean, stagedEffect(clean), 'staged');
|
|
191
207
|
}
|
|
192
208
|
catch (error) {
|
|
193
209
|
// A 2xx whose body could not be read: the staged bytes are all there.
|
|
194
210
|
if (!landed(error)) {
|
|
211
|
+
// Removed only if a copy may have landed: a refusal, or copies that
|
|
212
|
+
// never arrived, left nothing, and a command would wake a sandbox
|
|
213
|
+
// the edge's refusal left paused.
|
|
195
214
|
if ((0, outcome_js_1.mayHaveActed)(error))
|
|
196
215
|
await this.discard(staging);
|
|
197
216
|
throw error;
|
|
@@ -217,7 +236,7 @@ class Files {
|
|
|
217
236
|
return { path: clean, size };
|
|
218
237
|
if (result.exitCode === exports.EXIT_TARGET_IS_LINK) {
|
|
219
238
|
// A link: the files API follows it or refuses it, as for any upload.
|
|
220
|
-
const info = await this.send(clean, data, size, clean, (0, outcome_js_1.writeEffect)(clean));
|
|
239
|
+
const info = await this.send(clean, data, size, clean, (0, outcome_js_1.writeEffect)(clean), 'exec');
|
|
221
240
|
if (mode !== null)
|
|
222
241
|
await this.chmod(clean, mode);
|
|
223
242
|
return info;
|
|
@@ -228,18 +247,23 @@ class Files {
|
|
|
228
247
|
const denied = /Permission denied|Operation not permitted/.test(result.stderr);
|
|
229
248
|
throw new errors_js_1.PrizedError(denied ? 'permission_denied' : 'command_failed', `Could not write ${clean}: ${commandDetail(result)}`, { sandboxId });
|
|
230
249
|
}
|
|
231
|
-
/**
|
|
232
|
-
|
|
250
|
+
/**
|
|
251
|
+
* One upload to `path`, as the files API takes it; `target` is the file the
|
|
252
|
+
* caller asked for, and `effect` what the upload does to it. `exec` for an
|
|
253
|
+
* upload straight to the file: like a command, a resend after the answer
|
|
254
|
+
* was lost could overwrite what the sandbox wrote to the path since.
|
|
255
|
+
* `staged` for one to a fresh name: sent again like a read.
|
|
256
|
+
*/
|
|
257
|
+
async send(path, data, size, target, effect, retry) {
|
|
233
258
|
const result = await (0, http_js_1.request)(this.toolbox.config, {
|
|
234
259
|
method: 'POST',
|
|
235
260
|
url: this.url('upload', path),
|
|
236
261
|
body: data,
|
|
237
262
|
contentType: 'application/octet-stream',
|
|
238
|
-
|
|
239
|
-
// what the sandbox wrote to the path since.
|
|
240
|
-
retry: 'exec',
|
|
263
|
+
retry,
|
|
241
264
|
effect,
|
|
242
265
|
timeoutMs: exports.FILE_TIMEOUT_MS,
|
|
266
|
+
stallMs: http_js_1.timing.transferStallMs,
|
|
243
267
|
deadline: this.toolbox.config.now() + exports.FILE_TIMEOUT_MS,
|
|
244
268
|
sandboxId: this.toolbox.sandboxId,
|
|
245
269
|
});
|
package/dist/cjs/http.d.ts
CHANGED
|
@@ -6,11 +6,16 @@ export declare const DEFAULT_EDGE_URL = "https://term.prized.dev";
|
|
|
6
6
|
export interface ClientOptions {
|
|
7
7
|
/** Your API key (`dcp_…`). Default: the `PRIZED_API_KEY` environment variable. */
|
|
8
8
|
apiKey?: string;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* The API origin: https, or plain http only on this machine (`localhost`,
|
|
11
|
+
* `127.0.0.1`, `[::1]`). Default: `https://api.prized.dev`.
|
|
12
|
+
*/
|
|
10
13
|
baseUrl?: string;
|
|
11
14
|
/**
|
|
12
|
-
* The origin that serves exec and files
|
|
13
|
-
* returns for each sandbox (today
|
|
15
|
+
* The origin that serves exec and files, by the same rule as `baseUrl`.
|
|
16
|
+
* Default: the `toolboxBase` the API returns for each sandbox (today
|
|
17
|
+
* `https://term.prized.dev`), on a host trusted beside the API's
|
|
18
|
+
* (toolboxBaseFor).
|
|
14
19
|
*/
|
|
15
20
|
edgeUrl?: string;
|
|
16
21
|
/** A fetch implementation. Default: the global `fetch`. */
|
|
@@ -23,10 +28,13 @@ export interface ClientOptions {
|
|
|
23
28
|
* the request itself and the API's hold of a create, pause or resume. Default 500. */
|
|
24
29
|
pollIntervalMs?: number;
|
|
25
30
|
/**
|
|
26
|
-
* How long a
|
|
27
|
-
*
|
|
28
|
-
* the command. Default 120 000. `0`
|
|
29
|
-
* out. `exec`
|
|
31
|
+
* How long a wait keeps polling while its polls fail (no answer, a 5xx, a
|
|
32
|
+
* rate limit), in ms, before it gives up: a streamed or long `exec` then
|
|
33
|
+
* stops the command, a handle's `wait()` throws. Default 120 000. `0`
|
|
34
|
+
* gives up once one poll's retries run out. `exec` and `wait()` take it
|
|
35
|
+
* per call too. The start of a background, streamed or long `exec` is
|
|
36
|
+
* sent again for as long, so an edge that restarts mid-start does not
|
|
37
|
+
* cost the command.
|
|
30
38
|
*/
|
|
31
39
|
pollOutageMs?: number;
|
|
32
40
|
/** Sleep seam (tests inject one that never really waits). */
|
|
@@ -48,13 +56,21 @@ export interface Config {
|
|
|
48
56
|
sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
49
57
|
now: () => number;
|
|
50
58
|
random: () => number;
|
|
59
|
+
/**
|
|
60
|
+
* Whether an upload's body goes out as a stream the SDK feeds piece by
|
|
61
|
+
* piece, so a stalled one is seen (RequestSpec.stallMs): Node's own fetch
|
|
62
|
+
* only, which asks for each piece once the last has gone into the socket.
|
|
63
|
+
* Elsewhere (another runtime, a fetch you pass) the body goes as it is.
|
|
64
|
+
*/
|
|
65
|
+
streamBodies: boolean;
|
|
51
66
|
}
|
|
52
67
|
export declare function abortableSleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
53
68
|
/**
|
|
54
|
-
* How long a streamed or long exec
|
|
55
|
-
* before it
|
|
56
|
-
*
|
|
57
|
-
*
|
|
69
|
+
* How long a wait (a streamed or long exec, a handle's wait()) keeps polling
|
|
70
|
+
* while its polls fail before it gives up (pollOutageMs), and a streamed or
|
|
71
|
+
* long exec stops its command, and how long a start is sent again: long
|
|
72
|
+
* enough to ride out a dropped connection or an edge that restarts (about
|
|
73
|
+
* 70 s, the last time it crashed). The Python SDK's DEFAULT_POLL_OUTAGE.
|
|
58
74
|
*/
|
|
59
75
|
export declare const DEFAULT_POLL_OUTAGE_MS = 120000;
|
|
60
76
|
/** A pollOutageMs given (a number of ms ≥ 0), or undefined. */
|
|
@@ -63,7 +79,10 @@ export declare function resolveConfig(options?: ClientOptions): Config;
|
|
|
63
79
|
/**
|
|
64
80
|
* Where exec and files go for one sandbox. An explicit edgeUrl wins; else the
|
|
65
81
|
* API's own `toolboxBase` when it has the expected shape (https, or plain
|
|
66
|
-
* http on loopback for local development)
|
|
82
|
+
* http on loopback for local development, ending /v1/sandbox/<id>) on a host
|
|
83
|
+
* trusted beside the API's (term.prized.dev beside api.prized.dev; see
|
|
84
|
+
* trustedEdge); else the default edge, for Prized's own API only. The API key
|
|
85
|
+
* is never sent anywhere else: discovery is routing, not authority.
|
|
67
86
|
*/
|
|
68
87
|
export declare function toolboxBaseFor(config: Config, sandboxId: string, toolboxBase: string | null): string;
|
|
69
88
|
/**
|
|
@@ -88,13 +107,20 @@ export declare function warmEdge(config: Config): Promise<void>;
|
|
|
88
107
|
* Retries network failures, 429/502/503/504, and everything `exec`
|
|
89
108
|
* retries (the edge's 500 control_plane_unavailable among them). A GET
|
|
90
109
|
* retries any 500 too (the API's `internal`).
|
|
91
|
-
* - `exec`: a command, an upload
|
|
92
|
-
*
|
|
110
|
+
* - `exec`: a command, an upload, a remove or a label-selected delete:
|
|
111
|
+
* a second copy could affect later work. Retried only when it
|
|
93
112
|
* provably never arrived: a connection that never opened, or an answer
|
|
94
113
|
* saying so (the edge's rate limit, its lookup failing). A lost answer
|
|
95
114
|
* is never retried: it may already have run.
|
|
115
|
+
* - `staged`: a write's upload to a fresh name beside its target, which only
|
|
116
|
+
* a command after it puts in place (files.ts). Retried as `safe` (a lost
|
|
117
|
+
* answer, a timeout, 429/502/503/504) and `exec` both, a GET's 500 aside:
|
|
118
|
+
* every copy writes the same bytes under the same name, which nothing else
|
|
119
|
+
* writes, so a second one changes no file of the caller's. A 2xx whose body
|
|
120
|
+
* broke off is not sent again: the bytes are there. Its error says whether
|
|
121
|
+
* a copy may have reached the sandbox.
|
|
96
122
|
*/
|
|
97
|
-
export type RetryMode = 'safe' | 'exec';
|
|
123
|
+
export type RetryMode = 'safe' | 'exec' | 'staged';
|
|
98
124
|
/**
|
|
99
125
|
* Whether a fetch that threw never sent its request: the connection was
|
|
100
126
|
* refused, the name did not resolve, or connecting timed out. Anything else
|
|
@@ -123,6 +149,14 @@ export interface RequestSpec {
|
|
|
123
149
|
sandboxId?: string | null;
|
|
124
150
|
/** How to read a 2xx body. Default `json`. */
|
|
125
151
|
read?: 'json' | 'bytes' | 'none';
|
|
152
|
+
/**
|
|
153
|
+
* A file transfer's no-progress bound: an attempt that moves no bytes
|
|
154
|
+
* either way for this long (a connection dropped without a reset) ends as
|
|
155
|
+
* a timeout, sent again by `retry`'s rules while `deadline` allows. An
|
|
156
|
+
* upload's sending is watched only where its body is streamed
|
|
157
|
+
* (Config.streamBodies); its answer always is.
|
|
158
|
+
*/
|
|
159
|
+
stallMs?: number;
|
|
126
160
|
/**
|
|
127
161
|
* Told of every attempt that failed (no answer, an error answer, or a 2xx
|
|
128
162
|
* that could not be read), before it is retried or thrown. A create or
|
|
@@ -137,6 +171,12 @@ export interface RequestSpec {
|
|
|
137
171
|
* its error says so too.
|
|
138
172
|
*/
|
|
139
173
|
effect?: Effect;
|
|
174
|
+
/**
|
|
175
|
+
* An earlier copy of this keyed request may have reached the sandbox (the
|
|
176
|
+
* background start, sent again through an outage), so its error says so
|
|
177
|
+
* too.
|
|
178
|
+
*/
|
|
179
|
+
acted?: boolean;
|
|
140
180
|
}
|
|
141
181
|
export interface HttpResult {
|
|
142
182
|
status: number;
|
|
@@ -199,6 +239,25 @@ export declare class Polls {
|
|
|
199
239
|
}
|
|
200
240
|
/** Delay before retry number `attempt` (0-based): exponential with jitter. */
|
|
201
241
|
export declare function backoffMs(config: Config, attempt: number): number;
|
|
242
|
+
/**
|
|
243
|
+
* How long a file transfer may move no bytes before the attempt is given up
|
|
244
|
+
* (sdk/retry-spec.json transferStallMs; the Python SDK's TRANSFER_STALL).
|
|
245
|
+
* The edge holds a request about 20 s at most before it reads the body (its
|
|
246
|
+
* admission queue, then waking a paused sandbox), and ends a body that brings
|
|
247
|
+
* nothing for 60 s itself: kept above that, a stalled copy is over at the
|
|
248
|
+
* edge before the next one goes to the same staged name, so the two never
|
|
249
|
+
* race. Once an upload's last byte is handed over, its answer is waited for
|
|
250
|
+
* that and a second more for every uploadAnswerBytesPerSecond it sent: what
|
|
251
|
+
* the socket's buffers, or a proxy that takes a whole body before it sends it
|
|
252
|
+
* on, still hold moves unseen, and must not read as a stall (the Python SDK's
|
|
253
|
+
* UPLOAD_ANSWER_RATE). Mutable for tests only.
|
|
254
|
+
*/
|
|
255
|
+
export declare const timing: {
|
|
256
|
+
transferStallMs: number;
|
|
257
|
+
uploadAnswerBytesPerSecond: number;
|
|
258
|
+
};
|
|
259
|
+
/** A streamed upload goes out this many bytes at a time. */
|
|
260
|
+
export declare const UPLOAD_CHUNK: number;
|
|
202
261
|
/**
|
|
203
262
|
* What an error answer (not a 2xx: request() judges those) says about
|
|
204
263
|
* whether the sandbox acted: `never` for the codes that prove the request
|