@alchemy.run/daytona 0.0.0 → 2.0.0-beta.86
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 +76 -4
- package/lib/ApiKey.d.ts +99 -0
- package/lib/ApiKey.d.ts.map +1 -0
- package/lib/ApiKey.js +152 -0
- package/lib/ApiKey.js.map +1 -0
- package/lib/AuthProvider.d.ts +55 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +280 -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 +28 -0
- package/lib/Bootstrap.js.map +1 -0
- package/lib/ConnectSandbox.d.ts +72 -0
- package/lib/ConnectSandbox.d.ts.map +1 -0
- package/lib/ConnectSandbox.js +3 -0
- package/lib/ConnectSandbox.js.map +1 -0
- package/lib/ConnectSandboxHttp.d.ts +5 -0
- package/lib/ConnectSandboxHttp.d.ts.map +1 -0
- package/lib/ConnectSandboxHttp.js +22 -0
- package/lib/ConnectSandboxHttp.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/CreateSandbox.d.ts +86 -0
- package/lib/CreateSandbox.d.ts.map +1 -0
- package/lib/CreateSandbox.js +3 -0
- package/lib/CreateSandbox.js.map +1 -0
- package/lib/CreateSandboxHttp.d.ts +5 -0
- package/lib/CreateSandboxHttp.d.ts.map +1 -0
- package/lib/CreateSandboxHttp.js +65 -0
- package/lib/CreateSandboxHttp.js.map +1 -0
- package/lib/CreateSecret.d.ts +56 -0
- package/lib/CreateSecret.d.ts.map +1 -0
- package/lib/CreateSecret.js +3 -0
- package/lib/CreateSecret.js.map +1 -0
- package/lib/CreateSecretHttp.d.ts +5 -0
- package/lib/CreateSecretHttp.d.ts.map +1 -0
- package/lib/CreateSecretHttp.js +39 -0
- package/lib/CreateSecretHttp.js.map +1 -0
- package/lib/CreateSnapshot.d.ts +80 -0
- package/lib/CreateSnapshot.d.ts.map +1 -0
- package/lib/CreateSnapshot.js +3 -0
- package/lib/CreateSnapshot.js.map +1 -0
- package/lib/CreateSnapshotHttp.d.ts +5 -0
- package/lib/CreateSnapshotHttp.d.ts.map +1 -0
- package/lib/CreateSnapshotHttp.js +47 -0
- package/lib/CreateSnapshotHttp.js.map +1 -0
- package/lib/CreateVolume.d.ts +81 -0
- package/lib/CreateVolume.d.ts.map +1 -0
- package/lib/CreateVolume.js +10 -0
- package/lib/CreateVolume.js.map +1 -0
- package/lib/CreateVolumeHttp.d.ts +5 -0
- package/lib/CreateVolumeHttp.d.ts.map +1 -0
- package/lib/CreateVolumeHttp.js +31 -0
- package/lib/CreateVolumeHttp.js.map +1 -0
- package/lib/Credentials.d.ts +9 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +18 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/DaytonaBindings.d.ts +51 -0
- package/lib/DaytonaBindings.d.ts.map +1 -0
- package/lib/DaytonaBindings.js +61 -0
- package/lib/DaytonaBindings.js.map +1 -0
- package/lib/IdentityProvider.d.ts +80 -0
- package/lib/IdentityProvider.d.ts.map +1 -0
- package/lib/IdentityProvider.js +111 -0
- package/lib/IdentityProvider.js.map +1 -0
- package/lib/MountVolume.d.ts +86 -0
- package/lib/MountVolume.d.ts.map +1 -0
- package/lib/MountVolume.js +27 -0
- package/lib/MountVolume.js.map +1 -0
- package/lib/OAuthClient.d.ts +14 -0
- package/lib/OAuthClient.d.ts.map +1 -0
- package/lib/OAuthClient.js +23 -0
- package/lib/OAuthClient.js.map +1 -0
- package/lib/Organization.d.ts +40 -0
- package/lib/Organization.d.ts.map +1 -0
- package/lib/Organization.js +80 -0
- package/lib/Organization.js.map +1 -0
- package/lib/OrganizationInvitation.d.ts +70 -0
- package/lib/OrganizationInvitation.d.ts.map +1 -0
- package/lib/OrganizationInvitation.js +109 -0
- package/lib/OrganizationInvitation.js.map +1 -0
- package/lib/OrganizationMember.d.ts +86 -0
- package/lib/OrganizationMember.d.ts.map +1 -0
- package/lib/OrganizationMember.js +109 -0
- package/lib/OrganizationMember.js.map +1 -0
- package/lib/OrganizationOtelConfig.d.ts +59 -0
- package/lib/OrganizationOtelConfig.d.ts.map +1 -0
- package/lib/OrganizationOtelConfig.js +87 -0
- package/lib/OrganizationOtelConfig.js.map +1 -0
- package/lib/OrganizationRole.d.ts +66 -0
- package/lib/OrganizationRole.d.ts.map +1 -0
- package/lib/OrganizationRole.js +106 -0
- package/lib/OrganizationRole.js.map +1 -0
- package/lib/OrganizationSettings.d.ts +76 -0
- package/lib/OrganizationSettings.d.ts.map +1 -0
- package/lib/OrganizationSettings.js +107 -0
- package/lib/OrganizationSettings.js.map +1 -0
- package/lib/Providers.d.ts +14 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +53 -0
- package/lib/Providers.js.map +1 -0
- package/lib/ReadAuditLogs.d.ts +30 -0
- package/lib/ReadAuditLogs.d.ts.map +1 -0
- package/lib/ReadAuditLogs.js +17 -0
- package/lib/ReadAuditLogs.js.map +1 -0
- package/lib/ReadOrganization.d.ts +39 -0
- package/lib/ReadOrganization.d.ts.map +1 -0
- package/lib/ReadOrganization.js +39 -0
- package/lib/ReadOrganization.js.map +1 -0
- package/lib/ReadUsage.d.ts +31 -0
- package/lib/ReadUsage.d.ts.map +1 -0
- package/lib/ReadUsage.js +17 -0
- package/lib/ReadUsage.js.map +1 -0
- package/lib/Region.d.ts +78 -0
- package/lib/Region.d.ts.map +1 -0
- package/lib/Region.js +169 -0
- package/lib/Region.js.map +1 -0
- package/lib/Registry.d.ts +84 -0
- package/lib/Registry.d.ts.map +1 -0
- package/lib/Registry.js +97 -0
- package/lib/Registry.js.map +1 -0
- package/lib/Runner.d.ts +66 -0
- package/lib/Runner.d.ts.map +1 -0
- package/lib/Runner.js +116 -0
- package/lib/Runner.js.map +1 -0
- package/lib/Sandbox.d.ts +376 -0
- package/lib/Sandbox.d.ts.map +1 -0
- package/lib/Sandbox.js +565 -0
- package/lib/Sandbox.js.map +1 -0
- package/lib/SandboxClient.d.ts +258 -0
- package/lib/SandboxClient.d.ts.map +1 -0
- package/lib/SandboxClient.js +233 -0
- package/lib/SandboxClient.js.map +1 -0
- package/lib/Secret.d.ts +93 -0
- package/lib/Secret.d.ts.map +1 -0
- package/lib/Secret.js +130 -0
- package/lib/Secret.js.map +1 -0
- package/lib/Snapshot.d.ts +327 -0
- package/lib/Snapshot.d.ts.map +1 -0
- package/lib/Snapshot.js +447 -0
- package/lib/Snapshot.js.map +1 -0
- package/lib/Volume.d.ts +99 -0
- package/lib/Volume.d.ts.map +1 -0
- package/lib/Volume.js +152 -0
- package/lib/Volume.js.map +1 -0
- package/lib/WarmPool.d.ts +61 -0
- package/lib/WarmPool.d.ts.map +1 -0
- package/lib/WarmPool.js +81 -0
- package/lib/WarmPool.js.map +1 -0
- package/lib/Webhooks.d.ts +58 -0
- package/lib/Webhooks.d.ts.map +1 -0
- package/lib/Webhooks.js +75 -0
- package/lib/Webhooks.js.map +1 -0
- package/lib/index.d.ts +37 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +36 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/BuildContext.d.ts +16 -0
- package/lib/internal/BuildContext.d.ts.map +1 -0
- package/lib/internal/BuildContext.js +50 -0
- package/lib/internal/BuildContext.js.map +1 -0
- package/lib/internal/Declared.d.ts +29 -0
- package/lib/internal/Declared.d.ts.map +1 -0
- package/lib/internal/Declared.js +31 -0
- package/lib/internal/Declared.js.map +1 -0
- package/lib/internal/Derived.d.ts +40 -0
- package/lib/internal/Derived.d.ts.map +1 -0
- package/lib/internal/Derived.js +64 -0
- package/lib/internal/Derived.js.map +1 -0
- package/lib/internal/Errors.d.ts +4 -0
- package/lib/internal/Errors.d.ts.map +1 -0
- package/lib/internal/Errors.js +1 -0
- package/lib/internal/Errors.js.map +1 -0
- package/lib/internal/Labels.d.ts +20 -0
- package/lib/internal/Labels.d.ts.map +1 -0
- package/lib/internal/Labels.js +22 -0
- package/lib/internal/Labels.js.map +1 -0
- package/lib/internal/Names.d.ts +18 -0
- package/lib/internal/Names.d.ts.map +1 -0
- package/lib/internal/Names.js +25 -0
- package/lib/internal/Names.js.map +1 -0
- package/lib/internal/Organization.d.ts +27 -0
- package/lib/internal/Organization.d.ts.map +1 -0
- package/lib/internal/Organization.js +32 -0
- package/lib/internal/Organization.js.map +1 -0
- package/lib/internal/Process.d.ts +14 -0
- package/lib/internal/Process.d.ts.map +1 -0
- package/lib/internal/Process.js +15 -0
- package/lib/internal/Process.js.map +1 -0
- package/lib/internal/Program.d.ts +83 -0
- package/lib/internal/Program.d.ts.map +1 -0
- package/lib/internal/Program.js +169 -0
- package/lib/internal/Program.js.map +1 -0
- package/lib/internal/Ref.d.ts +14 -0
- package/lib/internal/Ref.d.ts.map +1 -0
- package/lib/internal/Ref.js +7 -0
- package/lib/internal/Ref.js.map +1 -0
- package/lib/internal/Scoped.d.ts +31 -0
- package/lib/internal/Scoped.d.ts.map +1 -0
- package/lib/internal/Scoped.js +48 -0
- package/lib/internal/Scoped.js.map +1 -0
- package/lib/internal/Tar.d.ts +13 -0
- package/lib/internal/Tar.d.ts.map +1 -0
- package/lib/internal/Tar.js +62 -0
- package/lib/internal/Tar.js.map +1 -0
- package/lib/internal/Toolbox.d.ts +11 -0
- package/lib/internal/Toolbox.d.ts.map +1 -0
- package/lib/internal/Toolbox.js +20 -0
- package/lib/internal/Toolbox.js.map +1 -0
- package/lib/internal/Wait.d.ts +49 -0
- package/lib/internal/Wait.d.ts.map +1 -0
- package/lib/internal/Wait.js +57 -0
- package/lib/internal/Wait.js.map +1 -0
- package/package.json +64 -3
- package/src/ApiKey.ts +221 -0
- package/src/AuthProvider.ts +406 -0
- package/src/Bootstrap.ts +55 -0
- package/src/ConnectSandbox.ts +76 -0
- package/src/ConnectSandboxHttp.ts +26 -0
- package/src/Contributions.ts +9 -0
- package/src/CreateSandbox.ts +107 -0
- package/src/CreateSandboxHttp.ts +97 -0
- package/src/CreateSecret.ts +73 -0
- package/src/CreateSecretHttp.ts +58 -0
- package/src/CreateSnapshot.ts +133 -0
- package/src/CreateSnapshotHttp.ts +77 -0
- package/src/CreateVolume.ts +104 -0
- package/src/CreateVolumeHttp.ts +50 -0
- package/src/Credentials.ts +37 -0
- package/src/DaytonaBindings.ts +111 -0
- package/src/IdentityProvider.ts +181 -0
- package/src/MountVolume.ts +122 -0
- package/src/OAuthClient.ts +27 -0
- package/src/Organization.ts +121 -0
- package/src/OrganizationInvitation.ts +174 -0
- package/src/OrganizationMember.ts +174 -0
- package/src/OrganizationOtelConfig.ts +138 -0
- package/src/OrganizationRole.ts +167 -0
- package/src/OrganizationSettings.ts +187 -0
- package/src/Providers.ts +93 -0
- package/src/ReadAuditLogs.ts +50 -0
- package/src/ReadOrganization.ts +83 -0
- package/src/ReadUsage.ts +51 -0
- package/src/Region.ts +226 -0
- package/src/Registry.ts +164 -0
- package/src/Runner.ts +166 -0
- package/src/Sandbox.ts +927 -0
- package/src/SandboxClient.ts +625 -0
- package/src/Secret.ts +203 -0
- package/src/Snapshot.ts +685 -0
- package/src/Volume.ts +229 -0
- package/src/WarmPool.ts +128 -0
- package/src/Webhooks.ts +121 -0
- package/src/globals.d.ts +2 -0
- package/src/index.ts +46 -0
- package/src/internal/BuildContext.ts +62 -0
- package/src/internal/Declared.ts +80 -0
- package/src/internal/Derived.ts +97 -0
- package/src/internal/Errors.ts +4 -0
- package/src/internal/Labels.ts +39 -0
- package/src/internal/Names.ts +37 -0
- package/src/internal/Organization.ts +40 -0
- package/src/internal/Process.ts +25 -0
- package/src/internal/Program.ts +239 -0
- package/src/internal/Ref.ts +15 -0
- package/src/internal/Scoped.ts +102 -0
- package/src/internal/Tar.ts +72 -0
- package/src/internal/Toolbox.ts +27 -0
- package/src/internal/Wait.ts +105 -0
package/src/Volume.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import * as volumes from "@distilled.cloud/daytona/volumes";
|
|
2
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
3
|
+
import { isResolved } from "alchemy/Diff";
|
|
4
|
+
import * as Provider from "alchemy/Provider";
|
|
5
|
+
import { Resource } from "alchemy/Resource";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Schedule from "effect/Schedule";
|
|
8
|
+
import { isOwnedName, resolveName } from "./internal/Names.ts";
|
|
9
|
+
import { waitForState, waitUntilGone } from "./internal/Wait.ts";
|
|
10
|
+
import type { Providers } from "./Providers.ts";
|
|
11
|
+
|
|
12
|
+
export interface VolumeProps {
|
|
13
|
+
/**
|
|
14
|
+
* Volume name, unique in the organization. If omitted, a unique name is
|
|
15
|
+
* generated from the stack, stage and logical ID. Changing it replaces
|
|
16
|
+
* the volume (and its files).
|
|
17
|
+
*/
|
|
18
|
+
name?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface VolumeAttributes {
|
|
22
|
+
/** Volume ID. */
|
|
23
|
+
volumeId: string;
|
|
24
|
+
/** Volume name. */
|
|
25
|
+
name: string;
|
|
26
|
+
/** Volume state (`ready` once usable). */
|
|
27
|
+
state: volumes.VolumeState;
|
|
28
|
+
/** When the volume was created. */
|
|
29
|
+
createdAt: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type Volume = Resource<
|
|
33
|
+
"Daytona.Volume",
|
|
34
|
+
VolumeProps,
|
|
35
|
+
VolumeAttributes,
|
|
36
|
+
never,
|
|
37
|
+
Providers,
|
|
38
|
+
VolumeLookup
|
|
39
|
+
>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Props for {@link Volume.lookup}: find a volume this stack
|
|
43
|
+
* doesn't manage by its name.
|
|
44
|
+
*/
|
|
45
|
+
export interface VolumeLookup {
|
|
46
|
+
/** Volume name. */
|
|
47
|
+
name: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A Daytona volume: a persistent disk, backed by object storage, that
|
|
52
|
+
* sandboxes mount at a path. Its files outlive every sandbox that mounts
|
|
53
|
+
* it, and several sandboxes can mount it at once.
|
|
54
|
+
*
|
|
55
|
+
* Declare a volume here for data your whole app shares, like a dataset or
|
|
56
|
+
* a package cache. For a volume per user or per session, create it at
|
|
57
|
+
* runtime with {@link CreateVolume}.
|
|
58
|
+
* @see https://www.daytona.io/docs/en/volumes/
|
|
59
|
+
*
|
|
60
|
+
* ### Creating a Volume
|
|
61
|
+
* **Example:** A shared cache volume
|
|
62
|
+
* ```typescript
|
|
63
|
+
* export const Cache = Daytona.Volume("Cache", {});
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* ### Mounting a Volume
|
|
67
|
+
* **Example:** Mount it in a sandbox declared at deploy
|
|
68
|
+
* ```typescript
|
|
69
|
+
* export const DevBox = Daytona.Sandbox("DevBox", {
|
|
70
|
+
* snapshot: Base,
|
|
71
|
+
* mounts: [{ volume: Cache, path: "/cache" }],
|
|
72
|
+
* });
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* **Example:** Mount it in sandboxes created at runtime
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const cache = yield* Daytona.MountVolume(Cache, { path: "/cache" });
|
|
78
|
+
* const sandbox = yield* sandboxes.create({ mounts: [cache] });
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* ### Looking up an existing volume
|
|
82
|
+
* `Volume.lookup` references a volume Alchemy doesn't manage (created in
|
|
83
|
+
* the Daytona dashboard or by another stack). It is read during plan/deploy,
|
|
84
|
+
* never created, updated or deleted.
|
|
85
|
+
*
|
|
86
|
+
* **Example:** Mount an existing volume
|
|
87
|
+
* ```typescript
|
|
88
|
+
* const data = Daytona.Volume.lookup({ name: "datasets" });
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @resource
|
|
92
|
+
* @product Volume
|
|
93
|
+
*/
|
|
94
|
+
export const Volume = Resource<Volume>("Daytona.Volume");
|
|
95
|
+
|
|
96
|
+
export const toVolumeAttributes = (v: volumes.VolumeDto): VolumeAttributes => ({
|
|
97
|
+
volumeId: v.id,
|
|
98
|
+
name: v.name,
|
|
99
|
+
state: v.state,
|
|
100
|
+
createdAt: v.createdAt,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const isLive = (v: volumes.VolumeDto) =>
|
|
104
|
+
v.state !== "deleted" && v.state !== "deleting" && v.state !== "pending_delete";
|
|
105
|
+
|
|
106
|
+
export const getVolumeByName = (name: string) =>
|
|
107
|
+
volumes.getVolumeByName({ name }).pipe(
|
|
108
|
+
Effect.map((v) => (isLive(v) ? v : undefined)),
|
|
109
|
+
Effect.catchTag("VolumeNotFound", () => Effect.undefined),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
export const getVolumeById = (volumeId: string) =>
|
|
113
|
+
volumes.getVolume({ volumeId }).pipe(
|
|
114
|
+
Effect.map((v) => (isLive(v) ? v : undefined)),
|
|
115
|
+
Effect.catchTag("VolumeNotFound", () => Effect.undefined),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
export const waitForVolume = (volumeId: string) =>
|
|
119
|
+
waitForState({
|
|
120
|
+
kind: "volume",
|
|
121
|
+
id: volumeId,
|
|
122
|
+
observe: volumes.getVolume({ volumeId }),
|
|
123
|
+
want: ["ready"],
|
|
124
|
+
fail: ["error"],
|
|
125
|
+
reason: (v) => v.errorReason,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The volume named `name`, created if missing. A create racing another
|
|
130
|
+
* caller (`VolumeAlreadyExists`) re-reads the winner.
|
|
131
|
+
*/
|
|
132
|
+
export const ensureVolume = Effect.fn(function* (name: string) {
|
|
133
|
+
const existing = yield* getVolumeByName(name);
|
|
134
|
+
const volume =
|
|
135
|
+
existing ??
|
|
136
|
+
(yield* volumes
|
|
137
|
+
.createVolume({ name })
|
|
138
|
+
.pipe(Effect.catchTag("VolumeAlreadyExists", () => volumes.getVolumeByName({ name }))));
|
|
139
|
+
return volume.state === "ready" ? volume : yield* waitForVolume(volume.id);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Delete a volume. With `wait` (deploy), retry while a sandbox still
|
|
144
|
+
* mounts it and wait until it is gone. At run time `wait` is off: a
|
|
145
|
+
* request fails at once with `VolumeInUse` and returns as soon as Daytona
|
|
146
|
+
* accepts the delete. Daytona releases a volume about a minute after the
|
|
147
|
+
* sandbox that wrote to it is deleted, which is longer than a request
|
|
148
|
+
* should wait.
|
|
149
|
+
*/
|
|
150
|
+
export const deleteVolumeById = Effect.fn(function* (
|
|
151
|
+
volumeId: string,
|
|
152
|
+
options: { readonly wait: boolean } = { wait: true },
|
|
153
|
+
) {
|
|
154
|
+
yield* volumes.deleteVolume({ volumeId }).pipe(
|
|
155
|
+
Effect.catchTags({
|
|
156
|
+
VolumeNotFound: () => Effect.void,
|
|
157
|
+
// Refused while the volume is still being created (wait for it) or is
|
|
158
|
+
// already being deleted (nothing left to do).
|
|
159
|
+
VolumeNotReady: (error) =>
|
|
160
|
+
Effect.flatMap(getVolumeById(volumeId), (live) =>
|
|
161
|
+
live === undefined ? Effect.void : Effect.fail(error),
|
|
162
|
+
),
|
|
163
|
+
}),
|
|
164
|
+
// VolumeInUse: a sandbox still mounts it, e.g. while a destroy deletes
|
|
165
|
+
// the sandboxes alongside; it frees up once they are gone.
|
|
166
|
+
Effect.retry({
|
|
167
|
+
while: (e) => e._tag === "VolumeNotReady" || (options.wait && e._tag === "VolumeInUse"),
|
|
168
|
+
schedule: Schedule.spaced("2 seconds"),
|
|
169
|
+
times: 30,
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
if (!options.wait) return;
|
|
173
|
+
yield* waitUntilGone(
|
|
174
|
+
"volume",
|
|
175
|
+
volumeId,
|
|
176
|
+
getVolumeById(volumeId).pipe(Effect.map((v) => v !== undefined)),
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export const VolumeProvider = () =>
|
|
181
|
+
Provider.effect(
|
|
182
|
+
Volume,
|
|
183
|
+
Effect.succeed({
|
|
184
|
+
stables: ["volumeId", "createdAt"],
|
|
185
|
+
list: Effect.fn(function* () {
|
|
186
|
+
const all = yield* volumes.listVolumes({});
|
|
187
|
+
return all.filter(isLive).map(toVolumeAttributes);
|
|
188
|
+
}),
|
|
189
|
+
diff: Effect.fn(function* ({ id, news, output }) {
|
|
190
|
+
if (!isResolved(news) || output === undefined) return undefined;
|
|
191
|
+
const name = yield* resolveName(
|
|
192
|
+
id,
|
|
193
|
+
news.name,
|
|
194
|
+
news.name === undefined ? output.name : undefined,
|
|
195
|
+
);
|
|
196
|
+
if (name !== output.name) return { action: "replace", deleteFirst: true } as const;
|
|
197
|
+
}),
|
|
198
|
+
lookup: Effect.fn(function* ({ name }) {
|
|
199
|
+
const found = yield* getVolumeByName(name);
|
|
200
|
+
return found === undefined ? undefined : toVolumeAttributes(found);
|
|
201
|
+
}),
|
|
202
|
+
|
|
203
|
+
read: Effect.fn(function* ({ id, olds, output }) {
|
|
204
|
+
const name = yield* resolveName(id, olds?.name, output?.name);
|
|
205
|
+
const found =
|
|
206
|
+
(output ? yield* getVolumeById(output.volumeId) : undefined) ??
|
|
207
|
+
(yield* getVolumeByName(name));
|
|
208
|
+
if (found === undefined) return undefined;
|
|
209
|
+
const attrs = toVolumeAttributes(found);
|
|
210
|
+
return (yield* isOwnedName(id, found.name, output !== undefined)) ? attrs : Unowned(attrs);
|
|
211
|
+
}),
|
|
212
|
+
reconcile: Effect.fn(function* ({ id, news, output }) {
|
|
213
|
+
// Observe by id first: the name may already be taken by a peer.
|
|
214
|
+
const name = yield* resolveName(id, news.name, output?.name);
|
|
215
|
+
const byId = output ? yield* getVolumeById(output.volumeId) : undefined;
|
|
216
|
+
// Ensure — volumes have nothing mutable beyond existence.
|
|
217
|
+
const volume =
|
|
218
|
+
byId && byId.name === name
|
|
219
|
+
? byId.state === "ready"
|
|
220
|
+
? byId
|
|
221
|
+
: yield* waitForVolume(byId.id)
|
|
222
|
+
: yield* ensureVolume(name);
|
|
223
|
+
return toVolumeAttributes(volume);
|
|
224
|
+
}),
|
|
225
|
+
delete: Effect.fn(function* ({ output }) {
|
|
226
|
+
yield* deleteVolumeById(output.volumeId);
|
|
227
|
+
}),
|
|
228
|
+
}),
|
|
229
|
+
);
|
package/src/WarmPool.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as warmPools from "@distilled.cloud/daytona/warm_pools";
|
|
2
|
+
import { isResolved } from "alchemy/Diff";
|
|
3
|
+
import * as Provider from "alchemy/Provider";
|
|
4
|
+
import { Resource } from "alchemy/Resource";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import { declarable } from "./internal/Declared.ts";
|
|
7
|
+
import { resolved, type Ref } from "./internal/Ref.ts";
|
|
8
|
+
import type { Providers } from "./Providers.ts";
|
|
9
|
+
|
|
10
|
+
/** A snapshot, by name. Accepts a `Daytona.Snapshot` or `{ name }`. */
|
|
11
|
+
export type WarmPoolSnapshot = { readonly name: string };
|
|
12
|
+
|
|
13
|
+
export interface WarmPoolProps {
|
|
14
|
+
/** Snapshot whose sandboxes are kept warm. Changing it replaces the pool. */
|
|
15
|
+
snapshot: Ref<WarmPoolSnapshot>;
|
|
16
|
+
/** How many started sandboxes to keep ready. Updated in place. */
|
|
17
|
+
size: number;
|
|
18
|
+
/** Region to keep them in. Changing it replaces the pool. */
|
|
19
|
+
target?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WarmPoolAttributes {
|
|
23
|
+
/** Warm pool ID. */
|
|
24
|
+
warmPoolId: string;
|
|
25
|
+
/** Snapshot name. */
|
|
26
|
+
snapshot: string;
|
|
27
|
+
/** Region. */
|
|
28
|
+
target: string;
|
|
29
|
+
/** Desired pool size. */
|
|
30
|
+
size: number;
|
|
31
|
+
/** Sandboxes currently ready in the pool. */
|
|
32
|
+
currentSize: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type WarmPool = Resource<
|
|
36
|
+
"Daytona.WarmPool",
|
|
37
|
+
WarmPoolProps,
|
|
38
|
+
WarmPoolAttributes,
|
|
39
|
+
never,
|
|
40
|
+
Providers
|
|
41
|
+
>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Keeps started sandboxes of a {@link Snapshot} ready, so creating a
|
|
45
|
+
* sandbox from that snapshot hands one over instead of booting a new one.
|
|
46
|
+
* Daytona uses the pool automatically for matching creates.
|
|
47
|
+
*
|
|
48
|
+
* Deleting the snapshot deletes its warm pools. Daytona Cloud doesn't
|
|
49
|
+
* serve warm pools to every organization; where it doesn't, deploying one
|
|
50
|
+
* fails with `WarmPoolsUnavailable`.
|
|
51
|
+
* @see https://www.daytona.io/docs/en/warm-pools/
|
|
52
|
+
*
|
|
53
|
+
* ### Keeping Sandboxes Warm
|
|
54
|
+
* **Example:** Four ready sandboxes of a snapshot
|
|
55
|
+
* ```typescript
|
|
56
|
+
* export const Pool = Daytona.WarmPool("Pool", { snapshot: Base, size: 4 });
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @resource
|
|
60
|
+
* @product Warm Pool
|
|
61
|
+
*/
|
|
62
|
+
export const WarmPool = declarable<WarmPool>()(Resource<WarmPool>("Daytona.WarmPool"));
|
|
63
|
+
|
|
64
|
+
const toAttributes = (p: warmPools.WarmPool): WarmPoolAttributes => ({
|
|
65
|
+
warmPoolId: p.id,
|
|
66
|
+
snapshot: p.snapshot,
|
|
67
|
+
target: p.target,
|
|
68
|
+
size: p.pool,
|
|
69
|
+
currentSize: p.currentSize,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const snapshotName = (snapshot: WarmPoolProps["snapshot"]) =>
|
|
73
|
+
Effect.succeed(resolved(snapshot).name);
|
|
74
|
+
|
|
75
|
+
export const WarmPoolProvider = () =>
|
|
76
|
+
Provider.effect(
|
|
77
|
+
WarmPool,
|
|
78
|
+
Effect.succeed({
|
|
79
|
+
stables: ["warmPoolId", "snapshot", "target"],
|
|
80
|
+
list: Effect.fn(function* () {
|
|
81
|
+
// Organizations Daytona doesn't serve warm pools to have none.
|
|
82
|
+
const pools = yield* warmPools
|
|
83
|
+
.listWarmPools({})
|
|
84
|
+
.pipe(Effect.catchTag("WarmPoolsUnavailable", () => Effect.succeed([])));
|
|
85
|
+
return pools.map(toAttributes);
|
|
86
|
+
}),
|
|
87
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
88
|
+
if (!isResolved(news) || output === undefined) return undefined;
|
|
89
|
+
const snapshot = yield* snapshotName(news.snapshot);
|
|
90
|
+
if (
|
|
91
|
+
snapshot !== output.snapshot ||
|
|
92
|
+
(news.target !== undefined && news.target !== output.target)
|
|
93
|
+
) {
|
|
94
|
+
return { action: "replace" } as const;
|
|
95
|
+
}
|
|
96
|
+
}),
|
|
97
|
+
read: Effect.fn(function* ({ output }) {
|
|
98
|
+
if (output === undefined) return undefined;
|
|
99
|
+
const found = (yield* warmPools.listWarmPools({})).find((p) => p.id === output.warmPoolId);
|
|
100
|
+
return found ? toAttributes(found) : undefined;
|
|
101
|
+
}),
|
|
102
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
103
|
+
const snapshot = yield* snapshotName(news.snapshot);
|
|
104
|
+
// Observe — a pool is identified by its snapshot and region.
|
|
105
|
+
const pools = yield* warmPools.listWarmPools({});
|
|
106
|
+
const current =
|
|
107
|
+
pools.find((p) => p.id === output?.warmPoolId) ??
|
|
108
|
+
pools.find(
|
|
109
|
+
(p) =>
|
|
110
|
+
p.snapshot === snapshot && (news.target === undefined || p.target === news.target),
|
|
111
|
+
);
|
|
112
|
+
// Ensure
|
|
113
|
+
if (current === undefined) {
|
|
114
|
+
return toAttributes(
|
|
115
|
+
yield* warmPools.createWarmPool({ snapshot, pool: news.size, target: news.target }),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
// Sync size
|
|
119
|
+
if (current.pool === news.size) return toAttributes(current);
|
|
120
|
+
return toAttributes(yield* warmPools.updateWarmPool({ id: current.id, pool: news.size }));
|
|
121
|
+
}),
|
|
122
|
+
delete: Effect.fn(function* ({ output }) {
|
|
123
|
+
yield* warmPools
|
|
124
|
+
.deleteWarmPool({ id: output.warmPoolId })
|
|
125
|
+
.pipe(Effect.catchTag("WarmPoolNotFound", () => Effect.void));
|
|
126
|
+
}),
|
|
127
|
+
}),
|
|
128
|
+
);
|
package/src/Webhooks.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as webhooks from "@distilled.cloud/daytona/webhooks";
|
|
2
|
+
import { isResolved } from "alchemy/Diff";
|
|
3
|
+
import * as Provider from "alchemy/Provider";
|
|
4
|
+
import { Resource } from "alchemy/Resource";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import { declarable } from "./internal/Declared.ts";
|
|
7
|
+
import {
|
|
8
|
+
movesOrganization,
|
|
9
|
+
organizationIdFor,
|
|
10
|
+
type OrganizationRef,
|
|
11
|
+
} from "./internal/Organization.ts";
|
|
12
|
+
import type { Ref } from "./internal/Ref.ts";
|
|
13
|
+
import { waitForState } from "./internal/Wait.ts";
|
|
14
|
+
import type { Providers } from "./Providers.ts";
|
|
15
|
+
|
|
16
|
+
export interface WebhooksProps {
|
|
17
|
+
/**
|
|
18
|
+
* Organization to act on, e.g. a `Daytona.Organization`. Defaults to the
|
|
19
|
+
* credentials' organization. Changing it replaces the resource.
|
|
20
|
+
*/
|
|
21
|
+
organization?: Ref<OrganizationRef>;
|
|
22
|
+
/**
|
|
23
|
+
* Bump to re-sync the organization's webhook endpoints with Daytona's
|
|
24
|
+
* event types on the next deploy.
|
|
25
|
+
*/
|
|
26
|
+
refresh?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WebhooksAttributes {
|
|
30
|
+
/** Organization ID. */
|
|
31
|
+
organizationId: string;
|
|
32
|
+
/** The Svix application Daytona delivers events through. */
|
|
33
|
+
svixApplicationId: string;
|
|
34
|
+
/** Refresh counter applied. */
|
|
35
|
+
refreshed: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type Webhooks = Resource<
|
|
39
|
+
"Daytona.Webhooks",
|
|
40
|
+
WebhooksProps,
|
|
41
|
+
WebhooksAttributes,
|
|
42
|
+
never,
|
|
43
|
+
Providers
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Turn on webhooks for the Daytona organization: sandbox, snapshot and
|
|
48
|
+
* volume events delivered to endpoints you register in Daytona's webhook
|
|
49
|
+
* portal. Daytona delivers through Svix, which owns the endpoints, so this
|
|
50
|
+
* resource only enables delivery; open the portal with
|
|
51
|
+
* `getWebhookControllerAppPortalAccess` (on {@link ReadOrganization}) to
|
|
52
|
+
* add endpoints. Webhooks can't be turned off again through the API, so
|
|
53
|
+
* destroy leaves them enabled.
|
|
54
|
+
* @see https://www.daytona.io/docs/en/webhooks/
|
|
55
|
+
*
|
|
56
|
+
* ### Enabling Webhooks
|
|
57
|
+
* **Example:** Enable delivery for the organization
|
|
58
|
+
* ```typescript
|
|
59
|
+
* export const Events = Daytona.Webhooks("Events", {});
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @resource
|
|
63
|
+
* @product Organization
|
|
64
|
+
*/
|
|
65
|
+
export const Webhooks = declarable<Webhooks>()(Resource<Webhooks>("Daytona.Webhooks"));
|
|
66
|
+
|
|
67
|
+
const status = (organizationId: string) =>
|
|
68
|
+
webhooks.getWebhookControllerInitializationStatus({ organizationId }).pipe(
|
|
69
|
+
Effect.map((s) => ({
|
|
70
|
+
...s,
|
|
71
|
+
state: s.svixApplicationId ? "ready" : s.lastError ? "error" : "pending",
|
|
72
|
+
})),
|
|
73
|
+
// Before initialization Daytona has no status to report.
|
|
74
|
+
Effect.catchTag("WebhooksNotInitialized", () =>
|
|
75
|
+
Effect.succeed({ svixApplicationId: undefined, lastError: undefined, state: "pending" }),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
export const WebhooksProvider = () =>
|
|
80
|
+
Provider.effect(
|
|
81
|
+
Webhooks,
|
|
82
|
+
Effect.succeed({
|
|
83
|
+
stables: ["organizationId", "svixApplicationId"],
|
|
84
|
+
nuke: { singleton: true },
|
|
85
|
+
list: () => Effect.succeed([]),
|
|
86
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
87
|
+
if (!isResolved(news) || output === undefined) return undefined;
|
|
88
|
+
if (yield* movesOrganization(news.organization, output))
|
|
89
|
+
return { action: "replace" } as const;
|
|
90
|
+
}),
|
|
91
|
+
reconcile: Effect.fn(function* ({ news, output }) {
|
|
92
|
+
const organizationId = yield* organizationIdFor(news.organization);
|
|
93
|
+
// Observe
|
|
94
|
+
let current = yield* status(organizationId);
|
|
95
|
+
// Ensure
|
|
96
|
+
if (current.svixApplicationId == null) {
|
|
97
|
+
yield* webhooks.webhookControllerInitializeWebhooks({ organizationId });
|
|
98
|
+
current = yield* waitForState({
|
|
99
|
+
kind: "webhooks",
|
|
100
|
+
id: organizationId,
|
|
101
|
+
observe: status(organizationId),
|
|
102
|
+
want: ["ready"],
|
|
103
|
+
fail: ["error"],
|
|
104
|
+
reason: (s) => s.lastError,
|
|
105
|
+
budget: "2 minutes",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// Sync
|
|
109
|
+
const refresh = news.refresh ?? 0;
|
|
110
|
+
if (output !== undefined && refresh !== output.refreshed) {
|
|
111
|
+
yield* webhooks.refreshWebhookControllerEndpoints({ organizationId });
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
organizationId,
|
|
115
|
+
svixApplicationId: current.svixApplicationId ?? "",
|
|
116
|
+
refreshed: refresh,
|
|
117
|
+
};
|
|
118
|
+
}),
|
|
119
|
+
delete: () => Effect.void,
|
|
120
|
+
}),
|
|
121
|
+
);
|
package/src/globals.d.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export * from "./ApiKey.ts";
|
|
2
|
+
export * from "./AuthProvider.ts";
|
|
3
|
+
export * from "./ConnectSandbox.ts";
|
|
4
|
+
export * from "./ConnectSandboxHttp.ts";
|
|
5
|
+
export * from "./CreateSandbox.ts";
|
|
6
|
+
export * from "./CreateSandboxHttp.ts";
|
|
7
|
+
export * from "./CreateSecret.ts";
|
|
8
|
+
export * from "./CreateSecretHttp.ts";
|
|
9
|
+
export * from "./CreateSnapshot.ts";
|
|
10
|
+
export * from "./CreateSnapshotHttp.ts";
|
|
11
|
+
export * from "./CreateVolume.ts";
|
|
12
|
+
export * from "./CreateVolumeHttp.ts";
|
|
13
|
+
export * from "./Credentials.ts";
|
|
14
|
+
export * from "./DaytonaBindings.ts";
|
|
15
|
+
export * from "./IdentityProvider.ts";
|
|
16
|
+
export * from "./MountVolume.ts";
|
|
17
|
+
export * from "./Organization.ts";
|
|
18
|
+
export * from "./OrganizationInvitation.ts";
|
|
19
|
+
export * from "./OrganizationMember.ts";
|
|
20
|
+
export * from "./OrganizationOtelConfig.ts";
|
|
21
|
+
export * from "./OrganizationRole.ts";
|
|
22
|
+
export * from "./OrganizationSettings.ts";
|
|
23
|
+
export * from "./Providers.ts";
|
|
24
|
+
export * from "./ReadAuditLogs.ts";
|
|
25
|
+
export * from "./ReadOrganization.ts";
|
|
26
|
+
export * from "./ReadUsage.ts";
|
|
27
|
+
export * from "./Region.ts";
|
|
28
|
+
export * from "./Registry.ts";
|
|
29
|
+
export * from "./Runner.ts";
|
|
30
|
+
export * from "./Sandbox.ts";
|
|
31
|
+
export * from "./Secret.ts";
|
|
32
|
+
export * from "./Snapshot.ts";
|
|
33
|
+
export * from "./Volume.ts";
|
|
34
|
+
export * from "./WarmPool.ts";
|
|
35
|
+
export * from "./Webhooks.ts";
|
|
36
|
+
export type {
|
|
37
|
+
Command,
|
|
38
|
+
ConnectedSandbox,
|
|
39
|
+
DaytonaSandbox,
|
|
40
|
+
ExecOptions,
|
|
41
|
+
ExecResult,
|
|
42
|
+
PtyOpenOptions,
|
|
43
|
+
SandboxFiles,
|
|
44
|
+
SandboxProcess,
|
|
45
|
+
SandboxPty,
|
|
46
|
+
} from "./SandboxClient.ts";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as BuildPlan from "@alchemy.run/docker/BuildPlan";
|
|
3
|
+
import { fromCredentials } from "@distilled.cloud/aws/Credentials";
|
|
4
|
+
import * as AwsEndpoint from "@distilled.cloud/aws/Endpoint";
|
|
5
|
+
import type { RegionName } from "@distilled.cloud/aws/Region";
|
|
6
|
+
import * as s3 from "@distilled.cloud/aws/s3";
|
|
7
|
+
import * as objectStorage from "@distilled.cloud/daytona/object_storage";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import * as Layer from "effect/Layer";
|
|
10
|
+
import * as Redacted from "effect/Redacted";
|
|
11
|
+
import { tar } from "./Tar.ts";
|
|
12
|
+
|
|
13
|
+
const redact = (value: string | Redacted.Redacted<string>) =>
|
|
14
|
+
Redacted.isRedacted(value) ? value : Redacted.make(value);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Upload a plan's context files to Daytona's build-context storage and
|
|
18
|
+
* return the hash `buildInfo.contextHashes` takes.
|
|
19
|
+
*
|
|
20
|
+
* Daytona's builders read each context from
|
|
21
|
+
* `<organizationId>/<hash>/context.tar` in the bucket `getPushAccess`
|
|
22
|
+
* names, with the temporary credentials it returns (the layout Daytona's
|
|
23
|
+
* own SDKs use). The files go in one archive at their context paths; an
|
|
24
|
+
* archive already uploaded under the same hash is reused.
|
|
25
|
+
*/
|
|
26
|
+
export const uploadBuildContext = Effect.fn(function* (plan: BuildPlan.BuildPlan) {
|
|
27
|
+
const hash = yield* Effect.sync(() =>
|
|
28
|
+
createHash("sha256")
|
|
29
|
+
.update(JSON.stringify(plan.files.map((f) => [f.path, f.mode, f.sha256])))
|
|
30
|
+
.digest("hex")
|
|
31
|
+
.slice(0, 32),
|
|
32
|
+
);
|
|
33
|
+
const access = yield* objectStorage.getPushAccess({});
|
|
34
|
+
const prefix = `${access.organizationId}/${hash}/`;
|
|
35
|
+
const storage = Layer.mergeAll(
|
|
36
|
+
fromCredentials(
|
|
37
|
+
{
|
|
38
|
+
accessKeyId: Redacted.make(access.accessKey),
|
|
39
|
+
secretAccessKey: redact(access.secret),
|
|
40
|
+
sessionToken: redact(access.sessionToken),
|
|
41
|
+
},
|
|
42
|
+
access.region as RegionName,
|
|
43
|
+
),
|
|
44
|
+
AwsEndpoint.of(access.storageUrl),
|
|
45
|
+
);
|
|
46
|
+
const existing = yield* s3
|
|
47
|
+
.listObjectsV2({ Bucket: access.bucket, Prefix: prefix, MaxKeys: 1 })
|
|
48
|
+
.pipe(Effect.provide(storage));
|
|
49
|
+
if ((existing.KeyCount ?? 0) > 0) return hash;
|
|
50
|
+
const entries = yield* Effect.forEach(plan.files, (file) =>
|
|
51
|
+
Effect.map(BuildPlan.readContextFile(file), (bytes) => ({
|
|
52
|
+
path: file.path,
|
|
53
|
+
mode: file.mode,
|
|
54
|
+
bytes,
|
|
55
|
+
})),
|
|
56
|
+
);
|
|
57
|
+
const body = yield* Effect.sync(() => tar(entries));
|
|
58
|
+
yield* s3
|
|
59
|
+
.putObject({ Bucket: access.bucket, Key: `${prefix}context.tar`, Body: body })
|
|
60
|
+
.pipe(Effect.provide(storage));
|
|
61
|
+
return hash;
|
|
62
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { PropsInput } from "alchemy/Input";
|
|
2
|
+
import * as Output from "alchemy/Output";
|
|
3
|
+
import { isResource } from "alchemy/Resource";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Redacted from "effect/Redacted";
|
|
6
|
+
import type { Providers } from "../Providers.ts";
|
|
7
|
+
|
|
8
|
+
type Leaf =
|
|
9
|
+
| string
|
|
10
|
+
| number
|
|
11
|
+
| boolean
|
|
12
|
+
| bigint
|
|
13
|
+
| symbol
|
|
14
|
+
| null
|
|
15
|
+
| undefined
|
|
16
|
+
| Redacted.Redacted<any>
|
|
17
|
+
| Uint8Array
|
|
18
|
+
| Date
|
|
19
|
+
| ((...args: any[]) => any);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A prop value, or the declaration of a resource that has it: `template: Node`
|
|
23
|
+
* instead of `template: yield* Node`, at any depth.
|
|
24
|
+
*/
|
|
25
|
+
export type Declared<T> = T extends Leaf
|
|
26
|
+
? T
|
|
27
|
+
: T extends Output.Output<any>
|
|
28
|
+
? T
|
|
29
|
+
: T extends ReadonlyArray<infer U>
|
|
30
|
+
? Declared<U>[] | Effect.Effect<T, never, Providers>
|
|
31
|
+
: { [K in keyof T]: Declared<T[K]> } | Effect.Effect<T, never, Providers>;
|
|
32
|
+
|
|
33
|
+
/** Props where any value may be a resource declaration. */
|
|
34
|
+
export type DeclaredProps<P> = { [K in keyof PropsInput<P>]: Declared<PropsInput<P>[K]> };
|
|
35
|
+
|
|
36
|
+
const isPlain = (value: unknown): value is Record<string, unknown> =>
|
|
37
|
+
typeof value === "object" &&
|
|
38
|
+
value !== null &&
|
|
39
|
+
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
40
|
+
|
|
41
|
+
/** A declaration still to be yielded: an Effect that isn't an Output or a resource. */
|
|
42
|
+
const isDeclaration = (value: unknown): value is Effect.Effect<unknown, never, Providers> =>
|
|
43
|
+
Effect.isEffect(value) && !Output.isOutput(value) && !isResource(value);
|
|
44
|
+
|
|
45
|
+
const hasDeclarations = (value: unknown): boolean =>
|
|
46
|
+
isDeclaration(value) ||
|
|
47
|
+
(Array.isArray(value) && value.some(hasDeclarations)) ||
|
|
48
|
+
(isPlain(value) && !isResource(value) && Object.values(value).some(hasDeclarations));
|
|
49
|
+
|
|
50
|
+
/** Yield every declaration in `value`, at any depth. */
|
|
51
|
+
export const resolveDeclarations = (value: unknown): Effect.Effect<unknown, never, Providers> =>
|
|
52
|
+
isDeclaration(value)
|
|
53
|
+
? value
|
|
54
|
+
: Array.isArray(value)
|
|
55
|
+
? Effect.forEach(value, resolveDeclarations)
|
|
56
|
+
: isPlain(value) && !isResource(value) && hasDeclarations(value)
|
|
57
|
+
? Effect.map(
|
|
58
|
+
Effect.forEach(Object.entries(value), ([key, entry]) =>
|
|
59
|
+
Effect.map(resolveDeclarations(entry), (resolved) => [key, resolved] as const),
|
|
60
|
+
),
|
|
61
|
+
Object.fromEntries,
|
|
62
|
+
)
|
|
63
|
+
: Effect.succeed(value);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Let a resource's props hold declarations of other resources, which are
|
|
67
|
+
* yielded when the resource is: `Boat.Sandbox("Box", { snapshot: Golden })`.
|
|
68
|
+
* Props without declarations are passed through unchanged.
|
|
69
|
+
*/
|
|
70
|
+
export const declarable =
|
|
71
|
+
<R extends { readonly Props: any }>() =>
|
|
72
|
+
<C extends object>(
|
|
73
|
+
resource: C,
|
|
74
|
+
): C & ((id: string, props: DeclaredProps<R["Props"]>) => Effect.Effect<R, never, Providers>) =>
|
|
75
|
+
new Proxy(resource as C & ((...args: unknown[]) => unknown), {
|
|
76
|
+
apply: (target, self, args: unknown[]) =>
|
|
77
|
+
typeof args[0] === "string" && hasDeclarations(args[1])
|
|
78
|
+
? Reflect.apply(target, self, [args[0], resolveDeclarations(args[1])])
|
|
79
|
+
: Reflect.apply(target, self, args),
|
|
80
|
+
}) as never;
|