@capsulos/sdk 0.1.1 → 0.2.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/capsule.d.ts +16 -1
- package/dist/capsule.js +35 -3
- package/dist/capsule.js.map +1 -1
- package/dist/types.d.ts +19 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/capsule.test.ts +48 -0
- package/src/capsule.ts +35 -2
- package/src/types.ts +20 -0
package/dist/capsule.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DaemonClient, type ClientOptions } from './client.js';
|
|
2
2
|
import { Ports } from './ports.js';
|
|
3
3
|
import { PtySession } from './pty.js';
|
|
4
|
-
import { type CapsuleSpec, type ExecOptions, type ExecResult, type FileEntry, type Health, type Provider, type PtyOptions } from './types.js';
|
|
4
|
+
import { type CapsuleSpec, type ExecOptions, type ExecResult, type FileEntry, type Health, type Provider, type PtyOptions, type SnapshotResult } from './types.js';
|
|
5
5
|
export interface CapsuleOptions extends ClientOptions {
|
|
6
6
|
/** How long to wait for the daemon to answer /health after creation. */
|
|
7
7
|
readyTimeoutMs?: number;
|
|
@@ -14,6 +14,7 @@ export declare class Capsule {
|
|
|
14
14
|
readonly id: string;
|
|
15
15
|
private readonly provider;
|
|
16
16
|
private readonly client;
|
|
17
|
+
private readonly readyTimeoutMs;
|
|
17
18
|
readonly commands: Commands;
|
|
18
19
|
readonly files: Files;
|
|
19
20
|
readonly pty: Pty;
|
|
@@ -25,6 +26,20 @@ export declare class Capsule {
|
|
|
25
26
|
/** Reconnect to a capsule that already exists (it may be suspended: the first request resumes it). */
|
|
26
27
|
static connect(provider: Provider, id: string, opts?: CapsuleOptions): Promise<Capsule>;
|
|
27
28
|
health(): Promise<Health>;
|
|
29
|
+
/**
|
|
30
|
+
* Freeze the capsule: processes stop, memory stays. Resume with `resume()`
|
|
31
|
+
* (or, on providers with auto-resume, with the next request). Throws when
|
|
32
|
+
* the provider has no explicit pause.
|
|
33
|
+
*/
|
|
34
|
+
pause(): Promise<void>;
|
|
35
|
+
/** Wake a paused capsule and wait until the daemon answers again. */
|
|
36
|
+
resume(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Save the capsule's disk as a new template under `name`; `Capsule.create`
|
|
39
|
+
* with that template starts from this state. Not every provider can
|
|
40
|
+
* (Lambda MicroVMs cannot image a live VM).
|
|
41
|
+
*/
|
|
42
|
+
snapshot(name: string): Promise<SnapshotResult>;
|
|
28
43
|
/** Put the capsule back in your pocket. */
|
|
29
44
|
kill(): Promise<void>;
|
|
30
45
|
}
|
package/dist/capsule.js
CHANGED
|
@@ -9,15 +9,17 @@ export class Capsule {
|
|
|
9
9
|
id;
|
|
10
10
|
provider;
|
|
11
11
|
client;
|
|
12
|
+
readyTimeoutMs;
|
|
12
13
|
commands;
|
|
13
14
|
files;
|
|
14
15
|
pty;
|
|
15
16
|
/** Servers listening inside the capsule, reachable through the daemon (preview URLs). */
|
|
16
17
|
ports;
|
|
17
|
-
constructor(id, provider, client) {
|
|
18
|
+
constructor(id, provider, client, readyTimeoutMs = 120_000) {
|
|
18
19
|
this.id = id;
|
|
19
20
|
this.provider = provider;
|
|
20
21
|
this.client = client;
|
|
22
|
+
this.readyTimeoutMs = readyTimeoutMs;
|
|
21
23
|
this.commands = new Commands(client);
|
|
22
24
|
this.files = new Files(client);
|
|
23
25
|
this.pty = new Pty(client);
|
|
@@ -43,16 +45,43 @@ export class Capsule {
|
|
|
43
45
|
if (failed.length) {
|
|
44
46
|
throw new CapsuleError(`mounts failed: ${failed.map((r) => `${r.path} (${r.driver}): ${r.error}`).join('; ')}`);
|
|
45
47
|
}
|
|
46
|
-
return new Capsule(handle.id, provider, client);
|
|
48
|
+
return new Capsule(handle.id, provider, client, opts.readyTimeoutMs);
|
|
47
49
|
}
|
|
48
50
|
/** Reconnect to a capsule that already exists (it may be suspended: the first request resumes it). */
|
|
49
51
|
static async connect(provider, id, opts = {}) {
|
|
50
52
|
const conn = await provider.connect(id);
|
|
51
|
-
return new Capsule(id, provider, new DaemonClient(conn, opts));
|
|
53
|
+
return new Capsule(id, provider, new DaemonClient(conn, opts), opts.readyTimeoutMs);
|
|
52
54
|
}
|
|
53
55
|
health() {
|
|
54
56
|
return this.client.health();
|
|
55
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Freeze the capsule: processes stop, memory stays. Resume with `resume()`
|
|
60
|
+
* (or, on providers with auto-resume, with the next request). Throws when
|
|
61
|
+
* the provider has no explicit pause.
|
|
62
|
+
*/
|
|
63
|
+
async pause() {
|
|
64
|
+
if (!this.provider.pause)
|
|
65
|
+
throw unsupported(this.provider, 'pause');
|
|
66
|
+
await this.provider.pause(this.id);
|
|
67
|
+
}
|
|
68
|
+
/** Wake a paused capsule and wait until the daemon answers again. */
|
|
69
|
+
async resume() {
|
|
70
|
+
if (!this.provider.resume)
|
|
71
|
+
throw unsupported(this.provider, 'resume');
|
|
72
|
+
await this.provider.resume(this.id);
|
|
73
|
+
await waitReady(this.client, this.readyTimeoutMs);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Save the capsule's disk as a new template under `name`; `Capsule.create`
|
|
77
|
+
* with that template starts from this state. Not every provider can
|
|
78
|
+
* (Lambda MicroVMs cannot image a live VM).
|
|
79
|
+
*/
|
|
80
|
+
async snapshot(name) {
|
|
81
|
+
if (!this.provider.snapshot)
|
|
82
|
+
throw unsupported(this.provider, 'snapshot');
|
|
83
|
+
return this.provider.snapshot(this.id, name);
|
|
84
|
+
}
|
|
56
85
|
/** Put the capsule back in your pocket. */
|
|
57
86
|
async kill() {
|
|
58
87
|
await this.provider.terminate(this.id);
|
|
@@ -105,6 +134,9 @@ export class Files {
|
|
|
105
134
|
return this.client.remove(path);
|
|
106
135
|
}
|
|
107
136
|
}
|
|
137
|
+
function unsupported(provider, op) {
|
|
138
|
+
return new CapsuleError(`the ${provider.name} provider does not support ${op}()`);
|
|
139
|
+
}
|
|
108
140
|
async function waitReady(client, timeoutMs, signal) {
|
|
109
141
|
const t0 = Date.now();
|
|
110
142
|
let lastErr;
|
package/dist/capsule.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capsule.js","sourceRoot":"","sources":["../src/capsule.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,uEAAuE;AACvE,0BAA0B;AAC1B,OAAO,EAAE,YAAY,EAAsB,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EACL,YAAY,
|
|
1
|
+
{"version":3,"file":"capsule.js","sourceRoot":"","sources":["../src/capsule.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -9,6 +9,25 @@ export interface Provider {
|
|
|
9
9
|
terminate(id: string): Promise<void>;
|
|
10
10
|
/** How to reach the daemon of an existing capsule (endpoint + auth headers). */
|
|
11
11
|
connect(id: string): Promise<Connection>;
|
|
12
|
+
/**
|
|
13
|
+
* Freeze the capsule now (memory kept, no CPU, no billing for compute on
|
|
14
|
+
* providers that stop charging). Optional: a provider without it leaves
|
|
15
|
+
* suspension to its idle policy.
|
|
16
|
+
*/
|
|
17
|
+
pause?(id: string): Promise<void>;
|
|
18
|
+
/** Wake a paused capsule and return when the provider reports it running (the daemon may need a moment more). */
|
|
19
|
+
resume?(id: string): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Save the capsule's current disk as a new template, usable in `create()`.
|
|
22
|
+
* Optional: Lambda MicroVMs cannot image a live VM (images are built from
|
|
23
|
+
* a zip), Docker can (`docker commit`).
|
|
24
|
+
*/
|
|
25
|
+
snapshot?(id: string, name: string): Promise<SnapshotResult>;
|
|
26
|
+
}
|
|
27
|
+
export interface SnapshotResult {
|
|
28
|
+
/** The template name to pass to `create()` on the same provider. */
|
|
29
|
+
template: string;
|
|
30
|
+
provider: string;
|
|
12
31
|
}
|
|
13
32
|
export interface CreateOptions {
|
|
14
33
|
/**
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,0EAA0E;AAC1E,kEAAkE;AAClE,gFAAgF;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,0EAA0E;AAC1E,kEAAkE;AAClE,gFAAgF;AA4LhF,MAAM,OAAO,YAAa,SAAQ,KAAK;IAM1B;IALX,sGAAsG;IAC7F,SAAS,CAAU;IAE5B,YACE,OAAe,EACN,MAAe,EACxB,OAA+B,EAAE;QAEjC,KAAK,CAAC,OAAO,CAAC,CAAC;QAHN,WAAM,GAAN,MAAM,CAAS;QAIxB,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,IAAI,CAAC,SAAS;YAAE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IACtD,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Capsule } from './capsule.js';
|
|
3
|
+
import type { Provider } from './types.js';
|
|
4
|
+
|
|
5
|
+
const bare: Provider = {
|
|
6
|
+
name: 'bare',
|
|
7
|
+
async create() {
|
|
8
|
+
throw new Error('unused');
|
|
9
|
+
},
|
|
10
|
+
async get() {
|
|
11
|
+
return null;
|
|
12
|
+
},
|
|
13
|
+
async list() {
|
|
14
|
+
return [];
|
|
15
|
+
},
|
|
16
|
+
async terminate() {},
|
|
17
|
+
async connect() {
|
|
18
|
+
return { baseUrl: 'http://capsule', headers: async () => ({}) };
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe('Capsule optional operations', () => {
|
|
23
|
+
it('names the provider and the operation when it is not supported', async () => {
|
|
24
|
+
const capsule = await Capsule.connect(bare, 'c1');
|
|
25
|
+
await expect(capsule.pause()).rejects.toThrow(/bare provider does not support pause\(\)/);
|
|
26
|
+
await expect(capsule.resume()).rejects.toThrow(/does not support resume\(\)/);
|
|
27
|
+
await expect(capsule.snapshot('x')).rejects.toThrow(/does not support snapshot\(\)/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('delegates pause and snapshot to the provider', async () => {
|
|
31
|
+
const calls: string[] = [];
|
|
32
|
+
const p: Provider = {
|
|
33
|
+
...bare,
|
|
34
|
+
name: 'full',
|
|
35
|
+
async pause(id) {
|
|
36
|
+
calls.push(`pause:${id}`);
|
|
37
|
+
},
|
|
38
|
+
async snapshot(id, name) {
|
|
39
|
+
calls.push(`snapshot:${id}:${name}`);
|
|
40
|
+
return { template: name, provider: 'full' };
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const capsule = await Capsule.connect(p, 'c1');
|
|
44
|
+
await capsule.pause();
|
|
45
|
+
expect(await capsule.snapshot('tpl')).toEqual({ template: 'tpl', provider: 'full' });
|
|
46
|
+
expect(calls).toEqual(['pause:c1', 'snapshot:c1:tpl']);
|
|
47
|
+
});
|
|
48
|
+
});
|
package/src/capsule.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type Health,
|
|
14
14
|
type Provider,
|
|
15
15
|
type PtyOptions,
|
|
16
|
+
type SnapshotResult,
|
|
16
17
|
} from './types.js';
|
|
17
18
|
|
|
18
19
|
export interface CapsuleOptions extends ClientOptions {
|
|
@@ -35,6 +36,7 @@ export class Capsule {
|
|
|
35
36
|
readonly id: string,
|
|
36
37
|
private readonly provider: Provider,
|
|
37
38
|
private readonly client: DaemonClient,
|
|
39
|
+
private readonly readyTimeoutMs = 120_000,
|
|
38
40
|
) {
|
|
39
41
|
this.commands = new Commands(client);
|
|
40
42
|
this.files = new Files(client);
|
|
@@ -62,19 +64,46 @@ export class Capsule {
|
|
|
62
64
|
if (failed.length) {
|
|
63
65
|
throw new CapsuleError(`mounts failed: ${failed.map((r) => `${r.path} (${r.driver}): ${r.error}`).join('; ')}`);
|
|
64
66
|
}
|
|
65
|
-
return new Capsule(handle.id, provider, client);
|
|
67
|
+
return new Capsule(handle.id, provider, client, opts.readyTimeoutMs);
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/** Reconnect to a capsule that already exists (it may be suspended: the first request resumes it). */
|
|
69
71
|
static async connect(provider: Provider, id: string, opts: CapsuleOptions = {}): Promise<Capsule> {
|
|
70
72
|
const conn = await provider.connect(id);
|
|
71
|
-
return new Capsule(id, provider, new DaemonClient(conn, opts));
|
|
73
|
+
return new Capsule(id, provider, new DaemonClient(conn, opts), opts.readyTimeoutMs);
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
health() {
|
|
75
77
|
return this.client.health();
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Freeze the capsule: processes stop, memory stays. Resume with `resume()`
|
|
82
|
+
* (or, on providers with auto-resume, with the next request). Throws when
|
|
83
|
+
* the provider has no explicit pause.
|
|
84
|
+
*/
|
|
85
|
+
async pause(): Promise<void> {
|
|
86
|
+
if (!this.provider.pause) throw unsupported(this.provider, 'pause');
|
|
87
|
+
await this.provider.pause(this.id);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Wake a paused capsule and wait until the daemon answers again. */
|
|
91
|
+
async resume(): Promise<void> {
|
|
92
|
+
if (!this.provider.resume) throw unsupported(this.provider, 'resume');
|
|
93
|
+
await this.provider.resume(this.id);
|
|
94
|
+
await waitReady(this.client, this.readyTimeoutMs);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Save the capsule's disk as a new template under `name`; `Capsule.create`
|
|
99
|
+
* with that template starts from this state. Not every provider can
|
|
100
|
+
* (Lambda MicroVMs cannot image a live VM).
|
|
101
|
+
*/
|
|
102
|
+
async snapshot(name: string): Promise<SnapshotResult> {
|
|
103
|
+
if (!this.provider.snapshot) throw unsupported(this.provider, 'snapshot');
|
|
104
|
+
return this.provider.snapshot(this.id, name);
|
|
105
|
+
}
|
|
106
|
+
|
|
78
107
|
/** Put the capsule back in your pocket. */
|
|
79
108
|
async kill(): Promise<void> {
|
|
80
109
|
await this.provider.terminate(this.id);
|
|
@@ -122,6 +151,10 @@ export class Files {
|
|
|
122
151
|
}
|
|
123
152
|
}
|
|
124
153
|
|
|
154
|
+
function unsupported(provider: Provider, op: string): CapsuleError {
|
|
155
|
+
return new CapsuleError(`the ${provider.name} provider does not support ${op}()`);
|
|
156
|
+
}
|
|
157
|
+
|
|
125
158
|
async function waitReady(client: DaemonClient, timeoutMs: number, signal?: AbortSignal): Promise<Health> {
|
|
126
159
|
const t0 = Date.now();
|
|
127
160
|
let lastErr: unknown;
|
package/src/types.ts
CHANGED
|
@@ -14,6 +14,26 @@ export interface Provider {
|
|
|
14
14
|
terminate(id: string): Promise<void>;
|
|
15
15
|
/** How to reach the daemon of an existing capsule (endpoint + auth headers). */
|
|
16
16
|
connect(id: string): Promise<Connection>;
|
|
17
|
+
/**
|
|
18
|
+
* Freeze the capsule now (memory kept, no CPU, no billing for compute on
|
|
19
|
+
* providers that stop charging). Optional: a provider without it leaves
|
|
20
|
+
* suspension to its idle policy.
|
|
21
|
+
*/
|
|
22
|
+
pause?(id: string): Promise<void>;
|
|
23
|
+
/** Wake a paused capsule and return when the provider reports it running (the daemon may need a moment more). */
|
|
24
|
+
resume?(id: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Save the capsule's current disk as a new template, usable in `create()`.
|
|
27
|
+
* Optional: Lambda MicroVMs cannot image a live VM (images are built from
|
|
28
|
+
* a zip), Docker can (`docker commit`).
|
|
29
|
+
*/
|
|
30
|
+
snapshot?(id: string, name: string): Promise<SnapshotResult>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SnapshotResult {
|
|
34
|
+
/** The template name to pass to `create()` on the same provider. */
|
|
35
|
+
template: string;
|
|
36
|
+
provider: string;
|
|
17
37
|
}
|
|
18
38
|
|
|
19
39
|
export interface CreateOptions {
|