| 1 | import { layerRuntime } from "@alchemy.run/cloudflare-runtime/core"; |
| 2 | import * as Context from "effect/Context"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as MutableHashMap from "effect/MutableHashMap"; |
| 6 | import * as Path from "effect/Path"; |
| 7 | import { AlchemyContext } from "../AlchemyContext.ts"; |
| 8 | import * as RpcProvider from "../Local/RpcProvider.ts"; |
| 9 | import { LOCAL_ID_PREFIX } from "../ProviderMode.ts"; |
| 10 | import { CloudflareEnvironment } from "./CloudflareEnvironment.ts"; |
| 11 | import type { Queue } from "./Queues/Queue.ts"; |
| 12 | import type { Consumer } from "./Queues/Consumer.ts"; |
| 13 | import { 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 | */ |
| 20 | export 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 | |
| 31 | export 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 | |
| 61 | const 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 | */ |
| 75 | export const localStorageDirectory = Effect.gen(function* () { |
| 76 | const { dotAlchemy } = yield* AlchemyContext; |
| 77 | const path = yield* Path.Path; |
| 78 | return path.join(dotAlchemy, "local"); |
| 79 | }); |
| 80 | |
| 81 | const 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 | |
| 99 | let _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 | */ |
| 116 | export const localRuntimeServices = () => |
| 117 | (_localRuntimeServices ??= makeLocalRuntimeServices()); |
| 118 | |
| 119 | export const isLocalId = (id: string | undefined): id is string => |
| 120 | typeof id === "string" && id.startsWith(LOCAL_ID_PREFIX); |
| 121 | export const isLiveId = (id: string | undefined): id is string => |
| 122 | typeof id === "string" && !id.startsWith(LOCAL_ID_PREFIX); |
| 123 | export const generateLocalId = (): string => |
| 124 | `${LOCAL_ID_PREFIX}${crypto.randomUUID()}`; |