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.
- package/lib/AlchemyContext.d.ts +0 -1
- package/lib/AlchemyContext.d.ts.map +1 -1
- package/lib/AlchemyContext.js.map +1 -1
- package/lib/Cli/commands/cloudflare.d.ts.map +1 -1
- package/lib/Cli/commands/cloudflare.js +5 -1
- package/lib/Cli/commands/cloudflare.js.map +1 -1
- package/lib/Cloudflare/Providers.d.ts.map +1 -1
- package/lib/Cloudflare/Providers.js +5 -1
- package/lib/Cloudflare/Providers.js.map +1 -1
- package/lib/Cloudflare/Queue/QueueConsumer.d.ts.map +1 -1
- package/lib/Cloudflare/Queue/QueueConsumer.js +1 -2
- package/lib/Cloudflare/Queue/QueueConsumer.js.map +1 -1
- package/lib/Cloudflare/StateStore/Api.d.ts +1 -1
- package/lib/Cloudflare/StateStore/Api.js +1 -1
- package/lib/Cloudflare/StateStore/State.d.ts +11 -65
- package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/State.js +307 -366
- package/lib/Cloudflare/StateStore/State.js.map +1 -1
- package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/Store.js +16 -29
- package/lib/Cloudflare/StateStore/Store.js.map +1 -1
- package/lib/Cloudflare/Vectorize/VectorizeIndex.d.ts +114 -0
- package/lib/Cloudflare/Vectorize/VectorizeIndex.d.ts.map +1 -0
- package/lib/Cloudflare/Vectorize/VectorizeIndex.js +157 -0
- package/lib/Cloudflare/Vectorize/VectorizeIndex.js.map +1 -0
- package/lib/Cloudflare/Vectorize/VectorizeIndexBinding.d.ts +38 -0
- package/lib/Cloudflare/Vectorize/VectorizeIndexBinding.d.ts.map +1 -0
- package/lib/Cloudflare/Vectorize/VectorizeIndexBinding.js +44 -0
- package/lib/Cloudflare/Vectorize/VectorizeIndexBinding.js.map +1 -0
- package/lib/Cloudflare/Vectorize/VectorizeMetadataIndex.d.ts +72 -0
- package/lib/Cloudflare/Vectorize/VectorizeMetadataIndex.d.ts.map +1 -0
- package/lib/Cloudflare/Vectorize/VectorizeMetadataIndex.js +131 -0
- package/lib/Cloudflare/Vectorize/VectorizeMetadataIndex.js.map +1 -0
- package/lib/Cloudflare/Vectorize/index.d.ts +4 -0
- package/lib/Cloudflare/Vectorize/index.d.ts.map +1 -0
- package/lib/Cloudflare/Vectorize/index.js +4 -0
- package/lib/Cloudflare/Vectorize/index.js.map +1 -0
- package/lib/Cloudflare/Workers/WorkerAsyncBindings.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerAsyncBindings.js +8 -0
- package/lib/Cloudflare/Workers/WorkerAsyncBindings.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBinding.d.ts +2 -1
- package/lib/Cloudflare/Workers/WorkerBinding.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBinding.js.map +1 -1
- package/lib/Cloudflare/index.d.ts +1 -0
- package/lib/Cloudflare/index.d.ts.map +1 -1
- package/lib/Cloudflare/index.js +1 -0
- package/lib/Cloudflare/index.js.map +1 -1
- package/lib/State/HttpStateStore.d.ts +4 -0
- package/lib/State/HttpStateStore.d.ts.map +1 -1
- package/lib/State/HttpStateStore.js +13 -0
- package/lib/State/HttpStateStore.js.map +1 -1
- package/lib/Util/poll.d.ts +30 -0
- package/lib/Util/poll.d.ts.map +1 -0
- package/lib/Util/poll.js +26 -0
- package/lib/Util/poll.js.map +1 -0
- package/lib/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/AlchemyContext.ts +0 -1
- package/src/Cli/commands/cloudflare.ts +5 -3
- package/src/Cloudflare/Providers.ts +7 -0
- package/src/Cloudflare/Queue/QueueConsumer.ts +0 -2
- package/src/Cloudflare/StateStore/Api.ts +1 -1
- package/src/Cloudflare/StateStore/State.ts +445 -507
- package/src/Cloudflare/StateStore/Store.ts +22 -35
- package/src/Cloudflare/Vectorize/VectorizeIndex.ts +261 -0
- package/src/Cloudflare/Vectorize/VectorizeIndexBinding.ts +103 -0
- package/src/Cloudflare/Vectorize/VectorizeMetadataIndex.ts +208 -0
- package/src/Cloudflare/Vectorize/index.ts +3 -0
- package/src/Cloudflare/Workers/WorkerAsyncBindings.ts +7 -0
- package/src/Cloudflare/Workers/WorkerBinding.ts +2 -0
- package/src/Cloudflare/index.ts +1 -0
- package/src/State/HttpStateStore.ts +28 -0
- 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
|
-
|
|
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
|
-
|
|
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 =
|
|
65
|
-
const counter = framed.
|
|
66
|
-
const ciphertext = framed.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
);
|
|
@@ -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
|
|