1import * as workers from "@distilled.cloud/cloudflare/workers";
2import * as ConfigProvider from "effect/ConfigProvider";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Logger from "effect/Logger";
6import * as Option from "effect/Option";
7import * as Stream from "effect/Stream";
8import { AuthProviders } from "../../Auth/AuthProvider.ts";
9import { resolveProfileName, withProfileOverride } from "../../Auth/Resolve.ts";
10import * as CloudflareAccess from "../../Cloudflare/Access.ts";
11import { CloudflareAuth } from "../../Cloudflare/Auth/AuthProvider.ts";
12import * as CloudflareEnvironment from "../../Cloudflare/CloudflareEnvironment.ts";
13import * as CloudflareCredentials from "../../Cloudflare/Credentials.ts";
14import { CloudflareLogs } from "../../Cloudflare/Logs.ts";
15import { STATE_STORE_SCRIPT_NAME } from "../../Cloudflare/StateStore/Api.ts";
16import {
17 bootstrap as bootstrapStateStore,
18 teardownStateStore,
19} from "../../Cloudflare/StateStore/State.ts";
20import { loadConfigProvider } from "../../Util/ConfigProvider.ts";
21import { fileLogger } from "../../Util/FileLogger.ts";
22import type { Target } from "../Session.ts";
23import type { LogEntry } from "./logs.ts";
24
25export interface StateTarget extends Target {
26 /** State-store worker to act on. @default the managed state-store worker */
27 readonly workerName?: string;
28}
29
30export interface BootstrapInput extends StateTarget {
31 readonly force?: boolean;
32}
33
34export interface StateLogsInput extends StateTarget {
35 readonly limit?: number;
36 readonly since?: Date;
37}
38
39const logEntry = (
40 scriptName: string,
41 line: { readonly timestamp: Date; readonly message: string },
42): LogEntry => ({
43 resource: {
44 fqn: scriptName,
45 logicalId: scriptName,
46 resourceType: "Cloudflare.Worker",
47 },
48 timestamp: line.timestamp,
49 message: line.message,
50});
51
52const services = Effect.fn(function* (target: StateTarget) {
53 const auth = Layer.provideMerge(
54 CloudflareAuth,
55 Layer.succeed(AuthProviders, {} satisfies AuthProviders["Service"]),
56 );
57 // `provideMerge` (not `mergeAll`) so `fromProfile` / `fromAuthProvider`
58 // actually *see* `--profile`. Sibling-merging ConfigProvider left those
59 // layers requiring it from the outer CLI `fromEnv()` — which is how
60 // `alchemy provider cloudflare bootstrap --profile <name>` ignored the
61 // flag and resolved the default profile (#252).
62 const config = ConfigProvider.layer(
63 withProfileOverride(
64 yield* loadConfigProvider(Option.fromNullishOr(target.envFile)),
65 target.profile,
66 ),
67 );
68 return Layer.mergeAll(
69 Layer.provideMerge(
70 Layer.mergeAll(
71 CloudflareCredentials.fromAuthProvider(),
72 CloudflareEnvironment.fromProfile(),
73 CloudflareAccess.AccessLive,
74 ),
75 auth,
76 ).pipe(Layer.provideMerge(config)),
77 Logger.layer([fileLogger("cloudflare.txt")], { mergeWithExisting: true }),
78 );
79});
80
81/**
82 * Resolve the profile, Cloudflare account, and provider layer every
83 * state-store route (`bootstrap`, `teardown`, logs) is scoped to.
84 */
85export const resolveStateStoreScope = Effect.fn(function* (
86 target: StateTarget,
87) {
88 const profile = yield* resolveProfileName(
89 Option.fromNullishOr(target.envFile),
90 target.profile,
91 );
92 const resolved = { ...target, profile };
93 const layer = yield* services(resolved);
94 const { accountId } = yield* Effect.flatten(
95 CloudflareEnvironment.CloudflareEnvironment,
96 ).pipe(Effect.provide(layer));
97 return {
98 layer,
99 profile,
100 accountId,
101 workerName: target.workerName ?? STATE_STORE_SCRIPT_NAME,
102 };
103});
104
105/** Provision (or adopt) the Cloudflare-hosted state-store worker. */
106export const bootstrap = Effect.fn("Alchemist.provider.cloudflare.bootstrap")(
107 function* (input: BootstrapInput) {
108 const { layer, accountId, workerName, profile } =
109 yield* resolveStateStoreScope(input);
110 return yield* Effect.gen(function* () {
111 const existed = yield* workers
112 .getScriptSetting({ accountId, scriptName: workerName })
113 .pipe(
114 Effect.as(true),
115 Effect.catchTag(
116 ["WorkerNotFound", "InvalidRoute", "WorkerHasNoVersions"],
117 () => Effect.succeed(false),
118 ),
119 );
120 const state = yield* bootstrapStateStore({
121 workerName,
122 force: input.force,
123 profile,
124 });
125 return {
126 accountId,
127 workerName,
128 status: !existed
129 ? ("created" as const)
130 : input.force
131 ? ("redeployed" as const)
132 : ("adopted" as const),
133 credentialsRefreshed: true,
134 stateStoreVersion: yield* state.getVersion(),
135 };
136 }).pipe(Effect.provide(layer));
137 },
138);
139
140/** Tear down the Cloudflare-hosted state store. */
141export const teardown = Effect.fn("Alchemist.provider.cloudflare.teardown")(
142 function* (input: StateTarget) {
143 const { layer, accountId, workerName, profile } =
144 yield* resolveStateStoreScope(input);
145 yield* Effect.provide(teardownStateStore({ workerName, profile }), layer);
146 return { accountId, workerName, deleted: [workerName] };
147 },
148);
149
150/** Query past log entries from the state-store worker, oldest first. */
151export const stateLogs = Effect.fn("Alchemist.provider.cloudflare.stateLogs")(
152 function* (input: StateLogsInput) {
153 const { layer, accountId, workerName } =
154 yield* resolveStateStoreScope(input);
155 const lines = yield* Effect.gen(function* () {
156 const telemetry = yield* CloudflareLogs;
157 return yield* telemetry.queryLogs({
158 accountId,
159 filters: [
160 {
161 key: "$workers.scriptName",
162 operation: "eq",
163 type: "string",
164 value: workerName,
165 },
166 ],
167 options: { limit: input.limit ?? 100, since: input.since },
168 });
169 }).pipe(Effect.provide(layer));
170 return lines
171 .map((line) => logEntry(workerName, line))
172 .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
173 },
174);
175
176/** Live-stream log entries from the state-store worker. */
177export const tailStateLogs = (input: StateTarget) =>
178 Stream.unwrap(
179 Effect.gen(function* () {
180 const { layer, accountId, workerName } =
181 yield* resolveStateStoreScope(input);
182 const telemetry = yield* Effect.provide(CloudflareLogs, layer);
183 return telemetry.tailScript({ accountId, scriptName: workerName }).pipe(
184 Stream.provide(layer),
185 Stream.map((line) => logEntry(workerName, line)),
186 );
187 }),
188 );