alchemy 2.0.0-beta.82 → 2.0.0-beta.83
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/lib/Cloudflare/Containers/Container.d.ts +128 -40
- package/lib/Cloudflare/Containers/Container.d.ts.map +1 -1
- package/lib/Cloudflare/Containers/Container.js +124 -24
- package/lib/Cloudflare/Containers/Container.js.map +1 -1
- package/lib/Cloudflare/Containers/ContainerApplication.d.ts +59 -50
- package/lib/Cloudflare/Containers/ContainerApplication.d.ts.map +1 -1
- package/lib/Cloudflare/Containers/ContainerApplication.js.map +1 -1
- package/lib/Cloudflare/Containers/ContainerClient.d.ts +20 -7
- package/lib/Cloudflare/Containers/ContainerClient.d.ts.map +1 -1
- package/lib/Cloudflare/Containers/ContainerClient.js +22 -5
- package/lib/Cloudflare/Containers/ContainerClient.js.map +1 -1
- package/lib/Cloudflare/Containers/ContainerConfiguration.d.ts +132 -13
- package/lib/Cloudflare/Containers/ContainerConfiguration.d.ts.map +1 -1
- package/lib/Cloudflare/Containers/ContainerConfiguration.js +219 -35
- package/lib/Cloudflare/Containers/ContainerConfiguration.js.map +1 -1
- package/lib/Cloudflare/Containers/ContainerPlatform.d.ts +1 -1
- package/lib/Cloudflare/Containers/ContainerPlatform.d.ts.map +1 -1
- package/lib/Cloudflare/Containers/ContainerPlatform.js +21 -2
- package/lib/Cloudflare/Containers/ContainerPlatform.js.map +1 -1
- package/lib/Cloudflare/Containers/ContainerProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Containers/ContainerProvider.js +74 -177
- package/lib/Cloudflare/Containers/ContainerProvider.js.map +1 -1
- package/lib/Cloudflare/Containers/DurableObjectApplication.d.ts +84 -0
- package/lib/Cloudflare/Containers/DurableObjectApplication.d.ts.map +1 -0
- package/lib/Cloudflare/Containers/DurableObjectApplication.js +249 -0
- package/lib/Cloudflare/Containers/DurableObjectApplication.js.map +1 -0
- package/lib/Cloudflare/Containers/LocalContainerProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Containers/LocalContainerProvider.js +20 -41
- package/lib/Cloudflare/Containers/LocalContainerProvider.js.map +1 -1
- package/lib/Cloudflare/Containers/index.d.ts +2 -1
- package/lib/Cloudflare/Containers/index.d.ts.map +1 -1
- package/lib/Cloudflare/Containers/index.js +2 -1
- package/lib/Cloudflare/Containers/index.js.map +1 -1
- package/lib/Cloudflare/Providers.d.ts.map +1 -1
- package/lib/Cloudflare/Providers.js +2 -1
- package/lib/Cloudflare/Providers.js.map +1 -1
- package/lib/Cloudflare/StateStore/Api.d.ts +3 -0
- package/lib/Cloudflare/Workers/DurableObject.d.ts +75 -1
- package/lib/Cloudflare/Workers/DurableObject.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/DurableObject.js +50 -0
- package/lib/Cloudflare/Workers/DurableObject.js.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js +29 -23
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +2 -0
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerAsyncBindings.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerAsyncBindings.js +67 -4
- package/lib/Cloudflare/Workers/WorkerAsyncBindings.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBinding.d.ts +6 -0
- package/lib/Cloudflare/Workers/WorkerBinding.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerProvider.js +54 -8
- package/lib/Cloudflare/Workers/WorkerProvider.js.map +1 -1
- package/lib/State/HttpStateApi.d.ts +13 -13
- package/package.json +6 -6
- package/src/Cloudflare/Containers/Container.ts +126 -56
- package/src/Cloudflare/Containers/ContainerApplication.ts +60 -53
- package/src/Cloudflare/Containers/ContainerClient.ts +47 -13
- package/src/Cloudflare/Containers/ContainerConfiguration.ts +316 -44
- package/src/Cloudflare/Containers/ContainerPlatform.ts +27 -3
- package/src/Cloudflare/Containers/ContainerProvider.ts +96 -256
- package/src/Cloudflare/Containers/DurableObjectApplication.ts +382 -0
- package/src/Cloudflare/Containers/LocalContainerProvider.ts +20 -48
- package/src/Cloudflare/Containers/index.ts +10 -0
- package/src/Cloudflare/Providers.ts +2 -0
- package/src/Cloudflare/Workers/DurableObject.ts +77 -1
- package/src/Cloudflare/Workers/LocalWorkerProvider.ts +32 -21
- package/src/Cloudflare/Workers/Worker.ts +2 -0
- package/src/Cloudflare/Workers/WorkerAsyncBindings.ts +95 -4
- package/src/Cloudflare/Workers/WorkerProvider.ts +78 -13
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import * as Containers from "@distilled.cloud/cloudflare/containers";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Schedule from "effect/Schedule";
|
|
4
|
+
import { isResolved } from "../../Diff.ts";
|
|
5
|
+
import * as ProviderLayer from "../../Local/ProviderLayer.ts";
|
|
6
|
+
import * as Namespace from "../../Namespace.ts";
|
|
7
|
+
import * as Provider from "../../Provider.ts";
|
|
8
|
+
import { Resource, type ResourceBinding } from "../../Resource.ts";
|
|
9
|
+
import { normalizeNulls } from "../../Util/stable.ts";
|
|
10
|
+
import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
|
|
11
|
+
import { localAccountId } from "../LocalAccount.ts";
|
|
12
|
+
import { generateLocalId, isLiveId } from "../LocalRuntime.ts";
|
|
13
|
+
import type { Providers } from "../Providers.ts";
|
|
14
|
+
import type { DurableObjectApplicationSettings } from "./ContainerApplication.ts";
|
|
15
|
+
import { createContainerApplicationName } from "./ContainerBundle.ts";
|
|
16
|
+
import {
|
|
17
|
+
ContainerConfigurationError,
|
|
18
|
+
durableObjectSettingsPatch,
|
|
19
|
+
mergeApplicationSettings,
|
|
20
|
+
} from "./ContainerConfiguration.ts";
|
|
21
|
+
|
|
22
|
+
export const DurableObjectApplicationTypeId = "Cloudflare.Containers.DurableObjectApplication";
|
|
23
|
+
|
|
24
|
+
export interface DurableObjectApplicationProps {
|
|
25
|
+
/** The Durable Object class whose namespace the application serves. */
|
|
26
|
+
className: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** What each bound Container contributes to its class's application. */
|
|
30
|
+
export interface DurableObjectApplicationBinding {
|
|
31
|
+
/** The Durable Object namespace the application is attached to. */
|
|
32
|
+
durableObjects?: { namespaceId: string };
|
|
33
|
+
/** The bound Container and the application-wide settings it declares. */
|
|
34
|
+
container?: { id: string; settings: DurableObjectApplicationSettings };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* INTERNAL — the one container application of a Durable Object class.
|
|
39
|
+
*
|
|
40
|
+
* Cloudflare allows exactly one container application per Durable Object
|
|
41
|
+
* namespace. Every `schedulingPolicy: "durable_object"` Container bound to a
|
|
42
|
+
* class declares this resource under the same logical id, so the engine
|
|
43
|
+
* registers it once; each Container then binds its application-wide settings
|
|
44
|
+
* onto it. The images themselves are Worker upload metadata, merged by the
|
|
45
|
+
* Worker.
|
|
46
|
+
*
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
export interface DurableObjectApplication extends Resource<
|
|
50
|
+
typeof DurableObjectApplicationTypeId,
|
|
51
|
+
DurableObjectApplicationProps,
|
|
52
|
+
{
|
|
53
|
+
/** The application id, which equals the Durable Object namespace id. Empty before creation. */
|
|
54
|
+
applicationId: string;
|
|
55
|
+
/** The generated application name. */
|
|
56
|
+
applicationName: string;
|
|
57
|
+
/** The Cloudflare account that owns the application. */
|
|
58
|
+
accountId: string;
|
|
59
|
+
/** The Durable Object class the application serves. */
|
|
60
|
+
className: string;
|
|
61
|
+
/** The Durable Object namespace attached to the application. */
|
|
62
|
+
durableObjects: { namespaceId: string } | undefined;
|
|
63
|
+
/** ISO-8601 creation time. Empty before creation. */
|
|
64
|
+
createdAt: string;
|
|
65
|
+
/** Application-wide container configuration (SSH and authorized keys). */
|
|
66
|
+
configuration: Containers.DurableObjectContainerConfiguration;
|
|
67
|
+
/** Application-wide logging. */
|
|
68
|
+
observability: Containers.ContainerApplicationObservability | undefined;
|
|
69
|
+
/** The merged settings last applied, used to reset settings a Container stops declaring. */
|
|
70
|
+
settings: DurableObjectApplicationSettings;
|
|
71
|
+
},
|
|
72
|
+
DurableObjectApplicationBinding,
|
|
73
|
+
Providers
|
|
74
|
+
> {}
|
|
75
|
+
|
|
76
|
+
export const DurableObjectApplication = Resource<DurableObjectApplication>(
|
|
77
|
+
DurableObjectApplicationTypeId,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Declare the container application of `className`, hosted by `worker`. Every
|
|
82
|
+
* Container bound to the class calls this; the engine dedupes the declaration
|
|
83
|
+
* by its fully qualified id, so all of them share one resource.
|
|
84
|
+
*/
|
|
85
|
+
export const declareDurableObjectApplication = (worker: { LogicalId: string }, className: string) =>
|
|
86
|
+
Namespace.push(
|
|
87
|
+
worker.LogicalId,
|
|
88
|
+
DurableObjectApplication(`${className}-Containers`, { className }),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Right after a Worker upload creates a Durable Object namespace, the
|
|
93
|
+
* containers API can report the namespace missing for a few seconds before it
|
|
94
|
+
* sees it. Retry creates that attach to it, bounded at ~30s.
|
|
95
|
+
*/
|
|
96
|
+
export const retryNamespacePropagation = <A, E extends { readonly _tag: string }, R>(
|
|
97
|
+
effect: Effect.Effect<A, E, R>,
|
|
98
|
+
) =>
|
|
99
|
+
effect.pipe(
|
|
100
|
+
Effect.tapError((error) =>
|
|
101
|
+
error._tag === "DurableObjectNamespaceNotFound"
|
|
102
|
+
? Effect.logDebug("Cloudflare Container create: namespace not visible yet, retrying")
|
|
103
|
+
: Effect.void,
|
|
104
|
+
),
|
|
105
|
+
Effect.retry({
|
|
106
|
+
while: (error) => error._tag === "DurableObjectNamespaceNotFound",
|
|
107
|
+
schedule: Schedule.spaced("2 seconds"),
|
|
108
|
+
times: 15,
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
/** Whether an application id names a created, live application (not a placeholder). */
|
|
113
|
+
const isLiveApplication = (id: string | undefined): id is string => !!id && isLiveId(id);
|
|
114
|
+
|
|
115
|
+
type Bindings = ReadonlyArray<ResourceBinding<DurableObjectApplicationBinding>>;
|
|
116
|
+
|
|
117
|
+
/** The one namespace every bound Container attaches the application to. */
|
|
118
|
+
const boundNamespace = (className: string, bindings: Bindings) => {
|
|
119
|
+
// A stale Worker namespace map can resolve a binding without an id; it
|
|
120
|
+
// does not request removing the live attachment.
|
|
121
|
+
const ids = [
|
|
122
|
+
...new Set(
|
|
123
|
+
bindings.flatMap((b) =>
|
|
124
|
+
b.data.durableObjects?.namespaceId ? [b.data.durableObjects.namespaceId] : [],
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
];
|
|
128
|
+
if (ids.length > 1) {
|
|
129
|
+
return Effect.fail(
|
|
130
|
+
new ContainerConfigurationError({
|
|
131
|
+
message: `The container application of Durable Object class '${className}' is bound to ${ids.length} namespaces (${ids.join(", ")}). A class has one namespace.`,
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return Effect.succeed(ids[0] === undefined ? undefined : { namespaceId: ids[0] });
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const boundSettings = (className: string, bindings: Bindings) =>
|
|
139
|
+
mergeApplicationSettings(
|
|
140
|
+
className,
|
|
141
|
+
bindings.flatMap((b) => (b.data.container ? [b.data.container] : [])),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check the bound settings for conflicts at plan time. The namespace id in
|
|
146
|
+
* the same bindings resolves only after the Worker uploads, so check the
|
|
147
|
+
* settings contributions on their own once they are resolved.
|
|
148
|
+
*/
|
|
149
|
+
const planSettings = (className: string, newBindings: unknown) => {
|
|
150
|
+
const contributions = (Array.isArray(newBindings) ? newBindings : []).flatMap(
|
|
151
|
+
(binding: { data?: DurableObjectApplicationBinding }) =>
|
|
152
|
+
binding?.data?.container ? [binding.data.container] : [],
|
|
153
|
+
);
|
|
154
|
+
return isResolved(contributions)
|
|
155
|
+
? Effect.asVoid(mergeApplicationSettings(className, contributions))
|
|
156
|
+
: Effect.void;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const placeholder = (
|
|
160
|
+
identity: Pick<
|
|
161
|
+
DurableObjectApplication["Attributes"],
|
|
162
|
+
"applicationId" | "applicationName" | "accountId" | "className" | "createdAt"
|
|
163
|
+
> &
|
|
164
|
+
Partial<DurableObjectApplication["Attributes"]>,
|
|
165
|
+
): DurableObjectApplication["Attributes"] => ({
|
|
166
|
+
durableObjects: undefined,
|
|
167
|
+
configuration: {},
|
|
168
|
+
observability: undefined,
|
|
169
|
+
settings: {},
|
|
170
|
+
...identity,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const toAttributes = (
|
|
174
|
+
application: Containers.GetContainerApplicationResponse,
|
|
175
|
+
className: string,
|
|
176
|
+
settings: DurableObjectApplicationSettings,
|
|
177
|
+
): DurableObjectApplication["Attributes"] => ({
|
|
178
|
+
applicationId: application.id,
|
|
179
|
+
applicationName: application.name,
|
|
180
|
+
accountId: application.accountId,
|
|
181
|
+
className,
|
|
182
|
+
durableObjects: normalizeNulls(application.durableObjects) as { namespaceId: string } | undefined,
|
|
183
|
+
createdAt: application.createdAt,
|
|
184
|
+
configuration: normalizeNulls(
|
|
185
|
+
application.configuration,
|
|
186
|
+
) as Containers.DurableObjectContainerConfiguration,
|
|
187
|
+
observability: normalizeNulls(application.observability ?? undefined),
|
|
188
|
+
settings,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
export const LiveDurableObjectApplicationProvider = () =>
|
|
192
|
+
Provider.succeed(DurableObjectApplication, {
|
|
193
|
+
stables: ["accountId", "className"],
|
|
194
|
+
diff: Effect.fn(function* ({ news, output, newBindings }) {
|
|
195
|
+
if (!isResolved(news)) return undefined;
|
|
196
|
+
// Conflicting settings fail the plan, before anything is published.
|
|
197
|
+
yield* planSettings(news.className, newBindings);
|
|
198
|
+
if (!isResolved(newBindings)) return undefined;
|
|
199
|
+
const namespace = yield* boundNamespace(news.className, newBindings);
|
|
200
|
+
const deployed = output?.durableObjects?.namespaceId;
|
|
201
|
+
if (deployed !== undefined && namespace !== undefined && deployed !== namespace.namespaceId) {
|
|
202
|
+
return yield* new ContainerConfigurationError({
|
|
203
|
+
message: `The container application of Durable Object class '${news.className}' cannot move to another Durable Object namespace.`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const { accountId } = yield* yield* CloudflareEnvironment;
|
|
207
|
+
if (
|
|
208
|
+
output !== undefined &&
|
|
209
|
+
isLiveApplication(output.applicationId) &&
|
|
210
|
+
output.accountId !== accountId
|
|
211
|
+
) {
|
|
212
|
+
return yield* new ContainerConfigurationError({
|
|
213
|
+
message: `The container application of Durable Object class '${news.className}' cannot move to another account in place.`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// Precreate only reserves a name; reconcile creates the application
|
|
217
|
+
// once the Worker has created its namespace.
|
|
218
|
+
if (!isLiveApplication(output?.applicationId)) {
|
|
219
|
+
return { action: "update", stables: ["accountId", "className"] } as const;
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}),
|
|
223
|
+
precreate: Effect.fn(function* ({ id, news, bindings }) {
|
|
224
|
+
// A new application is never diffed; fail conflicting settings here,
|
|
225
|
+
// before the Worker is uploaded.
|
|
226
|
+
yield* planSettings(news.className, bindings);
|
|
227
|
+
const { accountId } = yield* yield* CloudflareEnvironment;
|
|
228
|
+
// The application needs the Worker's namespace id, which does not exist
|
|
229
|
+
// yet. Reserve the name so the Worker's upload can reference it.
|
|
230
|
+
return placeholder({
|
|
231
|
+
applicationId: "",
|
|
232
|
+
applicationName: yield* createContainerApplicationName(id, undefined),
|
|
233
|
+
accountId,
|
|
234
|
+
className: news.className,
|
|
235
|
+
createdAt: "",
|
|
236
|
+
});
|
|
237
|
+
}),
|
|
238
|
+
reconcile: Effect.fn(function* ({ id, news, bindings, output, session }) {
|
|
239
|
+
const { accountId } = yield* yield* CloudflareEnvironment;
|
|
240
|
+
const className = news.className;
|
|
241
|
+
// Prefer the deployed name: regenerating would target a different
|
|
242
|
+
// application if the generator's output for this id ever drifts.
|
|
243
|
+
const name =
|
|
244
|
+
output?.applicationName || (yield* createContainerApplicationName(id, undefined));
|
|
245
|
+
const durableObjects =
|
|
246
|
+
(yield* boundNamespace(className, bindings)) ??
|
|
247
|
+
(output?.durableObjects?.namespaceId ? output.durableObjects : undefined);
|
|
248
|
+
if (!durableObjects) {
|
|
249
|
+
return yield* new ContainerConfigurationError({
|
|
250
|
+
message: `The container application of Durable Object class '${className}' requires a resolved Durable Object namespace. Reconcile its Worker first.`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const settings = yield* boundSettings(className, bindings);
|
|
254
|
+
|
|
255
|
+
// The application's id is the id of the namespace it serves.
|
|
256
|
+
const applicationId = durableObjects.namespaceId;
|
|
257
|
+
const getApplication = Containers.getContainerApplication({ accountId, applicationId });
|
|
258
|
+
const assertServesNamespace = (application: Containers.GetContainerApplicationResponse) =>
|
|
259
|
+
application.schedulingPolicy === "durable_object" &&
|
|
260
|
+
application.durableObjects?.namespaceId === applicationId
|
|
261
|
+
? Effect.void
|
|
262
|
+
: Effect.fail(
|
|
263
|
+
new ContainerConfigurationError({
|
|
264
|
+
message: `The container application '${application.name}' of Durable Object namespace '${applicationId}' is not Durable Object-managed. Declare a new Durable Object class for Durable Object-managed Containers.`,
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// 1. Observe. The namespace belongs to this stack's Worker, so an
|
|
269
|
+
// application already attached to it is this class's application.
|
|
270
|
+
let application = yield* getApplication.pipe(
|
|
271
|
+
Effect.catchTag("ContainerApplicationNotFound", () => Effect.succeed(undefined)),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// 2. Ensure
|
|
275
|
+
if (!application) {
|
|
276
|
+
yield* session.note(`Creating container application ${name}...`);
|
|
277
|
+
application = yield* retryNamespacePropagation(
|
|
278
|
+
Containers.createDurableObjectContainerApplication({
|
|
279
|
+
accountId,
|
|
280
|
+
name,
|
|
281
|
+
schedulingPolicy: "durable_object",
|
|
282
|
+
durableObjects,
|
|
283
|
+
configuration: {
|
|
284
|
+
...(settings.ssh !== undefined ? { wranglerSsh: settings.ssh } : {}),
|
|
285
|
+
...(settings.authorizedKeys !== undefined
|
|
286
|
+
? { authorizedKeys: settings.authorizedKeys }
|
|
287
|
+
: {}),
|
|
288
|
+
},
|
|
289
|
+
observability: settings.observability,
|
|
290
|
+
}),
|
|
291
|
+
).pipe(Effect.catchTag("DurableObjectAlreadyHasApplication", () => getApplication));
|
|
292
|
+
}
|
|
293
|
+
yield* assertServesNamespace(application);
|
|
294
|
+
|
|
295
|
+
// 3. Sync declared settings and reset settings no Container declares anymore.
|
|
296
|
+
const patch = durableObjectSettingsPatch(settings, output?.settings, application);
|
|
297
|
+
if (patch.configuration !== undefined || patch.observability !== undefined) {
|
|
298
|
+
application = yield* Containers.updateContainerApplication({
|
|
299
|
+
accountId,
|
|
300
|
+
applicationId,
|
|
301
|
+
...patch,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return toAttributes(application, className, settings);
|
|
305
|
+
}),
|
|
306
|
+
delete: Effect.fn(function* ({ output }) {
|
|
307
|
+
if (!isLiveApplication(output.applicationId)) return;
|
|
308
|
+
yield* Containers.deleteContainerApplication({
|
|
309
|
+
accountId: output.accountId,
|
|
310
|
+
applicationId: output.applicationId,
|
|
311
|
+
}).pipe(Effect.catchTag("ContainerApplicationNotFound", () => Effect.void));
|
|
312
|
+
}),
|
|
313
|
+
read: Effect.fn(function* ({ id, olds, output }) {
|
|
314
|
+
const { accountId } = yield* yield* CloudflareEnvironment;
|
|
315
|
+
const className = olds?.className ?? output?.className ?? "";
|
|
316
|
+
if (output !== undefined && isLiveApplication(output.applicationId)) {
|
|
317
|
+
return yield* Containers.getContainerApplication({
|
|
318
|
+
accountId: output.accountId,
|
|
319
|
+
applicationId: output.applicationId,
|
|
320
|
+
}).pipe(
|
|
321
|
+
Effect.map((application) => toAttributes(application, className, output.settings)),
|
|
322
|
+
Effect.catchTag("ContainerApplicationNotFound", () => Effect.succeed(undefined)),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
// Generated names identify this instance, so a match is ours.
|
|
326
|
+
const name =
|
|
327
|
+
output?.applicationName || (yield* createContainerApplicationName(id, undefined));
|
|
328
|
+
const found = yield* Containers.listContainerApplications({ accountId }).pipe(
|
|
329
|
+
Effect.map((apps) =>
|
|
330
|
+
apps.find((app) => app.name === name && app.schedulingPolicy === "durable_object"),
|
|
331
|
+
),
|
|
332
|
+
);
|
|
333
|
+
return found === undefined
|
|
334
|
+
? undefined
|
|
335
|
+
: toAttributes(found, className, output?.settings ?? {});
|
|
336
|
+
}),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Local (dev) provider: `alchemy dev` runs every image through workerd and
|
|
341
|
+
* Docker, so the application is a placeholder carrying a `dev:` id.
|
|
342
|
+
*/
|
|
343
|
+
export const LocalDurableObjectApplicationProvider = () =>
|
|
344
|
+
Provider.succeed(DurableObjectApplication, {
|
|
345
|
+
stables: ["accountId", "className"],
|
|
346
|
+
diff: Effect.fn(function* ({ news, newBindings }) {
|
|
347
|
+
if (!isResolved(news)) return undefined;
|
|
348
|
+
yield* planSettings(news.className, newBindings);
|
|
349
|
+
return undefined;
|
|
350
|
+
}),
|
|
351
|
+
precreate: Effect.fn(function* ({ id, news, bindings }) {
|
|
352
|
+
yield* planSettings(news.className, bindings);
|
|
353
|
+
return placeholder({
|
|
354
|
+
applicationId: generateLocalId(),
|
|
355
|
+
applicationName: yield* createContainerApplicationName(id, undefined),
|
|
356
|
+
accountId: yield* localAccountId,
|
|
357
|
+
className: news.className,
|
|
358
|
+
createdAt: new Date().toISOString(),
|
|
359
|
+
});
|
|
360
|
+
}),
|
|
361
|
+
reconcile: Effect.fn(function* ({ id, news, bindings, output }) {
|
|
362
|
+
return placeholder({
|
|
363
|
+
applicationId: output?.applicationId || generateLocalId(),
|
|
364
|
+
applicationName: yield* createContainerApplicationName(id, undefined),
|
|
365
|
+
accountId: yield* localAccountId,
|
|
366
|
+
className: news.className,
|
|
367
|
+
createdAt: output?.createdAt || new Date().toISOString(),
|
|
368
|
+
durableObjects: yield* boundNamespace(news.className, bindings),
|
|
369
|
+
settings: yield* boundSettings(news.className, bindings),
|
|
370
|
+
});
|
|
371
|
+
}),
|
|
372
|
+
delete: () => Effect.void,
|
|
373
|
+
read: Effect.fn(function* ({ output }) {
|
|
374
|
+
return output;
|
|
375
|
+
}),
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
export const DurableObjectApplicationProvider = () =>
|
|
379
|
+
ProviderLayer.dual(DurableObjectApplication, {
|
|
380
|
+
live: () => LiveDurableObjectApplicationProvider(),
|
|
381
|
+
local: () => LocalDurableObjectApplicationProvider(),
|
|
382
|
+
});
|
|
@@ -4,7 +4,7 @@ import * as Path from "effect/Path";
|
|
|
4
4
|
import * as Redacted from "effect/Redacted";
|
|
5
5
|
import * as Artifacts from "../../Artifacts.ts";
|
|
6
6
|
import { hashDirectory } from "../../Command/Memo.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { isResolved } from "../../Diff.ts";
|
|
8
8
|
import { isInlineDockerfile } from "../../Docker/Dockerfile.ts";
|
|
9
9
|
import * as RpcProvider from "../../Local/RpcProvider.ts";
|
|
10
10
|
import type { ResourceBinding } from "../../Resource.ts";
|
|
@@ -16,7 +16,6 @@ import type {
|
|
|
16
16
|
AnyContainerApplicationProps,
|
|
17
17
|
ContainerApplication,
|
|
18
18
|
DevContainerImage,
|
|
19
|
-
DurableObjectContainerProps,
|
|
20
19
|
} from "./ContainerApplication.ts";
|
|
21
20
|
import {
|
|
22
21
|
createContainerApplicationName,
|
|
@@ -26,9 +25,9 @@ import {
|
|
|
26
25
|
validateContainerImageProps,
|
|
27
26
|
} from "./ContainerBundle.ts";
|
|
28
27
|
import {
|
|
29
|
-
isDurableObjectContainer,
|
|
30
28
|
durableObjectPlaceholder,
|
|
31
|
-
|
|
29
|
+
hasImageSource,
|
|
30
|
+
isDurableObjectContainer,
|
|
32
31
|
validateContainerConfiguration,
|
|
33
32
|
} from "./ContainerConfiguration.ts";
|
|
34
33
|
import { ContainerPlatform } from "./ContainerPlatform.ts";
|
|
@@ -151,7 +150,6 @@ export const LocalContainerProvider = () =>
|
|
|
151
150
|
const news = input as AnyContainerApplicationProps;
|
|
152
151
|
const picked = {
|
|
153
152
|
schedulingPolicy: news.schedulingPolicy,
|
|
154
|
-
images: news.images,
|
|
155
153
|
main: news.main,
|
|
156
154
|
image: news.image,
|
|
157
155
|
dockerfile: news.dockerfile,
|
|
@@ -176,27 +174,6 @@ export const LocalContainerProvider = () =>
|
|
|
176
174
|
return picked as AnyContainerApplicationProps;
|
|
177
175
|
};
|
|
178
176
|
|
|
179
|
-
/**
|
|
180
|
-
* Prepare every named image of a Durable Object-managed container.
|
|
181
|
-
* Each image is cached under its own `${id}-${name}` artifact key.
|
|
182
|
-
*/
|
|
183
|
-
const prepareNamedImages = Effect.fn(function* (
|
|
184
|
-
id: string,
|
|
185
|
-
props: DurableObjectContainerProps,
|
|
186
|
-
) {
|
|
187
|
-
const devImages: Record<string, DevContainerImage> = {};
|
|
188
|
-
const hashes: Record<string, string> = {};
|
|
189
|
-
for (const [name, source] of Object.entries(props.images ?? {})) {
|
|
190
|
-
const image = yield* prepareImage(`${id}-${name}`, source);
|
|
191
|
-
devImages[name] = image.dev;
|
|
192
|
-
hashes[name] = image.hash;
|
|
193
|
-
}
|
|
194
|
-
return {
|
|
195
|
-
devImages,
|
|
196
|
-
hash: yield* namedImageHash(hashes),
|
|
197
|
-
};
|
|
198
|
-
});
|
|
199
|
-
|
|
200
177
|
const placeholderConfiguration = (
|
|
201
178
|
props: AnyContainerApplicationProps,
|
|
202
179
|
env: Record<string, string | Redacted.Redacted<string>>,
|
|
@@ -238,19 +215,22 @@ export const LocalContainerProvider = () =>
|
|
|
238
215
|
yield* validateContainerConfiguration(news, output?.schedulingPolicy);
|
|
239
216
|
const accountId = yield* localAccountId;
|
|
240
217
|
if (isDurableObjectContainer(news)) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
218
|
+
const attributes = durableObjectPlaceholder({
|
|
219
|
+
applicationId: output?.applicationId ?? generateLocalId(),
|
|
220
|
+
applicationName: yield* createContainerApplicationName(id, undefined),
|
|
221
|
+
accountId,
|
|
222
|
+
createdAt: output?.createdAt ?? new Date().toISOString(),
|
|
223
|
+
observability: news.observability,
|
|
224
|
+
});
|
|
225
|
+
if (!hasImageSource(news)) return attributes;
|
|
226
|
+
const { dev, hash } = yield* prepareImage(id, news);
|
|
244
227
|
return {
|
|
245
|
-
...
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}),
|
|
252
|
-
images: Object.fromEntries(imageNames.map((name) => [name, name])),
|
|
253
|
-
...(yield* prepareNamedImages(id, news)),
|
|
228
|
+
...attributes,
|
|
229
|
+
// workerd resolves each image by its key in the class's image
|
|
230
|
+
// map, so locally the image reference is the Container's id.
|
|
231
|
+
image: id,
|
|
232
|
+
dev,
|
|
233
|
+
hash: { image: hash },
|
|
254
234
|
} satisfies ContainerApplication["Attributes"];
|
|
255
235
|
}
|
|
256
236
|
const env = makeContainerEnv(news, accountId, bindings);
|
|
@@ -290,16 +270,8 @@ export const LocalContainerProvider = () =>
|
|
|
290
270
|
// disabled this check for every effectful container.
|
|
291
271
|
const imageInputs = resolvedImageInputs(news);
|
|
292
272
|
if (imageInputs !== undefined) {
|
|
293
|
-
if (isDurableObjectContainer(imageInputs)) {
|
|
294
|
-
|
|
295
|
-
for (const name of Object.keys(imageInputs.images ?? {})) {
|
|
296
|
-
yield* artifacts.delete(`container-image:${id}-${name}`);
|
|
297
|
-
}
|
|
298
|
-
const prepared = yield* prepareNamedImages(id, imageInputs);
|
|
299
|
-
const changed =
|
|
300
|
-
output.devImages === undefined ||
|
|
301
|
-
!deepEqual(prepared.hash.images, output.hash?.images);
|
|
302
|
-
return changed ? { action: "update" } : undefined;
|
|
273
|
+
if (isDurableObjectContainer(imageInputs) && !hasImageSource(imageInputs)) {
|
|
274
|
+
return output.image !== undefined ? { action: "update" } : undefined;
|
|
303
275
|
}
|
|
304
276
|
// Recompute fresh on every plan. `prepareImage` is memoized so
|
|
305
277
|
// a plan's diff→precreate→reconcile chain bundles once — but
|
|
@@ -15,9 +15,19 @@ export type {
|
|
|
15
15
|
ReservedContainerKey,
|
|
16
16
|
} from "./ContainerClient.ts";
|
|
17
17
|
export {
|
|
18
|
+
ContainerApplicationSettingsConflict,
|
|
18
19
|
ContainerConfigurationError,
|
|
19
20
|
ContainerImagePreparationError,
|
|
21
|
+
DurableObjectContainerBindingError,
|
|
22
|
+
DurableObjectContainerConflict,
|
|
23
|
+
DurableObjectContainersOnEffectNativeClass,
|
|
20
24
|
} from "./ContainerConfiguration.ts";
|
|
21
25
|
export { ContainerPlatform } from "./ContainerPlatform.ts";
|
|
22
26
|
export * from "./ContainerProvider.ts";
|
|
27
|
+
export {
|
|
28
|
+
DurableObjectApplication,
|
|
29
|
+
DurableObjectApplicationProvider,
|
|
30
|
+
type DurableObjectApplicationBinding,
|
|
31
|
+
type DurableObjectApplicationProps,
|
|
32
|
+
} from "./DurableObjectApplication.ts";
|
|
23
33
|
export * from "./LocalContainerProvider.ts";
|
|
@@ -196,6 +196,7 @@ export const providers = () =>
|
|
|
196
196
|
Configuration.Configuration,
|
|
197
197
|
Connectivity.DirectoryService,
|
|
198
198
|
Containers.ContainerPlatform,
|
|
199
|
+
Containers.DurableObjectApplication,
|
|
199
200
|
ContentScanning.ContentScanning,
|
|
200
201
|
ContentScanning.Expression,
|
|
201
202
|
CustomCertificates.CustomCertificate,
|
|
@@ -450,6 +451,7 @@ export const providers = () =>
|
|
|
450
451
|
Configuration.ConfigurationProvider(),
|
|
451
452
|
Connectivity.DirectoryServiceProvider(),
|
|
452
453
|
Containers.ContainerProvider(),
|
|
454
|
+
Containers.DurableObjectApplicationProvider(),
|
|
453
455
|
ContentScanning.ContentScanningProvider(),
|
|
454
456
|
ContentScanning.ExpressionProvider(),
|
|
455
457
|
CustomCertificates.CustomCertificateProvider(),
|
|
@@ -61,6 +61,8 @@ export interface DurableObjectLike<Shape = any> {
|
|
|
61
61
|
scriptName?: Input<string>;
|
|
62
62
|
/** @internal phantom */
|
|
63
63
|
transferredFrom?: DurableObjectTransferSource | DurableObjectTransferSource[];
|
|
64
|
+
/** @internal Durable Object-managed Containers attached to the class. */
|
|
65
|
+
containers?: ReadonlyArray<Container.Decl.Any>;
|
|
64
66
|
/** @internal phantom */
|
|
65
67
|
Shape?: Shape;
|
|
66
68
|
}
|
|
@@ -264,6 +266,30 @@ export interface DurableObjectProps {
|
|
|
264
266
|
// namespaceId?: string | undefined;
|
|
265
267
|
}
|
|
266
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Props of an async Worker's Durable Object binding,
|
|
271
|
+
* `Cloudflare.DurableObject(className, props)` in `env`.
|
|
272
|
+
*/
|
|
273
|
+
export interface DurableObjectBindingProps extends DurableObjectProps {
|
|
274
|
+
/**
|
|
275
|
+
* Durable Object-managed Containers (`schedulingPolicy: "durable_object"`)
|
|
276
|
+
* this class starts. Each adds its image to the class's container
|
|
277
|
+
* application under the Container's id, read at runtime from
|
|
278
|
+
* `ctx.container.images`. A Container may be listed by several classes.
|
|
279
|
+
*
|
|
280
|
+
* Only for a plain (async) Durable Object class the Worker hosts.
|
|
281
|
+
* Effect-native Durable Objects `yield*` their Containers instead; listing
|
|
282
|
+
* Containers for one fails with `DurableObjectContainersOnEffectNativeClass`.
|
|
283
|
+
*
|
|
284
|
+
* ```typescript
|
|
285
|
+
* env: {
|
|
286
|
+
* AGENT: Cloudflare.DurableObject("Agent", { containers: [Node, Python] }),
|
|
287
|
+
* }
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
containers?: ReadonlyArray<Container.Decl.Any> | undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
267
293
|
export interface DurableObjectClass extends Effect.Effect<DurableObject, never, DurableObject> {
|
|
268
294
|
<Self, Shape>(): {
|
|
269
295
|
<Name extends string>(
|
|
@@ -310,7 +336,7 @@ export interface DurableObjectClass extends Effect.Effect<DurableObject, never,
|
|
|
310
336
|
new (_: never): Shape;
|
|
311
337
|
};
|
|
312
338
|
};
|
|
313
|
-
<Shape>(name: string, props?:
|
|
339
|
+
<Shape>(name: string, props?: DurableObjectBindingProps): DurableObjectLike<Shape>;
|
|
314
340
|
<Shape, InitReq = never>(
|
|
315
341
|
name: string,
|
|
316
342
|
impl: Effect.Effect<Shape, never, DurableObjectServices | InitReq>,
|
|
@@ -1076,6 +1102,55 @@ export class DurableObjectScope extends Context.Service<DurableObjectScope, Dura
|
|
|
1076
1102
|
* });
|
|
1077
1103
|
* ```
|
|
1078
1104
|
*
|
|
1105
|
+
* ### Native Containers in an Async Worker
|
|
1106
|
+
* A plain Durable Object class can start Durable Object-managed Containers
|
|
1107
|
+
* (`schedulingPolicy: "durable_object"`). List them in its binding's
|
|
1108
|
+
* `containers`: each adds its image to the class's container application,
|
|
1109
|
+
* keyed by the Container's id in `ctx.container.images`. A Container may be
|
|
1110
|
+
* shared by several classes.
|
|
1111
|
+
*
|
|
1112
|
+
* **Example:** Two classes, one shared image
|
|
1113
|
+
* ```typescript
|
|
1114
|
+
* const Node = Cloudflare.Container("Node", {
|
|
1115
|
+
* schedulingPolicy: "durable_object",
|
|
1116
|
+
* image: "node:24-slim",
|
|
1117
|
+
* });
|
|
1118
|
+
* const Python = Cloudflare.Container("Python", {
|
|
1119
|
+
* schedulingPolicy: "durable_object",
|
|
1120
|
+
* image: "python:3.13-slim",
|
|
1121
|
+
* });
|
|
1122
|
+
* const Rust = Cloudflare.Container("Rust", {
|
|
1123
|
+
* schedulingPolicy: "durable_object",
|
|
1124
|
+
* image: "rust:1.90-slim",
|
|
1125
|
+
* });
|
|
1126
|
+
*
|
|
1127
|
+
* export const Worker = Cloudflare.Worker("App", {
|
|
1128
|
+
* main: "./src/worker.ts",
|
|
1129
|
+
* env: {
|
|
1130
|
+
* AGENT: Cloudflare.DurableObject("Agent", { containers: [Node, Python] }),
|
|
1131
|
+
* REVIEWER: Cloudflare.DurableObject("Reviewer", { containers: [Node, Rust] }),
|
|
1132
|
+
* },
|
|
1133
|
+
* });
|
|
1134
|
+
* ```
|
|
1135
|
+
*
|
|
1136
|
+
* **Example:** Starting a Container's image from the class
|
|
1137
|
+
* ```typescript
|
|
1138
|
+
* // src/worker.ts
|
|
1139
|
+
* export class Agent extends DurableObject {
|
|
1140
|
+
* async run(cmd: string[]) {
|
|
1141
|
+
* const container = this.ctx.container!;
|
|
1142
|
+
* if (!container.running) {
|
|
1143
|
+
* container.start({
|
|
1144
|
+
* image: container.images.Node,
|
|
1145
|
+
* entrypoint: ["sleep", "infinity"],
|
|
1146
|
+
* enableInternet: false,
|
|
1147
|
+
* });
|
|
1148
|
+
* }
|
|
1149
|
+
* return (await container.exec(cmd)).output();
|
|
1150
|
+
* }
|
|
1151
|
+
* }
|
|
1152
|
+
* ```
|
|
1153
|
+
*
|
|
1079
1154
|
* ### Moving a Class Between Workers
|
|
1080
1155
|
* A Durable Object class can move from one Worker to another with its
|
|
1081
1156
|
* data intact. The move is always **declared** — a class that disappears
|
|
@@ -1357,6 +1432,7 @@ export const DurableObject: DurableObjectClass = taggedFunction(
|
|
|
1357
1432
|
className: (args[1] as DurableObjectProps)?.className || namespace,
|
|
1358
1433
|
scriptName: (args[1] as DurableObjectProps)?.scriptName,
|
|
1359
1434
|
transferredFrom: (args[1] as DurableObjectProps)?.transferredFrom,
|
|
1435
|
+
containers: (args[1] as DurableObjectBindingProps)?.containers,
|
|
1360
1436
|
};
|
|
1361
1437
|
} else if (Effect.isEffect(propsOrImpl)) {
|
|
1362
1438
|
// inline Effect DO
|