1import { layerRuntime } from "@alchemy.run/cloudflare-runtime/core";
2import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as MutableHashMap from "effect/MutableHashMap";
6import * as Path from "effect/Path";
7import { AlchemyContext } from "../AlchemyContext.ts";
8import * as RpcProvider from "../Local/RpcProvider.ts";
9import { LOCAL_ID_PREFIX } from "../ProviderMode.ts";
10import { CloudflareEnvironment } from "./CloudflareEnvironment.ts";
11import type { Queue } from "./Queues/Queue.ts";
12import type { Consumer } from "./Queues/Consumer.ts";
13import { moduleExtension } from "../Util/Node.ts";
14
15/**
16 * The Cloudflare provider group module ([Local.ts](./Local.ts)) every
17 * Cloudflare local provider is registered in; the dev sidecar imports it on
18 * first use (see `Local/Sidecar.ts`).
19 */
20export const LOCAL_PROVIDERS_URL = import.meta.resolve(
21 // `import.meta.resolve(<string>)` is a runtime API — TypeScript's
22 // `rewriteRelativeImportExtensions` does NOT touch the string literal, so
23 // we have to pick the right extension ourselves. `import.meta.url` reflects
24 // the actual on-disk extension of *this* file (`.ts` when loaded from
25 // `src/` under Bun or vitest, `.js` when loaded from the compiled `lib/`
26 // under Node), which is exactly the signal we need.
27 `./Local${moduleExtension(import.meta.url)}`,
28 import.meta.url,
29);
30
31export class LocalRuntimeState extends Context.Service<
32 LocalRuntimeState,
33 {
34 readonly queues: MutableHashMap.MutableHashMap<
35 Queue["Attributes"]["queueId"],
36 Queue["Attributes"]
37 >;
38 readonly queueConsumers: MutableHashMap.MutableHashMap<
39 Consumer["Attributes"]["consumerId"],
40 Consumer["Attributes"]
41 >;
42 /**
43 * Restart hooks for locally running Workers, keyed by script name.
44 *
45 * A local workerd instance bakes its queue-consumer wiring in at start
46 * time (`runtime.start({ queueConsumers })`), but the `Consumer`
47 * resource that populates {@link queueConsumers} reconciles as a
48 * *sibling* of the Worker — the engine may start workerd before the
49 * consumer registers. Providers that mutate a worker's runtime wiring
50 * (e.g. `ConsumerProviderLocal`) invoke the script's hook after
51 * updating state so the running instance is reconfigured; the hook is
52 * a no-op until the worker has served at least once.
53 */
54 readonly workerRestarts: MutableHashMap.MutableHashMap<
55 string,
56 Effect.Effect<void>
57 >;
58 }
59>()("alchemy/cloudflare/LocalRuntimeState") {}
60
61const LocalRuntimeStateLive = Layer.succeed(
62 LocalRuntimeState,
63 LocalRuntimeState.of({
64 queues: MutableHashMap.empty(),
65 queueConsumers: MutableHashMap.empty(),
66 workerRestarts: MutableHashMap.empty(),
67 }),
68);
69
70/**
71 * Directory under `.alchemy` holding local-provider persistent state
72 * (workerd storage). Shared so every consumer — the local runtime layer and
73 * Vite child processes — points at the same storage.
74 */
75export const localStorageDirectory = Effect.gen(function* () {
76 const { dotAlchemy } = yield* AlchemyContext;
77 const path = yield* Path.Path;
78 return path.join(dotAlchemy, "local");
79});
80
81const makeLocalRuntimeServices = () =>
82 RpcProvider.providerServicesEffect(
83 Effect.gen(function* () {
84 const getEnv = yield* CloudflareEnvironment;
85 return Layer.merge(
86 LocalRuntimeStateLive,
87 layerRuntime({
88 api: {
89 accountId: getEnv.pipe(Effect.map((env) => env.accountId)),
90 },
91 storage: {
92 directory: yield* localStorageDirectory,
93 },
94 }),
95 );
96 }),
97 );
98
99let _localRuntimeServices:
100 | ReturnType<typeof makeLocalRuntimeServices>
101 | undefined;
102
103/**
104 * The shared local-runtime dependency layer (workerd `Runtime`,
105 * `WorkerProxy`, {@link LocalRuntimeState}) used by every Cloudflare local
106 * provider.
107 *
108 * Returns a **module-memoized layer reference**: local providers register
109 * via `ProviderLayer.dual`, which builds each provider's local variant
110 * lazily against the stack build's shared `Layer.MemoMap` — memoization is
111 * keyed by layer identity, so Worker/Queue/Consumer/Container composing
112 * this exact reference into their local thunks share one runtime instance
113 * per stack build (a fresh build gets a fresh instance via its own memo
114 * map; the layer blueprint itself is immutable).
115 */
116export const localRuntimeServices = () =>
117 (_localRuntimeServices ??= makeLocalRuntimeServices());
118
119export const isLocalId = (id: string | undefined): id is string =>
120 typeof id === "string" && id.startsWith(LOCAL_ID_PREFIX);
121export const isLiveId = (id: string | undefined): id is string =>
122 typeof id === "string" && !id.startsWith(LOCAL_ID_PREFIX);
123export const generateLocalId = (): string =>
124 `${LOCAL_ID_PREFIX}${crypto.randomUUID()}`;