1import type { Credentials } from "@distilled.cloud/cloudflare/Credentials";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import type * as HttpClient from "effect/http/HttpClient";
5import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
6import { SearchIndex } from "./SearchIndex.ts";
7import {
8 type SearchIndexAuth,
9 makeHttpSearchIndexClient,
10} from "./SearchIndexHttpClient.ts";
11import type { Index } from "./VectorizeIndex.ts";
12
13/**
14 * Local implementation of the {@link SearchIndex} binding — talks to a
15 * Vectorize index over the Cloudflare HTTP API using the **current
16 * credentials** instead of a native Worker binding (`SearchIndexBinding`).
17 *
18 * Provide it on an {@link Action} (or any deploy-time Effect) so you can
19 * insert, upsert, query, and fetch vectors with the same client you'd use
20 * inside a Worker — no Worker host, no `host.bind`, no minted token:
21 *
22 * @example Seeding an index from an Action
23 * ```typescript
24 * const Seed = Alchemy.Action(
25 * "Seed",
26 * Effect.gen(function* () {
27 * const vec = yield* Cloudflare.Vectorize.SearchIndex(index);
28 * return Effect.fn(function* () {
29 * yield* vec.upsert([
30 * { id: "1", values: [0.1, 0.2, 0.3, 0.4] },
31 * { id: "2", values: [0.9, 0.8, 0.7, 0.6] },
32 * ]);
33 * const matches = yield* vec.query([0.1, 0.2, 0.3, 0.4], { topK: 1 });
34 * return matches;
35 * });
36 * }).pipe(Effect.provide(Cloudflare.Vectorize.SearchIndexLocal)),
37 * );
38 * ```
39 *
40 * The index name is resolved at apply time through the ambient
41 * {@link RuntimeContext} (in an Action, that's the resolve context the engine
42 * provides around the body), so `SearchIndex(index)` works even though the
43 * index is created in the same deploy.
44 *
45 * `raw` and `queryById` have no Cloudflare HTTP equivalent and `Effect.die` —
46 * see {@link makeHttpSearchIndexClient}.
47 */
48export const SearchIndexLocal = Layer.effect(
49 SearchIndex,
50 Effect.gen(function* () {
51 // Account + credentials are ambient during stack-eval (the stack's
52 // providers layer). Capture the full context so the HTTP ops run with the
53 // current credentials — no `host.bind`, no minted token.
54 const { accountId } = yield* yield* CloudflareEnvironment;
55 const context = yield* Effect.context<
56 Credentials | HttpClient.HttpClient
57 >();
58 const auth: SearchIndexAuth = {
59 authorize: (eff) => eff.pipe(Effect.provideContext(context)),
60 accountId,
61 };
62
63 return Effect.fn(function* (index: Index) {
64 // Deferred accessor — resolves the index name against the tracker at
65 // apply time (in an Action, that's the engine's resolve context).
66 const indexName = yield* index.indexName;
67 return makeHttpSearchIndexClient(auth, indexName);
68 });
69 }),
70);