smolmachines 1.22.2 → 1.24.0
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/binding.d.ts +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/machine.d.ts +7 -2
- package/dist/machine.js +8 -1
- package/dist/native.d.ts +2 -2
- package/dist/transport.d.ts +31 -0
- package/dist/transport.js +158 -16
- package/dist/types.d.ts +13 -2
- package/package.json +4 -4
package/dist/binding.d.ts
CHANGED
|
@@ -265,12 +265,12 @@ export declare class NapiMachine {
|
|
|
265
265
|
* live RAM + disks (same host). `ports` are `{ host, guest }` inbound
|
|
266
266
|
* forwards for the clone. Returns a handle to the running clone.
|
|
267
267
|
*/
|
|
268
|
-
fork(name: string, ports?: Array<PortMappingConfig> | undefined | null, checkpointable?: boolean | undefined | null): Promise<NapiMachine>
|
|
268
|
+
fork(name: string, ports?: Array<PortMappingConfig> | undefined | null, checkpointable?: boolean | undefined | null, freezeSource?: boolean | undefined | null): Promise<NapiMachine>
|
|
269
269
|
/**
|
|
270
270
|
* Fork many clones from one retained snapshot and boot them in bounded
|
|
271
271
|
* parallel waves. Transactional: an error removes every clone in this call.
|
|
272
272
|
*/
|
|
273
|
-
forkBatch(names: Array<string>, ports?: Array<PortMappingConfig> | undefined | null, parallel?: number | undefined | null): Promise<Array<NapiMachine>>
|
|
273
|
+
forkBatch(names: Array<string>, ports?: Array<PortMappingConfig> | undefined | null, parallel?: number | undefined | null, freezeSource?: boolean | undefined | null): Promise<Array<NapiMachine>>
|
|
274
274
|
/** Execute a command directly in the VM (not in a container). */
|
|
275
275
|
exec(command: Array<string>, options?: ExecOptions | undefined | null): Promise<ExecResult>
|
|
276
276
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -20,5 +20,6 @@ export { Machine, Episode } from './machine';
|
|
|
20
20
|
export { RolloutClient, RolloutError, adapterSha256 } from './rollout';
|
|
21
21
|
export type { BuildRolloutJobOptions, EnsureRolloutExecutorOptions, RolloutBatchItem, RolloutClientOptions, RolloutCompletion, RolloutExecutorInfo, RolloutGenerateResponse, RolloutJob, RolloutPolicyInfo, RolloutPrompt, RolloutSampling, } from './rollout';
|
|
22
22
|
export { SmolError, NotSupportedError, InvalidConfigError, ExecutionError } from './errors';
|
|
23
|
+
export type { CloudCheckpointInfo } from './transport';
|
|
23
24
|
export type { LocalAvailability, LocalUnavailableCode } from './availability';
|
|
24
25
|
export type { MachineConfig, NetworkPolicy, RestoreCheckpointOptions, MachineState, ResourceSpec, MountSpec, PortSpec, BranchOptions, BranchBatchOptions, CheckpointOptions, ResizeOptions, MachineResources, ForkOptions, ForkBatchOptions, AssignOptions, ExecOptions, ExecResult, MachineUsageReport, ShareLink, PortableCheckpointInfo, MachineUsageTotals, MachineCostBreakdown, ImageInfo, MachineSummary, ListOptions, ExecEvent, ConnectOptions, EgressInterceptor, StartOptions, WaitReadyOptions, PortEndpoint, } from './types';
|
package/dist/machine.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* a given backend lacks throw `NotSupportedError`.
|
|
9
9
|
*/
|
|
10
10
|
import { type LocalAvailability } from "./availability";
|
|
11
|
-
import { type Transport } from "./transport";
|
|
11
|
+
import { type CloudCheckpointInfo, type Transport } from "./transport";
|
|
12
12
|
import type { MachineResources, ResizeOptions, AssignOptions, BranchBatchOptions, BranchOptions, CheckpointOptions, ConnectOptions, ExecEvent, ExecOptions, ExecResult, ForkBatchOptions, ForkOptions, ImageInfo, ListOptions, MachineConfig, MachineSummary, NetworkPolicy, RestoreCheckpointOptions, StartOptions, MachineUsageReport, ShareLink, PortableCheckpointInfo, PortEndpoint, PortSpec, WaitReadyOptions } from "./types";
|
|
13
13
|
export declare class Machine {
|
|
14
14
|
private readonly transport;
|
|
@@ -48,8 +48,13 @@ export declare class Machine {
|
|
|
48
48
|
* @param options `labels`: keep machines carrying every one of these
|
|
49
49
|
*/
|
|
50
50
|
static list(conn?: ConnectOptions, options?: ListOptions): Promise<MachineSummary[]>;
|
|
51
|
+
/** Upload a `.checkpoint` file taken on this computer to the cloud, so it
|
|
52
|
+
* can be restored there by the returned id. Restoring the file with a cloud
|
|
53
|
+
* target uploads it for you. */
|
|
54
|
+
static uploadCheckpoint(path: string, conn?: ConnectOptions, onProgress?: (sent: number, total: number) => void): Promise<CloudCheckpointInfo>;
|
|
51
55
|
/** Restore a local `.smolcheckpoint` path or durable cloud checkpoint id into
|
|
52
|
-
* a ready, immediately forkable machine.
|
|
56
|
+
* a ready, immediately forkable machine. With a cloud target, a local file is
|
|
57
|
+
* uploaded first, so a machine checkpointed here resumes in the cloud. */
|
|
53
58
|
static restoreCheckpoint(checkpointId: string, name: string, conn?: ConnectOptions, options?: RestoreCheckpointOptions): Promise<Machine>;
|
|
54
59
|
/** Export a local stored checkpoint directory as one portable file. */
|
|
55
60
|
static exportCheckpoint(source: string, output: string): number;
|
package/dist/machine.js
CHANGED
|
@@ -100,8 +100,15 @@ class Machine {
|
|
|
100
100
|
static async list(conn = {}, options = {}) {
|
|
101
101
|
return (0, transport_1.listMachines)(conn, options);
|
|
102
102
|
}
|
|
103
|
+
/** Upload a `.checkpoint` file taken on this computer to the cloud, so it
|
|
104
|
+
* can be restored there by the returned id. Restoring the file with a cloud
|
|
105
|
+
* target uploads it for you. */
|
|
106
|
+
static async uploadCheckpoint(path, conn = {}, onProgress) {
|
|
107
|
+
return (0, transport_1.uploadCheckpoint)(path, conn, onProgress);
|
|
108
|
+
}
|
|
103
109
|
/** Restore a local `.smolcheckpoint` path or durable cloud checkpoint id into
|
|
104
|
-
* a ready, immediately forkable machine.
|
|
110
|
+
* a ready, immediately forkable machine. With a cloud target, a local file is
|
|
111
|
+
* uploaded first, so a machine checkpointed here resumes in the cloud. */
|
|
105
112
|
static async restoreCheckpoint(checkpointId, name, conn, options) {
|
|
106
113
|
return new Machine(await (0, transport_1.restoreCheckpointTransport)(checkpointId, name, conn, options));
|
|
107
114
|
}
|
package/dist/native.d.ts
CHANGED
|
@@ -118,8 +118,8 @@ export interface NapiMachine {
|
|
|
118
118
|
startForkable(): Promise<void>;
|
|
119
119
|
checkpoint(output: string, store?: string): Promise<NativeCheckpointResult>;
|
|
120
120
|
resize(spec: NativeResizeConfig): Promise<NativeMachineResources>;
|
|
121
|
-
fork(name: string, ports?: NativePortMapping[], checkpointable?: boolean): Promise<NapiMachine>;
|
|
122
|
-
forkBatch(names: string[], ports?: NativePortMapping[], parallel?: number): Promise<NapiMachine[]>;
|
|
121
|
+
fork(name: string, ports?: NativePortMapping[], checkpointable?: boolean, freezeSource?: boolean): Promise<NapiMachine>;
|
|
122
|
+
forkBatch(names: string[], ports?: NativePortMapping[], parallel?: number, freezeSource?: boolean): Promise<NapiMachine[]>;
|
|
123
123
|
exec(command: string[], options?: NativeExecOptions): Promise<NativeExecResult>;
|
|
124
124
|
run(image: string, command: string[], options?: NativeExecOptions): Promise<NativeExecResult>;
|
|
125
125
|
pullImage(image: string): Promise<NativeImageInfo>;
|
package/dist/transport.d.ts
CHANGED
|
@@ -90,6 +90,12 @@ export declare function networkMode(network: boolean | undefined): {
|
|
|
90
90
|
mode: "open" | "blocked";
|
|
91
91
|
};
|
|
92
92
|
} | Record<string, never>;
|
|
93
|
+
/** The cloud API's form of a {@link NetworkPolicy}. */
|
|
94
|
+
export declare function cloudNetwork(policy: NetworkPolicy): {
|
|
95
|
+
mode: "open" | "blocked" | "allowCidrs";
|
|
96
|
+
cidrs?: string[];
|
|
97
|
+
hosts?: string[];
|
|
98
|
+
};
|
|
93
99
|
/** The engine's form of a {@link NetworkPolicy}. */
|
|
94
100
|
export declare function egressPolicy(policy: NetworkPolicy): {
|
|
95
101
|
network: boolean;
|
|
@@ -99,6 +105,16 @@ export declare function egressPolicy(policy: NetworkPolicy): {
|
|
|
99
105
|
/** Resolve the primary branch lifecycle name and its compatibility aliases. */
|
|
100
106
|
export declare function resolveBranchable(config: MachineConfig): boolean | undefined;
|
|
101
107
|
export declare function toNativeConfig(name: string, config: MachineConfig): NativeMachineConfig;
|
|
108
|
+
interface CloudConn {
|
|
109
|
+
baseUrl: string;
|
|
110
|
+
apiKey: string;
|
|
111
|
+
}
|
|
112
|
+
/** The sentence a person should read from a failed response's body. The
|
|
113
|
+
* control plane answers errors as RFC 9457 problem+json, whose `detail` is the
|
|
114
|
+
* sentence; older builds sent `{"error": …}` or plain text. A JSON object
|
|
115
|
+
* yields its first string among detail, error, message and title, so a message
|
|
116
|
+
* never pastes JSON; anything else is returned trimmed, as before. */
|
|
117
|
+
export declare function errorSentence(body: string): string;
|
|
102
118
|
/** Percent-encode each path segment but keep the `/` separators — the smolfleet
|
|
103
119
|
* files route is a wildcard (`/files/<path>`), so slashes are meaningful while
|
|
104
120
|
* spaces/?/#/% in a filename must be escaped. */
|
|
@@ -130,3 +146,18 @@ export declare function connectTransport(id: string, conn: ConnectOptions): Prom
|
|
|
130
146
|
/** Restore a durable cloud checkpoint into a new machine, then return only once
|
|
131
147
|
* the restored guest is ready. */
|
|
132
148
|
export declare function restoreCheckpointTransport(checkpointId: string, name: string, conn?: ConnectOptions, options?: RestoreCheckpointOptions): Promise<Transport>;
|
|
149
|
+
/** Upload a checkpoint file to the cloud; see {@link uploadCheckpointFile}. */
|
|
150
|
+
export declare function uploadCheckpoint(path: string, conn?: ConnectOptions, onProgress?: (sent: number, total: number) => void): Promise<CloudCheckpointInfo>;
|
|
151
|
+
/** A checkpoint as the cloud describes it. */
|
|
152
|
+
export interface CloudCheckpointInfo {
|
|
153
|
+
id: string;
|
|
154
|
+
status: string;
|
|
155
|
+
sizeBytes: number;
|
|
156
|
+
arch: string;
|
|
157
|
+
createdAt: string;
|
|
158
|
+
}
|
|
159
|
+
/** Upload a `.checkpoint` file captured on this computer so it can be
|
|
160
|
+
* restored in the cloud. The bytes go straight to object storage in parallel
|
|
161
|
+
* parts; the cloud then reads the checkpoint's own manifest. */
|
|
162
|
+
export declare function uploadCheckpointFile(conn: CloudConn, path: string, onProgress?: (sent: number, total: number) => void): Promise<CloudCheckpointInfo>;
|
|
163
|
+
export {};
|
package/dist/transport.js
CHANGED
|
@@ -11,16 +11,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
11
11
|
exports.cliConfigApiKey = cliConfigApiKey;
|
|
12
12
|
exports.resolveNetwork = resolveNetwork;
|
|
13
13
|
exports.networkMode = networkMode;
|
|
14
|
+
exports.cloudNetwork = cloudNetwork;
|
|
14
15
|
exports.egressPolicy = egressPolicy;
|
|
15
16
|
exports.resolveBranchable = resolveBranchable;
|
|
16
17
|
exports.toNativeConfig = toNativeConfig;
|
|
18
|
+
exports.errorSentence = errorSentence;
|
|
17
19
|
exports.encodePath = encodePath;
|
|
18
20
|
exports.selectsCloud = selectsCloud;
|
|
19
21
|
exports.makeTransport = makeTransport;
|
|
20
22
|
exports.listMachines = listMachines;
|
|
21
23
|
exports.connectTransport = connectTransport;
|
|
22
24
|
exports.restoreCheckpointTransport = restoreCheckpointTransport;
|
|
25
|
+
exports.uploadCheckpoint = uploadCheckpoint;
|
|
26
|
+
exports.uploadCheckpointFile = uploadCheckpointFile;
|
|
23
27
|
const node_fs_1 = require("node:fs");
|
|
28
|
+
const node_http_1 = require("node:http");
|
|
29
|
+
const node_https_1 = require("node:https");
|
|
24
30
|
const node_net_1 = require("node:net");
|
|
25
31
|
const node_os_1 = require("node:os");
|
|
26
32
|
const node_path_1 = require("node:path");
|
|
@@ -176,6 +182,15 @@ function networkMode(network) {
|
|
|
176
182
|
return { network: { mode: "blocked" } };
|
|
177
183
|
return {};
|
|
178
184
|
}
|
|
185
|
+
/** The cloud API's form of a {@link NetworkPolicy}. */
|
|
186
|
+
function cloudNetwork(policy) {
|
|
187
|
+
if (policy === "allow-all")
|
|
188
|
+
return { mode: "open" };
|
|
189
|
+
if (policy === "deny-all")
|
|
190
|
+
return { mode: "blocked" };
|
|
191
|
+
const { cidrs, hosts } = egressPolicy(policy);
|
|
192
|
+
return { mode: "allowCidrs", cidrs, hosts };
|
|
193
|
+
}
|
|
179
194
|
/** The engine's form of a {@link NetworkPolicy}. */
|
|
180
195
|
function egressPolicy(policy) {
|
|
181
196
|
if (policy === "allow-all")
|
|
@@ -585,7 +600,7 @@ class LocalTransport {
|
|
|
585
600
|
}));
|
|
586
601
|
let clone;
|
|
587
602
|
try {
|
|
588
|
-
clone = new LocalTransport(await this.inner.fork(name, nativePorts, opts.branchable ?? opts.checkpointable ?? false), this.handleSignals, undefined, this.handleSignals);
|
|
603
|
+
clone = new LocalTransport(await this.inner.fork(name, nativePorts, opts.branchable ?? opts.checkpointable ?? false, opts.freezeSource ?? false), this.handleSignals, undefined, this.handleSignals);
|
|
589
604
|
await clone.waitUntilReady();
|
|
590
605
|
return clone; // ctor registers for cleanup
|
|
591
606
|
}
|
|
@@ -602,7 +617,7 @@ class LocalTransport {
|
|
|
602
617
|
}));
|
|
603
618
|
const branches = [];
|
|
604
619
|
try {
|
|
605
|
-
const inners = await this.inner.forkBatch(names, nativePorts, Math.min(8, names.length));
|
|
620
|
+
const inners = await this.inner.forkBatch(names, nativePorts, Math.min(8, names.length), opts.freezeSource ?? false);
|
|
606
621
|
branches.push(...inners.map((inner) => new LocalTransport(inner, this.handleSignals, undefined, this.handleSignals)));
|
|
607
622
|
for (const branch of branches) {
|
|
608
623
|
await branch.waitUntilReady();
|
|
@@ -644,6 +659,29 @@ const NO_PORT_READY_PROBE_GRACE_MS = 2_000;
|
|
|
644
659
|
* request timeout (ms), covering network round-trip and server-side overhead so
|
|
645
660
|
* the client never aborts before the server has had a chance to finish. */
|
|
646
661
|
const CLOUD_EXEC_TIMEOUT_HEADROOM_MS = 30_000;
|
|
662
|
+
/** The sentence a person should read from a failed response's body. The
|
|
663
|
+
* control plane answers errors as RFC 9457 problem+json, whose `detail` is the
|
|
664
|
+
* sentence; older builds sent `{"error": …}` or plain text. A JSON object
|
|
665
|
+
* yields its first string among detail, error, message and title, so a message
|
|
666
|
+
* never pastes JSON; anything else is returned trimmed, as before. */
|
|
667
|
+
function errorSentence(body) {
|
|
668
|
+
const trimmed = body.trim();
|
|
669
|
+
try {
|
|
670
|
+
const parsed = JSON.parse(trimmed);
|
|
671
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
672
|
+
const fields = parsed;
|
|
673
|
+
for (const key of ["detail", "error", "message", "title"]) {
|
|
674
|
+
const value = fields[key];
|
|
675
|
+
if (typeof value === "string")
|
|
676
|
+
return value.trim();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
// Not JSON: the text is the message.
|
|
682
|
+
}
|
|
683
|
+
return trimmed;
|
|
684
|
+
}
|
|
647
685
|
/** Percent-encode each path segment but keep the `/` separators — the smolfleet
|
|
648
686
|
* files route is a wildcard (`/files/<path>`), so slashes are meaningful while
|
|
649
687
|
* spaces/?/#/% in a filename must be escaped. */
|
|
@@ -691,7 +729,7 @@ async function cloudFetch(conn, method, path, opts = {}) {
|
|
|
691
729
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
692
730
|
}
|
|
693
731
|
if (!res.ok) {
|
|
694
|
-
const text = await res.text().catch(() => "");
|
|
732
|
+
const text = errorSentence(await res.text().catch(() => ""));
|
|
695
733
|
// Surface the server's correlation id (every response carries `x-request-id`)
|
|
696
734
|
// in the error message — clients see the error body but not headers, so
|
|
697
735
|
// without this the id is invisible and support can't correlate the call.
|
|
@@ -940,7 +978,7 @@ class CloudTransport {
|
|
|
940
978
|
throw new errors_1.SmolError("CONNECTION", `cloud exec/stream failed: ${e.message}`);
|
|
941
979
|
}
|
|
942
980
|
if (!res.ok) {
|
|
943
|
-
const text = await res.text().catch(() => "");
|
|
981
|
+
const text = errorSentence(await res.text().catch(() => ""));
|
|
944
982
|
const rid = res.headers.get("x-request-id");
|
|
945
983
|
throw new errors_1.SmolError(res.status === 404
|
|
946
984
|
? "NOT_FOUND"
|
|
@@ -1123,6 +1161,7 @@ class CloudTransport {
|
|
|
1123
1161
|
name,
|
|
1124
1162
|
ports: portBody,
|
|
1125
1163
|
...(branchable ? { branchable: true } : {}),
|
|
1164
|
+
...(opts.freezeSource ? { freezeSource: true } : {}),
|
|
1126
1165
|
},
|
|
1127
1166
|
});
|
|
1128
1167
|
}
|
|
@@ -1136,6 +1175,7 @@ class CloudTransport {
|
|
|
1136
1175
|
name,
|
|
1137
1176
|
ports: portBody,
|
|
1138
1177
|
...(branchable ? { forkable: true } : {}),
|
|
1178
|
+
...(opts.freezeSource ? { freezeSource: true } : {}),
|
|
1139
1179
|
},
|
|
1140
1180
|
});
|
|
1141
1181
|
}
|
|
@@ -1158,6 +1198,8 @@ class CloudTransport {
|
|
|
1158
1198
|
body.count = opts.count;
|
|
1159
1199
|
if (opts.namePrefix !== undefined)
|
|
1160
1200
|
body.namePrefix = opts.namePrefix;
|
|
1201
|
+
if (opts.freezeSource)
|
|
1202
|
+
body.freezeSource = true;
|
|
1161
1203
|
let resp;
|
|
1162
1204
|
try {
|
|
1163
1205
|
resp = await cloudFetch(this.conn, "POST", `/v1/machines/${this.id}/branches/batch`, { json: body });
|
|
@@ -1605,18 +1647,19 @@ async function restoreCheckpointTransport(checkpointId, name, conn, options) {
|
|
|
1605
1647
|
if (options?.keepIdentity) {
|
|
1606
1648
|
throw new errors_1.InvalidConfigError("keepIdentity is local-only: a cloud restore always gives the new machine its own identity.");
|
|
1607
1649
|
}
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
const
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1650
|
+
const cloudConn = cloudConnFor(conn, "restoreCheckpoint");
|
|
1651
|
+
// A checkpoint file taken on this computer is uploaded first.
|
|
1652
|
+
const checkpoint = isCheckpointFile(checkpointId)
|
|
1653
|
+
? (await uploadCheckpointFile(cloudConn, (0, node_path_1.resolve)(checkpointId))).id
|
|
1654
|
+
: checkpointId;
|
|
1655
|
+
const created = await cloudFetch(cloudConn, "POST", `/v1/checkpoints/${encodeURIComponent(checkpoint)}/restore`, {
|
|
1656
|
+
json: {
|
|
1657
|
+
name,
|
|
1658
|
+
...(options?.networkPolicy !== undefined
|
|
1659
|
+
? { network: cloudNetwork(options.networkPolicy) }
|
|
1660
|
+
: {}),
|
|
1661
|
+
},
|
|
1662
|
+
});
|
|
1620
1663
|
const id = created.id;
|
|
1621
1664
|
try {
|
|
1622
1665
|
await cloudFetch(cloudConn, "POST", `/v1/machines/${id}/start`, {
|
|
@@ -1630,6 +1673,105 @@ async function restoreCheckpointTransport(checkpointId, name, conn, options) {
|
|
|
1630
1673
|
}
|
|
1631
1674
|
return new CloudTransport(cloudConn, created.name ?? name, id);
|
|
1632
1675
|
}
|
|
1676
|
+
/** The control plane and key a cloud operation uses: explicit options, then
|
|
1677
|
+
* the environment, then the CLI session. */
|
|
1678
|
+
function cloudConnFor(conn, operation) {
|
|
1679
|
+
const explicitKey = conn.apiKey ?? process.env.SMOL_CLOUD_TOKEN;
|
|
1680
|
+
const { apiKey: cliKey, endpoint: cliUrl } = cliSession(conn.target);
|
|
1681
|
+
const key = explicitKey ?? cliKey ?? cliConfigApiKey();
|
|
1682
|
+
if (!key) {
|
|
1683
|
+
throw new errors_1.InvalidConfigError(`${operation} requires an API key — ${NO_KEY_HINT}.`);
|
|
1684
|
+
}
|
|
1685
|
+
const baseUrl = (conn.baseUrl ??
|
|
1686
|
+
process.env.SMOL_CLOUD_URL ??
|
|
1687
|
+
cliUrl ??
|
|
1688
|
+
DEFAULT_CLOUD_URL).replace(/\/+$/, "");
|
|
1689
|
+
return { baseUrl, apiKey: key };
|
|
1690
|
+
}
|
|
1691
|
+
/** Upload a checkpoint file to the cloud; see {@link uploadCheckpointFile}. */
|
|
1692
|
+
function uploadCheckpoint(path, conn = {}, onProgress) {
|
|
1693
|
+
return uploadCheckpointFile(cloudConnFor(conn, "uploadCheckpoint"), (0, node_path_1.resolve)(path), onProgress);
|
|
1694
|
+
}
|
|
1695
|
+
function isCheckpointFile(checkpoint) {
|
|
1696
|
+
try {
|
|
1697
|
+
return (0, node_fs_1.statSync)(checkpoint).isFile();
|
|
1698
|
+
}
|
|
1699
|
+
catch {
|
|
1700
|
+
return false;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
const UPLOAD_PARALLELISM = 4;
|
|
1704
|
+
const UPLOAD_PART_ATTEMPTS = 4;
|
|
1705
|
+
/** Upload a `.checkpoint` file captured on this computer so it can be
|
|
1706
|
+
* restored in the cloud. The bytes go straight to object storage in parallel
|
|
1707
|
+
* parts; the cloud then reads the checkpoint's own manifest. */
|
|
1708
|
+
async function uploadCheckpointFile(conn, path, onProgress) {
|
|
1709
|
+
const size = (0, node_fs_1.statSync)(path).size;
|
|
1710
|
+
const upload = await cloudFetch(conn, "POST", "/v1/checkpoints", { json: { sizeBytes: size } });
|
|
1711
|
+
const sent = new Array(upload.uploadUrls.length).fill(0);
|
|
1712
|
+
const report = () => onProgress?.(sent.reduce((a, b) => a + b, 0), size);
|
|
1713
|
+
let next = 0;
|
|
1714
|
+
const worker = async () => {
|
|
1715
|
+
for (;;) {
|
|
1716
|
+
const part = next++;
|
|
1717
|
+
if (part >= upload.uploadUrls.length)
|
|
1718
|
+
return;
|
|
1719
|
+
const start = part * upload.partSizeBytes;
|
|
1720
|
+
const length = Math.min(upload.partSizeBytes, size - start);
|
|
1721
|
+
for (let attempt = 0;; attempt++) {
|
|
1722
|
+
try {
|
|
1723
|
+
await putPart(upload.uploadUrls[part], path, start, length, (n) => {
|
|
1724
|
+
sent[part] = n;
|
|
1725
|
+
report();
|
|
1726
|
+
});
|
|
1727
|
+
break;
|
|
1728
|
+
}
|
|
1729
|
+
catch (error) {
|
|
1730
|
+
sent[part] = 0;
|
|
1731
|
+
if (attempt + 1 >= UPLOAD_PART_ATTEMPTS)
|
|
1732
|
+
throw error;
|
|
1733
|
+
await new Promise((r) => setTimeout(r, 2_000 * 2 ** attempt));
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
await Promise.all(Array.from({ length: Math.min(UPLOAD_PARALLELISM, upload.uploadUrls.length) }, worker));
|
|
1739
|
+
return cloudFetch(conn, "POST", `/v1/checkpoints/${encodeURIComponent(upload.checkpoint.id)}/complete`, { timeoutMs: 30 * 60 * 1_000 });
|
|
1740
|
+
}
|
|
1741
|
+
/** PUT one slice of a file to a pre-signed URL. */
|
|
1742
|
+
function putPart(url, path, start, length, onSent) {
|
|
1743
|
+
return new Promise((resolve, reject) => {
|
|
1744
|
+
const send = url.startsWith("http://") ? node_http_1.request : node_https_1.request;
|
|
1745
|
+
const request = send(url, {
|
|
1746
|
+
method: "PUT",
|
|
1747
|
+
headers: { "content-length": String(length) },
|
|
1748
|
+
});
|
|
1749
|
+
request.on("response", (response) => {
|
|
1750
|
+
const chunks = [];
|
|
1751
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
1752
|
+
response.on("end", () => {
|
|
1753
|
+
const status = response.statusCode ?? 0;
|
|
1754
|
+
if (status >= 200 && status < 300)
|
|
1755
|
+
resolve();
|
|
1756
|
+
else
|
|
1757
|
+
reject(new errors_1.SmolError("CONNECTION", `upload part at byte ${start} failed: ${status} ${Buffer.concat(chunks).toString().slice(0, 300)}`));
|
|
1758
|
+
});
|
|
1759
|
+
});
|
|
1760
|
+
request.on("error", reject);
|
|
1761
|
+
if (length === 0) {
|
|
1762
|
+
request.end();
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
let sentBytes = 0;
|
|
1766
|
+
const stream = (0, node_fs_1.createReadStream)(path, { start, end: start + length - 1 });
|
|
1767
|
+
stream.on("data", (chunk) => {
|
|
1768
|
+
sentBytes += chunk.length;
|
|
1769
|
+
onSent(sentBytes);
|
|
1770
|
+
});
|
|
1771
|
+
stream.on("error", reject);
|
|
1772
|
+
stream.pipe(request);
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1633
1775
|
function looksLikeLocalCheckpoint(checkpoint) {
|
|
1634
1776
|
return ((0, node_fs_1.existsSync)(checkpoint) ||
|
|
1635
1777
|
checkpoint.endsWith(".smolcheckpoint") ||
|
package/dist/types.d.ts
CHANGED
|
@@ -88,6 +88,11 @@ export interface BranchOptions {
|
|
|
88
88
|
branchable?: boolean;
|
|
89
89
|
/** @deprecated Use `branchable`. */
|
|
90
90
|
checkpointable?: boolean;
|
|
91
|
+
/** Keep the source paused as a reusable branch base instead of resuming it.
|
|
92
|
+
* Later branches start from that same state, and a frozen source no longer
|
|
93
|
+
* counts toward the concurrency cap. The source cannot run commands again;
|
|
94
|
+
* work in its branches, or stop it. */
|
|
95
|
+
freezeSource?: boolean;
|
|
91
96
|
}
|
|
92
97
|
/** Backwards-compatible name for {@link BranchOptions}. */
|
|
93
98
|
export type ForkOptions = BranchOptions;
|
|
@@ -104,6 +109,11 @@ export interface BranchBatchOptions {
|
|
|
104
109
|
/** Inbound port forwards applied to every child. Empty = each child gets fresh
|
|
105
110
|
* host ports so children don't collide. */
|
|
106
111
|
ports?: PortSpec[];
|
|
112
|
+
/** Keep the source paused as a reusable branch base instead of resuming it.
|
|
113
|
+
* Later branches start from that same state, and a frozen source no longer
|
|
114
|
+
* counts toward the concurrency cap. The source cannot run commands again;
|
|
115
|
+
* work in its branches, or stop it. */
|
|
116
|
+
freezeSource?: boolean;
|
|
107
117
|
}
|
|
108
118
|
/** Backwards-compatible name for {@link BranchBatchOptions}. */
|
|
109
119
|
export type ForkBatchOptions = BranchBatchOptions;
|
|
@@ -264,8 +274,9 @@ export type NetworkPolicy = "allow-all" | "deny-all" | {
|
|
|
264
274
|
/** Options for `Machine.restoreCheckpoint`. */
|
|
265
275
|
export interface RestoreCheckpointOptions {
|
|
266
276
|
/** Apply this network policy to the restored machine before it boots.
|
|
267
|
-
*
|
|
268
|
-
* allow list or `deny-all`.
|
|
277
|
+
* Locally, the checkpoint must have been taken on the `"virtio-net"`
|
|
278
|
+
* backend for an allow list or `deny-all`. A cloud restore without one has
|
|
279
|
+
* networking blocked. */
|
|
269
280
|
networkPolicy?: NetworkPolicy;
|
|
270
281
|
/** Restore the machine as itself going back in time rather than as a clone:
|
|
271
282
|
* it keeps the hostname and machine ID it was saved with, which skips about
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smolmachines",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"description": "Embed isolated microVM sandboxes directly in your code — no server to run.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "smol-machines",
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
"typescript": "^5.6.0"
|
|
63
63
|
},
|
|
64
64
|
"optionalDependencies": {
|
|
65
|
-
"smolmachines-darwin-arm64": "1.
|
|
66
|
-
"smolmachines-linux-x64-gnu": "1.
|
|
67
|
-
"smolmachines-linux-arm64-gnu": "1.
|
|
65
|
+
"smolmachines-darwin-arm64": "1.24.0",
|
|
66
|
+
"smolmachines-linux-x64-gnu": "1.24.0",
|
|
67
|
+
"smolmachines-linux-arm64-gnu": "1.24.0"
|
|
68
68
|
}
|
|
69
69
|
}
|