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.
Files changed (72) hide show
  1. package/lib/Cloudflare/Containers/Container.d.ts +128 -40
  2. package/lib/Cloudflare/Containers/Container.d.ts.map +1 -1
  3. package/lib/Cloudflare/Containers/Container.js +124 -24
  4. package/lib/Cloudflare/Containers/Container.js.map +1 -1
  5. package/lib/Cloudflare/Containers/ContainerApplication.d.ts +59 -50
  6. package/lib/Cloudflare/Containers/ContainerApplication.d.ts.map +1 -1
  7. package/lib/Cloudflare/Containers/ContainerApplication.js.map +1 -1
  8. package/lib/Cloudflare/Containers/ContainerClient.d.ts +20 -7
  9. package/lib/Cloudflare/Containers/ContainerClient.d.ts.map +1 -1
  10. package/lib/Cloudflare/Containers/ContainerClient.js +22 -5
  11. package/lib/Cloudflare/Containers/ContainerClient.js.map +1 -1
  12. package/lib/Cloudflare/Containers/ContainerConfiguration.d.ts +132 -13
  13. package/lib/Cloudflare/Containers/ContainerConfiguration.d.ts.map +1 -1
  14. package/lib/Cloudflare/Containers/ContainerConfiguration.js +219 -35
  15. package/lib/Cloudflare/Containers/ContainerConfiguration.js.map +1 -1
  16. package/lib/Cloudflare/Containers/ContainerPlatform.d.ts +1 -1
  17. package/lib/Cloudflare/Containers/ContainerPlatform.d.ts.map +1 -1
  18. package/lib/Cloudflare/Containers/ContainerPlatform.js +21 -2
  19. package/lib/Cloudflare/Containers/ContainerPlatform.js.map +1 -1
  20. package/lib/Cloudflare/Containers/ContainerProvider.d.ts.map +1 -1
  21. package/lib/Cloudflare/Containers/ContainerProvider.js +74 -177
  22. package/lib/Cloudflare/Containers/ContainerProvider.js.map +1 -1
  23. package/lib/Cloudflare/Containers/DurableObjectApplication.d.ts +84 -0
  24. package/lib/Cloudflare/Containers/DurableObjectApplication.d.ts.map +1 -0
  25. package/lib/Cloudflare/Containers/DurableObjectApplication.js +249 -0
  26. package/lib/Cloudflare/Containers/DurableObjectApplication.js.map +1 -0
  27. package/lib/Cloudflare/Containers/LocalContainerProvider.d.ts.map +1 -1
  28. package/lib/Cloudflare/Containers/LocalContainerProvider.js +20 -41
  29. package/lib/Cloudflare/Containers/LocalContainerProvider.js.map +1 -1
  30. package/lib/Cloudflare/Containers/index.d.ts +2 -1
  31. package/lib/Cloudflare/Containers/index.d.ts.map +1 -1
  32. package/lib/Cloudflare/Containers/index.js +2 -1
  33. package/lib/Cloudflare/Containers/index.js.map +1 -1
  34. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  35. package/lib/Cloudflare/Providers.js +2 -1
  36. package/lib/Cloudflare/Providers.js.map +1 -1
  37. package/lib/Cloudflare/StateStore/Api.d.ts +3 -0
  38. package/lib/Cloudflare/Workers/DurableObject.d.ts +75 -1
  39. package/lib/Cloudflare/Workers/DurableObject.d.ts.map +1 -1
  40. package/lib/Cloudflare/Workers/DurableObject.js +50 -0
  41. package/lib/Cloudflare/Workers/DurableObject.js.map +1 -1
  42. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
  43. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +29 -23
  44. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
  45. package/lib/Cloudflare/Workers/Worker.d.ts +2 -0
  46. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  47. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  48. package/lib/Cloudflare/Workers/WorkerAsyncBindings.d.ts.map +1 -1
  49. package/lib/Cloudflare/Workers/WorkerAsyncBindings.js +67 -4
  50. package/lib/Cloudflare/Workers/WorkerAsyncBindings.js.map +1 -1
  51. package/lib/Cloudflare/Workers/WorkerBinding.d.ts +6 -0
  52. package/lib/Cloudflare/Workers/WorkerBinding.d.ts.map +1 -1
  53. package/lib/Cloudflare/Workers/WorkerProvider.d.ts.map +1 -1
  54. package/lib/Cloudflare/Workers/WorkerProvider.js +54 -8
  55. package/lib/Cloudflare/Workers/WorkerProvider.js.map +1 -1
  56. package/lib/State/HttpStateApi.d.ts +13 -13
  57. package/package.json +6 -6
  58. package/src/Cloudflare/Containers/Container.ts +126 -56
  59. package/src/Cloudflare/Containers/ContainerApplication.ts +60 -53
  60. package/src/Cloudflare/Containers/ContainerClient.ts +47 -13
  61. package/src/Cloudflare/Containers/ContainerConfiguration.ts +316 -44
  62. package/src/Cloudflare/Containers/ContainerPlatform.ts +27 -3
  63. package/src/Cloudflare/Containers/ContainerProvider.ts +96 -256
  64. package/src/Cloudflare/Containers/DurableObjectApplication.ts +382 -0
  65. package/src/Cloudflare/Containers/LocalContainerProvider.ts +20 -48
  66. package/src/Cloudflare/Containers/index.ts +10 -0
  67. package/src/Cloudflare/Providers.ts +2 -0
  68. package/src/Cloudflare/Workers/DurableObject.ts +77 -1
  69. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +32 -21
  70. package/src/Cloudflare/Workers/Worker.ts +2 -0
  71. package/src/Cloudflare/Workers/WorkerAsyncBindings.ts +95 -4
  72. package/src/Cloudflare/Workers/WorkerProvider.ts +78 -13
@@ -50,6 +50,10 @@ import { sha256 } from "../../Util/sha256.ts";
50
50
  import { ANSI_RESET, ansiFg, colorsEnabled } from "../../Util/Terminal.ts";
51
51
  import { theme } from "../../Util/Theme.ts";
52
52
  import type { DevContainerImage } from "../Containers/ContainerApplication.ts";
53
+ import {
54
+ assertBindingContainersOnAsyncClasses,
55
+ mergeWorkerContainers,
56
+ } from "../Containers/ContainerConfiguration.ts";
53
57
  import { localAccountId } from "../LocalAccount.ts";
54
58
  import {
55
59
  isLiveId,
@@ -61,6 +65,7 @@ import type { ConsumerSettings } from "../Queues/Consumer.ts";
61
65
  import type { WorkerAssetsConfig, WorkerProps } from "../Workers/Worker.ts";
62
66
  import { readAssetsConfigFiles } from "./Assets.ts";
63
67
  import { getCompatibility } from "./Compatibility.ts";
68
+ import { isDurableObjectExport } from "./DurableObject.ts";
64
69
  import { LocalEdge } from "./LocalEdge.ts";
65
70
  import { routePatternHost } from "./RoutePattern.ts";
66
71
  import { materializeRuntimeBindings, WorkerValidationError } from "./RuntimeBindings.ts";
@@ -361,6 +366,7 @@ export const LocalWorkerProvider = () =>
361
366
  // change (a Dockerfile edit, a rebuilt bundle) would never change
362
367
  // the config and the running container would serve stale code.
363
368
  const containerHashes: Record<string, string> = {};
369
+ const containerRows: NonNullable<Worker["Binding"]["containers"]> = [];
364
370
  const boundEnv: Record<string, any> = { ...props.env };
365
371
  for (const { data } of bindings) {
366
372
  if (data.env) Object.assign(boundEnv, data.env);
@@ -415,28 +421,33 @@ export const LocalWorkerProvider = () =>
415
421
  };
416
422
  }
417
423
  }
418
- if (data.containers) {
419
- const toRuntimeImage = (image: DevContainerImage) => ({
420
- ...image,
421
- env: unwrapRedacted(image.env),
422
- });
423
- for (const container of data.containers) {
424
- if (container.devImages !== undefined) {
425
- // Durable Object-managed: named images, selected at start().
426
- const images: Record<string, RuntimeContainerImage> = {};
427
- for (const [name, image] of Object.entries(container.devImages)) {
428
- images[name] = toRuntimeImage(image);
429
- }
430
- containers[container.className] = { images };
431
- } else if (container.dev) {
432
- containers[container.className] = toRuntimeImage(container.dev);
433
- } else {
434
- return yield* Effect.die(`Container ${container.className} has no dev image`);
435
- }
436
- if (container.hash !== undefined) {
437
- containerHashes[container.className] = container.hash;
438
- }
424
+ if (data.containers) containerRows.push(...data.containers);
425
+ }
426
+ // A Durable Object-managed class merges the images of every bound
427
+ // Container (keyed by Container id); a fleet class has one image.
428
+ const toRuntimeImage = (image: DevContainerImage) => ({
429
+ ...image,
430
+ env: unwrapRedacted(image.env),
431
+ });
432
+ const exportMap = (props.exports ?? {}) as Record<string, unknown>;
433
+ yield* assertBindingContainersOnAsyncClasses(
434
+ containerRows,
435
+ Object.keys(exportMap).filter((name) => isDurableObjectExport(exportMap[name])),
436
+ );
437
+ for (const container of yield* mergeWorkerContainers(containerRows)) {
438
+ if (container.devImages !== undefined) {
439
+ const images: Record<string, RuntimeContainerImage> = {};
440
+ for (const [key, image] of Object.entries(container.devImages)) {
441
+ images[key] = toRuntimeImage(image);
439
442
  }
443
+ containers[container.className] = { images };
444
+ } else if (container.dev) {
445
+ containers[container.className] = toRuntimeImage(container.dev);
446
+ } else {
447
+ return yield* Effect.die(`Container ${container.className} has no dev image`);
448
+ }
449
+ if (container.hash !== undefined) {
450
+ containerHashes[container.className] = container.hash;
440
451
  }
441
452
  }
442
453
  for (const [className, dev] of Object.entries(containers)) {
@@ -1318,6 +1318,8 @@ export type Worker<Bindings = any> = Resource<
1318
1318
  observability?: Pick<WorkerObservability, "traces">;
1319
1319
  containers?: {
1320
1320
  className: string;
1321
+ /** Set when an async `Cloudflare.DurableObject(name, { containers })` binding contributed the row. */
1322
+ attachedBy?: "binding";
1321
1323
  /** Application name and prepared images for the native container API. */
1322
1324
  name?: string;
1323
1325
  images?: Record<string, string>;
@@ -19,7 +19,14 @@ import { isDataset } from "../AnalyticsEngine/Dataset.ts";
19
19
  import { isNamespace } from "../Artifacts/Namespace.ts";
20
20
  import type { Container } from "../Containers/Container.ts";
21
21
  import type { ContainerApplication } from "../Containers/ContainerApplication.ts";
22
- import { workerContainerBinding } from "../Containers/ContainerConfiguration.ts";
22
+ import {
23
+ containerApplicationSettings,
24
+ DurableObjectContainerBindingError,
25
+ durableObjectContainerBinding,
26
+ isDurableObjectContainerResource,
27
+ workerContainerBinding,
28
+ } from "../Containers/ContainerConfiguration.ts";
29
+ import { declareDurableObjectApplication } from "../Containers/DurableObjectApplication.ts";
23
30
  import { isDatabase } from "../D1/Database.ts";
24
31
  import { isSendEmail } from "../Email/SendEmail.ts";
25
32
  import { isApp } from "../Flagship/App.ts";
@@ -48,7 +55,11 @@ import { isAI } from "./AI.ts";
48
55
  import { isAssets } from "./Assets.ts";
49
56
  import { isBinding as isWorkerOnlyBinding } from "./Binding.ts";
50
57
  import { isBrowser } from "./Browser.ts";
51
- import { isDurableObjectLike, normalizeTransferredFrom } from "./DurableObject.ts";
58
+ import {
59
+ type DurableObjectLike,
60
+ isDurableObjectLike,
61
+ normalizeTransferredFrom,
62
+ } from "./DurableObject.ts";
52
63
  import { isRateLimit } from "./RateLimit.ts";
53
64
  import { isSecretKey } from "./SecretKey.ts";
54
65
  import { isVersionMetadata } from "./VersionMetadata.ts";
@@ -264,8 +275,16 @@ export const bindWorkerAsyncBindings = Effect.fn(function* (
264
275
  } satisfies WorkflowBinding;
265
276
  }
266
277
 
278
+ // Durable Object-managed Containers attached to a hosted class. Both
279
+ // halves describe this env entry, so they share its `sid`.
280
+ const containers =
281
+ isDurableObjectLike(binding) && binding.containers?.length
282
+ ? yield* bindDurableObjectContainers(resource, bindingName, binding)
283
+ : undefined;
284
+
267
285
  yield* resource.bind`${bindingName}`({
268
286
  bindings: [resolvedBindingMeta],
287
+ containers,
269
288
  hyperdrives: isHyperdriveConnection(binding)
270
289
  ? yield* getHyperdriveDevOriginForHost(binding, resource)
271
290
  : undefined,
@@ -317,8 +336,11 @@ const isContainerApplicationResource = (value: unknown): value is ContainerAppli
317
336
  (value as { Type?: unknown }).Type === "Cloudflare.Container";
318
337
 
319
338
  /**
320
- * Wire a Container bound directly on an async Worker's `env`
321
- * (`env: { Sandbox: Cloudflare.Container("Sandbox", { image }) }`). Mirrors
339
+ * Wire a fleet Container bound directly on an async Worker's `env`
340
+ * (`env: { Sandbox: Cloudflare.Container("Sandbox", { image }) }`). A
341
+ * Durable Object-managed Container fails with
342
+ * {@link DurableObjectContainerBindingError}: it attaches through
343
+ * `Cloudflare.DurableObject(className, { containers })` instead. Mirrors
322
344
  * v1 alchemy, where a Container binding is a Durable Object namespace plus
323
345
  * its ContainerApplication in one declaration:
324
346
  *
@@ -358,6 +380,14 @@ const bindContainerClass = Effect.fn(function* (
358
380
  `Worker binding '${bindingName}' is a Container without a deployable image. Declare the container with props (image, or context/dockerfile) to bind it on an async Worker — effectful (main) containers require an Effect-native Durable Object host.`,
359
381
  );
360
382
  }
383
+ if (isDurableObjectContainerResource(application)) {
384
+ return yield* Effect.die(
385
+ new DurableObjectContainerBindingError({
386
+ bindingName,
387
+ message: `Worker binding '${bindingName}' is a Durable Object-managed Container ('${application.LogicalId}'). Attach it to the Durable Object class that starts it: env: { ${bindingName}: Cloudflare.DurableObject("${className}", { containers: [${application.LogicalId}] }) }.`,
388
+ }),
389
+ );
390
+ }
361
391
  // Both halves of the Worker's side describe the same env entry, so they
362
392
  // share one `sid`. Binding rows are collapsed by sid (last write wins), so
363
393
  // splitting them across two `bind` calls silently drops the namespace
@@ -384,6 +414,67 @@ const bindContainerClass = Effect.fn(function* (
384
414
  });
385
415
  });
386
416
 
417
+ /**
418
+ * Binding mistakes die with a {@link DurableObjectContainerBindingError}: the
419
+ * Worker declaration hook cannot fail, and these are programmer errors.
420
+ *
421
+ * Attach the Durable Object-managed Containers of
422
+ * `Cloudflare.DurableObject(className, { containers })` to the class: each
423
+ * adds its image to the class's one container application (declared once per
424
+ * class, deduped by id) and returns the Worker's `containers` row for it. The
425
+ * same machinery as an Effect-native Durable Object's `yield* Container`.
426
+ */
427
+ const bindDurableObjectContainers = Effect.fn(function* (
428
+ resource: Worker,
429
+ bindingName: string,
430
+ binding: DurableObjectLike,
431
+ ) {
432
+ const className = binding.className ?? binding.name;
433
+ if (binding.scriptName !== undefined) {
434
+ return yield* Effect.die(
435
+ new DurableObjectContainerBindingError({
436
+ bindingName,
437
+ message: `Worker binding '${bindingName}' attaches containers to Durable Object class '${className}' hosted by another Worker. Attach containers where the class is hosted.`,
438
+ }),
439
+ );
440
+ }
441
+ const classApplication = yield* declareDurableObjectApplication(resource, className);
442
+ const namespaceId = resource.durableObjectNamespaces.pipe(
443
+ Output.map((namespaces) => namespaces?.[className]),
444
+ );
445
+ const rows = [];
446
+ for (const decl of binding.containers ?? []) {
447
+ const declaration = (decl as { Application?: unknown }).Application;
448
+ const application =
449
+ isYieldableEffectLike(declaration) && !Output.isOutput(declaration)
450
+ ? yield* declaration as Effect.Effect<unknown>
451
+ : undefined;
452
+ if (
453
+ !isContainerApplicationResource(application) ||
454
+ !isDurableObjectContainerResource(application)
455
+ ) {
456
+ return yield* Effect.die(
457
+ new DurableObjectContainerBindingError({
458
+ bindingName,
459
+ message: `Cloudflare.DurableObject("${className}", { containers }) lists a Container that is not Durable Object-managed. Declare it with schedulingPolicy: "durable_object" and an image source (image, or context/dockerfile).`,
460
+ }),
461
+ );
462
+ }
463
+ yield* classApplication.bind`${application.LogicalId}`({
464
+ durableObjects: { namespaceId },
465
+ container: {
466
+ id: application.LogicalId,
467
+ settings: containerApplicationSettings(application),
468
+ },
469
+ });
470
+ rows.push({
471
+ ...durableObjectContainerBinding(className, classApplication, application),
472
+ attachedBy: "binding" as const,
473
+ });
474
+ }
475
+ return rows;
476
+ });
477
+
387
478
  type BindingSpec = InputProps<WorkerBinding>;
388
479
 
389
480
  /**
@@ -28,6 +28,10 @@ import { cachedFunction } from "../../Util/cached-function.ts";
28
28
  import { initialCwd } from "../../Util/Node.ts";
29
29
  import { sha256Object } from "../../Util/sha256.ts";
30
30
  import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
31
+ import {
32
+ assertBindingContainersOnAsyncClasses,
33
+ mergeWorkerContainers,
34
+ } from "../Containers/ContainerConfiguration.ts";
31
35
  import { localRuntimeServices } from "../LocalRuntime.ts";
32
36
  import { CloudflareLogs } from "../Logs.ts";
33
37
  import { detachQueueConsumersOfScript } from "../Queues/Consumer.ts";
@@ -3449,9 +3453,16 @@ export const LiveWorkerProvider = () =>
3449
3453
  // Compute delete-class candidates. Candidates are validated against
3450
3454
  // observed namespace ownership below — a class may already have been
3451
3455
  // transferred to another script by its new host's deploy.
3456
+ // Several bindings may name one class (e.g. two Containers sharing a
3457
+ // Durable Object class): a class stays while any binding hosts it.
3458
+ const currentDoClasses = new Set(currentDoBindings.map((binding) => binding.className));
3452
3459
  const deletedClassCandidates: string[] = [];
3453
3460
  for (const [logicalId, className] of Object.entries(oldDoClassNameByLogicalId)) {
3454
- if (!currentDoClassNameByLogicalId[logicalId]) {
3461
+ if (
3462
+ !currentDoClassNameByLogicalId[logicalId] &&
3463
+ !currentDoClasses.has(className) &&
3464
+ !deletedClassCandidates.includes(className)
3465
+ ) {
3455
3466
  deletedClassCandidates.push(className);
3456
3467
  }
3457
3468
  }
@@ -3469,7 +3480,9 @@ export const LiveWorkerProvider = () =>
3469
3480
  "className" in oldBinding &&
3470
3481
  oldBinding.className &&
3471
3482
  ownedLocally &&
3472
- !currentDoBindings.some((binding) => binding.bindingName === oldBinding.name)
3483
+ !currentDoBindings.some((binding) => binding.bindingName === oldBinding.name) &&
3484
+ !currentDoClasses.has(oldBinding.className) &&
3485
+ !deletedClassCandidates.includes(oldBinding.className)
3473
3486
  ) {
3474
3487
  deletedClassCandidates.push(oldBinding.className);
3475
3488
  }
@@ -3607,15 +3620,31 @@ export const LiveWorkerProvider = () =>
3607
3620
  }
3608
3621
 
3609
3622
  // Collect container-backed classes so we can send container metadata.
3610
- // `name` and `images` are only set for Durable Object-managed applications.
3611
- const containerClasses = new Map(
3612
- bindings.flatMap((b) =>
3613
- (b.data.containers ?? []).map(
3614
- ({ className, name, images }) => [className, { className, name, images }] as const,
3615
- ),
3616
- ),
3623
+ // A Durable Object-managed class merges the images of every bound
3624
+ // Container under one entry; `name` and `images` are only set for it.
3625
+ const containerRows = bindings.flatMap((b) => b.data.containers ?? []);
3626
+ yield* assertBindingContainersOnAsyncClasses(
3627
+ containerRows,
3628
+ effectNativeDurableObjectClasses(news.exports),
3629
+ );
3630
+ const containerClasses = (yield* mergeWorkerContainers(containerRows)).map(
3631
+ ({ className, name, images }) => ({ className, name, images }),
3617
3632
  );
3618
3633
 
3634
+ // Classes the deployed script hosts locally, by migration tag or by
3635
+ // an observed local Durable Object binding.
3636
+ const deployedDoClasses = new Set([
3637
+ ...Object.values(oldDoClassNameByLogicalId),
3638
+ ...oldBindings.flatMap((old) =>
3639
+ old.type === "durable_object_namespace" &&
3640
+ "className" in old &&
3641
+ old.className &&
3642
+ (!("scriptName" in old) || old.scriptName === undefined || old.scriptName === name)
3643
+ ? [old.className]
3644
+ : [],
3645
+ ),
3646
+ ]);
3647
+
3619
3648
  // Compute new, renamed, and transferred classes
3620
3649
  const newClasses: string[] = [];
3621
3650
  const newSqliteClasses: string[] = [];
@@ -3653,6 +3682,24 @@ export const LiveWorkerProvider = () =>
3653
3682
  previousClassName = observed.className;
3654
3683
  }
3655
3684
  }
3685
+ // Another binding already hosts this class on the deployed script
3686
+ // (several bindings may name one class): the class exists, so a
3687
+ // binding added for it creates nothing.
3688
+ if (
3689
+ (!previousClassName || previousClassName !== binding.className) &&
3690
+ deployedDoClasses.has(binding.className)
3691
+ ) {
3692
+ continue;
3693
+ }
3694
+ // A logical id that moved to another class while its old class is
3695
+ // still hosted by another binding is a new class, not a rename.
3696
+ if (
3697
+ previousClassName !== undefined &&
3698
+ previousClassName !== binding.className &&
3699
+ currentDoClasses.has(previousClassName)
3700
+ ) {
3701
+ previousClassName = undefined;
3702
+ }
3656
3703
  if (!previousClassName) {
3657
3704
  // A class new to this script is a host move when the declaration
3658
3705
  // says so: `transferredFrom` lists the former host(s) — moves
@@ -3685,9 +3732,17 @@ export const LiveWorkerProvider = () =>
3685
3732
  }
3686
3733
  // Default all new Durable Object classes to SQLite. Cloudflare
3687
3734
  // recommends SQLite for new namespaces, and container-backed
3688
- // Durable Objects require it.
3689
- newSqliteClasses.push(binding.className);
3690
- } else if (previousClassName !== binding.className) {
3735
+ // Durable Objects require it. Several bindings may name one class
3736
+ // (e.g. two Containers sharing a class); create it once.
3737
+ if (!newSqliteClasses.includes(binding.className)) {
3738
+ newSqliteClasses.push(binding.className);
3739
+ }
3740
+ } else if (
3741
+ previousClassName !== binding.className &&
3742
+ !renamedClasses.some(
3743
+ (rename) => rename.from === previousClassName && rename.to === binding.className,
3744
+ )
3745
+ ) {
3691
3746
  renamedClasses.push({ from: previousClassName, to: binding.className });
3692
3747
  }
3693
3748
  }
@@ -3732,7 +3787,7 @@ export const LiveWorkerProvider = () =>
3732
3787
  );
3733
3788
  const transferringIn = new Set(transferredClasses.map((transfer) => transfer.to));
3734
3789
 
3735
- const metadataContainers = [...containerClasses.values()];
3790
+ const metadataContainers = containerClasses;
3736
3791
 
3737
3792
  const compatibility = getCompatibility(news);
3738
3793
  const tailConsumers = resolveTailConsumers(news.tailConsumers);
@@ -4791,6 +4846,10 @@ export const LiveWorkerProvider = () =>
4791
4846
  // Container binding (mirrors reconcile's `containerClasses`).
4792
4847
  // Mapping every DO class to a container would wrongly mark plain DOs
4793
4848
  // as container-backed in the placeholder.
4849
+ yield* assertBindingContainersOnAsyncClasses(
4850
+ bindings.flatMap((b) => b.data.containers ?? []),
4851
+ exportDerived.map((binding) => binding.className),
4852
+ );
4794
4853
  const containers = Array.from(
4795
4854
  new Set(bindings.flatMap((b) => (b.data.containers ?? []).map((c) => c.className))),
4796
4855
  ).map((className) => ({ className }));
@@ -5776,6 +5835,12 @@ ${worker}${doClasses.map((className) => `export class ${className} extends Alche
5776
5835
  * binding-derived entry wins on a collision so the `alchemy:dos:` tag keys off
5777
5836
  * the same logical id (the binding sid) that `reconcile` writes.
5778
5837
  */
5838
+ /** The Durable Object classes a Worker implements as Effect-native exports. */
5839
+ function effectNativeDurableObjectClasses(exports: WorkerProps["exports"] | undefined) {
5840
+ const exportMap = (exports ?? {}) as Record<string, unknown>;
5841
+ return Object.keys(exportMap).filter((name) => isDurableObjectExport(exportMap[name]));
5842
+ }
5843
+
5779
5844
  function mergeDurableObjectClasses(
5780
5845
  exportDerived: ReadonlyArray<{
5781
5846
  logicalId: string;