import type * as runtime from "@cloudflare/workers-types";
import * as vectorize from "@distilled.cloud/cloudflare/vectorize";
import * as Effect from "effect/Effect";
import type * as HttpClient from "effect/unstable/http/HttpClient";
import type { Credentials } from "../Credentials.ts";
import type { SearchIndexClient } from "./SearchIndex.ts";
// Shared HTTP scaffolding for the Vectorize `SearchIndex` binding. NOT
// re-exported from `index.ts` — only the contract and the impl layers are
// public. A future token-scoped `SearchIndexHttp` layer reuses this builder
// with an auth minted from an `AccountApiToken`; `SearchIndexLocal` builds the
// auth from the ambient current-credentials context.
/**
* Injectable auth shared by the Local (current-credentials) impl and a future
* Http (scoped-token) impl. `authorize` discharges the
* `Credentials | HttpClient` requirement of a distilled op; `accountId` is the
* Cloudflare account the ops run against.
*/
export interface SearchIndexAuth {
authorize: (
eff: Effect.Effect,
) => Effect.Effect;
accountId: string;
}
/**
* Build a {@link SearchIndexClient} over the Vectorize HTTP API.
*
* `indexName` is an Effect so the resolution stays deferred to each call —
* inside an Action it resolves through the apply-time RuntimeContext. The
* credentials are provided ONLY around the distilled op (via `auth.authorize`),
* never around the name accessor, matching the D1 / KV Local variants. `orDie`
* mirrors the native binding, whose client methods surface transport failures
* as defects.
*
* Two methods have no Cloudflare HTTP equivalent and `Effect.die`:
* - `raw` — there is no HTTP-backed `runtime.Vectorize` object to hand back.
* - `queryById` — the HTTP query endpoint only accepts a raw vector, not an id.
*/
export const makeHttpSearchIndexClient = (
auth: SearchIndexAuth,
indexName: Effect.Effect,
): SearchIndexClient => {
const { accountId } = auth;
const local = (
fn: (
name: string,
) => Effect.Effect,
): Effect.Effect =>
Effect.flatMap(indexName, (name) => auth.authorize(fn(name))).pipe(
Effect.orDie,
);
return {
raw: Effect.die(
new Error(
"SearchIndex over HTTP: `raw` is not available — use a native Worker binding (SearchIndexBinding) for direct access.",
),
),
describe: () =>
local((name) =>
vectorize
.infoIndex({ accountId, indexName: name })
.pipe(Effect.map(toIndexInfo)),
),
query: (vector, options) =>
local((name) =>
vectorize
.queryIndex({
accountId,
indexName: name,
vector: Array.from(vector),
topK: options?.topK,
returnValues: options?.returnValues,
returnMetadata: toReturnMetadata(options?.returnMetadata),
filter: options?.filter,
})
.pipe(Effect.map(toMatches)),
),
queryById: () =>
Effect.die(
new Error(
"SearchIndex over HTTP: `queryById` is not supported — the HTTP query endpoint only accepts a raw query vector. Fetch the vector with `getByIds` and pass its values to `query`.",
),
),
insert: (vectors) =>
local((name) =>
vectorize
.insertIndex({
accountId,
indexName: name,
body: toNdjsonBlob(vectors),
})
.pipe(Effect.map(toMutation)),
),
upsert: (vectors) =>
local((name) =>
vectorize
.upsertIndex({
accountId,
indexName: name,
body: toNdjsonBlob(vectors),
})
.pipe(Effect.map(toMutation)),
),
deleteByIds: (ids) =>
local((name) =>
vectorize
.deleteByIdsIndex({ accountId, indexName: name, ids })
.pipe(Effect.map(toMutation)),
),
getByIds: (ids) =>
local((name) =>
vectorize
.getByIdsIndex({ accountId, indexName: name, ids })
.pipe(
Effect.map((result) => (result ?? []) as runtime.VectorizeVector[]),
),
),
} satisfies SearchIndexClient;
};
// ── shape adapters (runtime types <-> distilled HTTP types) ─────────────────
/**
* Serialize vectors to an ndjson Blob — the raw `application/x-ndjson` request
* body of the Vectorize v2 insert/upsert endpoints.
*/
const toNdjsonBlob = (vectors: runtime.VectorizeVector[]): Blob =>
new Blob([toNdjson(vectors)]);
/** Serialize vectors to ndjson — one JSON vector per line. */
const toNdjson = (vectors: runtime.VectorizeVector[]): string =>
vectors
.map((v) =>
JSON.stringify({
id: v.id,
// A `VectorFloatArray` (Float32Array) would `JSON.stringify` to an
// object, not an array — normalize to a plain number array.
values: Array.from(v.values),
...(v.namespace !== undefined ? { namespace: v.namespace } : {}),
...(v.metadata !== undefined ? { metadata: v.metadata } : {}),
}),
)
.join("\n");
const toReturnMetadata = (
value: runtime.VectorizeQueryOptions["returnMetadata"],
): "none" | "indexed" | "all" | undefined =>
value === undefined
? undefined
: typeof value === "boolean"
? value
? "all"
: "none"
: value;
const toMutation = (r: {
mutationId?: string | null;
}): runtime.VectorizeAsyncMutation => ({ mutationId: r.mutationId ?? "" });
const toIndexInfo = (
r: vectorize.InfoIndexResponse,
): runtime.VectorizeIndexInfo =>
({
vectorCount: r.vectorCount ?? 0,
dimensions: r.dimensions ?? 0,
processedUpToDatetime: r.processedUpToDatetime ?? undefined,
processedUpToMutation: r.processedUpToMutation ?? undefined,
}) as unknown as runtime.VectorizeIndexInfo;
const toMatches = (r: vectorize.QueryIndexResponse): runtime.VectorizeMatches =>
({
count: r.count ?? r.matches?.length ?? 0,
matches: (r.matches ?? []).map((m) => ({
id: m.id ?? "",
score: m.score ?? 0,
...(m.values != null ? { values: m.values } : {}),
...(m.namespace != null ? { namespace: m.namespace } : {}),
...(m.metadata != null ? { metadata: m.metadata } : {}),
})),
}) as unknown as runtime.VectorizeMatches;