@aerol-ai/aerolvm-sdk 0.3.2 → 0.4.1
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/MicroVM.d.ts +35 -2
- package/dist/MicroVM.js +37 -0
- package/dist/index.d.ts +2 -1
- package/dist/internal/client.d.ts +47 -3
- package/dist/internal/client.js +144 -9
- package/dist/types.d.ts +78 -2
- package/package.json +1 -1
package/dist/MicroVM.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type APIVersion } from "./internal/client.js";
|
|
1
|
+
import { type APIVersion, type RetryConfig } from "./internal/client.js";
|
|
2
2
|
import { Sandbox } from "./Sandbox.js";
|
|
3
3
|
import { Image } from "./Image.js";
|
|
4
|
-
import type { BuildImageOptions, BuildImageResult, CreateOptions, CreateSessionOptions, ExecStreamHandle, ExecStreamOptions, HealthStatus, IngressTarget, Lifecycle, ListOptions, MountSpecRedacted, RegisterSnapshotOptions, ResizeOptions, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions } from "./types.js";
|
|
4
|
+
import type { BuildImageOptions, BuildImageResult, CreateOptions, CreateSessionOptions, CreateTemplateOptions, ExecStreamHandle, ExecStreamOptions, HealthStatus, IngressTarget, Lifecycle, ListOptions, MountSpecRedacted, RegisterSnapshotOptions, ResizeOptions, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions, Template } from "./types.js";
|
|
5
5
|
type FetchLike = typeof fetch;
|
|
6
6
|
export interface MicroVMConfig {
|
|
7
7
|
patToken?: string;
|
|
@@ -14,6 +14,13 @@ export interface MicroVMConfig {
|
|
|
14
14
|
* other.
|
|
15
15
|
*/
|
|
16
16
|
apiVersion?: APIVersion;
|
|
17
|
+
/**
|
|
18
|
+
* Retry policy for transient transport errors (socket closed, connection
|
|
19
|
+
* reset) and retryable HTTP status codes (429, 502, 503, 504). The SDK
|
|
20
|
+
* retries up to 3 times with exponential backoff by default. Pass
|
|
21
|
+
* `{ maxRetries: 0 }` to disable.
|
|
22
|
+
*/
|
|
23
|
+
retry?: RetryConfig;
|
|
17
24
|
}
|
|
18
25
|
export declare class MicroVM {
|
|
19
26
|
readonly apiUrl: string;
|
|
@@ -51,6 +58,32 @@ export declare class MicroVM {
|
|
|
51
58
|
sessionLog(sandboxID: string, sessionID: string): Promise<Uint8Array>;
|
|
52
59
|
sessionRecording(sandboxID: string, sessionID: string): Promise<Uint8Array>;
|
|
53
60
|
attachSession(sandboxID: string, sessionID: string, options?: SessionAttachOptions): SessionAttachHandle;
|
|
61
|
+
/**
|
|
62
|
+
* Register a Firecracker rootfs template. Returns immediately with a
|
|
63
|
+
* `status: "pending"` row; poll {@link MicroVM.getTemplate} until the
|
|
64
|
+
* status reaches `"ready"` (fast-boot available) or `"ready_no_snapshot"`
|
|
65
|
+
* (cold boot only — see {@link Template.snapshotError}).
|
|
66
|
+
*
|
|
67
|
+
* Idempotent when {@link CreateTemplateOptions.id} is supplied: a
|
|
68
|
+
* duplicate ID returns 409 so a retried CI step does not create two
|
|
69
|
+
* rows for the same logical template.
|
|
70
|
+
*/
|
|
71
|
+
createTemplate(options: CreateTemplateOptions): Promise<Template>;
|
|
72
|
+
listTemplates(): Promise<Template[]>;
|
|
73
|
+
getTemplate(id: string): Promise<Template>;
|
|
74
|
+
deleteTemplate(id: string): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Re-run the snapshot phase against an existing template. Idempotent
|
|
77
|
+
* under concurrent retry: the daemon's CAS collapses N parallel calls
|
|
78
|
+
* for the same ready template into one rebuild kick. Returns the row in
|
|
79
|
+
* its post-transition state (typically `unhealthy`) — poll
|
|
80
|
+
* {@link MicroVM.getTemplate} to observe the transition back to `ready`.
|
|
81
|
+
*
|
|
82
|
+
* Returns 412 (raised as an error) when the template is in a state
|
|
83
|
+
* where rebuild is not safe (build in flight) or not supported
|
|
84
|
+
* (`ready_no_snapshot` / `failed` — those need delete+recreate today).
|
|
85
|
+
*/
|
|
86
|
+
rebuildTemplate(id: string): Promise<Template>;
|
|
54
87
|
/**
|
|
55
88
|
* Cluster-level DNS helpers. Exposed as a namespaced accessor (rather
|
|
56
89
|
* than a flat `ingressDNS()` method) so call sites read like
|
package/dist/MicroVM.js
CHANGED
|
@@ -19,6 +19,7 @@ export class MicroVM {
|
|
|
19
19
|
patToken,
|
|
20
20
|
fetch: config.fetch,
|
|
21
21
|
apiVersion: config.apiVersion,
|
|
22
|
+
retry: config.retry,
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
async create(options) {
|
|
@@ -109,6 +110,42 @@ export class MicroVM {
|
|
|
109
110
|
attachSession(sandboxID, sessionID, options = {}) {
|
|
110
111
|
return this.client.attachSession(sandboxID, sessionID, options);
|
|
111
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Register a Firecracker rootfs template. Returns immediately with a
|
|
115
|
+
* `status: "pending"` row; poll {@link MicroVM.getTemplate} until the
|
|
116
|
+
* status reaches `"ready"` (fast-boot available) or `"ready_no_snapshot"`
|
|
117
|
+
* (cold boot only — see {@link Template.snapshotError}).
|
|
118
|
+
*
|
|
119
|
+
* Idempotent when {@link CreateTemplateOptions.id} is supplied: a
|
|
120
|
+
* duplicate ID returns 409 so a retried CI step does not create two
|
|
121
|
+
* rows for the same logical template.
|
|
122
|
+
*/
|
|
123
|
+
async createTemplate(options) {
|
|
124
|
+
return this.client.createTemplate(options);
|
|
125
|
+
}
|
|
126
|
+
async listTemplates() {
|
|
127
|
+
return this.client.listTemplates();
|
|
128
|
+
}
|
|
129
|
+
async getTemplate(id) {
|
|
130
|
+
return this.client.getTemplate(id);
|
|
131
|
+
}
|
|
132
|
+
async deleteTemplate(id) {
|
|
133
|
+
await this.client.deleteTemplate(id);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Re-run the snapshot phase against an existing template. Idempotent
|
|
137
|
+
* under concurrent retry: the daemon's CAS collapses N parallel calls
|
|
138
|
+
* for the same ready template into one rebuild kick. Returns the row in
|
|
139
|
+
* its post-transition state (typically `unhealthy`) — poll
|
|
140
|
+
* {@link MicroVM.getTemplate} to observe the transition back to `ready`.
|
|
141
|
+
*
|
|
142
|
+
* Returns 412 (raised as an error) when the template is in a state
|
|
143
|
+
* where rebuild is not safe (build in flight) or not supported
|
|
144
|
+
* (`ready_no_snapshot` / `failed` — those need delete+recreate today).
|
|
145
|
+
*/
|
|
146
|
+
async rebuildTemplate(id) {
|
|
147
|
+
return this.client.rebuildTemplate(id);
|
|
148
|
+
}
|
|
112
149
|
/**
|
|
113
150
|
* Cluster-level DNS helpers. Exposed as a namespaced accessor (rather
|
|
114
151
|
* than a flat `ingressDNS()` method) so call sites read like
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export { MicroVM } from "./MicroVM.js";
|
|
|
2
2
|
export { Sandbox } from "./Sandbox.js";
|
|
3
3
|
export { Image } from "./Image.js";
|
|
4
4
|
export type { MicroVMConfig } from "./MicroVM.js";
|
|
5
|
-
export type {
|
|
5
|
+
export type { RetryConfig } from "./internal/client.js";
|
|
6
|
+
export type { AddCustomDomainOptions, BinaryLike, BuildImageOptions, BuildImagePushOptions, BuildImageResult, CreateOptions, CreateSessionOptions, CreateTemplateOptions, CustomDomain, CustomDomainDNSRecords, CustomDomainStatus, DNSRecord, DNSRecordType, ExecRequest, ExecResult, ExecExitInfo, ExposedPort, ExposePortOptions, ExposeProtocol, ExposeResult, Failover, FailoverPolicy, HealthStatus, IngressTarget, IngressTargetSource, Lifecycle, MountSpec, MountSpecRedacted, MountType, RegisterSnapshotOptions, RegistryAuth, ResizeOptions, Sandbox as SandboxData, SandboxSnapshot, SandboxStatus, Session, SessionAttachHandle, SessionAttachOptions, SessionStatus, Template, TemplatePushState, TemplateStatus, UpdateLifecycleOptions, } from "./types.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Image } from "../Image.js";
|
|
2
|
-
import type { BinaryLike, BuildImageOptions, BuildImageResult, CreateOptions, CreateSessionOptions, CustomDomain, CustomDomainDNSRecords, ExecRequest, ExecResult, ExecStreamHandle, ExecStreamOptions, ExposedPort, ExposePortOptions, ExposeResult, Failover, HealthStatus, IngressTarget, Lifecycle, ListOptions, MountSpecRedacted, NetworkUsage, RegisterSnapshotOptions, SetNetworkLimitsOptions, ResizeOptions, Sandbox, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions } from "../types.js";
|
|
2
|
+
import type { AddCustomDomainOptions, BinaryLike, BuildImageOptions, BuildImageResult, CreateOptions, CreateSessionOptions, CreateTemplateOptions, CustomDomain, CustomDomainDNSRecords, ExecRequest, ExecResult, ExecStreamHandle, ExecStreamOptions, ExposedPort, ExposePortOptions, ExposeResult, Failover, HealthStatus, IngressTarget, Lifecycle, ListOptions, MountSpecRedacted, NetworkUsage, RegisterSnapshotOptions, SetNetworkLimitsOptions, ResizeOptions, Sandbox, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions, Template } from "../types.js";
|
|
3
3
|
type FetchLike = typeof fetch;
|
|
4
4
|
/**
|
|
5
5
|
* Wire version of the v1-style sandbox endpoints (`/v1/sandboxes`, etc.) this
|
|
@@ -16,6 +16,27 @@ type FetchLike = typeof fetch;
|
|
|
16
16
|
* string image.
|
|
17
17
|
*/
|
|
18
18
|
export type APIVersion = "v1";
|
|
19
|
+
/**
|
|
20
|
+
* Retry configuration for transient transport and server errors.
|
|
21
|
+
* All fields are optional — the SDK ships sensible defaults.
|
|
22
|
+
*/
|
|
23
|
+
export interface RetryConfig {
|
|
24
|
+
/**
|
|
25
|
+
* Maximum number of retry attempts after the initial request fails.
|
|
26
|
+
* Set to `0` to disable retry entirely. Defaults to `3`.
|
|
27
|
+
*/
|
|
28
|
+
maxRetries?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Initial backoff delay in milliseconds. Subsequent retries double this
|
|
31
|
+
* value (exponential backoff). Defaults to `200`.
|
|
32
|
+
*/
|
|
33
|
+
baseDelayMs?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Hard ceiling on the computed delay in milliseconds. Prevents unbounded
|
|
36
|
+
* waits when `maxRetries` is high. Defaults to `5000`.
|
|
37
|
+
*/
|
|
38
|
+
maxDelayMs?: number;
|
|
39
|
+
}
|
|
19
40
|
export interface APIClientConfig {
|
|
20
41
|
baseURL: string;
|
|
21
42
|
patToken?: string;
|
|
@@ -26,6 +47,12 @@ export interface APIClientConfig {
|
|
|
26
47
|
* a future major SDK release.
|
|
27
48
|
*/
|
|
28
49
|
apiVersion?: APIVersion;
|
|
50
|
+
/**
|
|
51
|
+
* Retry policy for transient transport errors (socket closed, connection
|
|
52
|
+
* reset) and retryable HTTP status codes (429, 502, 503, 504). Pass
|
|
53
|
+
* `{ maxRetries: 0 }` to disable retry entirely.
|
|
54
|
+
*/
|
|
55
|
+
retry?: RetryConfig;
|
|
29
56
|
}
|
|
30
57
|
export declare class APIClient {
|
|
31
58
|
readonly baseURL: string;
|
|
@@ -33,6 +60,7 @@ export declare class APIClient {
|
|
|
33
60
|
private readonly patToken;
|
|
34
61
|
private readonly fetchFn;
|
|
35
62
|
private readonly versionPrefix;
|
|
63
|
+
private readonly retryConfig;
|
|
36
64
|
constructor(config: APIClientConfig);
|
|
37
65
|
/**
|
|
38
66
|
* Build a versioned API path. Pass the suffix beginning with "/" (e.g.
|
|
@@ -99,7 +127,7 @@ export declare class APIClient {
|
|
|
99
127
|
* with an already-registered hostname is idempotent and returns the
|
|
100
128
|
* existing list.
|
|
101
129
|
*/
|
|
102
|
-
addCustomDomain(id: string, hostname: string): Promise<CustomDomain[]>;
|
|
130
|
+
addCustomDomain(id: string, hostname: string, options?: AddCustomDomainOptions): Promise<CustomDomain[]>;
|
|
103
131
|
listCustomDomains(id: string): Promise<CustomDomain[]>;
|
|
104
132
|
removeCustomDomain(id: string, hostname: string): Promise<void>;
|
|
105
133
|
/**
|
|
@@ -118,9 +146,25 @@ export declare class APIClient {
|
|
|
118
146
|
mounts(id: string): Promise<MountSpecRedacted[]>;
|
|
119
147
|
getNetworkUsage(id: string): Promise<NetworkUsage>;
|
|
120
148
|
setNetworkLimits(id: string, options: SetNetworkLimitsOptions): Promise<NetworkUsage>;
|
|
149
|
+
createTemplate(options: CreateTemplateOptions): Promise<Template>;
|
|
150
|
+
listTemplates(): Promise<Template[]>;
|
|
151
|
+
getTemplate(id: string): Promise<Template>;
|
|
152
|
+
deleteTemplate(id: string): Promise<void>;
|
|
153
|
+
rebuildTemplate(id: string): Promise<Template>;
|
|
121
154
|
private wrap;
|
|
122
155
|
private doJSON;
|
|
123
156
|
private doBytes;
|
|
157
|
+
/**
|
|
158
|
+
* Low-level HTTP request with automatic retry for transient failures.
|
|
159
|
+
*
|
|
160
|
+
* Transport-level errors (socket closed, connection reset, DNS blip) are
|
|
161
|
+
* always retried regardless of HTTP method — the request never reached the
|
|
162
|
+
* server so there is no idempotency concern.
|
|
163
|
+
*
|
|
164
|
+
* HTTP-level retryable codes (429, 502, 503, 504) are retried for ALL
|
|
165
|
+
* methods because every mutating endpoint in the daemon is already
|
|
166
|
+
* designed for idempotent retry (e.g. INSERT OR IGNORE + disambiguation).
|
|
167
|
+
*/
|
|
124
168
|
private request;
|
|
125
169
|
}
|
|
126
170
|
export declare class SandboxResource implements Sandbox {
|
|
@@ -175,7 +219,7 @@ export declare class SandboxResource implements Sandbox {
|
|
|
175
219
|
* so the closures always see the current `id` even after `refresh()`.
|
|
176
220
|
*/
|
|
177
221
|
get customDomains(): {
|
|
178
|
-
add(hostname: string): Promise<CustomDomain[]>;
|
|
222
|
+
add(hostname: string, options?: AddCustomDomainOptions): Promise<CustomDomain[]>;
|
|
179
223
|
remove(hostname: string): Promise<void>;
|
|
180
224
|
list(): Promise<CustomDomain[]>;
|
|
181
225
|
dns(): Promise<CustomDomainDNSRecords>;
|
package/dist/internal/client.js
CHANGED
|
@@ -5,18 +5,71 @@ const DEFAULT_API_VERSION = "v1";
|
|
|
5
5
|
const PATH_PREFIXES = {
|
|
6
6
|
v1: V1_PATH_PREFIX,
|
|
7
7
|
};
|
|
8
|
+
/** HTTP status codes the retry loop considers transient. */
|
|
9
|
+
const RETRYABLE_STATUS_CODES = new Set([429, 502, 503, 504]);
|
|
10
|
+
/** Default retry settings when the caller doesn't supply a RetryConfig. */
|
|
11
|
+
const DEFAULT_RETRY = {
|
|
12
|
+
maxRetries: 3,
|
|
13
|
+
baseDelayMs: 200,
|
|
14
|
+
maxDelayMs: 5_000,
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Returns `true` when the thrown `error` looks like a transport-level failure
|
|
18
|
+
* that vanishes on retry (connection reset, socket closed by peer, DNS
|
|
19
|
+
* blip). `undici` and Node core use several `cause.code` values; we check
|
|
20
|
+
* the full cause chain.
|
|
21
|
+
*/
|
|
22
|
+
function isTransientTransportError(error) {
|
|
23
|
+
const codes = new Set([
|
|
24
|
+
"UND_ERR_SOCKET",
|
|
25
|
+
"ECONNRESET",
|
|
26
|
+
"ECONNREFUSED",
|
|
27
|
+
"ETIMEDOUT",
|
|
28
|
+
"EPIPE",
|
|
29
|
+
"EAI_AGAIN",
|
|
30
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
31
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
32
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
33
|
+
]);
|
|
34
|
+
let current = error;
|
|
35
|
+
for (let depth = 0; depth < 5 && current != null; depth++) {
|
|
36
|
+
if (current instanceof Error) {
|
|
37
|
+
const code = current.code;
|
|
38
|
+
if (code && codes.has(code))
|
|
39
|
+
return true;
|
|
40
|
+
if (current.message && /socket hang up|other side closed/i.test(current.message))
|
|
41
|
+
return true;
|
|
42
|
+
current = current.cause;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
/** Sleep with ±25% jitter so concurrent callers don't thundering-herd. */
|
|
51
|
+
function jitteredDelay(baseMs) {
|
|
52
|
+
const jitter = 1 + (Math.random() - 0.5) * 0.5; // 0.75 – 1.25
|
|
53
|
+
return new Promise((resolve) => setTimeout(resolve, Math.round(baseMs * jitter)));
|
|
54
|
+
}
|
|
8
55
|
export class APIClient {
|
|
9
56
|
baseURL;
|
|
10
57
|
apiVersion;
|
|
11
58
|
patToken;
|
|
12
59
|
fetchFn;
|
|
13
60
|
versionPrefix;
|
|
61
|
+
retryConfig;
|
|
14
62
|
constructor(config) {
|
|
15
63
|
this.baseURL = config.baseURL.replace(/\/+$/, "");
|
|
16
64
|
this.patToken = config.patToken ?? "";
|
|
17
65
|
this.fetchFn = config.fetch ?? fetch;
|
|
18
66
|
this.apiVersion = config.apiVersion ?? DEFAULT_API_VERSION;
|
|
19
67
|
this.versionPrefix = PATH_PREFIXES[this.apiVersion];
|
|
68
|
+
this.retryConfig = {
|
|
69
|
+
maxRetries: config.retry?.maxRetries ?? DEFAULT_RETRY.maxRetries,
|
|
70
|
+
baseDelayMs: config.retry?.baseDelayMs ?? DEFAULT_RETRY.baseDelayMs,
|
|
71
|
+
maxDelayMs: config.retry?.maxDelayMs ?? DEFAULT_RETRY.maxDelayMs,
|
|
72
|
+
};
|
|
20
73
|
}
|
|
21
74
|
/**
|
|
22
75
|
* Build a versioned API path. Pass the suffix beginning with "/" (e.g.
|
|
@@ -226,8 +279,12 @@ export class APIClient {
|
|
|
226
279
|
* with an already-registered hostname is idempotent and returns the
|
|
227
280
|
* existing list.
|
|
228
281
|
*/
|
|
229
|
-
async addCustomDomain(id, hostname) {
|
|
230
|
-
const
|
|
282
|
+
async addCustomDomain(id, hostname, options) {
|
|
283
|
+
const body = { hostname };
|
|
284
|
+
if (options?.port !== undefined && options.port !== 0) {
|
|
285
|
+
body.target_port = options.port;
|
|
286
|
+
}
|
|
287
|
+
const response = await this.doJSON("POST", v1SandboxCustomDomainsPath(this.versionPrefix, id), body);
|
|
231
288
|
return response.custom_domains.map(fromApiCustomDomain);
|
|
232
289
|
}
|
|
233
290
|
async listCustomDomains(id) {
|
|
@@ -271,6 +328,31 @@ export class APIClient {
|
|
|
271
328
|
const response = await this.doJSON("PATCH", `${this.versionPrefix}/sandboxes/${id}/network/limits`, toApiSetNetworkLimitsOptions(options));
|
|
272
329
|
return fromApiNetworkUsage(response);
|
|
273
330
|
}
|
|
331
|
+
async createTemplate(options) {
|
|
332
|
+
const response = await this.doJSON("POST", this.versioned("/templates"), {
|
|
333
|
+
id: options.id,
|
|
334
|
+
image: options.image,
|
|
335
|
+
min_size_mib: options.minSizeMiB,
|
|
336
|
+
});
|
|
337
|
+
return fromApiTemplate(response);
|
|
338
|
+
}
|
|
339
|
+
async listTemplates() {
|
|
340
|
+
const response = await this.doJSON("GET", this.versioned("/templates"));
|
|
341
|
+
// The daemon returns `null` rather than `[]` when the table is empty;
|
|
342
|
+
// normalize so callers always get an array.
|
|
343
|
+
return (response ?? []).map(fromApiTemplate);
|
|
344
|
+
}
|
|
345
|
+
async getTemplate(id) {
|
|
346
|
+
const response = await this.doJSON("GET", `${this.versionPrefix}/templates/${id}`);
|
|
347
|
+
return fromApiTemplate(response);
|
|
348
|
+
}
|
|
349
|
+
async deleteTemplate(id) {
|
|
350
|
+
await this.doJSON("DELETE", `${this.versionPrefix}/templates/${id}`);
|
|
351
|
+
}
|
|
352
|
+
async rebuildTemplate(id) {
|
|
353
|
+
const response = await this.doJSON("POST", `${this.versionPrefix}/templates/${id}/rebuild`);
|
|
354
|
+
return fromApiTemplate(response);
|
|
355
|
+
}
|
|
274
356
|
wrap(sandbox) {
|
|
275
357
|
return new SandboxResource(this, fromApiSandbox(sandbox));
|
|
276
358
|
}
|
|
@@ -298,16 +380,49 @@ export class APIClient {
|
|
|
298
380
|
}
|
|
299
381
|
return new Uint8Array(await response.arrayBuffer());
|
|
300
382
|
}
|
|
301
|
-
|
|
383
|
+
/**
|
|
384
|
+
* Low-level HTTP request with automatic retry for transient failures.
|
|
385
|
+
*
|
|
386
|
+
* Transport-level errors (socket closed, connection reset, DNS blip) are
|
|
387
|
+
* always retried regardless of HTTP method — the request never reached the
|
|
388
|
+
* server so there is no idempotency concern.
|
|
389
|
+
*
|
|
390
|
+
* HTTP-level retryable codes (429, 502, 503, 504) are retried for ALL
|
|
391
|
+
* methods because every mutating endpoint in the daemon is already
|
|
392
|
+
* designed for idempotent retry (e.g. INSERT OR IGNORE + disambiguation).
|
|
393
|
+
*/
|
|
394
|
+
async request(method, path, init = {}) {
|
|
302
395
|
const headers = new Headers(init.headers);
|
|
303
396
|
if (this.patToken !== "") {
|
|
304
397
|
headers.set("Authorization", `Bearer ${this.patToken}`);
|
|
305
398
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
399
|
+
const url = `${this.baseURL}${path}`;
|
|
400
|
+
const requestInit = { ...init, method, headers };
|
|
401
|
+
const { maxRetries, baseDelayMs, maxDelayMs } = this.retryConfig;
|
|
402
|
+
let lastError;
|
|
403
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
404
|
+
try {
|
|
405
|
+
const response = await this.fetchFn(url, requestInit);
|
|
406
|
+
// Retry on transient HTTP status codes.
|
|
407
|
+
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < maxRetries) {
|
|
408
|
+
const delay = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
409
|
+
await jitteredDelay(delay);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
return response;
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
lastError = error;
|
|
416
|
+
// Only retry transport-level failures; anything else propagates.
|
|
417
|
+
if (!isTransientTransportError(error) || attempt >= maxRetries) {
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
const delay = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
421
|
+
await jitteredDelay(delay);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Should be unreachable — the loop always throws or returns.
|
|
425
|
+
throw lastError;
|
|
311
426
|
}
|
|
312
427
|
}
|
|
313
428
|
export class SandboxResource {
|
|
@@ -378,7 +493,7 @@ export class SandboxResource {
|
|
|
378
493
|
const client = this.client;
|
|
379
494
|
const id = this.id;
|
|
380
495
|
return {
|
|
381
|
-
add: (hostname) => client.addCustomDomain(id, hostname),
|
|
496
|
+
add: (hostname, options) => client.addCustomDomain(id, hostname, options),
|
|
382
497
|
remove: (hostname) => client.removeCustomDomain(id, hostname),
|
|
383
498
|
list: () => client.listCustomDomains(id),
|
|
384
499
|
dns: () => client.customDomainDNS(id),
|
|
@@ -540,6 +655,25 @@ function fromApiCreateSandboxResponse(response) {
|
|
|
540
655
|
sshPrivateKey: response.ssh_private_key,
|
|
541
656
|
};
|
|
542
657
|
}
|
|
658
|
+
function fromApiTemplate(template) {
|
|
659
|
+
return {
|
|
660
|
+
id: template.id,
|
|
661
|
+
image: template.image,
|
|
662
|
+
status: template.status,
|
|
663
|
+
rootfsSizeBytes: template.rootfs_size_bytes,
|
|
664
|
+
minSizeMiB: template.min_size_mib,
|
|
665
|
+
lastError: template.last_error,
|
|
666
|
+
createdAt: template.created_at,
|
|
667
|
+
updatedAt: template.updated_at,
|
|
668
|
+
readyAt: template.ready_at,
|
|
669
|
+
snapshotSizeBytes: template.snapshot_size_bytes,
|
|
670
|
+
snapshotError: template.snapshot_error,
|
|
671
|
+
hasSnapshot: template.has_snapshot,
|
|
672
|
+
hasOverlay: template.has_overlay,
|
|
673
|
+
pushState: template.push_state,
|
|
674
|
+
pushError: template.push_error,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
543
677
|
function fromApiSandboxSnapshot(snapshot) {
|
|
544
678
|
return {
|
|
545
679
|
name: snapshot.name,
|
|
@@ -580,6 +714,7 @@ function fromApiCustomDomain(domain) {
|
|
|
580
714
|
lastError: domain.last_error,
|
|
581
715
|
createdAt: domain.created_at,
|
|
582
716
|
updatedAt: domain.updated_at,
|
|
717
|
+
targetPort: domain.target_port,
|
|
583
718
|
};
|
|
584
719
|
}
|
|
585
720
|
function fromApiExposedPort(port) {
|
package/dist/types.d.ts
CHANGED
|
@@ -164,7 +164,7 @@ export interface CreateOptions {
|
|
|
164
164
|
*
|
|
165
165
|
* GPU access is not supported with `"gvisor"`.
|
|
166
166
|
*/
|
|
167
|
-
runtime?: "docker" | "gvisor" | "kata";
|
|
167
|
+
runtime?: "docker" | "gvisor" | "kata" | "firecracker";
|
|
168
168
|
/**
|
|
169
169
|
* Attach GPU resources to the sandbox. Omit for CPU-only workloads.
|
|
170
170
|
* Not compatible with runtime="gvisor".
|
|
@@ -240,6 +240,22 @@ export interface CustomDomain {
|
|
|
240
240
|
lastError?: string;
|
|
241
241
|
createdAt: string;
|
|
242
242
|
updatedAt: string;
|
|
243
|
+
/**
|
|
244
|
+
* Target container port traffic to this hostname dials. `0` (or omitted)
|
|
245
|
+
* means the sandbox's toolbox port — the default that preserves the
|
|
246
|
+
* pre-target-port behavior. Set once at attach time; to change it, remove
|
|
247
|
+
* the binding and re-add it with the new port.
|
|
248
|
+
*/
|
|
249
|
+
targetPort?: number;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Options for `sandbox.customDomains.add()`. `port` pins the container port
|
|
253
|
+
* traffic to this hostname dials; omit it to route to the sandbox's toolbox
|
|
254
|
+
* port (the default). Re-adding the same hostname with a different `port`
|
|
255
|
+
* returns 409 — detach first.
|
|
256
|
+
*/
|
|
257
|
+
export interface AddCustomDomainOptions {
|
|
258
|
+
port?: number;
|
|
243
259
|
}
|
|
244
260
|
/**
|
|
245
261
|
* Where the SDK derived the ingress address(es) from. Reflects how the
|
|
@@ -359,7 +375,7 @@ export interface Sandbox {
|
|
|
359
375
|
* Container runtime this sandbox is running under. Empty string indicates
|
|
360
376
|
* a pre-migration row that resolves to the host default at start time.
|
|
361
377
|
*/
|
|
362
|
-
runtime: "" | "docker" | "gvisor" | "kata";
|
|
378
|
+
runtime: "" | "docker" | "gvisor" | "kata" | "firecracker";
|
|
363
379
|
/** GPU configuration this sandbox was created with. Absent means no GPU. */
|
|
364
380
|
gpus?: GPUOptions;
|
|
365
381
|
}
|
|
@@ -454,3 +470,63 @@ export interface HealthStatus {
|
|
|
454
470
|
version: string;
|
|
455
471
|
}
|
|
456
472
|
export type BinaryLike = Uint8Array | ArrayBuffer | Blob | string;
|
|
473
|
+
/**
|
|
474
|
+
* Lifecycle states reported by a Firecracker rootfs template. See
|
|
475
|
+
* `plans/snapshot-clone-fast-boot.md` for the state machine.
|
|
476
|
+
*
|
|
477
|
+
* - `pending`, `building_rootfs`, `snapshotting`: the build pipeline is in
|
|
478
|
+
* flight; no sandbox creation against this template will succeed yet.
|
|
479
|
+
* - `ready`: cold boot + fast snapshot clone both available.
|
|
480
|
+
* - `ready_no_snapshot`: cold boot works; snapshot phase failed and
|
|
481
|
+
* sandboxes will not get the fast-boot path until a rebuild succeeds.
|
|
482
|
+
* - `unhealthy`: snapshot was detected as corrupt; an async rebuild is in
|
|
483
|
+
* flight (or pending the next daemon restart).
|
|
484
|
+
* - `failed`: terminal state from a failed initial build; the row must be
|
|
485
|
+
* deleted and recreated.
|
|
486
|
+
*/
|
|
487
|
+
export type TemplateStatus = "pending" | "building_rootfs" | "snapshotting" | "ready" | "ready_no_snapshot" | "failed" | "unhealthy";
|
|
488
|
+
/**
|
|
489
|
+
* Background-push state for the template's artifacts (rootfs.ext4 +
|
|
490
|
+
* snapshot.*). "active" means push succeeded or push is disabled; "pending"
|
|
491
|
+
* / "pushing" are in-flight states; "error" carries `pushError` for the
|
|
492
|
+
* last failure.
|
|
493
|
+
*/
|
|
494
|
+
export type TemplatePushState = "active" | "pending" | "pushing" | "error";
|
|
495
|
+
/**
|
|
496
|
+
* A Firecracker rootfs template. Created once from an OCI image and
|
|
497
|
+
* boot-shared across many sandboxes. Use {@link MicroVM.createTemplate} to
|
|
498
|
+
* register one, then pass its {@link Template.id} on subsequent sandbox
|
|
499
|
+
* `create` calls (when the sandbox runtime is `firecracker`).
|
|
500
|
+
*/
|
|
501
|
+
export interface Template {
|
|
502
|
+
id: string;
|
|
503
|
+
image: string;
|
|
504
|
+
status: TemplateStatus;
|
|
505
|
+
rootfsSizeBytes?: number;
|
|
506
|
+
/** Optional floor for the ext4 image size, in MiB. */
|
|
507
|
+
minSizeMiB?: number;
|
|
508
|
+
/** Last build-pipeline error, populated when `status` is `failed`. */
|
|
509
|
+
lastError?: string;
|
|
510
|
+
createdAt: string;
|
|
511
|
+
updatedAt: string;
|
|
512
|
+
/** First time the template reached `ready` / `ready_no_snapshot`. */
|
|
513
|
+
readyAt?: string;
|
|
514
|
+
snapshotSizeBytes?: number;
|
|
515
|
+
/** Populated when `status` is `ready_no_snapshot` — why the snapshot phase failed. */
|
|
516
|
+
snapshotError?: string;
|
|
517
|
+
/** True when the template has a usable snapshot for fast-boot. */
|
|
518
|
+
hasSnapshot: boolean;
|
|
519
|
+
/** True when the template was built with a per-sandbox writable overlay placeholder. */
|
|
520
|
+
hasOverlay: boolean;
|
|
521
|
+
pushState?: TemplatePushState;
|
|
522
|
+
/** Populated when `pushState` is `error` — the last push failure. */
|
|
523
|
+
pushError?: string;
|
|
524
|
+
}
|
|
525
|
+
export interface CreateTemplateOptions {
|
|
526
|
+
/** Optional explicit ID. Empty means the daemon generates one. Supplying an explicit ID lets retries be idempotent — a duplicate ID returns 409. */
|
|
527
|
+
id?: string;
|
|
528
|
+
/** skopeo-style image reference, e.g. `"docker://python:3.11"`. */
|
|
529
|
+
image: string;
|
|
530
|
+
/** Optional floor for the ext4 image size, in MiB. */
|
|
531
|
+
minSizeMiB?: number;
|
|
532
|
+
}
|