veryfront 0.1.280 → 0.1.281
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/esm/deno.js +1 -2
- package/esm/src/sandbox/lazy-sandbox.d.ts +17 -0
- package/esm/src/sandbox/lazy-sandbox.d.ts.map +1 -1
- package/esm/src/sandbox/lazy-sandbox.js +107 -9
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +1 -1
- package/src/deno.js +1 -2
- package/src/src/sandbox/lazy-sandbox.ts +132 -10
- package/src/src/utils/version-constant.ts +1 -1
package/esm/deno.js
CHANGED
|
@@ -5,6 +5,13 @@ export interface LazySandboxOptions extends SandboxOptions {
|
|
|
5
5
|
pollIntervalMs?: number;
|
|
6
6
|
heartbeatIntervalMs?: number;
|
|
7
7
|
heartbeatGraceMs?: number;
|
|
8
|
+
execStartTimeoutMs?: number;
|
|
9
|
+
execStartMaxAttempts?: number;
|
|
10
|
+
execStartRetryDelayMs?: number;
|
|
11
|
+
resolveRuntimeEndpoint?: (input: {
|
|
12
|
+
endpoint: string;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
}) => string;
|
|
8
15
|
}
|
|
9
16
|
/** Lazily provisions sandbox sessions and keeps them alive while in use. */
|
|
10
17
|
export declare class LazySandbox {
|
|
@@ -15,6 +22,10 @@ export declare class LazySandbox {
|
|
|
15
22
|
private readonly pollIntervalMs;
|
|
16
23
|
private readonly heartbeatIntervalMs;
|
|
17
24
|
private readonly heartbeatGraceMs;
|
|
25
|
+
private readonly execStartTimeoutMs;
|
|
26
|
+
private readonly execStartMaxAttempts;
|
|
27
|
+
private readonly execStartRetryDelayMs;
|
|
28
|
+
private readonly resolveRuntimeEndpointOption;
|
|
18
29
|
private endpoint;
|
|
19
30
|
private sessionId;
|
|
20
31
|
private sessionProjectId;
|
|
@@ -55,6 +66,12 @@ export declare class LazySandbox {
|
|
|
55
66
|
private resolveExecOptions;
|
|
56
67
|
private resolveCommandJobEndpoint;
|
|
57
68
|
private updateTrackedCommandJob;
|
|
69
|
+
private startExec;
|
|
70
|
+
private fetchExecStart;
|
|
71
|
+
private waitForExecStartRetry;
|
|
72
|
+
private reprovisionAfterExecStartFailure;
|
|
73
|
+
private resolveRuntimeEndpoint;
|
|
74
|
+
private requireSessionId;
|
|
58
75
|
private authHeaders;
|
|
59
76
|
private jsonHeaders;
|
|
60
77
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lazy-sandbox.d.ts","sourceRoot":"","sources":["../../../src/src/sandbox/lazy-sandbox.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,gBAAgB,EAErB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,eAAe,EAGpB,KAAK,cAAc,EAEpB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,kBAAmB,SAAQ,cAAc;IACxD,YAAY,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"lazy-sandbox.d.ts","sourceRoot":"","sources":["../../../src/src/sandbox/lazy-sandbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,15 @@ const DEFAULT_STARTUP_TIMEOUT_MS = 180_000;
|
|
|
5
5
|
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
6
6
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
7
7
|
const DEFAULT_HEARTBEAT_GRACE_MS = 5_000;
|
|
8
|
+
const DEFAULT_EXEC_START_TIMEOUT_MS = 30_000;
|
|
9
|
+
const DEFAULT_EXEC_START_MAX_ATTEMPTS = 3;
|
|
10
|
+
const DEFAULT_EXEC_START_RETRY_DELAY_MS = 1_000;
|
|
11
|
+
const REPROVISIONABLE_EXEC_START_ERROR_CODES = new Set([
|
|
12
|
+
"ECONNREFUSED",
|
|
13
|
+
"ECONNRESET",
|
|
14
|
+
"ENOTFOUND",
|
|
15
|
+
"EHOSTUNREACH",
|
|
16
|
+
]);
|
|
8
17
|
/** Lazily provisions sandbox sessions and keeps them alive while in use. */
|
|
9
18
|
export class LazySandbox {
|
|
10
19
|
apiUrl;
|
|
@@ -14,6 +23,10 @@ export class LazySandbox {
|
|
|
14
23
|
pollIntervalMs;
|
|
15
24
|
heartbeatIntervalMs;
|
|
16
25
|
heartbeatGraceMs;
|
|
26
|
+
execStartTimeoutMs;
|
|
27
|
+
execStartMaxAttempts;
|
|
28
|
+
execStartRetryDelayMs;
|
|
29
|
+
resolveRuntimeEndpointOption;
|
|
17
30
|
endpoint = null;
|
|
18
31
|
sessionId = null;
|
|
19
32
|
sessionProjectId = null;
|
|
@@ -31,6 +44,11 @@ export class LazySandbox {
|
|
|
31
44
|
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
32
45
|
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
33
46
|
this.heartbeatGraceMs = options.heartbeatGraceMs ?? DEFAULT_HEARTBEAT_GRACE_MS;
|
|
47
|
+
this.execStartTimeoutMs = options.execStartTimeoutMs ?? DEFAULT_EXEC_START_TIMEOUT_MS;
|
|
48
|
+
this.execStartMaxAttempts = options.execStartMaxAttempts ?? DEFAULT_EXEC_START_MAX_ATTEMPTS;
|
|
49
|
+
this.execStartRetryDelayMs = options.execStartRetryDelayMs ??
|
|
50
|
+
DEFAULT_EXEC_START_RETRY_DELAY_MS;
|
|
51
|
+
this.resolveRuntimeEndpointOption = options.resolveRuntimeEndpoint;
|
|
34
52
|
}
|
|
35
53
|
async ensure() {
|
|
36
54
|
if (this.endpoint)
|
|
@@ -71,13 +89,17 @@ export class LazySandbox {
|
|
|
71
89
|
}
|
|
72
90
|
async *executeStream(command, options) {
|
|
73
91
|
await this.touchSession();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
let res;
|
|
93
|
+
try {
|
|
94
|
+
res = await this.startExec(command, options);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (!shouldReprovisionAfterExecStartFailure(error)) {
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
await this.reprovisionAfterExecStartFailure();
|
|
101
|
+
await this.touchSession();
|
|
102
|
+
res = await this.startExec(command, options);
|
|
81
103
|
}
|
|
82
104
|
if (!res.body) {
|
|
83
105
|
throw new Error("Exec response has no body");
|
|
@@ -127,7 +149,7 @@ export class LazySandbox {
|
|
|
127
149
|
}
|
|
128
150
|
async startCommandJob(command, options) {
|
|
129
151
|
await this.touchSession();
|
|
130
|
-
const endpoint = this.
|
|
152
|
+
const endpoint = this.resolveRuntimeEndpoint();
|
|
131
153
|
const res = await fetch(`${endpoint}/exec/jobs`, {
|
|
132
154
|
method: "POST",
|
|
133
155
|
headers: this.jsonHeaders(),
|
|
@@ -400,7 +422,7 @@ export class LazySandbox {
|
|
|
400
422
|
return trackedEndpoint;
|
|
401
423
|
}
|
|
402
424
|
await this.ensure();
|
|
403
|
-
return this.
|
|
425
|
+
return this.resolveRuntimeEndpoint();
|
|
404
426
|
}
|
|
405
427
|
updateTrackedCommandJob(job, endpoint) {
|
|
406
428
|
if (job.status === "running") {
|
|
@@ -415,6 +437,65 @@ export class LazySandbox {
|
|
|
415
437
|
this.startHeartbeatLoop();
|
|
416
438
|
}
|
|
417
439
|
}
|
|
440
|
+
async startExec(command, options) {
|
|
441
|
+
const endpoint = this.resolveRuntimeEndpoint();
|
|
442
|
+
const body = JSON.stringify({ command, ...this.resolveExecOptions(options) });
|
|
443
|
+
for (let attempt = 1; attempt <= this.execStartMaxAttempts; attempt += 1) {
|
|
444
|
+
try {
|
|
445
|
+
const res = await this.fetchExecStart(`${endpoint}/exec`, {
|
|
446
|
+
method: "POST",
|
|
447
|
+
headers: this.jsonHeaders(),
|
|
448
|
+
body,
|
|
449
|
+
});
|
|
450
|
+
if (res.ok) {
|
|
451
|
+
return res;
|
|
452
|
+
}
|
|
453
|
+
if (isRetryableExecStartStatus(res.status) && attempt < this.execStartMaxAttempts) {
|
|
454
|
+
await this.waitForExecStartRetry();
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
throw REQUEST_ERROR.create({ detail: `Exec failed: ${res.status} ${await res.text()}` });
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
if (!isRetryableExecStartError(error) || attempt >= this.execStartMaxAttempts) {
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
await this.waitForExecStartRetry();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
throw new Error("Sandbox exec failed before a request was made");
|
|
467
|
+
}
|
|
468
|
+
async fetchExecStart(url, init) {
|
|
469
|
+
const controller = new AbortController();
|
|
470
|
+
const timeout = dntShim.setTimeout(() => controller.abort(), this.execStartTimeoutMs);
|
|
471
|
+
try {
|
|
472
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
473
|
+
}
|
|
474
|
+
finally {
|
|
475
|
+
clearTimeout(timeout);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
waitForExecStartRetry() {
|
|
479
|
+
return new Promise((resolve) => dntShim.setTimeout(resolve, this.execStartRetryDelayMs));
|
|
480
|
+
}
|
|
481
|
+
async reprovisionAfterExecStartFailure() {
|
|
482
|
+
const sessionId = this.sessionId;
|
|
483
|
+
if (!sessionId)
|
|
484
|
+
return;
|
|
485
|
+
await this.deleteSession(sessionId);
|
|
486
|
+
this.resetSessionState(sessionId);
|
|
487
|
+
}
|
|
488
|
+
resolveRuntimeEndpoint() {
|
|
489
|
+
const endpoint = this.requireEndpoint();
|
|
490
|
+
const sessionId = this.requireSessionId();
|
|
491
|
+
return this.resolveRuntimeEndpointOption?.({ endpoint, sessionId }) ?? endpoint;
|
|
492
|
+
}
|
|
493
|
+
requireSessionId() {
|
|
494
|
+
if (!this.sessionId) {
|
|
495
|
+
throw new Error("Sandbox session unavailable");
|
|
496
|
+
}
|
|
497
|
+
return this.sessionId;
|
|
498
|
+
}
|
|
418
499
|
authHeaders() {
|
|
419
500
|
return { Authorization: `Bearer ${this.authToken}` };
|
|
420
501
|
}
|
|
@@ -425,6 +506,23 @@ export class LazySandbox {
|
|
|
425
506
|
};
|
|
426
507
|
}
|
|
427
508
|
}
|
|
509
|
+
function isRetryableExecStartStatus(status) {
|
|
510
|
+
return status === 502 || status === 503 || status === 504;
|
|
511
|
+
}
|
|
512
|
+
function isRetryableExecStartError(error) {
|
|
513
|
+
return error instanceof Error && /fetch failed/i.test(error.message);
|
|
514
|
+
}
|
|
515
|
+
function shouldReprovisionAfterExecStartFailure(error) {
|
|
516
|
+
if (!(error instanceof Error)) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
const cause = error.cause;
|
|
520
|
+
if (typeof cause !== "object" || cause === null || !("code" in cause)) {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
return typeof cause.code === "string" &&
|
|
524
|
+
REPROVISIONABLE_EXEC_START_ERROR_CODES.has(cause.code);
|
|
525
|
+
}
|
|
428
526
|
function mapCommandJob(json) {
|
|
429
527
|
return {
|
|
430
528
|
id: json.id,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.281";
|
|
2
2
|
//# sourceMappingURL=version-constant.d.ts.map
|
package/package.json
CHANGED
package/src/deno.js
CHANGED
|
@@ -19,6 +19,10 @@ export interface LazySandboxOptions extends SandboxOptions {
|
|
|
19
19
|
pollIntervalMs?: number;
|
|
20
20
|
heartbeatIntervalMs?: number;
|
|
21
21
|
heartbeatGraceMs?: number;
|
|
22
|
+
execStartTimeoutMs?: number;
|
|
23
|
+
execStartMaxAttempts?: number;
|
|
24
|
+
execStartRetryDelayMs?: number;
|
|
25
|
+
resolveRuntimeEndpoint?: (input: { endpoint: string; sessionId: string }) => string;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
interface SandboxSessionRecord {
|
|
@@ -31,6 +35,15 @@ const DEFAULT_STARTUP_TIMEOUT_MS = 180_000;
|
|
|
31
35
|
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
32
36
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
33
37
|
const DEFAULT_HEARTBEAT_GRACE_MS = 5_000;
|
|
38
|
+
const DEFAULT_EXEC_START_TIMEOUT_MS = 30_000;
|
|
39
|
+
const DEFAULT_EXEC_START_MAX_ATTEMPTS = 3;
|
|
40
|
+
const DEFAULT_EXEC_START_RETRY_DELAY_MS = 1_000;
|
|
41
|
+
const REPROVISIONABLE_EXEC_START_ERROR_CODES = new Set([
|
|
42
|
+
"ECONNREFUSED",
|
|
43
|
+
"ECONNRESET",
|
|
44
|
+
"ENOTFOUND",
|
|
45
|
+
"EHOSTUNREACH",
|
|
46
|
+
]);
|
|
34
47
|
|
|
35
48
|
/** Lazily provisions sandbox sessions and keeps them alive while in use. */
|
|
36
49
|
export class LazySandbox {
|
|
@@ -41,6 +54,15 @@ export class LazySandbox {
|
|
|
41
54
|
private readonly pollIntervalMs: number;
|
|
42
55
|
private readonly heartbeatIntervalMs: number;
|
|
43
56
|
private readonly heartbeatGraceMs: number;
|
|
57
|
+
private readonly execStartTimeoutMs: number;
|
|
58
|
+
private readonly execStartMaxAttempts: number;
|
|
59
|
+
private readonly execStartRetryDelayMs: number;
|
|
60
|
+
private readonly resolveRuntimeEndpointOption:
|
|
61
|
+
| ((input: {
|
|
62
|
+
endpoint: string;
|
|
63
|
+
sessionId: string;
|
|
64
|
+
}) => string)
|
|
65
|
+
| undefined;
|
|
44
66
|
|
|
45
67
|
private endpoint: string | null = null;
|
|
46
68
|
private sessionId: string | null = null;
|
|
@@ -60,6 +82,11 @@ export class LazySandbox {
|
|
|
60
82
|
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
61
83
|
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
62
84
|
this.heartbeatGraceMs = options.heartbeatGraceMs ?? DEFAULT_HEARTBEAT_GRACE_MS;
|
|
85
|
+
this.execStartTimeoutMs = options.execStartTimeoutMs ?? DEFAULT_EXEC_START_TIMEOUT_MS;
|
|
86
|
+
this.execStartMaxAttempts = options.execStartMaxAttempts ?? DEFAULT_EXEC_START_MAX_ATTEMPTS;
|
|
87
|
+
this.execStartRetryDelayMs = options.execStartRetryDelayMs ??
|
|
88
|
+
DEFAULT_EXEC_START_RETRY_DELAY_MS;
|
|
89
|
+
this.resolveRuntimeEndpointOption = options.resolveRuntimeEndpoint;
|
|
63
90
|
}
|
|
64
91
|
|
|
65
92
|
async ensure(): Promise<void> {
|
|
@@ -105,15 +132,17 @@ export class LazySandbox {
|
|
|
105
132
|
|
|
106
133
|
async *executeStream(command: string, options?: ExecOptions): AsyncGenerator<ExecStreamEvent> {
|
|
107
134
|
await this.touchSession();
|
|
135
|
+
let res: Response;
|
|
136
|
+
try {
|
|
137
|
+
res = await this.startExec(command, options);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (!shouldReprovisionAfterExecStartFailure(error)) {
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
108
142
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
body: JSON.stringify({ command, ...this.resolveExecOptions(options) }),
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
if (!res.ok) {
|
|
116
|
-
throw REQUEST_ERROR.create({ detail: `Exec failed: ${res.status} ${await res.text()}` });
|
|
143
|
+
await this.reprovisionAfterExecStartFailure();
|
|
144
|
+
await this.touchSession();
|
|
145
|
+
res = await this.startExec(command, options);
|
|
117
146
|
}
|
|
118
147
|
|
|
119
148
|
if (!res.body) {
|
|
@@ -175,7 +204,7 @@ export class LazySandbox {
|
|
|
175
204
|
|
|
176
205
|
async startCommandJob(command: string, options?: ExecOptions): Promise<CommandJob> {
|
|
177
206
|
await this.touchSession();
|
|
178
|
-
const endpoint = this.
|
|
207
|
+
const endpoint = this.resolveRuntimeEndpoint();
|
|
179
208
|
|
|
180
209
|
const res = await fetch(`${endpoint}/exec/jobs`, {
|
|
181
210
|
method: "POST",
|
|
@@ -501,7 +530,7 @@ export class LazySandbox {
|
|
|
501
530
|
}
|
|
502
531
|
|
|
503
532
|
await this.ensure();
|
|
504
|
-
return this.
|
|
533
|
+
return this.resolveRuntimeEndpoint();
|
|
505
534
|
}
|
|
506
535
|
|
|
507
536
|
private updateTrackedCommandJob(job: Pick<CommandJob, "id" | "status">, endpoint: string): void {
|
|
@@ -520,6 +549,77 @@ export class LazySandbox {
|
|
|
520
549
|
}
|
|
521
550
|
}
|
|
522
551
|
|
|
552
|
+
private async startExec(command: string, options?: ExecOptions): Promise<Response> {
|
|
553
|
+
const endpoint = this.resolveRuntimeEndpoint();
|
|
554
|
+
const body = JSON.stringify({ command, ...this.resolveExecOptions(options) });
|
|
555
|
+
|
|
556
|
+
for (let attempt = 1; attempt <= this.execStartMaxAttempts; attempt += 1) {
|
|
557
|
+
try {
|
|
558
|
+
const res = await this.fetchExecStart(`${endpoint}/exec`, {
|
|
559
|
+
method: "POST",
|
|
560
|
+
headers: this.jsonHeaders(),
|
|
561
|
+
body,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
if (res.ok) {
|
|
565
|
+
return res;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (isRetryableExecStartStatus(res.status) && attempt < this.execStartMaxAttempts) {
|
|
569
|
+
await this.waitForExecStartRetry();
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
throw REQUEST_ERROR.create({ detail: `Exec failed: ${res.status} ${await res.text()}` });
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (!isRetryableExecStartError(error) || attempt >= this.execStartMaxAttempts) {
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
await this.waitForExecStartRetry();
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
throw new Error("Sandbox exec failed before a request was made");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private async fetchExecStart(url: string, init: RequestInit): Promise<Response> {
|
|
587
|
+
const controller = new AbortController();
|
|
588
|
+
const timeout = dntShim.setTimeout(() => controller.abort(), this.execStartTimeoutMs);
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
592
|
+
} finally {
|
|
593
|
+
clearTimeout(timeout);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private waitForExecStartRetry(): Promise<void> {
|
|
598
|
+
return new Promise((resolve) => dntShim.setTimeout(resolve, this.execStartRetryDelayMs));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private async reprovisionAfterExecStartFailure(): Promise<void> {
|
|
602
|
+
const sessionId = this.sessionId;
|
|
603
|
+
if (!sessionId) return;
|
|
604
|
+
|
|
605
|
+
await this.deleteSession(sessionId);
|
|
606
|
+
this.resetSessionState(sessionId);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private resolveRuntimeEndpoint(): string {
|
|
610
|
+
const endpoint = this.requireEndpoint();
|
|
611
|
+
const sessionId = this.requireSessionId();
|
|
612
|
+
return this.resolveRuntimeEndpointOption?.({ endpoint, sessionId }) ?? endpoint;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private requireSessionId(): string {
|
|
616
|
+
if (!this.sessionId) {
|
|
617
|
+
throw new Error("Sandbox session unavailable");
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return this.sessionId;
|
|
621
|
+
}
|
|
622
|
+
|
|
523
623
|
private authHeaders(): HeadersInit {
|
|
524
624
|
return { Authorization: `Bearer ${this.authToken}` };
|
|
525
625
|
}
|
|
@@ -532,6 +632,28 @@ export class LazySandbox {
|
|
|
532
632
|
}
|
|
533
633
|
}
|
|
534
634
|
|
|
635
|
+
function isRetryableExecStartStatus(status: number): boolean {
|
|
636
|
+
return status === 502 || status === 503 || status === 504;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function isRetryableExecStartError(error: unknown): boolean {
|
|
640
|
+
return error instanceof Error && /fetch failed/i.test(error.message);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function shouldReprovisionAfterExecStartFailure(error: unknown): boolean {
|
|
644
|
+
if (!(error instanceof Error)) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const cause = error.cause;
|
|
649
|
+
if (typeof cause !== "object" || cause === null || !("code" in cause)) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return typeof cause.code === "string" &&
|
|
654
|
+
REPROVISIONABLE_EXEC_START_ERROR_CODES.has(cause.code);
|
|
655
|
+
}
|
|
656
|
+
|
|
535
657
|
function mapCommandJob(json: Record<string, unknown>): CommandJob {
|
|
536
658
|
return {
|
|
537
659
|
id: json.id as string,
|