alchemy 2.0.0-beta.45 → 2.0.0-beta.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/lib/AlchemyContext.d.ts +0 -1
  2. package/lib/AlchemyContext.d.ts.map +1 -1
  3. package/lib/AlchemyContext.js.map +1 -1
  4. package/lib/Cli/commands/cloudflare.d.ts.map +1 -1
  5. package/lib/Cli/commands/cloudflare.js +5 -1
  6. package/lib/Cli/commands/cloudflare.js.map +1 -1
  7. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  8. package/lib/Cloudflare/Providers.js +5 -1
  9. package/lib/Cloudflare/Providers.js.map +1 -1
  10. package/lib/Cloudflare/Queue/QueueConsumer.d.ts.map +1 -1
  11. package/lib/Cloudflare/Queue/QueueConsumer.js +1 -2
  12. package/lib/Cloudflare/Queue/QueueConsumer.js.map +1 -1
  13. package/lib/Cloudflare/StateStore/Api.d.ts +1 -1
  14. package/lib/Cloudflare/StateStore/Api.js +1 -1
  15. package/lib/Cloudflare/StateStore/State.d.ts +11 -65
  16. package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
  17. package/lib/Cloudflare/StateStore/State.js +307 -366
  18. package/lib/Cloudflare/StateStore/State.js.map +1 -1
  19. package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -1
  20. package/lib/Cloudflare/StateStore/Store.js +16 -29
  21. package/lib/Cloudflare/StateStore/Store.js.map +1 -1
  22. package/lib/Cloudflare/Vectorize/VectorizeIndex.d.ts +114 -0
  23. package/lib/Cloudflare/Vectorize/VectorizeIndex.d.ts.map +1 -0
  24. package/lib/Cloudflare/Vectorize/VectorizeIndex.js +157 -0
  25. package/lib/Cloudflare/Vectorize/VectorizeIndex.js.map +1 -0
  26. package/lib/Cloudflare/Vectorize/VectorizeIndexBinding.d.ts +38 -0
  27. package/lib/Cloudflare/Vectorize/VectorizeIndexBinding.d.ts.map +1 -0
  28. package/lib/Cloudflare/Vectorize/VectorizeIndexBinding.js +44 -0
  29. package/lib/Cloudflare/Vectorize/VectorizeIndexBinding.js.map +1 -0
  30. package/lib/Cloudflare/Vectorize/VectorizeMetadataIndex.d.ts +72 -0
  31. package/lib/Cloudflare/Vectorize/VectorizeMetadataIndex.d.ts.map +1 -0
  32. package/lib/Cloudflare/Vectorize/VectorizeMetadataIndex.js +131 -0
  33. package/lib/Cloudflare/Vectorize/VectorizeMetadataIndex.js.map +1 -0
  34. package/lib/Cloudflare/Vectorize/index.d.ts +4 -0
  35. package/lib/Cloudflare/Vectorize/index.d.ts.map +1 -0
  36. package/lib/Cloudflare/Vectorize/index.js +4 -0
  37. package/lib/Cloudflare/Vectorize/index.js.map +1 -0
  38. package/lib/Cloudflare/Workers/WorkerAsyncBindings.d.ts.map +1 -1
  39. package/lib/Cloudflare/Workers/WorkerAsyncBindings.js +8 -0
  40. package/lib/Cloudflare/Workers/WorkerAsyncBindings.js.map +1 -1
  41. package/lib/Cloudflare/Workers/WorkerBinding.d.ts +2 -1
  42. package/lib/Cloudflare/Workers/WorkerBinding.d.ts.map +1 -1
  43. package/lib/Cloudflare/Workers/WorkerBinding.js.map +1 -1
  44. package/lib/Cloudflare/index.d.ts +1 -0
  45. package/lib/Cloudflare/index.d.ts.map +1 -1
  46. package/lib/Cloudflare/index.js +1 -0
  47. package/lib/Cloudflare/index.js.map +1 -1
  48. package/lib/State/HttpStateStore.d.ts +4 -0
  49. package/lib/State/HttpStateStore.d.ts.map +1 -1
  50. package/lib/State/HttpStateStore.js +13 -0
  51. package/lib/State/HttpStateStore.js.map +1 -1
  52. package/lib/Util/poll.d.ts +30 -0
  53. package/lib/Util/poll.d.ts.map +1 -0
  54. package/lib/Util/poll.js +26 -0
  55. package/lib/Util/poll.js.map +1 -0
  56. package/lib/tsconfig.test.tsbuildinfo +1 -1
  57. package/package.json +7 -7
  58. package/src/AlchemyContext.ts +0 -1
  59. package/src/Cli/commands/cloudflare.ts +5 -3
  60. package/src/Cloudflare/Providers.ts +7 -0
  61. package/src/Cloudflare/Queue/QueueConsumer.ts +0 -2
  62. package/src/Cloudflare/StateStore/Api.ts +1 -1
  63. package/src/Cloudflare/StateStore/State.ts +445 -507
  64. package/src/Cloudflare/StateStore/Store.ts +22 -35
  65. package/src/Cloudflare/Vectorize/VectorizeIndex.ts +261 -0
  66. package/src/Cloudflare/Vectorize/VectorizeIndexBinding.ts +103 -0
  67. package/src/Cloudflare/Vectorize/VectorizeMetadataIndex.ts +208 -0
  68. package/src/Cloudflare/Vectorize/index.ts +3 -0
  69. package/src/Cloudflare/Workers/WorkerAsyncBindings.ts +7 -0
  70. package/src/Cloudflare/Workers/WorkerBinding.ts +2 -0
  71. package/src/Cloudflare/index.ts +1 -0
  72. package/src/State/HttpStateStore.ts +28 -0
  73. package/src/Util/poll.ts +47 -0
@@ -32,7 +32,7 @@ export default class Store extends DurableObjectNamespace<Store>()(
32
32
  const cryptoKey = yield* Effect.tryPromise(() =>
33
33
  crypto.subtle.importKey(
34
34
  "raw",
35
- hexToBytes(keyHex),
35
+ Buffer.from(keyHex, "hex"),
36
36
  { name: "AES-CTR" },
37
37
  false,
38
38
  ["encrypt", "decrypt"],
@@ -53,22 +53,31 @@ export default class Store extends DurableObjectNamespace<Store>()(
53
53
  ),
54
54
  );
55
55
  // Frame as a single base64 string: nonce || ciphertext.
56
- const framed = allocBytes(counter.byteLength + ct.byteLength);
57
- framed.set(counter, 0);
58
- framed.set(ct, counter.byteLength);
59
- return toB64(framed);
56
+ return Buffer.concat([counter, ct]).toString("base64");
60
57
  }).pipe(Effect.orDie);
61
58
 
62
59
  const decryptEntry = (entry: string) =>
63
60
  Effect.tryPromise(async () => {
64
- const framed = fromB64(entry);
65
- const counter = framed.slice(0, NONCE_BYTES);
66
- const ciphertext = framed.slice(NONCE_BYTES);
67
- const pt = await crypto.subtle.decrypt(
68
- { name: "AES-CTR", counter, length: 64 },
69
- cryptoKey,
70
- ciphertext,
71
- );
61
+ const framed = Buffer.from(entry, "base64");
62
+ const counter = framed.subarray(0, NONCE_BYTES);
63
+ const ciphertext = framed.subarray(NONCE_BYTES);
64
+ let pt;
65
+ try {
66
+ pt = await crypto.subtle.decrypt(
67
+ { name: "AES-CTR", counter, length: 64 },
68
+ cryptoKey,
69
+ ciphertext,
70
+ );
71
+ } catch (error) {
72
+ // We return undefined here because in 2.0.0-beta.45, we rotated encryption keys unnecessarily.
73
+ // So, we catch a decryption error here and return undefined instead.
74
+ // The engine should reconcile, hopefully, but users may lose some data
75
+ console.error(
76
+ "Error decrypting entry. Returning undefined instead.",
77
+ error,
78
+ );
79
+ return undefined;
80
+ }
72
81
  return JSON.parse(new TextDecoder().decode(pt)) as ResourceState;
73
82
  }).pipe(Effect.orDie);
74
83
 
@@ -303,25 +312,3 @@ const parseResourceKey = (
303
312
  */
304
313
  const allocBytes = (size: number): Uint8Array<ArrayBuffer> =>
305
314
  new Uint8Array(new ArrayBuffer(size));
306
-
307
- const toB64 = (bytes: Uint8Array): string => {
308
- let s = "";
309
- for (let i = 0; i < bytes.byteLength; i++)
310
- s += String.fromCharCode(bytes[i]!);
311
- return btoa(s);
312
- };
313
-
314
- const fromB64 = (s: string): Uint8Array<ArrayBuffer> => {
315
- const bin = atob(s);
316
- const bytes = allocBytes(bin.length);
317
- for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
318
- return bytes;
319
- };
320
-
321
- const hexToBytes = (hex: string): Uint8Array<ArrayBuffer> => {
322
- const bytes = allocBytes(hex.length / 2);
323
- for (let i = 0; i < bytes.length; i++) {
324
- bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
325
- }
326
- return bytes;
327
- };
@@ -0,0 +1,261 @@
1
+ import * as vectorize from "@distilled.cloud/cloudflare/vectorize";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Predicate from "effect/Predicate";
4
+
5
+ import { isResolved } from "../../Diff.ts";
6
+ import { createPhysicalName } from "../../PhysicalName.ts";
7
+ import * as Provider from "../../Provider.ts";
8
+ import { Resource } from "../../Resource.ts";
9
+ import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
10
+ import type { Providers } from "../Providers.ts";
11
+ import { VectorizeIndexBinding } from "./VectorizeIndexBinding.ts";
12
+
13
+ const VectorizeIndexTypeId = "Cloudflare.VectorizeIndex" as const;
14
+ type VectorizeIndexTypeId = typeof VectorizeIndexTypeId;
15
+
16
+ export type DistanceMetric = "cosine" | "euclidean" | "dot-product";
17
+
18
+ export type VectorizePreset =
19
+ | "@cf/baai/bge-small-en-v1.5"
20
+ | "@cf/baai/bge-base-en-v1.5"
21
+ | "@cf/baai/bge-large-en-v1.5"
22
+ | "openai/text-embedding-ada-002"
23
+ | "cohere/embed-multilingual-v2.0"
24
+ // Keep the union open so new Cloudflare presets aren't blocked by stale types.
25
+ | (string & {});
26
+
27
+ const DEFAULT_METRIC: DistanceMetric = "cosine";
28
+
29
+ export type VectorizeIndexProps = {
30
+ /**
31
+ * Name of the index. If omitted, a unique name will be generated.
32
+ * Must be lowercase alphanumeric with hyphens. Changing it triggers a
33
+ * replacement.
34
+ * @default ${app}-${stage}-${id}
35
+ */
36
+ name?: string;
37
+ /**
38
+ * Number of dimensions for each vector stored in the index. Required
39
+ * unless `preset` is provided. Cannot be changed after creation —
40
+ * updating this property triggers a replacement.
41
+ */
42
+ dimensions?: number;
43
+ /**
44
+ * Distance metric used for similarity search. Cannot be changed after
45
+ * creation — updating this property triggers a replacement.
46
+ * @default "cosine"
47
+ */
48
+ metric?: DistanceMetric;
49
+ /**
50
+ * A managed embedding model preset that fixes `dimensions` and `metric`
51
+ * to match the named model. Mutually exclusive with `dimensions`/`metric`.
52
+ * Cannot be changed after creation — updating this triggers a replacement.
53
+ */
54
+ preset?: VectorizePreset;
55
+ /**
56
+ * Human-readable description of the index. Vectorize has no update API,
57
+ * so changing the description triggers a replacement.
58
+ */
59
+ description?: string;
60
+ };
61
+
62
+ export type VectorizeIndexAttributes = {
63
+ indexName: string;
64
+ dimensions: number | undefined;
65
+ metric: DistanceMetric | undefined;
66
+ description: string | undefined;
67
+ accountId: string;
68
+ createdOn: string | undefined;
69
+ modifiedOn: string | undefined;
70
+ };
71
+
72
+ export type VectorizeIndex = Resource<
73
+ VectorizeIndexTypeId,
74
+ VectorizeIndexProps,
75
+ VectorizeIndexAttributes,
76
+ never,
77
+ Providers
78
+ >;
79
+
80
+ /**
81
+ * A Cloudflare Vectorize index for storing and querying vector embeddings.
82
+ *
83
+ * Vectorize is a globally distributed vector database. Create an index as a
84
+ * resource, then bind it to a Worker to insert, upsert, and query vectors.
85
+ *
86
+ * A Vectorize index is identified by its name and is immutable: its
87
+ * dimensions, metric, preset, and description are all fixed at creation.
88
+ * Changing any of them triggers a replacement.
89
+ *
90
+ * @section Creating an Index
91
+ * @example Index with explicit dimensions and metric
92
+ * ```typescript
93
+ * const index = yield* Cloudflare.VectorizeIndex("my-index", {
94
+ * dimensions: 768,
95
+ * metric: "cosine",
96
+ * });
97
+ * ```
98
+ *
99
+ * @example Index from a managed embedding model preset
100
+ * A preset fixes the dimensions and metric to match the named model.
101
+ * ```typescript
102
+ * const index = yield* Cloudflare.VectorizeIndex("my-index", {
103
+ * preset: "@cf/baai/bge-base-en-v1.5",
104
+ * });
105
+ * ```
106
+ *
107
+ * @example Index with a description
108
+ * ```typescript
109
+ * const index = yield* Cloudflare.VectorizeIndex("my-index", {
110
+ * dimensions: 1536,
111
+ * metric: "euclidean",
112
+ * description: "Product catalog embeddings",
113
+ * });
114
+ * ```
115
+ *
116
+ * @section Binding to a Worker
117
+ * @example Querying an index inside a Worker
118
+ * ```typescript
119
+ * const index = yield* Cloudflare.VectorizeConnection.bind(MyIndex);
120
+ *
121
+ * // Insert vectors
122
+ * yield* index.upsert([
123
+ * { id: "1", values: [0.1, 0.2, 0.3], metadata: { title: "doc" } },
124
+ * ]);
125
+ *
126
+ * // Query the nearest neighbors
127
+ * const matches = yield* index.query([0.1, 0.2, 0.3], { topK: 5 });
128
+ * ```
129
+ *
130
+ * @see https://developers.cloudflare.com/vectorize/
131
+ */
132
+ export const VectorizeIndex = Resource<VectorizeIndex>(VectorizeIndexTypeId)({
133
+ bind: VectorizeIndexBinding.bind,
134
+ });
135
+
136
+ /**
137
+ * Returns true if the given value is a VectorizeIndex resource.
138
+ */
139
+ export const isVectorizeIndex = (value: unknown): value is VectorizeIndex =>
140
+ Predicate.hasProperty(value, "Type") && value.Type === VectorizeIndexTypeId;
141
+
142
+ export const VectorizeIndexProvider = () =>
143
+ Provider.effect(
144
+ VectorizeIndex,
145
+ Effect.gen(function* () {
146
+ const { accountId } = yield* CloudflareEnvironment;
147
+ const createIndex = yield* vectorize.createIndex;
148
+ const getIndex = yield* vectorize.getIndex;
149
+ const deleteIndex = yield* vectorize.deleteIndex;
150
+
151
+ const createIndexName = (id: string, name: string | undefined) =>
152
+ Effect.gen(function* () {
153
+ return name ?? (yield* createPhysicalName({ id, lowercase: true }));
154
+ });
155
+
156
+ const buildConfig = (
157
+ news: VectorizeIndexProps,
158
+ ): vectorize.CreateIndexRequest["config"] =>
159
+ news.preset !== undefined
160
+ ? // `VectorizePreset` is intentionally open (`| (string & {})`) so
161
+ // new Cloudflare presets aren't blocked by stale types. The
162
+ // distilled type is the strict-at-release-time union; cast
163
+ // through for the API call.
164
+ ({
165
+ preset: news.preset as never,
166
+ } as vectorize.CreateIndexRequest["config"])
167
+ : {
168
+ dimensions: news.dimensions!,
169
+ metric: news.metric ?? DEFAULT_METRIC,
170
+ };
171
+
172
+ return {
173
+ stables: ["indexName", "accountId"],
174
+ diff: Effect.fn(function* ({ id, olds = {}, news = {}, output }) {
175
+ if (!isResolved(news)) return undefined;
176
+ if ((output?.accountId ?? accountId) !== accountId) {
177
+ return { action: "replace" } as const;
178
+ }
179
+ const name = yield* createIndexName(id, news.name);
180
+ const oldName = output?.indexName
181
+ ? output.indexName
182
+ : yield* createIndexName(id, olds.name);
183
+ if (
184
+ oldName !== name ||
185
+ (news.preset ?? undefined) !== (olds.preset ?? undefined) ||
186
+ (news.dimensions ?? undefined) !== (olds.dimensions ?? undefined) ||
187
+ (news.metric ?? DEFAULT_METRIC) !==
188
+ (olds.metric ?? DEFAULT_METRIC) ||
189
+ (news.description ?? undefined) !== (olds.description ?? undefined)
190
+ ) {
191
+ return { action: "replace" } as const;
192
+ }
193
+ return undefined;
194
+ }),
195
+ read: Effect.fn(function* ({ id, output, olds }) {
196
+ const acct = output?.accountId ?? accountId;
197
+ const name =
198
+ output?.indexName ?? (yield* createIndexName(id, olds?.name));
199
+ return yield* getIndex({ accountId: acct, indexName: name }).pipe(
200
+ Effect.map((index) => toAttributes(index, name, acct)),
201
+ Effect.catchTag(["NotFound", "Gone"], () =>
202
+ Effect.succeed(undefined),
203
+ ),
204
+ );
205
+ }),
206
+ reconcile: Effect.fn(function* ({ id, news = {} }) {
207
+ const indexName = yield* createIndexName(id, news.name);
208
+
209
+ // Observe — read the live index by name. The name is the stable
210
+ // identifier; fall back through a NotFound to the create path so
211
+ // we recover from out-of-band deletes or partial state-persistence.
212
+ let observed = yield* getIndex({
213
+ accountId,
214
+ indexName,
215
+ }).pipe(
216
+ Effect.catchTag(["NotFound", "Gone"], () =>
217
+ Effect.succeed(undefined),
218
+ ),
219
+ );
220
+
221
+ // Ensure — create if missing. Cloudflare returns 409 Conflict when
222
+ // an index with the same name already exists; tolerate the race by
223
+ // re-reading it.
224
+ if (!observed) {
225
+ observed = yield* createIndex({
226
+ accountId,
227
+ name: indexName,
228
+ config: buildConfig(news),
229
+ description: news.description,
230
+ }).pipe(
231
+ Effect.catchTag("IndexAlreadyExists", () =>
232
+ getIndex({ accountId, indexName }),
233
+ ),
234
+ );
235
+ }
236
+
237
+ return toAttributes(observed, indexName, accountId);
238
+ }),
239
+ delete: Effect.fn(function* ({ output }) {
240
+ yield* deleteIndex({
241
+ accountId: output.accountId,
242
+ indexName: output.indexName,
243
+ }).pipe(Effect.catchTag(["NotFound", "Gone"], () => Effect.void));
244
+ }),
245
+ };
246
+ }),
247
+ );
248
+
249
+ const toAttributes = (
250
+ index: vectorize.GetIndexResponse | vectorize.CreateIndexResponse,
251
+ name: string,
252
+ accountId: string,
253
+ ): VectorizeIndexAttributes => ({
254
+ indexName: index.name ?? name,
255
+ dimensions: index.config?.dimensions,
256
+ metric: index.config?.metric,
257
+ description: index.description ?? undefined,
258
+ accountId,
259
+ createdOn: index.createdOn ?? undefined,
260
+ modifiedOn: index.modifiedOn ?? undefined,
261
+ });
@@ -0,0 +1,103 @@
1
+ import type * as runtime from "@cloudflare/workers-types";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Layer from "effect/Layer";
4
+ import * as Option from "effect/Option";
5
+ import * as Binding from "../../Binding.ts";
6
+ import type { ResourceLike } from "../../Resource.ts";
7
+ import { isWorker, WorkerEnvironment } from "../Workers/Worker.ts";
8
+ import type { VectorizeIndex } from "./VectorizeIndex.ts";
9
+
10
+ export interface VectorizeIndexClient {
11
+ /**
12
+ * An Effect that resolves to the raw underlying Cloudflare Vectorize
13
+ * binding. Use this for direct access not covered by the helpers below.
14
+ */
15
+ raw: Effect.Effect<runtime.Vectorize>;
16
+ /** Get information about the bound index (dimensions, vector count). */
17
+ describe: () => Effect.Effect<runtime.VectorizeIndexInfo>;
18
+ /** Find the nearest neighbors of `vector`. */
19
+ query: (
20
+ vector: runtime.VectorFloatArray | number[],
21
+ options?: runtime.VectorizeQueryOptions,
22
+ ) => Effect.Effect<runtime.VectorizeMatches>;
23
+ /** Find the nearest neighbors of an existing vector by its id. */
24
+ queryById: (
25
+ vectorId: string,
26
+ options?: runtime.VectorizeQueryOptions,
27
+ ) => Effect.Effect<runtime.VectorizeMatches>;
28
+ /** Insert vectors. Throws if any provided id already exists. */
29
+ insert: (
30
+ vectors: runtime.VectorizeVector[],
31
+ ) => Effect.Effect<runtime.VectorizeAsyncMutation>;
32
+ /** Upsert vectors, replacing any existing vectors with matching ids. */
33
+ upsert: (
34
+ vectors: runtime.VectorizeVector[],
35
+ ) => Effect.Effect<runtime.VectorizeAsyncMutation>;
36
+ /** Delete vectors by id. */
37
+ deleteByIds: (ids: string[]) => Effect.Effect<runtime.VectorizeAsyncMutation>;
38
+ /** Fetch vectors by id. */
39
+ getByIds: (ids: string[]) => Effect.Effect<runtime.VectorizeVector[]>;
40
+ }
41
+
42
+ export class VectorizeIndexBinding extends Binding.Service<
43
+ VectorizeIndexBinding,
44
+ (index: VectorizeIndex) => Effect.Effect<VectorizeIndexClient>
45
+ >()("Cloudflare.Vectorize.IndexBinding") {}
46
+
47
+ export const VectorizeIndexBindingLive = Layer.effect(
48
+ VectorizeIndexBinding,
49
+ Effect.gen(function* () {
50
+ const Policy = yield* VectorizeIndexBindingPolicy;
51
+
52
+ return Effect.fn(function* (index: VectorizeIndex) {
53
+ yield* Policy(index);
54
+ const rawEff = yield* Effect.serviceOption(WorkerEnvironment).pipe(
55
+ Effect.map(Option.getOrUndefined),
56
+ Effect.map((env) => env?.[index.LogicalId]! as runtime.Vectorize),
57
+ Effect.cached,
58
+ );
59
+
60
+ const withRuntime = <A>(fn: (raw: runtime.Vectorize) => Promise<A>) =>
61
+ Effect.flatMap(rawEff, (raw) => Effect.promise(() => fn(raw)));
62
+
63
+ return {
64
+ raw: rawEff,
65
+ describe: () => withRuntime((raw) => raw.describe()),
66
+ query: (vector, options) =>
67
+ withRuntime((raw) => raw.query(vector, options)),
68
+ queryById: (vectorId, options) =>
69
+ withRuntime((raw) => raw.queryById(vectorId, options)),
70
+ insert: (vectors) => withRuntime((raw) => raw.insert(vectors)),
71
+ upsert: (vectors) => withRuntime((raw) => raw.upsert(vectors)),
72
+ deleteByIds: (ids) => withRuntime((raw) => raw.deleteByIds(ids)),
73
+ getByIds: (ids) => withRuntime((raw) => raw.getByIds(ids)),
74
+ } satisfies VectorizeIndexClient;
75
+ });
76
+ }),
77
+ );
78
+
79
+ export class VectorizeIndexBindingPolicy extends Binding.Policy<
80
+ VectorizeIndexBindingPolicy,
81
+ (index: VectorizeIndex) => Effect.Effect<void>
82
+ >()("Cloudflare.Vectorize.IndexBinding") {}
83
+
84
+ export const VectorizeIndexBindingPolicyLive =
85
+ VectorizeIndexBindingPolicy.layer.succeed(
86
+ Effect.fn(function* (host: ResourceLike, index: VectorizeIndex) {
87
+ if (isWorker(host)) {
88
+ yield* host.bind`${index}`({
89
+ bindings: [
90
+ {
91
+ type: "vectorize",
92
+ name: index.LogicalId,
93
+ indexName: index.indexName,
94
+ },
95
+ ],
96
+ });
97
+ } else {
98
+ return yield* Effect.die(
99
+ new Error(`IndexBinding does not support runtime '${host.Type}'`),
100
+ );
101
+ }
102
+ }),
103
+ );
@@ -0,0 +1,208 @@
1
+ import * as vectorize from "@distilled.cloud/cloudflare/vectorize";
2
+ import * as Effect from "effect/Effect";
3
+
4
+ import { isResolved } from "../../Diff.ts";
5
+ import * as Provider from "../../Provider.ts";
6
+ import { Resource } from "../../Resource.ts";
7
+ import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
8
+ import type { Providers } from "../Providers.ts";
9
+
10
+ export type MetadataIndexType = "string" | "number" | "boolean";
11
+
12
+ export type VectorizeMetadataIndexProps = {
13
+ /**
14
+ * Name of the parent Vectorize index. Pass `index.indexName` from a
15
+ * `VectorizeIndex` to track the dependency. Changing the parent index
16
+ * triggers a replacement.
17
+ */
18
+ indexName: string;
19
+ /**
20
+ * The metadata property to index. Filter expressions in `query` use this
21
+ * name (e.g. `{ category: { $eq: "books" } }`). Cannot be changed after
22
+ * creation — updating triggers a replacement.
23
+ */
24
+ propertyName: string;
25
+ /**
26
+ * The type of metadata values stored under `propertyName`. Cannot be
27
+ * changed after creation — updating triggers a replacement.
28
+ */
29
+ indexType: MetadataIndexType;
30
+ };
31
+
32
+ export type VectorizeMetadataIndexAttributes = {
33
+ propertyName: string;
34
+ indexType: MetadataIndexType;
35
+ indexName: string;
36
+ accountId: string;
37
+ mutationId: string | undefined;
38
+ };
39
+
40
+ export type VectorizeMetadataIndex = Resource<
41
+ "Cloudflare.VectorizeMetadataIndex",
42
+ VectorizeMetadataIndexProps,
43
+ VectorizeMetadataIndexAttributes,
44
+ never,
45
+ Providers
46
+ >;
47
+
48
+ /**
49
+ * A metadata index on a Cloudflare Vectorize index.
50
+ *
51
+ * Metadata indexes enable filtering query results by metadata properties.
52
+ * Without a metadata index on a property, that property cannot be used in
53
+ * the `filter` of a `query()` call.
54
+ *
55
+ * A metadata index is identified by its parent index and `propertyName` and
56
+ * is immutable — changing the property name, type, or parent index triggers
57
+ * a replacement.
58
+ *
59
+ * @section Creating a Metadata Index
60
+ * @example Index a string metadata property
61
+ * ```typescript
62
+ * const index = yield* Cloudflare.VectorizeIndex("my-index", {
63
+ * dimensions: 768,
64
+ * metric: "cosine",
65
+ * });
66
+ *
67
+ * yield* Cloudflare.VectorizeMetadataIndex("CategoryMetaIndex", {
68
+ * indexName: index.indexName,
69
+ * propertyName: "category",
70
+ * indexType: "string",
71
+ * });
72
+ * ```
73
+ *
74
+ * @example Index a numeric metadata property
75
+ * ```typescript
76
+ * yield* Cloudflare.VectorizeMetadataIndex("PriceMetaIndex", {
77
+ * indexName: index.indexName,
78
+ * propertyName: "price",
79
+ * indexType: "number",
80
+ * });
81
+ * ```
82
+ *
83
+ * @see https://developers.cloudflare.com/vectorize/reference/metadata-filtering/
84
+ */
85
+ export const VectorizeMetadataIndex = Resource<VectorizeMetadataIndex>(
86
+ "Cloudflare.VectorizeMetadataIndex",
87
+ );
88
+
89
+ export const VectorizeMetadataIndexProvider = () =>
90
+ Provider.effect(
91
+ VectorizeMetadataIndex,
92
+ Effect.gen(function* () {
93
+ const { accountId } = yield* CloudflareEnvironment;
94
+ const createMetadataIndex = yield* vectorize.createIndexMetadataIndex;
95
+ const deleteMetadataIndex = yield* vectorize.deleteIndexMetadataIndex;
96
+ const listMetadataIndexes = yield* vectorize.listIndexMetadataIndexes;
97
+
98
+ const findExisting = (
99
+ acct: string,
100
+ indexName: string,
101
+ propertyName: string,
102
+ ) =>
103
+ listMetadataIndexes({
104
+ accountId: acct,
105
+ indexName,
106
+ }).pipe(
107
+ Effect.map((res) => {
108
+ const index = res.metadataIndexes?.find(
109
+ (m) => m.propertyName === propertyName,
110
+ );
111
+ return index
112
+ ? {
113
+ propertyName: index.propertyName,
114
+ indexType:
115
+ index.indexType?.toLowerCase() as MetadataIndexType,
116
+ }
117
+ : undefined;
118
+ }),
119
+ Effect.catchTag(["NotFound", "Gone"], () =>
120
+ Effect.succeed(undefined),
121
+ ),
122
+ );
123
+
124
+ return {
125
+ stables: ["propertyName", "indexName", "accountId"],
126
+ diff: Effect.fn(function* ({ olds, news, output }) {
127
+ if (!isResolved(news)) return undefined;
128
+ if ((output?.accountId ?? accountId) !== accountId) {
129
+ return { action: "replace" } as const;
130
+ }
131
+ const newIndexName = news.indexName;
132
+ const oldIndexName = output?.indexName ?? olds.indexName;
133
+ if (
134
+ (oldIndexName ?? newIndexName) !== newIndexName ||
135
+ (olds.propertyName ?? news.propertyName) !== news.propertyName ||
136
+ (olds.indexType ?? news.indexType) !== news.indexType
137
+ ) {
138
+ return { action: "replace" } as const;
139
+ }
140
+ return undefined;
141
+ }),
142
+ read: Effect.fn(function* ({ output, olds }) {
143
+ const acct = output?.accountId ?? accountId;
144
+ const indexName = output?.indexName ?? olds?.indexName;
145
+ const propertyName = output?.propertyName ?? olds?.propertyName;
146
+ if (!indexName || !propertyName) return undefined;
147
+ const existing = yield* findExisting(acct, indexName, propertyName);
148
+ if (!existing?.propertyName || !existing.indexType) return undefined;
149
+ return {
150
+ propertyName: existing.propertyName,
151
+ indexType: existing.indexType,
152
+ indexName,
153
+ accountId: acct,
154
+ mutationId: output?.mutationId,
155
+ };
156
+ }),
157
+ reconcile: Effect.fn(function* ({ news, output }) {
158
+ const acct = output?.accountId ?? accountId;
159
+ const indexName = news.indexName;
160
+
161
+ // Observe — list metadata indexes on the parent and look for one
162
+ // matching propertyName.
163
+ const existing = yield* findExisting(
164
+ acct,
165
+ indexName,
166
+ news.propertyName,
167
+ );
168
+
169
+ // Ensure — create if missing. Cloudflare returns 409 Conflict on
170
+ // duplicate; tolerate the race by reusing the prior mutationId.
171
+ let mutationId = output?.mutationId;
172
+ if (!existing) {
173
+ const created = yield* createMetadataIndex({
174
+ accountId: acct,
175
+ indexName,
176
+ propertyName: news.propertyName,
177
+ indexType: news.indexType,
178
+ }).pipe(
179
+ Effect.catchTag("MetadataIndexAlreadyExists", () =>
180
+ Effect.succeed({ mutationId: output?.mutationId }),
181
+ ),
182
+ );
183
+ mutationId = created.mutationId ?? undefined;
184
+ }
185
+
186
+ return {
187
+ propertyName: news.propertyName,
188
+ indexType: news.indexType,
189
+ indexName,
190
+ accountId: acct,
191
+ mutationId,
192
+ };
193
+ }),
194
+ delete: Effect.fn(function* ({ output }) {
195
+ yield* deleteMetadataIndex({
196
+ accountId: output.accountId,
197
+ indexName: output.indexName,
198
+ propertyName: output.propertyName,
199
+ }).pipe(
200
+ Effect.catchTag(
201
+ ["NotFound", "Gone", "MetadataIndexNotFound"],
202
+ () => Effect.void,
203
+ ),
204
+ );
205
+ }),
206
+ };
207
+ }),
208
+ );
@@ -0,0 +1,3 @@
1
+ export * from "./VectorizeIndex.ts";
2
+ export * from "./VectorizeIndexBinding.ts";
3
+ export * from "./VectorizeMetadataIndex.ts";
@@ -19,6 +19,7 @@ import { isImages } from "../Images/Images.ts";
19
19
  import { isKVNamespace } from "../KV/KVNamespace.ts";
20
20
  import { isQueue } from "../Queue/Queue.ts";
21
21
  import { isR2Bucket } from "../R2/R2Bucket.ts";
22
+ import { isVectorizeIndex } from "../Vectorize/VectorizeIndex.ts";
22
23
  import { isAssets } from "./Assets.ts";
23
24
  import { isDurableObjectNamespaceLike } from "./DurableObjectNamespace.ts";
24
25
  import type { WorkerBindingProps } from "./Worker.ts";
@@ -190,6 +191,12 @@ const toBinding = (
190
191
  name: bindingName,
191
192
  service: binding.workerName,
192
193
  };
194
+ } else if (isVectorizeIndex(binding)) {
195
+ return {
196
+ type: "vectorize",
197
+ name: bindingName,
198
+ indexName: binding.indexName,
199
+ };
193
200
  } else {
194
201
  return {
195
202
  type: "json",
@@ -17,6 +17,7 @@ import { Images } from "../Images/Images.ts";
17
17
  import type { KVNamespace } from "../KV/KVNamespace.ts";
18
18
  import type { Queue } from "../Queue/Queue.ts";
19
19
  import type { R2Bucket } from "../R2/R2Bucket.ts";
20
+ import type { VectorizeIndex } from "../Vectorize/VectorizeIndex.ts";
20
21
  import type { Assets } from "./Assets.ts";
21
22
  import type { DurableObjectNamespaceLike } from "./DurableObjectNamespace.ts";
22
23
  import { makeRpcStub } from "./Rpc.ts";
@@ -50,6 +51,7 @@ export type WorkerBindingResource =
50
51
  | BrowserRendering
51
52
  | Images
52
53
  | Hyperdrive
54
+ | VectorizeIndex
53
55
  | Worker
54
56
  | DurableObjectNamespaceLike<any>;
55
57