@taladb/cloudflare 0.7.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 thinkgrid-labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@taladb/cloudflare",
3
+ "version": "0.7.3",
4
+ "description": "TalaDB adapter for Cloudflare Workers — in-memory WASM database with Durable Objects snapshot persistence",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "types": "src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "import": "./src/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "src/index.js",
16
+ "src/index.d.ts"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/thinkgrid-labs/taladb.git",
21
+ "directory": "packages/taladb-cloudflare"
22
+ },
23
+ "homepage": "https://taladb.dev/guide/cloudflare",
24
+ "bugs": {
25
+ "url": "https://github.com/thinkgrid-labs/taladb/issues"
26
+ },
27
+ "license": "MIT",
28
+ "peerDependencies": {
29
+ "taladb": "0.7.3",
30
+ "@taladb/web": "0.7.3"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.9.3",
34
+ "@cloudflare/workers-types": "^4.0.0"
35
+ }
36
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,84 @@
1
+ import type { Collection, CollectionOptions, Document, Filter, TalaDB } from 'taladb';
2
+
3
+ /**
4
+ * Thrown when a document fails schema validation on `insert` or `insertMany`.
5
+ * Mirrors `TalaDbValidationError` from the `taladb` package.
6
+ */
7
+ export declare class TalaDbValidationError extends Error {
8
+ readonly cause: unknown;
9
+ constructor(cause: unknown, context?: string);
10
+ }
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // CloudflareDB — TalaDB-compatible handle for Cloudflare Workers
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export interface CloudflareDB extends Omit<TalaDB, 'compact' | 'close'> {
17
+ collection<T extends Document = Document>(name: string, options?: CollectionOptions<T>): Collection<T>;
18
+ /** Persist the current snapshot to Durable Objects storage. */
19
+ flush(): Promise<void>;
20
+ /** Compact the in-memory redb instance (no-op on in-memory backend). */
21
+ compact(): Promise<void>;
22
+ close(): Promise<void>;
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // openDurableDB
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Open a TalaDB database backed by Durable Objects storage.
31
+ *
32
+ * Call once per request (or cache on the DO instance via `getDB()`).
33
+ * After mutations call `db.flush()` to persist the snapshot.
34
+ *
35
+ * @param storage `this.ctx.storage` from the Durable Object constructor.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * const db = await openDurableDB(this.ctx.storage);
40
+ * const users = db.collection<User>('users');
41
+ * await users.insert({ name: 'Alice' });
42
+ * await db.flush();
43
+ * ```
44
+ */
45
+ export function openDurableDB(storage: DurableObjectStorage): Promise<CloudflareDB>;
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // TalaDBDurableObject — base class
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /**
52
+ * Base Durable Object class that manages a TalaDB database.
53
+ *
54
+ * Extend and export this from your Worker entrypoint, then bind it in
55
+ * `wrangler.toml` as a Durable Object binding.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * import { TalaDBDurableObject } from '@taladb/cloudflare';
60
+ *
61
+ * export class MyDB extends TalaDBDurableObject {
62
+ * async fetch(request: Request): Promise<Response> {
63
+ * const db = await this.getDB();
64
+ * const users = db.collection<{ name: string }>('users');
65
+ *
66
+ * if (request.method === 'POST') {
67
+ * const body = await request.json<{ name: string }>();
68
+ * const id = await users.insert(body);
69
+ * await db.flush();
70
+ * return Response.json({ id });
71
+ * }
72
+ *
73
+ * return Response.json(await users.find());
74
+ * }
75
+ * }
76
+ * ```
77
+ */
78
+ export class TalaDBDurableObject {
79
+ protected ctx: DurableObjectState;
80
+ constructor(ctx: DurableObjectState, env: unknown);
81
+ /** Get (or lazily open) the TalaDB database for this DO instance. */
82
+ getDB(): Promise<CloudflareDB>;
83
+ fetch(request: Request): Promise<Response>;
84
+ }
package/src/index.js ADDED
@@ -0,0 +1,354 @@
1
+ /**
2
+ * @taladb/cloudflare — TalaDB adapter for Cloudflare Workers.
3
+ *
4
+ * Uses the existing @taladb/web WASM (in-memory mode — no OPFS) and persists
5
+ * state to Durable Objects storage as a binary snapshot.
6
+ *
7
+ * ## Architecture
8
+ *
9
+ * Each Durable Object instance holds one TalaDB database in WASM memory.
10
+ * On every mutating request the snapshot is flushed to Durable Objects
11
+ * `storage.put()`. When the DO hibernates and wakes up, `init()` restores the
12
+ * database from the last saved snapshot via `storage.get()`.
13
+ *
14
+ * ## Usage
15
+ *
16
+ * ```ts
17
+ * import { TalaDBDurableObject, openDurableDB } from '@taladb/cloudflare';
18
+ * import { WorkerDB } from '@taladb/web/pkg/taladb_web.js';
19
+ *
20
+ * export class MyDB extends TalaDBDurableObject {}
21
+ *
22
+ * export default {
23
+ * async fetch(request, env) {
24
+ * const id = env.MY_DB.idFromName('default');
25
+ * const stub = env.MY_DB.get(id);
26
+ * return stub.fetch(request);
27
+ * }
28
+ * };
29
+ * ```
30
+ *
31
+ * Or use `openDurableDB` inside a Durable Object's `fetch` method to get a
32
+ * full TalaDB-compatible API object:
33
+ *
34
+ * ```ts
35
+ * const db = await openDurableDB(this.storage);
36
+ * const users = db.collection('users');
37
+ * await users.insert({ name: 'Alice' });
38
+ * await db.flush(); // persist snapshot to DO storage
39
+ * await db.compact();
40
+ * ```
41
+ */
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // WASM lazy loader — import once per Worker isolate lifetime
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /** @type {import('../../../taladb-web/pkg/taladb_web.js').WorkerDB | null} */
48
+ let WorkerDBClass = null;
49
+ let wasmInitPromise = null;
50
+
51
+ async function loadWasm() {
52
+ if (WorkerDBClass) return WorkerDBClass;
53
+ if (wasmInitPromise) return wasmInitPromise;
54
+
55
+ wasmInitPromise = (async () => {
56
+ const wasm = await import('@taladb/web/pkg/taladb_web.js');
57
+ await wasm.default();
58
+ WorkerDBClass = wasm.WorkerDB;
59
+ return WorkerDBClass;
60
+ })();
61
+
62
+ return wasmInitPromise;
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Snapshot persistence key used in Durable Objects storage
67
+ // ---------------------------------------------------------------------------
68
+
69
+ const SNAPSHOT_KEY = '__taladb_snapshot__';
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Schema validation helpers
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /**
76
+ * Thrown when a document fails schema validation on insert.
77
+ * Compatible with the `TalaDbValidationError` exported from `taladb`.
78
+ */
79
+ export class TalaDbValidationError extends Error {
80
+ constructor(cause, context) {
81
+ const label = context ? ` (${context})` : '';
82
+ const msg = cause instanceof Error ? cause.message : String(cause);
83
+ super(`TalaDB schema validation failed${label}: ${msg}`);
84
+ this.name = 'TalaDbValidationError';
85
+ this.cause = cause;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Wraps a CloudflareCollection to run writes through a schema.parse() call.
91
+ * @template T
92
+ * @param {CloudflareCollection} col
93
+ * @param {{ schema: { parse(data: unknown): T }, validateOnRead?: boolean }} options
94
+ * @returns {CloudflareCollection}
95
+ */
96
+ function applyCloudflareSchema(col, options) {
97
+ const { schema, validateOnRead = false } = options;
98
+
99
+ function parseWrite(doc, label) {
100
+ try {
101
+ return schema.parse(doc);
102
+ } catch (err) {
103
+ throw new TalaDbValidationError(err, label);
104
+ }
105
+ }
106
+
107
+ function parseRead(doc) {
108
+ try {
109
+ return schema.parse(doc);
110
+ } catch (err) {
111
+ throw new TalaDbValidationError(err, 'read');
112
+ }
113
+ }
114
+
115
+ return Object.assign(Object.create(Object.getPrototypeOf(col)), col, {
116
+ async insert(doc) {
117
+ parseWrite(doc, 'insert');
118
+ return col.insert(doc);
119
+ },
120
+ async insertMany(docs) {
121
+ docs.forEach((doc, i) => parseWrite(doc, `insertMany[${i}]`));
122
+ return col.insertMany(docs);
123
+ },
124
+ async find(filter) {
125
+ const docs = await col.find(filter);
126
+ return validateOnRead ? docs.map(parseRead) : docs;
127
+ },
128
+ async findOne(filter) {
129
+ const doc = await col.findOne(filter);
130
+ return validateOnRead && doc !== null ? parseRead(doc) : doc;
131
+ },
132
+ });
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // openDurableDB — create a TalaDB-compatible database backed by DO storage
137
+ // ---------------------------------------------------------------------------
138
+
139
+ /**
140
+ * Open a TalaDB database inside a Durable Object.
141
+ *
142
+ * Loads the snapshot from `storage.get(SNAPSHOT_KEY)` on first call (or after
143
+ * hibernation). Call `db.flush()` after mutations to persist the snapshot back
144
+ * to Durable Objects storage.
145
+ *
146
+ * @param {DurableObjectStorage} storage — `this.ctx.storage` from the DO
147
+ * @returns {Promise<CloudflareDB>}
148
+ */
149
+ export async function openDurableDB(storage) {
150
+ const WDB = await loadWasm();
151
+
152
+ const snapshotBytes = await storage.get(SNAPSHOT_KEY);
153
+ const db = snapshotBytes
154
+ ? WDB.openWithSnapshot(new Uint8Array(snapshotBytes))
155
+ : WDB.openInMemory();
156
+
157
+ return new CloudflareDB(db, storage);
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // CloudflareDB — wraps WorkerDB with a TalaDB-compatible API
162
+ // ---------------------------------------------------------------------------
163
+
164
+ class CloudflareDB {
165
+ /** @param {import('../../../taladb-web/pkg/taladb_web.js').WorkerDB} workerDb */
166
+ constructor(workerDb, storage) {
167
+ this._db = workerDb;
168
+ this._storage = storage;
169
+ }
170
+
171
+ collection(name, options) {
172
+ const col = new CloudflareCollection(this._db, name, this);
173
+ return options?.schema ? applyCloudflareSchema(col, options) : col;
174
+ }
175
+
176
+ /**
177
+ * Persist the current in-memory snapshot to Durable Objects storage.
178
+ * Call after every mutation batch, or at the end of a request handler.
179
+ */
180
+ async flush() {
181
+ const bytes = this._db.exportSnapshot();
182
+ await this._storage.put(SNAPSHOT_KEY, bytes.buffer);
183
+ }
184
+
185
+ /** Compact the in-memory redb instance (no-op for in-memory backend). */
186
+ async compact() {
187
+ this._db.compact();
188
+ }
189
+
190
+ async close() {}
191
+ }
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // CloudflareCollection — synchronous Rust calls wrapped as async
195
+ // ---------------------------------------------------------------------------
196
+
197
+ class CloudflareCollection {
198
+ constructor(db, name, parent) {
199
+ this._db = db;
200
+ this._name = name;
201
+ this._parent = parent;
202
+ }
203
+
204
+ async insert(doc) {
205
+ const id = this._db.insert(this._name, JSON.stringify(doc));
206
+ return id;
207
+ }
208
+
209
+ async insertMany(docs) {
210
+ const json = this._db.insertMany(this._name, JSON.stringify(docs));
211
+ return JSON.parse(json);
212
+ }
213
+
214
+ async find(filter) {
215
+ const json = this._db.find(this._name, filter ? JSON.stringify(filter) : 'null');
216
+ return JSON.parse(json);
217
+ }
218
+
219
+ async findOne(filter) {
220
+ const json = this._db.findOne(this._name, filter ? JSON.stringify(filter) : 'null');
221
+ return JSON.parse(json);
222
+ }
223
+
224
+ async updateOne(filter, update) {
225
+ return this._db.updateOne(this._name, JSON.stringify(filter), JSON.stringify(update));
226
+ }
227
+
228
+ async updateMany(filter, update) {
229
+ return this._db.updateMany(this._name, JSON.stringify(filter), JSON.stringify(update));
230
+ }
231
+
232
+ async deleteOne(filter) {
233
+ return this._db.deleteOne(this._name, JSON.stringify(filter));
234
+ }
235
+
236
+ async deleteMany(filter) {
237
+ return this._db.deleteMany(this._name, JSON.stringify(filter));
238
+ }
239
+
240
+ async count(filter) {
241
+ return this._db.count(this._name, filter ? JSON.stringify(filter) : 'null');
242
+ }
243
+
244
+ async createIndex(field) {
245
+ this._db.createIndex(this._name, field);
246
+ }
247
+
248
+ async dropIndex(field) {
249
+ this._db.dropIndex(this._name, field);
250
+ }
251
+
252
+ async createFtsIndex(field) {
253
+ this._db.createFtsIndex(this._name, field);
254
+ }
255
+
256
+ async dropFtsIndex(field) {
257
+ this._db.dropFtsIndex(this._name, field);
258
+ }
259
+
260
+ async createVectorIndex(field, options) {
261
+ if (options.indexType === 'hnsw') {
262
+ throw new Error('HNSW vector indexes are not available in Cloudflare Workers (requires native threads). Use Node.js or React Native.');
263
+ }
264
+ this._db.createVectorIndex(
265
+ this._name, field, options.dimensions,
266
+ options.metric ?? null, null, null, null,
267
+ );
268
+ }
269
+
270
+ async dropVectorIndex(field) {
271
+ this._db.dropVectorIndex(this._name, field);
272
+ }
273
+
274
+ async upgradeVectorIndex(_field) {
275
+ throw new Error('HNSW vector indexes are not available in Cloudflare Workers.');
276
+ }
277
+
278
+ async listIndexes() {
279
+ return JSON.parse(this._db.listIndexes(this._name));
280
+ }
281
+
282
+ async findNearest(field, vector, topK, filter) {
283
+ const json = this._db.findNearest(
284
+ this._name, field,
285
+ JSON.stringify(vector), topK,
286
+ filter ? JSON.stringify(filter) : 'null',
287
+ );
288
+ return JSON.parse(json);
289
+ }
290
+
291
+ subscribe(_filter, _callback) {
292
+ throw new Error('subscribe() is not supported in Cloudflare Workers. Use find() inside request handlers instead.');
293
+ }
294
+ }
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // TalaDBDurableObject — base class for Durable Objects using TalaDB
298
+ // ---------------------------------------------------------------------------
299
+
300
+ /**
301
+ * Base Durable Object class. Extend this and export it from your Worker.
302
+ *
303
+ * @example
304
+ * ```ts
305
+ * import { TalaDBDurableObject } from '@taladb/cloudflare';
306
+ *
307
+ * export class MyDB extends TalaDBDurableObject {
308
+ * async fetch(request: Request): Promise<Response> {
309
+ * const db = await this.getDB();
310
+ * const users = db.collection('users');
311
+ *
312
+ * if (request.method === 'POST') {
313
+ * const body = await request.json();
314
+ * const id = await users.insert(body);
315
+ * await db.flush();
316
+ * return Response.json({ id });
317
+ * }
318
+ *
319
+ * const all = await users.find();
320
+ * return Response.json(all);
321
+ * }
322
+ * }
323
+ * ```
324
+ */
325
+ export class TalaDBDurableObject {
326
+ constructor(ctx, _env) {
327
+ this.ctx = ctx;
328
+ this._db = null;
329
+ }
330
+
331
+ /**
332
+ * Get (or lazily open) the TalaDB database for this Durable Object instance.
333
+ * Caches the open database for the lifetime of the isolate.
334
+ *
335
+ * @returns {Promise<CloudflareDB>}
336
+ */
337
+ async getDB() {
338
+ if (!this._db) {
339
+ this._db = await openDurableDB(this.ctx.storage);
340
+ }
341
+ return this._db;
342
+ }
343
+
344
+ /**
345
+ * Default fetch handler — override in your subclass.
346
+ * @param {Request} _request
347
+ * @returns {Promise<Response>}
348
+ */
349
+ async fetch(_request) {
350
+ return new Response('TalaDB Durable Object — override fetch() in your subclass', {
351
+ status: 200,
352
+ });
353
+ }
354
+ }