@blaxel/core 0.2.87-preview.171 → 0.2.87-preview.175
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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/common/h2fetch.js +54 -17
- package/dist/cjs/common/settings.js +29 -3
- package/dist/cjs/common/transient-retry.js +145 -0
- package/dist/cjs/sandbox/drive/drive.js +11 -6
- package/dist/cjs/sandbox/filesystem/filesystem.js +126 -158
- package/dist/cjs/sandbox/process/process.js +29 -16
- package/dist/cjs/types/common/h2fetch.d.ts +7 -0
- package/dist/cjs/types/common/settings.d.ts +22 -2
- package/dist/cjs/types/common/transient-retry.d.ts +30 -0
- package/dist/cjs/types/sandbox/filesystem/filesystem.d.ts +3 -0
- package/dist/cjs-browser/.tsbuildinfo +1 -1
- package/dist/cjs-browser/common/h2fetch.js +1 -0
- package/dist/cjs-browser/common/settings.js +29 -3
- package/dist/cjs-browser/common/transient-retry.js +145 -0
- package/dist/cjs-browser/sandbox/drive/drive.js +11 -6
- package/dist/cjs-browser/sandbox/filesystem/filesystem.js +126 -158
- package/dist/cjs-browser/sandbox/process/process.js +29 -16
- package/dist/cjs-browser/types/common/h2fetch.d.ts +7 -0
- package/dist/cjs-browser/types/common/settings.d.ts +22 -2
- package/dist/cjs-browser/types/common/transient-retry.d.ts +30 -0
- package/dist/cjs-browser/types/sandbox/filesystem/filesystem.d.ts +3 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/common/h2fetch.js +53 -17
- package/dist/esm/common/settings.js +29 -3
- package/dist/esm/common/transient-retry.js +141 -0
- package/dist/esm/sandbox/drive/drive.js +11 -6
- package/dist/esm/sandbox/filesystem/filesystem.js +124 -157
- package/dist/esm/sandbox/process/process.js +29 -16
- package/dist/esm-browser/.tsbuildinfo +1 -1
- package/dist/esm-browser/common/h2fetch.js +1 -0
- package/dist/esm-browser/common/settings.js +29 -3
- package/dist/esm-browser/common/transient-retry.js +141 -0
- package/dist/esm-browser/sandbox/drive/drive.js +11 -6
- package/dist/esm-browser/sandbox/filesystem/filesystem.js +124 -157
- package/dist/esm-browser/sandbox/process/process.js +29 -16
- package/package.json +1 -1
|
@@ -1,98 +1,22 @@
|
|
|
1
1
|
import { fs } from "../../common/node.js";
|
|
2
2
|
import { settings } from "../../common/settings.js";
|
|
3
|
+
import { withUploadSlot } from "../../common/h2fetch.js";
|
|
4
|
+
import { isTransientResetError, retryOnTransientReset } from "../../common/transient-retry.js";
|
|
3
5
|
import { SandboxAction } from "../action.js";
|
|
4
6
|
import { deleteFilesystemByPath, deleteFilesystemMultipartByUploadIdAbort, getFilesystemByPath, getFilesystemContentSearchByPath, getFilesystemFindByPath, getFilesystemSearchByPath, getWatchFilesystemByPath, postFilesystemMultipartByUploadIdComplete, postFilesystemMultipartInitiateByPath, putFilesystemByPath, putFilesystemMultipartByUploadIdPart } from "../client/index.js";
|
|
5
7
|
// Multipart upload constants
|
|
6
8
|
const MULTIPART_THRESHOLD = 5 * 1024 * 1024; // 5MB
|
|
7
9
|
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB per part
|
|
8
10
|
const MAX_PARALLEL_UPLOADS = 3; // Number of parallel part uploads
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
// Deliberately excluded: bare "INTERNAL_ERROR" and "fetch failed". Both are
|
|
20
|
-
// too generic on their own (an application 500 body or any failed fetch would
|
|
21
|
-
// match), so we only treat them as transient when paired with a transport
|
|
22
|
-
// error code on the cause (see isTransientUploadError).
|
|
23
|
-
const TRANSIENT_RESET_MARKERS = [
|
|
24
|
-
"ENHANCE_YOUR_CALM", // H2 flow-control backpressure reset
|
|
25
|
-
"NGHTTP2_INTERNAL_ERROR", // H2 internal stream error (qualified form)
|
|
26
|
-
"ERR_HTTP2", // node http2 error code family
|
|
27
|
-
"GOAWAY", // peer is draining the connection
|
|
28
|
-
"HTTP/2 session closed before response", // thrown by our own h2 transport
|
|
29
|
-
"HTTP/2 session sent GOAWAY before response",
|
|
30
|
-
];
|
|
31
|
-
// Node-level error codes (from `error.code` / `error.cause.code`) that mean
|
|
32
|
-
// the connection itself dropped mid-flight and the request never completed.
|
|
33
|
-
// These are safe to retry for an idempotent part upload.
|
|
34
|
-
const TRANSIENT_ERROR_CODES = new Set([
|
|
35
|
-
"ECONNRESET",
|
|
36
|
-
"ECONNREFUSED",
|
|
37
|
-
"ETIMEDOUT",
|
|
38
|
-
"EPIPE",
|
|
39
|
-
"ERR_HTTP2_STREAM_ERROR",
|
|
40
|
-
"ERR_HTTP2_GOAWAY_SESSION",
|
|
41
|
-
"ERR_HTTP2_SESSION_ERROR",
|
|
42
|
-
]);
|
|
43
|
-
function collectErrorText(error) {
|
|
44
|
-
const messages = [];
|
|
45
|
-
const codes = [];
|
|
46
|
-
// Walk the error -> cause chain (bounded) so a transport error wrapped by a
|
|
47
|
-
// higher-level "fetch failed" is still classified correctly.
|
|
48
|
-
let current = error;
|
|
49
|
-
for (let depth = 0; depth < 5 && current && typeof current === "object"; depth++) {
|
|
50
|
-
const node = current;
|
|
51
|
-
if (typeof node.message === "string")
|
|
52
|
-
messages.push(node.message);
|
|
53
|
-
if (typeof node.code === "string")
|
|
54
|
-
codes.push(node.code);
|
|
55
|
-
current = node.cause;
|
|
56
|
-
}
|
|
57
|
-
return { messages, codes };
|
|
58
|
-
}
|
|
59
|
-
function isTransientUploadError(error) {
|
|
60
|
-
if (!error || typeof error !== "object") {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
const { messages, codes } = collectErrorText(error);
|
|
64
|
-
// 1. An explicit transient transport error code anywhere in the chain.
|
|
65
|
-
if (codes.some((code) => TRANSIENT_ERROR_CODES.has(code))) {
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
// 2. An unambiguous protocol-level reset marker in any message.
|
|
69
|
-
if (messages.some((text) => TRANSIENT_RESET_MARKERS.some((marker) => text.includes(marker)))) {
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
function nextRetryDelayMs(attempt) {
|
|
75
|
-
// Linear backoff (200ms, 400ms, ...) plus up to one extra base delay of
|
|
76
|
-
// random jitter so concurrent part retries do not all fire on the same tick.
|
|
77
|
-
const base = RETRY_BASE_DELAY_MS * attempt;
|
|
78
|
-
const jitter = Math.floor(Math.random() * RETRY_BASE_DELAY_MS);
|
|
79
|
-
return base + jitter;
|
|
80
|
-
}
|
|
81
|
-
async function retryOnTransient(fn) {
|
|
82
|
-
const retries = settings.fsPartRetries; // 0 = off (current default behavior)
|
|
83
|
-
let attempt = 0;
|
|
84
|
-
for (;;) {
|
|
85
|
-
try {
|
|
86
|
-
return await fn();
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
attempt++;
|
|
90
|
-
if (retries <= 0 || attempt > retries || !isTransientUploadError(error)) {
|
|
91
|
-
throw error;
|
|
92
|
-
}
|
|
93
|
-
await new Promise((resolve) => setTimeout(resolve, nextRetryDelayMs(attempt)));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
11
|
+
// The transient-reset classifier and retry loop live in
|
|
12
|
+
// common/transient-retry.ts, shared with the idempotent sandbox-op retry
|
|
13
|
+
// (read/list/etc.) so every path judges "transient" identically. These aliases
|
|
14
|
+
// preserve the import surface the ENG-2680 fault tests already depend on.
|
|
15
|
+
export const isTransientUploadError = isTransientResetError;
|
|
16
|
+
export function retryOnTransient(fn) {
|
|
17
|
+
// Upload path keeps its own (lower) fsPartRetries budget; the shared helper
|
|
18
|
+
// otherwise defaults to the higher idempotent-read budget (sandboxReadRetries).
|
|
19
|
+
return retryOnTransientReset(fn, { retries: settings.fsPartRetries });
|
|
96
20
|
}
|
|
97
21
|
export class SandboxFileSystem extends SandboxAction {
|
|
98
22
|
process;
|
|
@@ -174,28 +98,44 @@ export class SandboxFileSystem extends SandboxAction {
|
|
|
174
98
|
if (fileBlob.size > MULTIPART_THRESHOLD) {
|
|
175
99
|
return await this.uploadWithMultipart(path, fileBlob, "0644");
|
|
176
100
|
}
|
|
177
|
-
// Use regular upload for small files
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// Build URL
|
|
101
|
+
// Use regular upload for small files. Run the PUT under the same upload
|
|
102
|
+
// reliability wrapper as multipart parts: bound concurrency (ENG-2680) and
|
|
103
|
+
// retry transient connection resets (ECONNRESET/GOAWAY/ENHANCE_YOUR_CALM). A
|
|
104
|
+
// PUT of the same bytes to the same path is idempotent, so retry is safe.
|
|
105
|
+
// The FormData is rebuilt per attempt so a retried request has a fresh body.
|
|
183
106
|
let url = `${this.url}/filesystem/${path}`;
|
|
184
107
|
if (this.forcedUrl) {
|
|
185
108
|
url = `${this.forcedUrl.toString()}/filesystem/${path}`;
|
|
186
109
|
}
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
110
|
+
const h2Domain = this.sandbox?.h2Domain;
|
|
111
|
+
const putOnce = async () => {
|
|
112
|
+
const formData = new FormData();
|
|
113
|
+
formData.append("file", fileBlob, "test-binary.bin");
|
|
114
|
+
formData.append("permissions", "0644");
|
|
115
|
+
formData.append("path", path);
|
|
116
|
+
// A forceUrl (session-token) sandbox carries its own headers and must not
|
|
117
|
+
// require global credentials; settings.headers now throws without them
|
|
118
|
+
// (ENG-2698). Mirror streamLogs/execWithStreaming, which already pick
|
|
119
|
+
// sandbox.headers for forceUrl sessions.
|
|
120
|
+
const headers = this.sandbox.forceUrl ? this.sandbox.headers : settings.headers;
|
|
121
|
+
const response = await this.h2Fetch(url, {
|
|
122
|
+
method: 'PUT',
|
|
123
|
+
headers: {
|
|
124
|
+
...headers,
|
|
125
|
+
},
|
|
126
|
+
body: formData,
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
const errorText = await response.text();
|
|
130
|
+
throw new Error(`Failed to write binary: ${response.status} ${errorText}`);
|
|
131
|
+
}
|
|
132
|
+
return await response.json();
|
|
133
|
+
};
|
|
134
|
+
// Acquire the upload slot per-attempt INSIDE the retry so the per-domain cap
|
|
135
|
+
// bounds in-flight streams, not retry sequences: the slot is released during
|
|
136
|
+
// backoff, so a failing PUT never pins a slot while it sleeps (Mendral review).
|
|
137
|
+
const putWithSlot = h2Domain ? () => withUploadSlot(h2Domain, putOnce) : putOnce;
|
|
138
|
+
return retryOnTransient(putWithSlot);
|
|
199
139
|
}
|
|
200
140
|
async writeTree(files, destinationPath = null) {
|
|
201
141
|
const options = {
|
|
@@ -220,30 +160,37 @@ export class SandboxFileSystem extends SandboxAction {
|
|
|
220
160
|
}
|
|
221
161
|
async read(path) {
|
|
222
162
|
path = this.formatPath(path);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
163
|
+
// Idempotent GET: self-heal a transient connection reset (e.g. first call
|
|
164
|
+
// after sandbox resume). Non-transient errors (404, etc.) are not retried.
|
|
165
|
+
return retryOnTransientReset(async () => {
|
|
166
|
+
const { response, data, error } = await getFilesystemByPath(this.withClient({
|
|
167
|
+
path: { path },
|
|
168
|
+
baseUrl: this.url,
|
|
169
|
+
}));
|
|
170
|
+
this.handleResponseError(response, data, error);
|
|
171
|
+
if (data && 'content' in data) {
|
|
172
|
+
return data.content;
|
|
173
|
+
}
|
|
174
|
+
throw new Error("Unsupported file type");
|
|
175
|
+
});
|
|
232
176
|
}
|
|
233
177
|
async readBinary(path) {
|
|
234
178
|
path = this.formatPath(path);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
179
|
+
// Idempotent GET: self-heal a transient connection reset.
|
|
180
|
+
return retryOnTransientReset(async () => {
|
|
181
|
+
const { response, data, error } = await getFilesystemByPath(this.withClient({
|
|
182
|
+
path: { path },
|
|
183
|
+
baseUrl: this.url,
|
|
184
|
+
headers: {
|
|
185
|
+
'Accept': 'application/octet-stream',
|
|
186
|
+
},
|
|
187
|
+
}));
|
|
188
|
+
this.handleResponseError(response, data, error);
|
|
189
|
+
if (typeof data === 'string') {
|
|
190
|
+
return new Blob([data]);
|
|
191
|
+
}
|
|
192
|
+
return data;
|
|
193
|
+
});
|
|
247
194
|
}
|
|
248
195
|
async download(src, destinationPath, { mode = 0o644 } = {}) {
|
|
249
196
|
if (!fs) {
|
|
@@ -266,15 +213,19 @@ export class SandboxFileSystem extends SandboxAction {
|
|
|
266
213
|
}
|
|
267
214
|
async ls(path) {
|
|
268
215
|
path = this.formatPath(path);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
216
|
+
// Idempotent GET: self-heal a transient connection reset (the file-tree
|
|
217
|
+
// refresh that customers hit as intermittent 500s after sandbox resume).
|
|
218
|
+
return retryOnTransientReset(async () => {
|
|
219
|
+
const { response, data, error } = await getFilesystemByPath(this.withClient({
|
|
220
|
+
path: { path },
|
|
221
|
+
baseUrl: this.url,
|
|
222
|
+
}));
|
|
223
|
+
this.handleResponseError(response, data, error);
|
|
224
|
+
if (!data || !('files' in data || 'subdirectories' in data)) {
|
|
225
|
+
throw new Error(JSON.stringify({ error: "Directory not found" }));
|
|
226
|
+
}
|
|
227
|
+
return data;
|
|
228
|
+
});
|
|
278
229
|
}
|
|
279
230
|
async search(query, path = "/", options) {
|
|
280
231
|
const formattedPath = this.formatPath(path);
|
|
@@ -291,13 +242,15 @@ export class SandboxFileSystem extends SandboxAction {
|
|
|
291
242
|
if (options?.excludeHidden !== undefined) {
|
|
292
243
|
queryParams.excludeHidden = options.excludeHidden;
|
|
293
244
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
245
|
+
return retryOnTransientReset(async () => {
|
|
246
|
+
const result = await getFilesystemSearchByPath(this.withClient({
|
|
247
|
+
path: { path: formattedPath },
|
|
248
|
+
query: queryParams,
|
|
249
|
+
baseUrl: this.url,
|
|
250
|
+
}));
|
|
251
|
+
this.handleResponseError(result.response, result.data, result.error);
|
|
252
|
+
return result.data;
|
|
253
|
+
});
|
|
301
254
|
}
|
|
302
255
|
async find(path, options) {
|
|
303
256
|
const formattedPath = this.formatPath(path);
|
|
@@ -317,13 +270,15 @@ export class SandboxFileSystem extends SandboxAction {
|
|
|
317
270
|
if (options?.excludeHidden !== undefined) {
|
|
318
271
|
queryParams.excludeHidden = options.excludeHidden;
|
|
319
272
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
273
|
+
return retryOnTransientReset(async () => {
|
|
274
|
+
const result = await getFilesystemFindByPath(this.withClient({
|
|
275
|
+
path: { path: formattedPath },
|
|
276
|
+
query: queryParams,
|
|
277
|
+
baseUrl: this.url,
|
|
278
|
+
}));
|
|
279
|
+
this.handleResponseError(result.response, result.data, result.error);
|
|
280
|
+
return result.data;
|
|
281
|
+
});
|
|
327
282
|
}
|
|
328
283
|
async grep(query, path = "/", options) {
|
|
329
284
|
const formattedPath = this.formatPath(path);
|
|
@@ -345,13 +300,15 @@ export class SandboxFileSystem extends SandboxAction {
|
|
|
345
300
|
if (options?.excludeDirs && options.excludeDirs.length > 0) {
|
|
346
301
|
queryParams.excludeDirs = options.excludeDirs.join(',');
|
|
347
302
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
303
|
+
return retryOnTransientReset(async () => {
|
|
304
|
+
const result = await getFilesystemContentSearchByPath(this.withClient({
|
|
305
|
+
path: { path: formattedPath },
|
|
306
|
+
query: queryParams,
|
|
307
|
+
baseUrl: this.url,
|
|
308
|
+
}));
|
|
309
|
+
this.handleResponseError(result.response, result.data, result.error);
|
|
310
|
+
return result.data;
|
|
311
|
+
});
|
|
355
312
|
}
|
|
356
313
|
async cp(source, destination, { maxWait = 180000 } = {}) {
|
|
357
314
|
let process = await this.process.exec({
|
|
@@ -503,6 +460,12 @@ export class SandboxFileSystem extends SandboxAction {
|
|
|
503
460
|
const size = blob.size;
|
|
504
461
|
const numParts = Math.ceil(size / CHUNK_SIZE);
|
|
505
462
|
const parts = [];
|
|
463
|
+
// Bound concurrent upload-part streams on the shared H2 connection so many
|
|
464
|
+
// parts (within and across files) cannot burst past the server's
|
|
465
|
+
// rapid-reset limit (ENG-2680). Scoped to uploads; default cap is 2. With
|
|
466
|
+
// no h2Domain the parts go over globalThis.fetch on separate connections,
|
|
467
|
+
// so the shared-connection cap does not apply.
|
|
468
|
+
const h2Domain = this.sandbox?.h2Domain;
|
|
506
469
|
// Upload parts in batches for parallel processing
|
|
507
470
|
for (let i = 0; i < numParts; i += MAX_PARALLEL_UPLOADS) {
|
|
508
471
|
const batch = [];
|
|
@@ -511,7 +474,11 @@ export class SandboxFileSystem extends SandboxAction {
|
|
|
511
474
|
const start = (partNumber - 1) * CHUNK_SIZE;
|
|
512
475
|
const end = Math.min(start + CHUNK_SIZE, size);
|
|
513
476
|
const chunk = blob.slice(start, end);
|
|
514
|
-
|
|
477
|
+
// Slot acquired per-attempt inside the retry (see writeBinary): bounds
|
|
478
|
+
// concurrent part streams, not retry sequences; freed during backoff.
|
|
479
|
+
const doPart = () => this.uploadPart(uploadId, partNumber, chunk);
|
|
480
|
+
const partWithSlot = h2Domain ? () => withUploadSlot(h2Domain, doPart) : doPart;
|
|
481
|
+
batch.push(retryOnTransient(partWithSlot));
|
|
515
482
|
}
|
|
516
483
|
// Wait for batch to complete
|
|
517
484
|
const batchResults = await Promise.all(batch);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { settings } from "../../common/settings.js";
|
|
2
2
|
import { SandboxAction } from "../action.js";
|
|
3
|
+
import { retryOnTransientReset } from "../../common/transient-retry.js";
|
|
3
4
|
import { deleteProcessByIdentifier, deleteProcessByIdentifierKill, getProcess, getProcessByIdentifier, getProcessByIdentifierLogs, postProcess } from "../client/index.js";
|
|
4
5
|
export class SandboxProcess extends SandboxAction {
|
|
5
6
|
constructor(sandbox) {
|
|
@@ -299,19 +300,27 @@ export class SandboxProcess extends SandboxAction {
|
|
|
299
300
|
return data;
|
|
300
301
|
}
|
|
301
302
|
async get(identifier) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
303
|
+
// Idempotent GET: self-heal a transient connection reset (also makes the
|
|
304
|
+
// wait() poll loop resilient). exec() stays un-retried — it is a
|
|
305
|
+
// non-idempotent POST and retrying it duplicates the process (ENG-2340).
|
|
306
|
+
return retryOnTransientReset(async () => {
|
|
307
|
+
const { response, data, error } = await getProcessByIdentifier(this.withClient({
|
|
308
|
+
path: { identifier },
|
|
309
|
+
baseUrl: this.url,
|
|
310
|
+
}));
|
|
311
|
+
this.handleResponseError(response, data, error);
|
|
312
|
+
return data;
|
|
313
|
+
});
|
|
308
314
|
}
|
|
309
315
|
async list() {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
316
|
+
// Idempotent GET: self-heal a transient connection reset.
|
|
317
|
+
return retryOnTransientReset(async () => {
|
|
318
|
+
const { response, data, error } = await getProcess(this.withClient({
|
|
319
|
+
baseUrl: this.url,
|
|
320
|
+
}));
|
|
321
|
+
this.handleResponseError(response, data, error);
|
|
322
|
+
return data;
|
|
323
|
+
});
|
|
315
324
|
}
|
|
316
325
|
async stop(identifier) {
|
|
317
326
|
const { response, data, error } = await deleteProcessByIdentifier(this.withClient({
|
|
@@ -330,11 +339,15 @@ export class SandboxProcess extends SandboxAction {
|
|
|
330
339
|
return data;
|
|
331
340
|
}
|
|
332
341
|
async logs(identifier, type = "all") {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
342
|
+
// Idempotent GET: self-heal a transient connection reset.
|
|
343
|
+
const data = await retryOnTransientReset(async () => {
|
|
344
|
+
const { response, data, error } = await getProcessByIdentifierLogs(this.withClient({
|
|
345
|
+
path: { identifier },
|
|
346
|
+
baseUrl: this.url,
|
|
347
|
+
}));
|
|
348
|
+
this.handleResponseError(response, data, error);
|
|
349
|
+
return data;
|
|
350
|
+
});
|
|
338
351
|
if (type === "all") {
|
|
339
352
|
return data?.logs || "";
|
|
340
353
|
}
|