1import type * as runtime from "@cloudflare/workers-types";
2import * as D1Client from "@effect/sql-d1/D1Client";
3import * as Context from "effect/Context";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Sql from "effect/sql/SqlClient";
7import { makeExecutionMemo } from "../Runtime/ExecutionMemo.ts";
8import { proxyChain } from "../Util/proxy-chain.ts";
9
10/**
11 * A source of the raw Cloudflare `D1Database` binding. Structurally matches
12 * the client returned by `Cloudflare.D1.QueryDatabase(db)` (its `raw`
13 * accessor), so both the client and a bare effect are accepted.
14 */
15export type D1DatabaseSource<E, R> =
16 | Effect.Effect<runtime.D1Database, E, R>
17 | { readonly raw: Effect.Effect<runtime.D1Database, E, R> };
18
19/**
20 * Options forwarded to `@effect/sql-d1`'s `D1Client` (everything except the
21 * `db` binding itself, which alchemy resolves from the Worker environment).
22 */
23export type D1Config = Omit<D1Client.D1ClientConfig, "db">;
24
25/**
26 * Open an `@effect/sql-d1` client over a Cloudflare D1 binding.
27 *
28 * Accepts the client returned by `Cloudflare.D1.QueryDatabase(db)` — or its
29 * `raw` effect directly — and returns a `D1Client` (which implements the
30 * generic `SqlClient` interface) wrapped in a chainable Proxy, so it can be
31 * resolved once at Worker init and used from any handler:
32 *
33 * ```typescript
34 * import * as SQL from "alchemy/SQL/D1";
35 *
36 * const d1 = yield* Cloudflare.D1.QueryDatabase(Db);
37 * const sql = yield* SQL.D1(d1);
38 *
39 * fetch: Effect.gen(function* () {
40 * const users = yield* sql`SELECT * FROM users`;
41 * });
42 * ```
43 *
44 * The client build is deferred until the first query and memoized on the
45 * current execution's `Scope` (via {@link makeExecutionMemo}), so the
46 * `D1Client` (and its prepared-statement cache) is built at most once per
47 * execution — a Worker `fetch`/`queue`/`scheduled` event, a Durable Object
48 * call, or a Workflow run — and torn down when the event settles. Deploy /
49 * plan-time invocations never touch D1.
50 *
51 * @binding
52 */
53export const D1 = <E = never, R = never>(
54 database: D1DatabaseSource<E, R>,
55 config?: D1Config,
56) =>
57 Effect.map(
58 makeExecutionMemo(
59 Effect.gen(function* () {
60 const db = yield* Effect.isEffect(database) ? database : database.raw;
61 const d1Ctx = yield* Layer.build(D1Client.layer({ ...config, db }));
62 return Context.get(d1Ctx, D1Client.D1Client);
63 }),
64 ),
65 (client) => proxyChain<D1Client.D1Client>(client),
66 );
67
68/**
69 * Provide an `@effect/sql-d1` client as the `D1Client` and generic
70 * `SqlClient` services, so cloud-agnostic services written against
71 * `SqlClient.SqlClient` (or drizzle's `effect-d1` driver, which depends on
72 * `D1Client`) run on a bound D1 database:
73 *
74 * ```typescript
75 * const d1 = yield* Cloudflare.D1.QueryDatabase(Db);
76 * const app = yield* makeApp.pipe(Effect.provide(SQL.D1Layer(d1)));
77 * ```
78 *
79 * The layer itself builds synchronously at Worker init; the underlying
80 * `D1Client` is created lazily per execution (see {@link D1}).
81 */
82export const D1Layer = <E = never, R = never>(
83 database: D1DatabaseSource<E, R>,
84 config?: D1Config,
85) =>
86 // Derive SqlClient from the single D1Client build so both tags share one
87 // per-execution client (and one prepared-statement cache).
88 Layer.effect(
89 Sql.SqlClient,
90 Effect.gen(function* () {
91 return yield* D1Client.D1Client;
92 }),
93 ).pipe(
94 Layer.provideMerge(Layer.effect(D1Client.D1Client, D1(database, config))),
95 );