@watasu/sdk 0.1.25 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -4
- package/dist/codeInterpreter.d.ts +6 -6
- package/dist/codeInterpreter.js +33 -9
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/sandbox.d.ts +31 -3
- package/dist/sandbox.js +75 -4
- package/dist/volume.d.ts +120 -0
- package/dist/volume.js +267 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,23 +31,63 @@ await sbx.betaPause()
|
|
|
31
31
|
await sbx.resume({ timeoutMs: 300_000 })
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
Choose what happens when the sandbox timeout expires:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const sbx = await Sandbox.create({
|
|
38
|
+
lifecycle: { onTimeout: 'pause', autoResume: true },
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`onTimeout: 'kill'` is the default. `onTimeout: 'pause'` keeps the retained
|
|
43
|
+
disk after timeout; `autoResume` lets a later data-plane request resume that
|
|
44
|
+
paused sandbox automatically.
|
|
45
|
+
|
|
46
|
+
Mount a named persistent volume when the sandbox starts:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const sbx = await Sandbox.create({
|
|
50
|
+
volumeMounts: {
|
|
51
|
+
'/workspace/cache': 'cache',
|
|
52
|
+
'/data/models': { name: 'models' },
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Create and edit a persistent volume while it is detached:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { Volume } from '@watasu/sdk'
|
|
61
|
+
|
|
62
|
+
const volume = await Volume.create('cache')
|
|
63
|
+
await volume.makeDir('/workspace')
|
|
64
|
+
await volume.writeFile('/workspace/status.txt', 'ready\n', { mode: '0644' })
|
|
65
|
+
console.log(await volume.readFile('/workspace/status.txt'))
|
|
66
|
+
console.log((await volume.list('/workspace')).map((entry) => entry.path))
|
|
67
|
+
await volume.remove('/workspace/status.txt')
|
|
68
|
+
await volume.destroy()
|
|
69
|
+
```
|
|
70
|
+
|
|
34
71
|
## Code Interpreter
|
|
35
72
|
|
|
36
73
|
```ts
|
|
37
74
|
import { Sandbox } from '@watasu/sdk/code-interpreter'
|
|
38
75
|
|
|
39
76
|
const sbx = await Sandbox.create()
|
|
77
|
+
const context = await sbx.createCodeContext()
|
|
40
78
|
const execution = await sbx.runCode("print('hello')\n2 + 3", {
|
|
41
|
-
|
|
79
|
+
context,
|
|
42
80
|
onStdout: (message) => console.log(message.line),
|
|
43
81
|
})
|
|
44
82
|
|
|
45
83
|
console.log(execution.text)
|
|
84
|
+
await sbx.removeCodeContext(context)
|
|
46
85
|
await sbx.kill()
|
|
47
86
|
```
|
|
48
87
|
|
|
49
|
-
`@watasu/sdk/code-interpreter` starts the `code-interpreter` template by default
|
|
50
|
-
and returns structured `results`, `logs`,
|
|
88
|
+
`@watasu/sdk/code-interpreter` starts the `code-interpreter` template by default.
|
|
89
|
+
Code runs in persistent Python contexts and returns structured `results`, `logs`,
|
|
90
|
+
and `error` fields for each execution.
|
|
51
91
|
|
|
52
92
|
## MCP Gateway
|
|
53
93
|
|
|
@@ -191,7 +231,10 @@ instructions into Watasu's package-spec builder.
|
|
|
191
231
|
import { Sandbox } from '@watasu/sdk'
|
|
192
232
|
|
|
193
233
|
const sbx = await Sandbox.create()
|
|
194
|
-
const metrics = await sbx.getMetrics(
|
|
234
|
+
const metrics = await sbx.getMetrics({
|
|
235
|
+
start: new Date(Date.now() - 5 * 60_000),
|
|
236
|
+
end: new Date(),
|
|
237
|
+
})
|
|
195
238
|
const snapshot = await sbx.createSnapshot({ name: 'ready' })
|
|
196
239
|
const snapshots = await sbx.listSnapshots().nextItems()
|
|
197
240
|
const allSnapshots = await Sandbox.listSnapshots({ limit: 100 }).nextItems()
|
|
@@ -2,7 +2,7 @@ import { Sandbox as BaseSandbox, SandboxConnectOpts, SandboxCreateOpts } from '.
|
|
|
2
2
|
export type RunCodeLanguage = 'python' | 'python3' | string;
|
|
3
3
|
export interface RunCodeOpts {
|
|
4
4
|
language?: RunCodeLanguage;
|
|
5
|
-
context?: Context;
|
|
5
|
+
context?: Context | string;
|
|
6
6
|
onStdout?: (message: OutputMessage) => void;
|
|
7
7
|
onStderr?: (message: OutputMessage) => void;
|
|
8
8
|
onResult?: (result: Result) => void;
|
|
@@ -86,15 +86,15 @@ export declare class Sandbox extends BaseSandbox {
|
|
|
86
86
|
/** Create a persistent code context. */
|
|
87
87
|
createCodeContext(_opts?: CreateCodeContextOpts): Promise<Context>;
|
|
88
88
|
/** Remove a persistent code context. */
|
|
89
|
-
removeCodeContext(
|
|
89
|
+
removeCodeContext(context: Context | string, opts?: {
|
|
90
90
|
requestTimeoutMs?: number;
|
|
91
|
-
}): Promise<
|
|
91
|
+
}): Promise<void>;
|
|
92
92
|
/** List persistent code contexts. */
|
|
93
|
-
listCodeContexts(
|
|
93
|
+
listCodeContexts(opts?: {
|
|
94
94
|
requestTimeoutMs?: number;
|
|
95
95
|
}): Promise<Context[]>;
|
|
96
96
|
/** Restart a persistent code context. */
|
|
97
|
-
restartCodeContext(
|
|
97
|
+
restartCodeContext(context: Context | string, opts?: {
|
|
98
98
|
requestTimeoutMs?: number;
|
|
99
|
-
}): Promise<
|
|
99
|
+
}): Promise<void>;
|
|
100
100
|
}
|
package/dist/codeInterpreter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InvalidArgumentError
|
|
1
|
+
import { InvalidArgumentError } from './errors.js';
|
|
2
2
|
import { Sandbox as BaseSandbox } from './sandbox.js';
|
|
3
3
|
/** One stdout or stderr line emitted by code execution. */
|
|
4
4
|
export class OutputMessage {
|
|
@@ -172,19 +172,33 @@ export class Sandbox extends BaseSandbox {
|
|
|
172
172
|
}
|
|
173
173
|
/** Create a persistent code context. */
|
|
174
174
|
async createCodeContext(_opts = {}) {
|
|
175
|
-
|
|
175
|
+
const response = await this.runtimePostJson('/runtime/v1/code/contexts', compactRecord({
|
|
176
|
+
cwd: _opts.cwd,
|
|
177
|
+
language: _opts.language,
|
|
178
|
+
}), {
|
|
179
|
+
requestTimeoutMs: _opts.requestTimeoutMs,
|
|
180
|
+
});
|
|
181
|
+
return contextFromApi(response);
|
|
176
182
|
}
|
|
177
183
|
/** Remove a persistent code context. */
|
|
178
|
-
async removeCodeContext(
|
|
179
|
-
|
|
184
|
+
async removeCodeContext(context, opts = {}) {
|
|
185
|
+
await this.runtimeDeleteJson(`/runtime/v1/code/contexts/${encodeURIComponent(requireContextId(context))}`, {
|
|
186
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
187
|
+
});
|
|
180
188
|
}
|
|
181
189
|
/** List persistent code contexts. */
|
|
182
|
-
async listCodeContexts(
|
|
183
|
-
|
|
190
|
+
async listCodeContexts(opts = {}) {
|
|
191
|
+
const response = await this.runtimeGetJson('/runtime/v1/code/contexts', {
|
|
192
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
193
|
+
});
|
|
194
|
+
const contexts = Array.isArray(response) ? response : arrayOfUnknown(response.contexts);
|
|
195
|
+
return contexts.map((item) => contextFromApi(record(item)));
|
|
184
196
|
}
|
|
185
197
|
/** Restart a persistent code context. */
|
|
186
|
-
async restartCodeContext(
|
|
187
|
-
|
|
198
|
+
async restartCodeContext(context, opts = {}) {
|
|
199
|
+
await this.runtimePostJson(`/runtime/v1/code/contexts/${encodeURIComponent(requireContextId(context))}/restart`, {}, {
|
|
200
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
201
|
+
});
|
|
188
202
|
}
|
|
189
203
|
}
|
|
190
204
|
function executionFromApi(payload) {
|
|
@@ -219,7 +233,17 @@ function emitCallbacks(execution, opts) {
|
|
|
219
233
|
opts.onError?.(execution.error);
|
|
220
234
|
}
|
|
221
235
|
function contextId(context) {
|
|
222
|
-
|
|
236
|
+
if (context === undefined)
|
|
237
|
+
return undefined;
|
|
238
|
+
return requireContextId(context);
|
|
239
|
+
}
|
|
240
|
+
function requireContextId(context) {
|
|
241
|
+
if (typeof context === 'string')
|
|
242
|
+
return context;
|
|
243
|
+
return context.id;
|
|
244
|
+
}
|
|
245
|
+
function contextFromApi(payload) {
|
|
246
|
+
return new Context(String(payload.id ?? ''), stringValue(payload.language), stringValue(payload.cwd));
|
|
223
247
|
}
|
|
224
248
|
function compactRecord(payload) {
|
|
225
249
|
return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== undefined));
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { ApiError, AuthenticationError, ConflictError, FileNotFoundError, Invali
|
|
|
2
2
|
export { ConnectionConfig, KEEPALIVE_PING_INTERVAL_SEC } from './connectionConfig.js';
|
|
3
3
|
export { Sandbox, SandboxPaginator, SnapshotPaginator } from './sandbox.js';
|
|
4
4
|
export { Sandbox as CodeInterpreterSandbox } from './codeInterpreter.js';
|
|
5
|
-
export type { CreateSnapshotOpts, RestoreSnapshotOpts, SandboxCreateOpts, SandboxConnectOpts, SandboxInfo, SandboxListOpts, SandboxMetrics, McpServer, McpServerName, SandboxNetworkSelector, SandboxNetworkUpdate, SandboxNetworkUpdateOpts, SandboxUrlOpts, SnapshotInfo, FileUrlInfo, } from './sandbox.js';
|
|
5
|
+
export type { CreateSnapshotOpts, RestoreSnapshotOpts, SandboxCreateOpts, SandboxConnectOpts, SandboxInfo, SandboxInfoLifecycle, SandboxLifecycle, SandboxListOpts, SandboxMetrics, SandboxMetricsOpts, McpServer, McpServerName, SandboxNetworkSelector, SandboxNetworkUpdate, SandboxNetworkUpdateOpts, SandboxUrlOpts, SnapshotInfo, FileUrlInfo, } from './sandbox.js';
|
|
6
6
|
export type { CreateCodeContextOpts, RunCodeLanguage, RunCodeOpts, } from './codeInterpreter.js';
|
|
7
7
|
export { Context as CodeInterpreterContext, Execution as CodeInterpreterExecution, ExecutionError as CodeInterpreterExecutionError, OutputMessage as CodeInterpreterOutputMessage, Result as CodeInterpreterResult, } from './codeInterpreter.js';
|
|
8
8
|
export { CommandExitError, CommandHandle, Commands } from './commands.js';
|
|
@@ -17,6 +17,8 @@ export { Pty } from './pty.js';
|
|
|
17
17
|
export type { PtyConnectOpts, PtyCreateOpts, PtySize } from './pty.js';
|
|
18
18
|
export { Terminal, TerminalManager, TerminalOutput } from './terminal.js';
|
|
19
19
|
export type { TerminalOpts } from './terminal.js';
|
|
20
|
+
export { Volume } from './volume.js';
|
|
21
|
+
export type { VolumeApiParams, VolumeConnectionConfig, VolumeEntryStat, VolumeFileType, VolumeInfo, VolumeListFilesOpts, VolumeListOpts, VolumeMetadataOpts, VolumeReadFileOpts, VolumeReadFormat, VolumeWriteData, VolumeWriteFileOpts, } from './volume.js';
|
|
20
22
|
export { ProcessSocket, base64DecodeBytes, base64DecodeText, base64Encode } from './processSocket.js';
|
|
21
23
|
export { ReadyCmd, Template, TemplateBase, waitForFile, waitForPort, waitForProcess, waitForTimeout, waitForURL, waitForUrl, } from './template.js';
|
|
22
24
|
export type { BuildInfo, BuildOptions, BuildStatusReason, CopyItem, GetBuildStatusOptions, LogEntry, ReadyCommand, TemplateBuildStatus, TemplateBuildStatusResponse, TemplateBuilder, TemplateClass, TemplateFactory, TemplateFinal, TemplateFromImage, TemplateOptions, TemplateTag, TemplateTagInfo, } from './template.js';
|
package/dist/index.js
CHANGED
|
@@ -9,5 +9,6 @@ export { FileType, Filesystem, FilesystemWatcher, WatchHandle } from './filesyst
|
|
|
9
9
|
export { Git } from './git.js';
|
|
10
10
|
export { Pty } from './pty.js';
|
|
11
11
|
export { Terminal, TerminalManager, TerminalOutput } from './terminal.js';
|
|
12
|
+
export { Volume } from './volume.js';
|
|
12
13
|
export { ProcessSocket, base64DecodeBytes, base64DecodeText, base64Encode } from './processSocket.js';
|
|
13
14
|
export { ReadyCmd, Template, TemplateBase, waitForFile, waitForPort, waitForProcess, waitForTimeout, waitForURL, waitForUrl, } from './template.js';
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -19,7 +19,16 @@ export interface SandboxCreateOpts extends ConnectionOpts {
|
|
|
19
19
|
team?: string;
|
|
20
20
|
/** MCP gateway configuration to launch inside an `mcp-gateway` sandbox. */
|
|
21
21
|
mcp?: McpServer;
|
|
22
|
-
|
|
22
|
+
/** Timeout lifecycle policy. Defaults to killing the sandbox at timeout. */
|
|
23
|
+
lifecycle?: SandboxLifecycle;
|
|
24
|
+
/** Persistent volumes to mount, keyed by guest path. */
|
|
25
|
+
volumeMounts?: Record<string, string | {
|
|
26
|
+
name: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
export interface SandboxLifecycle {
|
|
30
|
+
onTimeout: 'kill' | 'pause';
|
|
31
|
+
autoResume?: boolean;
|
|
23
32
|
}
|
|
24
33
|
export type SandboxNetworkSelector = string | string[];
|
|
25
34
|
export interface SandboxNetworkUpdate {
|
|
@@ -58,10 +67,19 @@ export interface SandboxInfo {
|
|
|
58
67
|
templateId?: string;
|
|
59
68
|
name?: string;
|
|
60
69
|
state?: string;
|
|
70
|
+
lifecycle?: SandboxInfoLifecycle;
|
|
71
|
+
volumeMounts?: Array<{
|
|
72
|
+
name: string;
|
|
73
|
+
path: string;
|
|
74
|
+
}>;
|
|
61
75
|
metadata: Record<string, string>;
|
|
62
76
|
startedAt?: string;
|
|
63
77
|
endAt?: string;
|
|
64
78
|
}
|
|
79
|
+
export interface SandboxInfoLifecycle {
|
|
80
|
+
onTimeout: 'kill' | 'pause' | string;
|
|
81
|
+
autoResume: boolean;
|
|
82
|
+
}
|
|
65
83
|
export interface SandboxMetrics {
|
|
66
84
|
sandboxId?: string;
|
|
67
85
|
state?: string;
|
|
@@ -71,6 +89,12 @@ export interface SandboxMetrics {
|
|
|
71
89
|
memoryMb?: number;
|
|
72
90
|
raw: Record<string, unknown>;
|
|
73
91
|
}
|
|
92
|
+
export interface SandboxMetricsOpts extends ConnectionOpts {
|
|
93
|
+
/** Start time for the metrics. Defaults to the sandbox start time. */
|
|
94
|
+
start?: Date;
|
|
95
|
+
/** End time for the metrics. Defaults to the current time. */
|
|
96
|
+
end?: Date;
|
|
97
|
+
}
|
|
74
98
|
export interface SnapshotInfo {
|
|
75
99
|
snapshotId: string;
|
|
76
100
|
sandboxId?: string;
|
|
@@ -194,7 +218,7 @@ export declare class Sandbox {
|
|
|
194
218
|
/** Destroy a sandbox by id. */
|
|
195
219
|
static kill(sandboxId: string, opts?: ConnectionOpts | string): Promise<boolean>;
|
|
196
220
|
/** Fetch sandbox metrics by id. */
|
|
197
|
-
static getMetrics(sandboxId: string, opts?:
|
|
221
|
+
static getMetrics(sandboxId: string, opts?: SandboxMetricsOpts): Promise<SandboxMetrics[]>;
|
|
198
222
|
/** Atomically replace a sandbox's network egress policy by id. */
|
|
199
223
|
static updateNetwork(sandboxId: string, network: SandboxNetworkUpdate, opts?: SandboxNetworkUpdateOpts): Promise<void>;
|
|
200
224
|
private static putNetwork;
|
|
@@ -221,7 +245,7 @@ export declare class Sandbox {
|
|
|
221
245
|
/** Fetch the latest control-plane metadata for this sandbox. */
|
|
222
246
|
getInfo(): Promise<SandboxInfo>;
|
|
223
247
|
/** Fetch latest sandbox metrics. */
|
|
224
|
-
getMetrics(opts?:
|
|
248
|
+
getMetrics(opts?: SandboxMetricsOpts): Promise<SandboxMetrics[]>;
|
|
225
249
|
/** Create a Watasu checkpoint using snapshot naming. */
|
|
226
250
|
createSnapshot(opts?: CreateSnapshotOpts): Promise<SnapshotInfo>;
|
|
227
251
|
/** Delete a snapshot by id. */
|
|
@@ -265,5 +289,9 @@ export declare class Sandbox {
|
|
|
265
289
|
private fileUrl;
|
|
266
290
|
/** POST JSON to the sandbox data-plane runtime API. */
|
|
267
291
|
protected runtimePostJson(path: string, json: Record<string, unknown>, opts?: ConnectionOpts): Promise<Record<string, unknown>>;
|
|
292
|
+
/** GET JSON from the sandbox data-plane runtime API. */
|
|
293
|
+
protected runtimeGetJson(path: string, opts?: ConnectionOpts): Promise<Record<string, unknown>>;
|
|
294
|
+
/** DELETE JSON from the sandbox data-plane runtime API. */
|
|
295
|
+
protected runtimeDeleteJson(path: string, opts?: ConnectionOpts): Promise<Record<string, unknown>>;
|
|
268
296
|
private configOptions;
|
|
269
297
|
}
|
package/dist/sandbox.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Commands } from './commands.js';
|
|
2
2
|
import { ConnectionConfig, SESSION_OPERATION_REQUEST_TIMEOUT_MS } from './connectionConfig.js';
|
|
3
|
-
import { DataPlaneClient, ControlClient } from './transport.js';
|
|
3
|
+
import { DataPlaneClient, ControlClient, withQuery } from './transport.js';
|
|
4
4
|
import { ConflictError, FileNotFoundError, NotFoundError, SandboxError, unsupported } from './errors.js';
|
|
5
5
|
import { Filesystem } from './filesystem.js';
|
|
6
6
|
import { Git } from './git.js';
|
|
@@ -123,8 +123,6 @@ export class Sandbox {
|
|
|
123
123
|
const template = typeof templateOrOpts === 'string'
|
|
124
124
|
? templateOrOpts
|
|
125
125
|
: templateOrOpts?.template ?? (sandboxOpts.mcp === undefined ? this.defaultTemplate : undefined);
|
|
126
|
-
if (sandboxOpts.volumeMounts !== undefined)
|
|
127
|
-
unsupported('volumeMounts');
|
|
128
126
|
const config = new ConnectionConfig(sandboxOpts);
|
|
129
127
|
const control = new ControlClient(config);
|
|
130
128
|
const sandboxPayload = {
|
|
@@ -136,6 +134,8 @@ export class Sandbox {
|
|
|
136
134
|
};
|
|
137
135
|
putIfPresent(sandboxPayload, 'template_id', template);
|
|
138
136
|
putIfPresent(sandboxPayload, 'mcp', sandboxOpts.mcp);
|
|
137
|
+
putIfPresent(sandboxPayload, 'lifecycle', lifecyclePayload(sandboxOpts.lifecycle));
|
|
138
|
+
putIfPresent(sandboxPayload, 'volume_mounts', volumeMountsPayload(sandboxOpts.volumeMounts));
|
|
139
139
|
Object.assign(sandboxPayload, networkUpdatePayload(sandboxOpts.network));
|
|
140
140
|
putIfPresent(sandboxPayload, 'team', sandboxOpts.team);
|
|
141
141
|
const response = await control.post('/sandboxes', {
|
|
@@ -229,7 +229,7 @@ export class Sandbox {
|
|
|
229
229
|
/** Fetch sandbox metrics by id. */
|
|
230
230
|
static async getMetrics(sandboxId, opts = {}) {
|
|
231
231
|
const control = new ControlClient(new ConnectionConfig(opts));
|
|
232
|
-
const payload = await control.get(
|
|
232
|
+
const payload = await control.get(metricsPath(sandboxId, opts), {
|
|
233
233
|
requestTimeoutMs: opts.requestTimeoutMs,
|
|
234
234
|
});
|
|
235
235
|
return metricsList(payload.metrics ?? payload);
|
|
@@ -465,6 +465,18 @@ export class Sandbox {
|
|
|
465
465
|
requestTimeoutMs: opts.requestTimeoutMs,
|
|
466
466
|
});
|
|
467
467
|
}
|
|
468
|
+
/** GET JSON from the sandbox data-plane runtime API. */
|
|
469
|
+
async runtimeGetJson(path, opts = {}) {
|
|
470
|
+
return this.dataPlane.getJson(path, {
|
|
471
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/** DELETE JSON from the sandbox data-plane runtime API. */
|
|
475
|
+
async runtimeDeleteJson(path, opts = {}) {
|
|
476
|
+
return this.dataPlane.deleteJson(path, {
|
|
477
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
468
480
|
configOptions() {
|
|
469
481
|
return {
|
|
470
482
|
apiKey: this.config.apiKey,
|
|
@@ -515,6 +527,15 @@ function snapshotListPath(opts, nextToken) {
|
|
|
515
527
|
const query = params.toString();
|
|
516
528
|
return query ? `/sandbox_snapshots?${query}` : '/sandbox_snapshots';
|
|
517
529
|
}
|
|
530
|
+
function metricsPath(sandboxId, opts) {
|
|
531
|
+
return withQuery(`/sandboxes/${sandboxId}/metrics`, {
|
|
532
|
+
start: dateTimestampSeconds(opts.start),
|
|
533
|
+
end: dateTimestampSeconds(opts.end),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
function dateTimestampSeconds(value) {
|
|
537
|
+
return value === undefined ? undefined : Math.round(value.getTime() / 1000);
|
|
538
|
+
}
|
|
518
539
|
function fileUrlInfo(payload) {
|
|
519
540
|
return {
|
|
520
541
|
method: String(payload.method ?? ''),
|
|
@@ -538,6 +559,8 @@ function sandboxInfo(payload) {
|
|
|
538
559
|
templateId: typeof payload.template_id === 'string' ? payload.template_id : templateSlug(payload.template),
|
|
539
560
|
name: typeof payload.name === 'string' ? payload.name : undefined,
|
|
540
561
|
state: typeof payload.state === 'string' ? payload.state : undefined,
|
|
562
|
+
lifecycle: sandboxLifecycleInfo(payload.lifecycle),
|
|
563
|
+
volumeMounts: volumeMountsInfo(payload.volume_mounts ?? payload.volumeMounts),
|
|
541
564
|
metadata: recordOfStrings(payload.metadata),
|
|
542
565
|
startedAt: typeof payload.started_at === 'string'
|
|
543
566
|
? payload.started_at
|
|
@@ -547,6 +570,45 @@ function sandboxInfo(payload) {
|
|
|
547
570
|
: typeof payload.deadline_at === 'string' ? payload.deadline_at : undefined,
|
|
548
571
|
};
|
|
549
572
|
}
|
|
573
|
+
function lifecyclePayload(lifecycle) {
|
|
574
|
+
if (lifecycle === undefined)
|
|
575
|
+
return undefined;
|
|
576
|
+
const onTimeout = lifecycle.onTimeout ?? 'kill';
|
|
577
|
+
const autoResume = lifecycle.autoResume ?? false;
|
|
578
|
+
if (autoResume && onTimeout !== 'pause') {
|
|
579
|
+
throw new SandboxError("lifecycle.autoResume can only be true when lifecycle.onTimeout is 'pause'");
|
|
580
|
+
}
|
|
581
|
+
return { on_timeout: onTimeout, auto_resume: autoResume };
|
|
582
|
+
}
|
|
583
|
+
function volumeMountsPayload(volumeMounts) {
|
|
584
|
+
if (volumeMounts === undefined)
|
|
585
|
+
return undefined;
|
|
586
|
+
return Object.entries(volumeMounts).map(([path, volume]) => ({
|
|
587
|
+
path,
|
|
588
|
+
name: typeof volume === 'string' ? volume : volume.name,
|
|
589
|
+
}));
|
|
590
|
+
}
|
|
591
|
+
function volumeMountsInfo(value) {
|
|
592
|
+
if (!Array.isArray(value))
|
|
593
|
+
return undefined;
|
|
594
|
+
return value
|
|
595
|
+
.map((item) => {
|
|
596
|
+
const entry = record(item);
|
|
597
|
+
return { name: String(entry.name ?? ''), path: String(entry.path ?? '') };
|
|
598
|
+
})
|
|
599
|
+
.filter((entry) => entry.name !== '' && entry.path !== '');
|
|
600
|
+
}
|
|
601
|
+
function sandboxLifecycleInfo(value) {
|
|
602
|
+
const lifecycle = record(value);
|
|
603
|
+
const onTimeout = stringValue(lifecycle.on_timeout ?? lifecycle.onTimeout);
|
|
604
|
+
const autoResume = booleanValue(lifecycle.auto_resume ?? lifecycle.autoResume);
|
|
605
|
+
if (onTimeout === undefined && autoResume === undefined)
|
|
606
|
+
return undefined;
|
|
607
|
+
return {
|
|
608
|
+
onTimeout: onTimeout ?? 'kill',
|
|
609
|
+
autoResume: autoResume ?? false,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
550
612
|
function metricsList(value) {
|
|
551
613
|
if (Array.isArray(value))
|
|
552
614
|
return value.map((item) => metricsInfo(record(item)));
|
|
@@ -596,6 +658,15 @@ function stringValue(value) {
|
|
|
596
658
|
function numberValue(value) {
|
|
597
659
|
return typeof value === 'number' ? value : undefined;
|
|
598
660
|
}
|
|
661
|
+
function booleanValue(value) {
|
|
662
|
+
if (typeof value === 'boolean')
|
|
663
|
+
return value;
|
|
664
|
+
if (value === 'true' || value === '1')
|
|
665
|
+
return true;
|
|
666
|
+
if (value === 'false' || value === '0')
|
|
667
|
+
return false;
|
|
668
|
+
return undefined;
|
|
669
|
+
}
|
|
599
670
|
function templateSlug(value) {
|
|
600
671
|
const template = record(value);
|
|
601
672
|
return typeof template.slug === 'string' ? template.slug : undefined;
|
package/dist/volume.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Blob } from 'node:buffer';
|
|
2
|
+
import { ConnectionConfig, type ConnectionOpts } from './connectionConfig.js';
|
|
3
|
+
import { ControlClient } from './transport.js';
|
|
4
|
+
export type VolumeFileType = 'file' | 'directory' | 'symlink' | string;
|
|
5
|
+
export type VolumeReadFormat = 'text' | 'bytes' | 'blob' | 'stream';
|
|
6
|
+
export type VolumeWriteData = string | Uint8Array | ArrayBuffer | Blob;
|
|
7
|
+
/** Control-plane metadata for a persistent Watasu volume. */
|
|
8
|
+
export interface VolumeInfo {
|
|
9
|
+
volumeId: string;
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
state?: string;
|
|
13
|
+
token?: string;
|
|
14
|
+
sizeMb?: number;
|
|
15
|
+
sizeBytes?: number;
|
|
16
|
+
node?: string;
|
|
17
|
+
metadata: Record<string, string>;
|
|
18
|
+
createdAt?: string;
|
|
19
|
+
updatedAt?: string;
|
|
20
|
+
raw: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
/** File or directory metadata returned by volume content operations. */
|
|
23
|
+
export interface VolumeEntryStat {
|
|
24
|
+
path: string;
|
|
25
|
+
name: string;
|
|
26
|
+
type: VolumeFileType;
|
|
27
|
+
size?: number;
|
|
28
|
+
mode?: number;
|
|
29
|
+
uid?: number;
|
|
30
|
+
gid?: number;
|
|
31
|
+
atime?: unknown;
|
|
32
|
+
mtime?: unknown;
|
|
33
|
+
ctime?: unknown;
|
|
34
|
+
raw: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
export interface VolumeApiParams extends ConnectionOpts {
|
|
37
|
+
team?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface VolumeConnectionConfig extends ConnectionOpts {
|
|
40
|
+
}
|
|
41
|
+
export interface VolumeListOpts extends ConnectionOpts {
|
|
42
|
+
team?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface VolumeListFilesOpts extends ConnectionOpts {
|
|
45
|
+
depth?: number;
|
|
46
|
+
}
|
|
47
|
+
export interface VolumeReadFileOpts extends ConnectionOpts {
|
|
48
|
+
format?: VolumeReadFormat;
|
|
49
|
+
}
|
|
50
|
+
export interface VolumeWriteFileOpts extends ConnectionOpts {
|
|
51
|
+
uid?: number;
|
|
52
|
+
gid?: number;
|
|
53
|
+
mode?: number | string;
|
|
54
|
+
force?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface VolumeMetadataOpts extends ConnectionOpts {
|
|
57
|
+
uid?: number;
|
|
58
|
+
gid?: number;
|
|
59
|
+
mode?: number | string;
|
|
60
|
+
}
|
|
61
|
+
/** Persistent volume that can be mounted into sandboxes and edited while detached. */
|
|
62
|
+
export declare class Volume {
|
|
63
|
+
readonly volumeId: string;
|
|
64
|
+
readonly id: string;
|
|
65
|
+
readonly name: string;
|
|
66
|
+
readonly token?: string;
|
|
67
|
+
private readonly config;
|
|
68
|
+
private readonly control;
|
|
69
|
+
constructor(opts: {
|
|
70
|
+
volumeId: string;
|
|
71
|
+
name?: string;
|
|
72
|
+
token?: string;
|
|
73
|
+
connectionConfig: ConnectionConfig;
|
|
74
|
+
control?: ControlClient;
|
|
75
|
+
});
|
|
76
|
+
/** Create a persistent volume and return a connected SDK object. */
|
|
77
|
+
static create(name: string, opts?: VolumeApiParams): Promise<Volume>;
|
|
78
|
+
/** Connect to an existing volume by id or name. */
|
|
79
|
+
static connect(volumeId: string, opts?: VolumeConnectionConfig): Promise<Volume>;
|
|
80
|
+
/** Fetch metadata for an existing volume by id or name. */
|
|
81
|
+
static getInfo(volumeId: string, opts?: VolumeConnectionConfig): Promise<VolumeInfo>;
|
|
82
|
+
/** List volumes visible to the configured API key. */
|
|
83
|
+
static list(opts?: VolumeListOpts): Promise<VolumeInfo[]>;
|
|
84
|
+
/** Destroy a volume by id or name. Returns false when it does not exist. */
|
|
85
|
+
static destroy(volumeId: string, opts?: VolumeConnectionConfig): Promise<boolean>;
|
|
86
|
+
/** Alias for `destroy`. */
|
|
87
|
+
static delete(volumeId: string, opts?: VolumeConnectionConfig): Promise<boolean>;
|
|
88
|
+
/** Fetch this volume's latest metadata. */
|
|
89
|
+
getInfo(): Promise<VolumeInfo>;
|
|
90
|
+
/** Fetch metadata for a path inside this volume. */
|
|
91
|
+
getInfo(path: string, opts?: ConnectionOpts): Promise<VolumeEntryStat>;
|
|
92
|
+
/** List files and directories under `path`. */
|
|
93
|
+
list(path?: string, opts?: VolumeListFilesOpts): Promise<VolumeEntryStat[]>;
|
|
94
|
+
/** Create a directory inside the detached volume. */
|
|
95
|
+
makeDir(path: string, opts?: VolumeWriteFileOpts): Promise<VolumeEntryStat>;
|
|
96
|
+
/** Return whether a path exists inside the detached volume. */
|
|
97
|
+
exists(path: string, opts?: ConnectionOpts): Promise<boolean>;
|
|
98
|
+
/** Update ownership or mode metadata for a path. */
|
|
99
|
+
updateMetadata(path: string, opts?: VolumeMetadataOpts): Promise<VolumeEntryStat>;
|
|
100
|
+
/** Read a file from the detached volume. */
|
|
101
|
+
readFile(path: string, opts: VolumeReadFileOpts & {
|
|
102
|
+
format: 'bytes';
|
|
103
|
+
}): Promise<Uint8Array>;
|
|
104
|
+
readFile(path: string, opts: VolumeReadFileOpts & {
|
|
105
|
+
format: 'blob';
|
|
106
|
+
}): Promise<Blob>;
|
|
107
|
+
readFile(path: string, opts: VolumeReadFileOpts & {
|
|
108
|
+
format: 'stream';
|
|
109
|
+
}): Promise<ReadableStream<Uint8Array>>;
|
|
110
|
+
readFile(path: string, opts?: VolumeReadFileOpts): Promise<string>;
|
|
111
|
+
/** Write a file into the detached volume. */
|
|
112
|
+
writeFile(path: string, data: VolumeWriteData, opts?: VolumeWriteFileOpts): Promise<VolumeEntryStat>;
|
|
113
|
+
/** Remove a file or directory from the detached volume. */
|
|
114
|
+
remove(path: string, opts?: ConnectionOpts): Promise<boolean>;
|
|
115
|
+
/** Destroy this volume. Returns false when it no longer exists. */
|
|
116
|
+
destroy(opts?: ConnectionOpts): Promise<boolean>;
|
|
117
|
+
/** Alias for `destroy`. */
|
|
118
|
+
delete(opts?: ConnectionOpts): Promise<boolean>;
|
|
119
|
+
private configOptions;
|
|
120
|
+
}
|
package/dist/volume.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { Blob } from 'node:buffer';
|
|
2
|
+
import { ConnectionConfig } from './connectionConfig.js';
|
|
3
|
+
import { NotFoundError, SandboxError } from './errors.js';
|
|
4
|
+
import { base64DecodeBytes, base64DecodeText, base64Encode } from './processSocket.js';
|
|
5
|
+
import { ControlClient, withQuery } from './transport.js';
|
|
6
|
+
/** Persistent volume that can be mounted into sandboxes and edited while detached. */
|
|
7
|
+
export class Volume {
|
|
8
|
+
volumeId;
|
|
9
|
+
id;
|
|
10
|
+
name;
|
|
11
|
+
token;
|
|
12
|
+
config;
|
|
13
|
+
control;
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
this.volumeId = String(opts.volumeId);
|
|
16
|
+
this.id = this.volumeId;
|
|
17
|
+
this.name = opts.name ?? this.volumeId;
|
|
18
|
+
this.token = opts.token;
|
|
19
|
+
this.config = opts.connectionConfig;
|
|
20
|
+
this.control = opts.control ?? new ControlClient(this.config);
|
|
21
|
+
}
|
|
22
|
+
/** Create a persistent volume and return a connected SDK object. */
|
|
23
|
+
static async create(name, opts = {}) {
|
|
24
|
+
const config = new ConnectionConfig(opts);
|
|
25
|
+
const control = new ControlClient(config);
|
|
26
|
+
const payload = await control.post('/volumes', {
|
|
27
|
+
json: compactRecord({ name, team: opts.team }),
|
|
28
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
29
|
+
});
|
|
30
|
+
return volumeFromPayload(payload, config, control);
|
|
31
|
+
}
|
|
32
|
+
/** Connect to an existing volume by id or name. */
|
|
33
|
+
static async connect(volumeId, opts = {}) {
|
|
34
|
+
const config = new ConnectionConfig(opts);
|
|
35
|
+
const control = new ControlClient(config);
|
|
36
|
+
const payload = await control.get(`/volumes/${encodeURIComponent(volumeId)}`, {
|
|
37
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
38
|
+
});
|
|
39
|
+
return volumeFromPayload(payload, config, control);
|
|
40
|
+
}
|
|
41
|
+
/** Fetch metadata for an existing volume by id or name. */
|
|
42
|
+
static async getInfo(volumeId, opts = {}) {
|
|
43
|
+
const config = new ConnectionConfig(opts);
|
|
44
|
+
const control = new ControlClient(config);
|
|
45
|
+
const payload = await control.get(`/volumes/${encodeURIComponent(volumeId)}`, {
|
|
46
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
47
|
+
});
|
|
48
|
+
return volumeInfo(record(payload.volume ?? payload));
|
|
49
|
+
}
|
|
50
|
+
/** List volumes visible to the configured API key. */
|
|
51
|
+
static async list(opts = {}) {
|
|
52
|
+
const config = new ConnectionConfig(opts);
|
|
53
|
+
const control = new ControlClient(config);
|
|
54
|
+
const path = withQuery('/volumes', { team: opts.team });
|
|
55
|
+
const payload = await control.get(path, { requestTimeoutMs: opts.requestTimeoutMs });
|
|
56
|
+
const volumes = Array.isArray(payload.volumes) ? payload.volumes : [];
|
|
57
|
+
return volumes.map((item) => volumeInfo(record(item)));
|
|
58
|
+
}
|
|
59
|
+
/** Destroy a volume by id or name. Returns false when it does not exist. */
|
|
60
|
+
static async destroy(volumeId, opts = {}) {
|
|
61
|
+
const config = new ConnectionConfig(opts);
|
|
62
|
+
const control = new ControlClient(config);
|
|
63
|
+
try {
|
|
64
|
+
await control.delete(`/volumes/${encodeURIComponent(volumeId)}`, {
|
|
65
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
66
|
+
});
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error instanceof NotFoundError)
|
|
71
|
+
return false;
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Alias for `destroy`. */
|
|
76
|
+
static async delete(volumeId, opts = {}) {
|
|
77
|
+
return this.destroy(volumeId, opts);
|
|
78
|
+
}
|
|
79
|
+
async getInfo(path, opts = {}) {
|
|
80
|
+
if (path === undefined) {
|
|
81
|
+
return Volume.getInfo(this.volumeId, this.configOptions(opts));
|
|
82
|
+
}
|
|
83
|
+
const payload = await this.control.get(withQuery(`/volumes/${this.volumeId}/path`, { path }), {
|
|
84
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
85
|
+
});
|
|
86
|
+
return volumeEntry(record(payload.file ?? payload));
|
|
87
|
+
}
|
|
88
|
+
/** List files and directories under `path`. */
|
|
89
|
+
async list(path = '/', opts = {}) {
|
|
90
|
+
const payload = await this.control.get(withQuery(`/volumes/${this.volumeId}/directories`, {
|
|
91
|
+
path,
|
|
92
|
+
depth: opts.depth,
|
|
93
|
+
}), {
|
|
94
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
95
|
+
});
|
|
96
|
+
const entries = Array.isArray(payload.entries) ? payload.entries : [];
|
|
97
|
+
return entries.map((item) => volumeEntry(record(item)));
|
|
98
|
+
}
|
|
99
|
+
/** Create a directory inside the detached volume. */
|
|
100
|
+
async makeDir(path, opts = {}) {
|
|
101
|
+
const payload = await this.control.post(`/volumes/${this.volumeId}/directories`, {
|
|
102
|
+
json: compactRecord({ path, ...metadataPayload(opts) }),
|
|
103
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
104
|
+
});
|
|
105
|
+
return volumeEntry(record(payload.file ?? payload));
|
|
106
|
+
}
|
|
107
|
+
/** Return whether a path exists inside the detached volume. */
|
|
108
|
+
async exists(path, opts = {}) {
|
|
109
|
+
try {
|
|
110
|
+
await this.getInfo(path, opts);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error instanceof NotFoundError)
|
|
115
|
+
return false;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Update ownership or mode metadata for a path. */
|
|
120
|
+
async updateMetadata(path, opts = {}) {
|
|
121
|
+
const payload = await this.control.patch(`/volumes/${this.volumeId}/path`, {
|
|
122
|
+
json: compactRecord({ path, ...metadataPayload(opts) }),
|
|
123
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
124
|
+
});
|
|
125
|
+
return volumeEntry(record(payload.file ?? payload));
|
|
126
|
+
}
|
|
127
|
+
async readFile(path, opts = {}) {
|
|
128
|
+
const payload = await this.control.get(withQuery(`/volumes/${this.volumeId}/files`, { path }), {
|
|
129
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
130
|
+
});
|
|
131
|
+
const file = record(payload.file ?? payload);
|
|
132
|
+
const content = file.content_b64 ?? file.contentBase64 ?? file.content ?? '';
|
|
133
|
+
switch (opts.format ?? 'text') {
|
|
134
|
+
case 'bytes':
|
|
135
|
+
return base64DecodeBytes(content);
|
|
136
|
+
case 'blob':
|
|
137
|
+
return new Blob([base64DecodeBytes(content)]);
|
|
138
|
+
case 'stream':
|
|
139
|
+
return new Blob([base64DecodeBytes(content)]).stream();
|
|
140
|
+
case 'text':
|
|
141
|
+
return file.content_b64 || file.contentBase64 ? base64DecodeText(content) : String(content);
|
|
142
|
+
default:
|
|
143
|
+
throw new SandboxError(`unsupported volume read format: ${String(opts.format)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Write a file into the detached volume. */
|
|
147
|
+
async writeFile(path, data, opts = {}) {
|
|
148
|
+
const bytes = await bytesFromWriteData(data);
|
|
149
|
+
const payload = await this.control.put(`/volumes/${this.volumeId}/files`, {
|
|
150
|
+
json: compactRecord({
|
|
151
|
+
path,
|
|
152
|
+
content_b64: base64Encode(bytes),
|
|
153
|
+
...metadataPayload(opts),
|
|
154
|
+
force: opts.force,
|
|
155
|
+
}),
|
|
156
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
157
|
+
});
|
|
158
|
+
return volumeEntry(record(payload.file ?? payload));
|
|
159
|
+
}
|
|
160
|
+
/** Remove a file or directory from the detached volume. */
|
|
161
|
+
async remove(path, opts = {}) {
|
|
162
|
+
await this.control.delete(withQuery(`/volumes/${this.volumeId}/path`, { path }), {
|
|
163
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
164
|
+
});
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
/** Destroy this volume. Returns false when it no longer exists. */
|
|
168
|
+
async destroy(opts = {}) {
|
|
169
|
+
return Volume.destroy(this.volumeId, this.configOptions(opts));
|
|
170
|
+
}
|
|
171
|
+
/** Alias for `destroy`. */
|
|
172
|
+
async delete(opts = {}) {
|
|
173
|
+
return this.destroy(opts);
|
|
174
|
+
}
|
|
175
|
+
configOptions(opts = {}) {
|
|
176
|
+
return {
|
|
177
|
+
apiKey: this.config.apiKey,
|
|
178
|
+
apiUrl: this.config.apiUrl,
|
|
179
|
+
dataPlaneDomain: this.config.dataPlaneDomain,
|
|
180
|
+
requestTimeoutMs: this.config.requestTimeoutMs,
|
|
181
|
+
...opts,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function volumeFromPayload(payload, config, control) {
|
|
186
|
+
const info = volumeInfo(record(payload.volume ?? payload));
|
|
187
|
+
return new Volume({
|
|
188
|
+
volumeId: info.volumeId,
|
|
189
|
+
name: info.name,
|
|
190
|
+
token: info.token,
|
|
191
|
+
connectionConfig: config,
|
|
192
|
+
control,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function volumeInfo(payload) {
|
|
196
|
+
const id = payload.volume_id ?? payload.volumeId ?? payload.id;
|
|
197
|
+
if (id === undefined)
|
|
198
|
+
throw new SandboxError('volume response did not include id');
|
|
199
|
+
return {
|
|
200
|
+
volumeId: String(id),
|
|
201
|
+
id: String(id),
|
|
202
|
+
name: String(payload.name ?? id),
|
|
203
|
+
state: stringValue(payload.state),
|
|
204
|
+
token: stringValue(payload.token),
|
|
205
|
+
sizeMb: numberValue(payload.size_mb ?? payload.sizeMb),
|
|
206
|
+
sizeBytes: numberValue(payload.size_bytes ?? payload.sizeBytes),
|
|
207
|
+
node: stringValue(payload.node ?? payload.node_name ?? payload.nodeName),
|
|
208
|
+
metadata: recordOfStrings(payload.metadata),
|
|
209
|
+
createdAt: stringValue(payload.created_at ?? payload.createdAt),
|
|
210
|
+
updatedAt: stringValue(payload.updated_at ?? payload.updatedAt),
|
|
211
|
+
raw: payload,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function volumeEntry(payload) {
|
|
215
|
+
return {
|
|
216
|
+
path: String(payload.path ?? ''),
|
|
217
|
+
name: String(payload.name ?? ''),
|
|
218
|
+
type: String(payload.type ?? 'file'),
|
|
219
|
+
size: numberValue(payload.size ?? payload.bytes),
|
|
220
|
+
mode: numberValue(payload.mode),
|
|
221
|
+
uid: numberValue(payload.uid),
|
|
222
|
+
gid: numberValue(payload.gid),
|
|
223
|
+
atime: payload.atime,
|
|
224
|
+
mtime: payload.mtime,
|
|
225
|
+
ctime: payload.ctime,
|
|
226
|
+
raw: payload,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async function bytesFromWriteData(data) {
|
|
230
|
+
if (typeof data === 'string')
|
|
231
|
+
return new TextEncoder().encode(data);
|
|
232
|
+
if (data instanceof Uint8Array)
|
|
233
|
+
return data;
|
|
234
|
+
if (data instanceof ArrayBuffer)
|
|
235
|
+
return new Uint8Array(data);
|
|
236
|
+
if (data instanceof Blob)
|
|
237
|
+
return new Uint8Array(await data.arrayBuffer());
|
|
238
|
+
throw new SandboxError('unsupported volume write data');
|
|
239
|
+
}
|
|
240
|
+
function metadataPayload(opts) {
|
|
241
|
+
return compactRecord({
|
|
242
|
+
uid: opts.uid,
|
|
243
|
+
gid: opts.gid,
|
|
244
|
+
mode: opts.mode,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function compactRecord(payload) {
|
|
248
|
+
return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== undefined));
|
|
249
|
+
}
|
|
250
|
+
function record(value) {
|
|
251
|
+
return value && typeof value === 'object' ? value : {};
|
|
252
|
+
}
|
|
253
|
+
function recordOfStrings(value) {
|
|
254
|
+
if (!value || typeof value !== 'object')
|
|
255
|
+
return {};
|
|
256
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, String(item)]));
|
|
257
|
+
}
|
|
258
|
+
function stringValue(value) {
|
|
259
|
+
if (typeof value === 'string')
|
|
260
|
+
return value;
|
|
261
|
+
if (typeof value === 'number')
|
|
262
|
+
return String(value);
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
function numberValue(value) {
|
|
266
|
+
return typeof value === 'number' ? value : undefined;
|
|
267
|
+
}
|