@alchemy.run/archil 0.0.0 → 2.0.0-beta.87
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/LICENSE +201 -0
- package/README.md +57 -4
- package/lib/AWS/S3Disk.d.ts +68 -0
- package/lib/AWS/S3Disk.d.ts.map +1 -0
- package/lib/AWS/S3Disk.js +100 -0
- package/lib/AWS/S3Disk.js.map +1 -0
- package/lib/AWS/index.d.ts +2 -0
- package/lib/AWS/index.d.ts.map +1 -0
- package/lib/AWS/index.js +2 -0
- package/lib/AWS/index.js.map +1 -0
- package/lib/ApiToken.d.ts +63 -0
- package/lib/ApiToken.d.ts.map +1 -0
- package/lib/ApiToken.js +121 -0
- package/lib/ApiToken.js.map +1 -0
- package/lib/ArchilBindings.d.ts +65 -0
- package/lib/ArchilBindings.d.ts.map +1 -0
- package/lib/ArchilBindings.js +128 -0
- package/lib/ArchilBindings.js.map +1 -0
- package/lib/AuthProvider.d.ts +40 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +87 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/Bootstrap.d.ts +8 -0
- package/lib/Bootstrap.d.ts.map +1 -0
- package/lib/Bootstrap.js +27 -0
- package/lib/Bootstrap.js.map +1 -0
- package/lib/Branch.d.ts +101 -0
- package/lib/Branch.d.ts.map +1 -0
- package/lib/Branch.js +117 -0
- package/lib/Branch.js.map +1 -0
- package/lib/BranchDisk.d.ts +42 -0
- package/lib/BranchDisk.d.ts.map +1 -0
- package/lib/BranchDisk.js +14 -0
- package/lib/BranchDisk.js.map +1 -0
- package/lib/Checkpoint.d.ts +78 -0
- package/lib/Checkpoint.d.ts.map +1 -0
- package/lib/Checkpoint.js +95 -0
- package/lib/Checkpoint.js.map +1 -0
- package/lib/Clients.d.ts +369 -0
- package/lib/Clients.d.ts.map +1 -0
- package/lib/Clients.js +334 -0
- package/lib/Clients.js.map +1 -0
- package/lib/ConnectSandbox.d.ts +69 -0
- package/lib/ConnectSandbox.d.ts.map +1 -0
- package/lib/ConnectSandbox.js +21 -0
- package/lib/ConnectSandbox.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/CreateDisk.d.ts +90 -0
- package/lib/CreateDisk.d.ts.map +1 -0
- package/lib/CreateDisk.js +47 -0
- package/lib/CreateDisk.js.map +1 -0
- package/lib/CreateImage.d.ts +42 -0
- package/lib/CreateImage.d.ts.map +1 -0
- package/lib/CreateImage.js +19 -0
- package/lib/CreateImage.js.map +1 -0
- package/lib/CreateSandbox.d.ts +104 -0
- package/lib/CreateSandbox.d.ts.map +1 -0
- package/lib/CreateSandbox.js +98 -0
- package/lib/CreateSandbox.js.map +1 -0
- package/lib/Credentials.d.ts +44 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +57 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/Disk.d.ts +283 -0
- package/lib/Disk.d.ts.map +1 -0
- package/lib/Disk.js +294 -0
- package/lib/Disk.js.map +1 -0
- package/lib/DiskObjectsHttp.d.ts +20 -0
- package/lib/DiskObjectsHttp.d.ts.map +1 -0
- package/lib/DiskObjectsHttp.js +51 -0
- package/lib/DiskObjectsHttp.js.map +1 -0
- package/lib/DiskUser.d.ts +189 -0
- package/lib/DiskUser.d.ts.map +1 -0
- package/lib/DiskUser.js +243 -0
- package/lib/DiskUser.js.map +1 -0
- package/lib/ExecDisk.d.ts +57 -0
- package/lib/ExecDisk.d.ts.map +1 -0
- package/lib/ExecDisk.js +3 -0
- package/lib/ExecDisk.js.map +1 -0
- package/lib/ExecDiskHttp.d.ts +5 -0
- package/lib/ExecDiskHttp.d.ts.map +1 -0
- package/lib/ExecDiskHttp.js +18 -0
- package/lib/ExecDiskHttp.js.map +1 -0
- package/lib/ExecDisks.d.ts +54 -0
- package/lib/ExecDisks.d.ts.map +1 -0
- package/lib/ExecDisks.js +37 -0
- package/lib/ExecDisks.js.map +1 -0
- package/lib/Image.d.ts +188 -0
- package/lib/Image.d.ts.map +1 -0
- package/lib/Image.js +191 -0
- package/lib/Image.js.map +1 -0
- package/lib/InferEnv.d.ts +26 -0
- package/lib/InferEnv.d.ts.map +1 -0
- package/lib/InferEnv.js +1 -0
- package/lib/InferEnv.js.map +1 -0
- package/lib/MountDisk.d.ts +190 -0
- package/lib/MountDisk.d.ts.map +1 -0
- package/lib/MountDisk.js +227 -0
- package/lib/MountDisk.js.map +1 -0
- package/lib/Providers.d.ts +25 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +48 -0
- package/lib/Providers.js.map +1 -0
- package/lib/ReadDisk.d.ts +55 -0
- package/lib/ReadDisk.d.ts.map +1 -0
- package/lib/ReadDisk.js +3 -0
- package/lib/ReadDisk.js.map +1 -0
- package/lib/ReadWriteDisk.d.ts +26 -0
- package/lib/ReadWriteDisk.d.ts.map +1 -0
- package/lib/ReadWriteDisk.js +3 -0
- package/lib/ReadWriteDisk.js.map +1 -0
- package/lib/Sandbox.d.ts +507 -0
- package/lib/Sandbox.d.ts.map +1 -0
- package/lib/Sandbox.js +729 -0
- package/lib/Sandbox.js.map +1 -0
- package/lib/SandboxOptions.d.ts +185 -0
- package/lib/SandboxOptions.d.ts.map +1 -0
- package/lib/SandboxOptions.js +102 -0
- package/lib/SandboxOptions.js.map +1 -0
- package/lib/SandboxPortToken.d.ts +64 -0
- package/lib/SandboxPortToken.d.ts.map +1 -0
- package/lib/SandboxPortToken.js +77 -0
- package/lib/SandboxPortToken.js.map +1 -0
- package/lib/WriteDisk.d.ts +37 -0
- package/lib/WriteDisk.d.ts.map +1 -0
- package/lib/WriteDisk.js +3 -0
- package/lib/WriteDisk.js.map +1 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +28 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/DeployerKey.d.ts +21 -0
- package/lib/internal/DeployerKey.d.ts.map +1 -0
- package/lib/internal/DeployerKey.js +34 -0
- package/lib/internal/DeployerKey.js.map +1 -0
- package/lib/internal/archil.d.ts +87 -0
- package/lib/internal/archil.d.ts.map +1 -0
- package/lib/internal/archil.js +154 -0
- package/lib/internal/archil.js.map +1 -0
- package/lib/internal/hosted.d.ts +90 -0
- package/lib/internal/hosted.d.ts.map +1 -0
- package/lib/internal/hosted.js +291 -0
- package/lib/internal/hosted.js.map +1 -0
- package/package.json +79 -3
- package/src/AWS/S3Disk.ts +125 -0
- package/src/AWS/index.ts +1 -0
- package/src/ApiToken.ts +168 -0
- package/src/ArchilBindings.ts +179 -0
- package/src/AuthProvider.ts +122 -0
- package/src/Bootstrap.ts +54 -0
- package/src/Branch.ts +199 -0
- package/src/BranchDisk.ts +57 -0
- package/src/Checkpoint.ts +145 -0
- package/src/Clients.ts +954 -0
- package/src/ConnectSandbox.ts +91 -0
- package/src/Contributions.ts +9 -0
- package/src/CreateDisk.ts +165 -0
- package/src/CreateImage.ts +73 -0
- package/src/CreateSandbox.ts +258 -0
- package/src/Credentials.ts +101 -0
- package/src/Disk.ts +468 -0
- package/src/DiskObjectsHttp.ts +64 -0
- package/src/DiskUser.ts +369 -0
- package/src/ExecDisk.ts +62 -0
- package/src/ExecDiskHttp.ts +25 -0
- package/src/ExecDisks.ts +100 -0
- package/src/Image.ts +316 -0
- package/src/InferEnv.ts +27 -0
- package/src/MountDisk.ts +466 -0
- package/src/Providers.ts +75 -0
- package/src/ReadDisk.ts +61 -0
- package/src/ReadWriteDisk.ts +31 -0
- package/src/Sandbox.ts +1064 -0
- package/src/SandboxOptions.ts +230 -0
- package/src/SandboxPortToken.ts +125 -0
- package/src/WriteDisk.ts +42 -0
- package/src/globals.d.ts +2 -0
- package/src/index.ts +59 -0
- package/src/internal/DeployerKey.ts +63 -0
- package/src/internal/archil.ts +273 -0
- package/src/internal/hosted.ts +402 -0
package/src/Branch.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import * as archil from "@distilled.cloud/archil";
|
|
2
|
+
import { isResolved } from "alchemy/Diff";
|
|
3
|
+
import { createPhysicalName } from "alchemy/PhysicalName";
|
|
4
|
+
import * as Provider from "alchemy/Provider";
|
|
5
|
+
import { Resource } from "alchemy/Resource";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import type { Providers } from "./Providers.ts";
|
|
8
|
+
|
|
9
|
+
export interface BranchProps {
|
|
10
|
+
/** Id of the disk to branch (e.g. `disk.diskId`). Changing it replaces the branch. */
|
|
11
|
+
disk: string;
|
|
12
|
+
/**
|
|
13
|
+
* Branch name, unique within the disk. If omitted, a unique name is
|
|
14
|
+
* generated from the stack, stage, and logical ID. Changing it replaces
|
|
15
|
+
* the branch.
|
|
16
|
+
*/
|
|
17
|
+
name?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Name of the committed checkpoint the branch starts from (e.g.
|
|
20
|
+
* `checkpoint.name`). Changing it replaces the branch.
|
|
21
|
+
*/
|
|
22
|
+
fromCheckpoint: string;
|
|
23
|
+
/**
|
|
24
|
+
* Branch the checkpoint was taken on. Omit for a checkpoint on the root
|
|
25
|
+
* disk. Changing it replaces the branch.
|
|
26
|
+
*/
|
|
27
|
+
fromBranch?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BranchAttributes {
|
|
31
|
+
/** Id of the disk the branch belongs to. */
|
|
32
|
+
diskId: string;
|
|
33
|
+
/** Branch name. Mount it with `archil mount <disk> <path> --branch <name>`. */
|
|
34
|
+
name: string;
|
|
35
|
+
/** Id of the branch's own writable filesystem. */
|
|
36
|
+
filesystemId: string;
|
|
37
|
+
/** Checkpoint the branch started from. */
|
|
38
|
+
fromCheckpoint: string;
|
|
39
|
+
/** Filesystem the checkpoint was taken on. */
|
|
40
|
+
fromFilesystemId: string;
|
|
41
|
+
/** When the branch was created. */
|
|
42
|
+
createdAt: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type Branch = Resource<
|
|
46
|
+
"Archil.Branch",
|
|
47
|
+
BranchProps,
|
|
48
|
+
BranchAttributes,
|
|
49
|
+
never,
|
|
50
|
+
Providers,
|
|
51
|
+
BranchLookup
|
|
52
|
+
>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Props for {@link Branch.lookup}: find a branch this stack
|
|
56
|
+
* doesn't manage by its disk and name.
|
|
57
|
+
*/
|
|
58
|
+
export interface BranchLookup {
|
|
59
|
+
/** Id of the disk the branch belongs to. */
|
|
60
|
+
disk: string;
|
|
61
|
+
/** Branch name. */
|
|
62
|
+
name: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* An independent, writable fork of a disk, started from a committed
|
|
67
|
+
* checkpoint in about 35 ms whatever the disk's size. Writes on the branch
|
|
68
|
+
* never reach its source or its siblings. Branches share the disk's
|
|
69
|
+
* credentials, so a disk user can mount any branch of its disk.
|
|
70
|
+
*
|
|
71
|
+
* Archil has no API to delete a branch yet, so destroying the resource
|
|
72
|
+
* only removes it from Alchemy's state. Branches made per session at run
|
|
73
|
+
* time come from {@link BranchDisk}.
|
|
74
|
+
* @see https://docs.archil.com/concepts/branches-and-checkpoints
|
|
75
|
+
*
|
|
76
|
+
* ### Creating a Branch
|
|
77
|
+
* **Example:** A staging copy of a seeded disk
|
|
78
|
+
* ```typescript
|
|
79
|
+
* export const Staging = Effect.gen(function* () {
|
|
80
|
+
* const seed = yield* Seed;
|
|
81
|
+
* const base = yield* Base;
|
|
82
|
+
* return yield* Archil.Branch("Staging", {
|
|
83
|
+
* disk: seed.diskId,
|
|
84
|
+
* fromCheckpoint: base.name,
|
|
85
|
+
* });
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* ### Looking up an existing branch
|
|
90
|
+
* `Branch.lookup` references a branch Alchemy doesn't manage (created in
|
|
91
|
+
* the Archil console or by another stack). It is read during plan/deploy,
|
|
92
|
+
* never created, updated or deleted.
|
|
93
|
+
*
|
|
94
|
+
* **Example:** Reference an existing branch
|
|
95
|
+
* ```typescript
|
|
96
|
+
* const main = Archil.Branch.lookup({ disk: "dsk_123", name: "main" });
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @resource
|
|
100
|
+
* @product Disk
|
|
101
|
+
*/
|
|
102
|
+
export const Branch = Resource<Branch>("Archil.Branch");
|
|
103
|
+
|
|
104
|
+
/** Map a branch response to attributes. */
|
|
105
|
+
export const toBranchAttributes = (branch: archil.BranchResponse): BranchAttributes => ({
|
|
106
|
+
diskId: branch.root_filesystem_id,
|
|
107
|
+
name: branch.branch_name,
|
|
108
|
+
filesystemId: branch.filesystem_id,
|
|
109
|
+
fromCheckpoint: branch.from_checkpoint_name,
|
|
110
|
+
fromFilesystemId: branch.from_checkpoint_filesystem_id,
|
|
111
|
+
createdAt: branch.created_at,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/** Get a branch; `undefined` when it (or its disk) doesn't exist. */
|
|
115
|
+
export const getBranch = (diskId: string, name: string) =>
|
|
116
|
+
archil.getBranch({ id: diskId, name }).pipe(
|
|
117
|
+
Effect.map(({ data }) => toBranchAttributes(data)),
|
|
118
|
+
Effect.catchTag("NotFound", () => Effect.undefined),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
/** Create a branch, or return the existing one with that name. */
|
|
122
|
+
export const ensureBranch = Effect.fn(function* (input: {
|
|
123
|
+
disk: string;
|
|
124
|
+
name: string;
|
|
125
|
+
fromCheckpoint: string;
|
|
126
|
+
fromBranch?: string;
|
|
127
|
+
}) {
|
|
128
|
+
const existing = yield* getBranch(input.disk, input.name);
|
|
129
|
+
if (existing) return existing;
|
|
130
|
+
return yield* archil
|
|
131
|
+
.createBranch({
|
|
132
|
+
id: input.disk,
|
|
133
|
+
branch_name: input.name,
|
|
134
|
+
from_checkpoint_name: input.fromCheckpoint,
|
|
135
|
+
from_branch: input.fromBranch,
|
|
136
|
+
})
|
|
137
|
+
.pipe(
|
|
138
|
+
Effect.map(({ data }) => toBranchAttributes(data)),
|
|
139
|
+
// A concurrent create of the same name won the race.
|
|
140
|
+
Effect.catchTag("Conflict", (conflict) =>
|
|
141
|
+
getBranch(input.disk, input.name).pipe(
|
|
142
|
+
Effect.flatMap((found) => (found ? Effect.succeed(found) : Effect.fail(conflict))),
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const branchName = (id: string, name: string | undefined) =>
|
|
149
|
+
name ? Effect.succeed(name) : createPhysicalName({ id, maxLength: 63, delimiter: "-" });
|
|
150
|
+
|
|
151
|
+
export const BranchProvider = () =>
|
|
152
|
+
Provider.effect(
|
|
153
|
+
Branch,
|
|
154
|
+
Effect.gen(function* () {
|
|
155
|
+
return {
|
|
156
|
+
stables: [
|
|
157
|
+
"diskId",
|
|
158
|
+
"name",
|
|
159
|
+
"filesystemId",
|
|
160
|
+
"fromCheckpoint",
|
|
161
|
+
"fromFilesystemId",
|
|
162
|
+
"createdAt",
|
|
163
|
+
],
|
|
164
|
+
// No delete API: nuke can't remove branches (they go with their disk).
|
|
165
|
+
nuke: { skip: true },
|
|
166
|
+
diff: Effect.fn(function* ({ id, olds, news, output }) {
|
|
167
|
+
if (!isResolved(news) || olds === undefined) return undefined;
|
|
168
|
+
const name = output?.name ?? (yield* branchName(id, olds.name));
|
|
169
|
+
if (
|
|
170
|
+
news.disk !== olds.disk ||
|
|
171
|
+
(news.name !== undefined && news.name !== name) ||
|
|
172
|
+
news.fromCheckpoint !== olds.fromCheckpoint ||
|
|
173
|
+
news.fromBranch !== olds.fromBranch
|
|
174
|
+
) {
|
|
175
|
+
return { action: "replace" } as const;
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}),
|
|
179
|
+
lookup: ({ disk, name }) => getBranch(disk, name),
|
|
180
|
+
|
|
181
|
+
read: Effect.fn(function* ({ id, olds, output }) {
|
|
182
|
+
if (!output && !olds) return undefined;
|
|
183
|
+
const diskId = output?.diskId ?? olds!.disk;
|
|
184
|
+
const name = output?.name ?? (yield* branchName(id, olds?.name));
|
|
185
|
+
return yield* getBranch(diskId, name);
|
|
186
|
+
}),
|
|
187
|
+
reconcile: Effect.fn(function* ({ id, news, output }) {
|
|
188
|
+
return yield* ensureBranch({
|
|
189
|
+
disk: news.disk,
|
|
190
|
+
name: output?.name ?? (yield* branchName(id, news.name)),
|
|
191
|
+
fromCheckpoint: news.fromCheckpoint,
|
|
192
|
+
fromBranch: news.fromBranch,
|
|
193
|
+
});
|
|
194
|
+
}),
|
|
195
|
+
// Archil has no branch delete API; the branch goes with its disk.
|
|
196
|
+
delete: () => Effect.void,
|
|
197
|
+
};
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as Binding from "alchemy/Binding";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import { ApiAccess } from "./ArchilBindings.ts";
|
|
5
|
+
import { makeBranchDiskClient, type BranchDiskClient } from "./Clients.ts";
|
|
6
|
+
import type { Disk } from "./Disk.ts";
|
|
7
|
+
|
|
8
|
+
export type { BranchDiskClient } from "./Clients.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fork one disk per session or task at run time: each branch is an
|
|
12
|
+
* independent, writable copy started from a committed checkpoint in about
|
|
13
|
+
* 35 ms, whatever the disk's size. Seed the disk once at deploy time with an
|
|
14
|
+
* {@link Checkpoint}, then branch from it per request.
|
|
15
|
+
*
|
|
16
|
+
* Mount a branch on your own machines with `MountDisk` and `branch`.
|
|
17
|
+
* Branches share the disk's credentials. Sandboxes can't mount a branch.
|
|
18
|
+
*
|
|
19
|
+
* ### Branching per Session
|
|
20
|
+
* **Example:** A fresh workspace for every session
|
|
21
|
+
* ```typescript
|
|
22
|
+
* export default class Api extends Cloudflare.Worker<Api>()(
|
|
23
|
+
* "Api",
|
|
24
|
+
* { main: import.meta.url },
|
|
25
|
+
* Effect.gen(function* () {
|
|
26
|
+
* const seeds = yield* Archil.BranchDisk(Seed);
|
|
27
|
+
* return {
|
|
28
|
+
* fetch: Effect.gen(function* () {
|
|
29
|
+
* const branch = yield* seeds.branch({ name: "session-42", fromCheckpoint: "base" });
|
|
30
|
+
* return yield* HttpServerResponse.json(branch);
|
|
31
|
+
* }),
|
|
32
|
+
* };
|
|
33
|
+
* }).pipe(Effect.provide(Archil.BranchDiskHttp)),
|
|
34
|
+
* ) {}
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @binding
|
|
38
|
+
* @product Disk
|
|
39
|
+
*/
|
|
40
|
+
export interface BranchDisk extends Binding.Service<
|
|
41
|
+
BranchDisk,
|
|
42
|
+
"Archil.BranchDisk",
|
|
43
|
+
(disk: Disk) => Effect.Effect<BranchDiskClient>
|
|
44
|
+
> {}
|
|
45
|
+
|
|
46
|
+
export const BranchDisk = Binding.Service<BranchDisk>("Archil.BranchDisk");
|
|
47
|
+
|
|
48
|
+
/** {@link BranchDisk} over Archil's HTTP API, usable from any host. */
|
|
49
|
+
export const BranchDiskHttp = Layer.effect(
|
|
50
|
+
BranchDisk,
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
const access = yield* ApiAccess;
|
|
53
|
+
return Effect.fn(function* (disk: Disk) {
|
|
54
|
+
return makeBranchDiskClient(yield* disk.diskId, yield* access());
|
|
55
|
+
});
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as archil from "@distilled.cloud/archil";
|
|
2
|
+
import { deepEqual, isResolved } from "alchemy/Diff";
|
|
3
|
+
import { createPhysicalName } from "alchemy/PhysicalName";
|
|
4
|
+
import * as Provider from "alchemy/Provider";
|
|
5
|
+
import { Resource } from "alchemy/Resource";
|
|
6
|
+
import * as Data from "effect/Data";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import type { Providers } from "./Providers.ts";
|
|
9
|
+
|
|
10
|
+
/** A checkpoint's setup command exited non-zero. */
|
|
11
|
+
export class CheckpointSetupFailed extends Data.TaggedError("Archil.CheckpointSetupFailed")<{
|
|
12
|
+
readonly checkpoint: string;
|
|
13
|
+
readonly exitCode: number;
|
|
14
|
+
readonly stderr: string;
|
|
15
|
+
}> {}
|
|
16
|
+
|
|
17
|
+
export interface CheckpointProps {
|
|
18
|
+
/** Id of the disk to checkpoint (e.g. `disk.diskId`). Changing it replaces the checkpoint. */
|
|
19
|
+
disk: string;
|
|
20
|
+
/**
|
|
21
|
+
* Checkpoint name, unique within its branch. If omitted, a unique name
|
|
22
|
+
* is generated from the stack, stage, and logical ID. Changing it
|
|
23
|
+
* replaces the checkpoint.
|
|
24
|
+
*/
|
|
25
|
+
name?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Shell commands that seed the disk before the checkpoint is taken. They
|
|
28
|
+
* run in a serverless microVM with the disk mounted at `/mnt/archil` (the
|
|
29
|
+
* working directory). Changing them takes a new checkpoint.
|
|
30
|
+
*/
|
|
31
|
+
run?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CheckpointAttributes {
|
|
35
|
+
/** Id of the disk. */
|
|
36
|
+
diskId: string;
|
|
37
|
+
/** Checkpoint name. Pass it to a {@link Branch} or `BranchDisk` as `fromCheckpoint`. */
|
|
38
|
+
name: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type Checkpoint = Resource<
|
|
42
|
+
"Archil.Checkpoint",
|
|
43
|
+
CheckpointProps,
|
|
44
|
+
CheckpointAttributes,
|
|
45
|
+
never,
|
|
46
|
+
Providers
|
|
47
|
+
>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A named, point-in-time snapshot of a disk, the seeded base that branches
|
|
51
|
+
* fork from. Alchemy runs the `run` commands against the disk, then takes
|
|
52
|
+
* the checkpoint in the same microVM, so the checkpoint holds exactly what
|
|
53
|
+
* they produced. A checkpoint takes about 15 ms whatever the disk's size,
|
|
54
|
+
* and never changes after it's taken.
|
|
55
|
+
*
|
|
56
|
+
* Archil can't delete checkpoints yet, so destroying the resource only
|
|
57
|
+
* removes it from Alchemy's state.
|
|
58
|
+
* @see https://docs.archil.com/concepts/branches-and-checkpoints
|
|
59
|
+
*
|
|
60
|
+
* ### Seeding a Base
|
|
61
|
+
* **Example:** Clone and install once, branch per session
|
|
62
|
+
* ```typescript
|
|
63
|
+
* export const Seed = Archil.Disk("Seed", {});
|
|
64
|
+
* export const Base = Effect.gen(function* () {
|
|
65
|
+
* const seed = yield* Seed;
|
|
66
|
+
* return yield* Archil.Checkpoint("Base", {
|
|
67
|
+
* disk: seed.diskId,
|
|
68
|
+
* name: "base",
|
|
69
|
+
* run: [
|
|
70
|
+
* "git clone --depth 1 https://github.com/alchemy-run/alchemy repo",
|
|
71
|
+
* "cd repo && corepack enable && pnpm install --frozen-lockfile",
|
|
72
|
+
* ],
|
|
73
|
+
* });
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* @resource
|
|
78
|
+
* @product Disk
|
|
79
|
+
*/
|
|
80
|
+
export const Checkpoint = Resource<Checkpoint>("Archil.Checkpoint");
|
|
81
|
+
|
|
82
|
+
const checkpointName = (id: string, name: string | undefined) =>
|
|
83
|
+
name ? Effect.succeed(name) : createPhysicalName({ id, maxLength: 63, delimiter: "-" });
|
|
84
|
+
|
|
85
|
+
const shellQuote = (value: string) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
86
|
+
|
|
87
|
+
/** Seed a disk and take a checkpoint of it in one serverless exec. */
|
|
88
|
+
export const takeCheckpoint = Effect.fn(function* (
|
|
89
|
+
diskId: string,
|
|
90
|
+
name: string,
|
|
91
|
+
run: ReadonlyArray<string>,
|
|
92
|
+
) {
|
|
93
|
+
const script = [
|
|
94
|
+
"set -e",
|
|
95
|
+
"cd /mnt/archil",
|
|
96
|
+
...run.map((command) => `(${command})`),
|
|
97
|
+
"sync",
|
|
98
|
+
`archil checkpoints create /mnt/archil ${shellQuote(name)}`,
|
|
99
|
+
].join("\n");
|
|
100
|
+
const { data } = yield* archil.execDisk({ id: diskId, command: script });
|
|
101
|
+
if (data.exitCode !== 0) {
|
|
102
|
+
return yield* new CheckpointSetupFailed({
|
|
103
|
+
checkpoint: name,
|
|
104
|
+
exitCode: data.exitCode,
|
|
105
|
+
stderr: data.stderr,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return { diskId, name } satisfies CheckpointAttributes;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export const CheckpointProvider = () =>
|
|
112
|
+
Provider.effect(
|
|
113
|
+
Checkpoint,
|
|
114
|
+
Effect.gen(function* () {
|
|
115
|
+
return {
|
|
116
|
+
stables: ["diskId", "name"],
|
|
117
|
+
// No list or delete API: checkpoints go with their disk.
|
|
118
|
+
nuke: { skip: true },
|
|
119
|
+
diff: Effect.fn(function* ({ id, olds, news, output }) {
|
|
120
|
+
if (!isResolved(news) || olds === undefined) return undefined;
|
|
121
|
+
const name = output?.name ?? (yield* checkpointName(id, olds.name));
|
|
122
|
+
if (
|
|
123
|
+
news.disk !== olds.disk ||
|
|
124
|
+
(news.name !== undefined && news.name !== name) ||
|
|
125
|
+
!deepEqual(news.run ?? [], olds.run ?? [])
|
|
126
|
+
) {
|
|
127
|
+
return { action: "replace" } as const;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}),
|
|
131
|
+
// Checkpoints have no read API; state is authoritative.
|
|
132
|
+
read: Effect.fn(function* ({ output }) {
|
|
133
|
+
return output;
|
|
134
|
+
}),
|
|
135
|
+
reconcile: Effect.fn(function* ({ id, news, output, session }) {
|
|
136
|
+
if (output && output.diskId === news.disk) return output;
|
|
137
|
+
const name = yield* checkpointName(id, news.name);
|
|
138
|
+
yield* session.note(`Seeding disk and taking checkpoint ${name}...`);
|
|
139
|
+
return yield* takeCheckpoint(news.disk, name, news.run ?? []);
|
|
140
|
+
}),
|
|
141
|
+
// Archil has no checkpoint delete API.
|
|
142
|
+
delete: () => Effect.void,
|
|
143
|
+
};
|
|
144
|
+
}),
|
|
145
|
+
);
|