taladb 0.8.3 → 0.9.0

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.
@@ -21,6 +21,103 @@ async function loadConfig(_configPath) {
21
21
  return {};
22
22
  }
23
23
 
24
+ // src/sync.ts
25
+ var CURSOR_COLLECTION = "__taladb_sync";
26
+ async function resolveCollections(handle, options) {
27
+ const base = options.collections ?? await handle.listCollectionNames();
28
+ const excluded = new Set(options.exclude ?? []);
29
+ return base.filter((c) => !excluded.has(c) && !c.startsWith("_"));
30
+ }
31
+ function unsupportedSync(runtime) {
32
+ const err = () => new Error(
33
+ `TalaDB sync is not yet available on the ${runtime} runtime (Node.js is supported today; browser and React Native are in progress). Track it on the roadmap.`
34
+ );
35
+ return {
36
+ sync: () => Promise.reject(err()),
37
+ exportChanges: () => Promise.reject(err()),
38
+ importChanges: () => Promise.reject(err())
39
+ };
40
+ }
41
+ async function readCursor(cursorCol, target) {
42
+ const doc = await cursorCol.findOne({ target });
43
+ return { pushMs: doc?.pushMs ?? 0, pullMs: doc?.pullMs ?? 0 };
44
+ }
45
+ async function writeCursor(cursorCol, target, cursor) {
46
+ const updated = await cursorCol.updateOne({ target }, { $set: { ...cursor } });
47
+ if (!updated) {
48
+ await cursorCol.insert({ target, ...cursor });
49
+ }
50
+ }
51
+ async function runSync(handle, adapter, options) {
52
+ const direction = options.direction ?? "both";
53
+ const target = options.target ?? "default";
54
+ const doPush = direction === "push" || direction === "both";
55
+ const doPull = direction === "pull" || direction === "both";
56
+ if (doPull && !adapter.pull) {
57
+ throw new Error(`sync direction '${direction}' requires adapter.pull()`);
58
+ }
59
+ if (doPush && !adapter.push) {
60
+ throw new Error(`sync direction '${direction}' requires adapter.push()`);
61
+ }
62
+ const collections = await resolveCollections(handle, options);
63
+ const cursorCol = handle.collection(CURSOR_COLLECTION);
64
+ const cursor = await readCursor(cursorCol, target);
65
+ const local = doPush ? await handle.exportChanges(collections, 0) : "[]";
66
+ let pulled = 0;
67
+ if (doPull) {
68
+ const remote = await adapter.pull(0);
69
+ if (remote && remote !== "[]") {
70
+ pulled = await handle.importChanges(remote);
71
+ }
72
+ }
73
+ let pushed = 0;
74
+ if (doPush && local !== "[]") {
75
+ pushed = JSON.parse(local).length;
76
+ await adapter.push(local);
77
+ }
78
+ await writeCursor(cursorCol, target, {
79
+ pushMs: cursor.pushMs,
80
+ pullMs: cursor.pullMs
81
+ });
82
+ return { pushed, pulled, cursor: 0 };
83
+ }
84
+
85
+ // src/http-adapter.ts
86
+ var HttpSyncAdapter = class {
87
+ constructor(options) {
88
+ this.endpoint = options.endpoint.replace(/\/$/, "");
89
+ this.headers = options.headers ?? {};
90
+ const f = options.fetch ?? globalThis.fetch?.bind(globalThis);
91
+ if (!f) {
92
+ throw new Error(
93
+ "HttpSyncAdapter: no fetch available. Pass options.fetch on runtimes without a global fetch."
94
+ );
95
+ }
96
+ this.fetchFn = f;
97
+ this.pushPath = options.paths?.push ?? "/push";
98
+ this.pullPath = options.paths?.pull ?? "/pull";
99
+ }
100
+ async push(changeset) {
101
+ const res = await this.fetchFn(`${this.endpoint}${this.pushPath}`, {
102
+ method: "POST",
103
+ headers: { "content-type": "application/json", ...this.headers },
104
+ body: changeset
105
+ });
106
+ if (!res.ok) {
107
+ throw new Error(`HttpSyncAdapter push failed: ${res.status} ${res.statusText}`);
108
+ }
109
+ }
110
+ async pull(sinceMs) {
111
+ const url = `${this.endpoint}${this.pullPath}?since=${encodeURIComponent(String(sinceMs))}`;
112
+ const res = await this.fetchFn(url, { method: "GET", headers: this.headers });
113
+ if (!res.ok) {
114
+ throw new Error(`HttpSyncAdapter pull failed: ${res.status} ${res.statusText}`);
115
+ }
116
+ const body = (await res.text()).trim();
117
+ return body.length === 0 ? "[]" : body;
118
+ }
119
+ };
120
+
24
121
  // src/index.ts
25
122
  var TalaDbValidationError = class extends Error {
26
123
  constructor(cause, context) {
@@ -116,28 +213,44 @@ var WorkerProxy = class {
116
213
  this.pending.clear();
117
214
  }
118
215
  };
119
- function makePoller(findFn, callback) {
216
+ function makePoller(findFn, callback, onError) {
120
217
  let active = true;
121
218
  let lastJson = "";
219
+ let running = false;
220
+ let rerun = false;
122
221
  const poll = async () => {
123
222
  if (!active) return;
223
+ if (running) {
224
+ rerun = true;
225
+ return;
226
+ }
227
+ running = true;
124
228
  try {
125
229
  const docs = await findFn();
230
+ if (!active) return;
126
231
  const json = JSON.stringify(docs);
127
232
  if (json !== lastJson) {
128
233
  lastJson = json;
129
234
  callback(docs);
130
235
  }
131
- } catch {
236
+ } catch (error) {
237
+ if (active) onError?.(error);
238
+ } finally {
239
+ running = false;
240
+ if (active) {
241
+ if (rerun) {
242
+ rerun = false;
243
+ void poll();
244
+ } else setTimeout(poll, 300);
245
+ }
132
246
  }
133
- if (active) setTimeout(poll, 300);
134
247
  };
135
248
  poll();
136
249
  return () => {
137
250
  active = false;
138
251
  };
139
252
  }
140
- async function createBrowserDB(dbName, config) {
253
+ async function createBrowserDB(dbName, config, passphrase) {
141
254
  const workerUrl = new URL("@taladb/web/worker/taladb.worker.js", "react-native://unreachable");
142
255
  const worker = new Worker(workerUrl, { type: "module", name: "taladb" });
143
256
  const proxy = new WorkerProxy(worker);
@@ -145,7 +258,13 @@ async function createBrowserDB(dbName, config) {
145
258
  proxy.abort(new Error(`taladb worker error: ${e.message ?? "unknown"}`));
146
259
  };
147
260
  const configJson = config !== void 0 ? JSON.stringify(config) : void 0;
148
- await proxy.send("init", { dbName, configJson });
261
+ try {
262
+ await proxy.send("init", { dbName, configJson, passphrase });
263
+ } catch (e) {
264
+ proxy.abort(e instanceof Error ? e : new Error(String(e)));
265
+ worker.terminate();
266
+ throw e;
267
+ }
149
268
  const nudgeCallbacks = /* @__PURE__ */ new Set();
150
269
  let channel = null;
151
270
  if (typeof BroadcastChannel !== "undefined") {
@@ -197,8 +316,17 @@ async function createBrowserDB(dbName, config) {
197
316
  collection: name,
198
317
  filterJson: filter ? s(filter) : "null"
199
318
  }),
319
+ aggregate: async (pipeline) => {
320
+ const json = await proxy.send("aggregate", {
321
+ collection: name,
322
+ pipelineJson: s(pipeline)
323
+ });
324
+ return JSON.parse(json);
325
+ },
200
326
  createIndex: (field) => proxy.send("createIndex", { collection: name, field }),
201
327
  dropIndex: (field) => proxy.send("dropIndex", { collection: name, field }),
328
+ createCompoundIndex: (fields) => proxy.send("createCompoundIndex", { collection: name, fieldsJson: JSON.stringify(fields) }),
329
+ dropCompoundIndex: (fields) => proxy.send("dropCompoundIndex", { collection: name, fieldsJson: JSON.stringify(fields) }),
202
330
  createFtsIndex: (field) => proxy.send("createFtsIndex", { collection: name, field }),
203
331
  dropFtsIndex: (field) => proxy.send("dropFtsIndex", { collection: name, field }),
204
332
  createVectorIndex: (field, options) => {
@@ -229,12 +357,19 @@ async function createBrowserDB(dbName, config) {
229
357
  });
230
358
  return JSON.parse(json);
231
359
  },
232
- subscribe: (filter, callback) => {
360
+ subscribe: (filter, callback, onError) => {
233
361
  let active = true;
234
362
  let lastJson = "";
235
363
  let timer = null;
364
+ let running = false;
365
+ let rerun = false;
236
366
  const poll = async () => {
237
367
  if (!active) return;
368
+ if (running) {
369
+ rerun = true;
370
+ return;
371
+ }
372
+ running = true;
238
373
  if (timer !== null) {
239
374
  clearTimeout(timer);
240
375
  timer = null;
@@ -244,13 +379,22 @@ async function createBrowserDB(dbName, config) {
244
379
  collection: name,
245
380
  filterJson: filter ? s(filter) : "null"
246
381
  });
382
+ if (!active) return;
247
383
  if (json !== lastJson) {
248
384
  lastJson = json;
249
385
  callback(JSON.parse(json));
250
386
  }
251
- } catch {
387
+ } catch (error) {
388
+ if (active) onError?.(error);
389
+ } finally {
390
+ running = false;
391
+ }
392
+ if (active) {
393
+ if (rerun) {
394
+ rerun = false;
395
+ void poll();
396
+ } else timer = setTimeout(poll, 300);
252
397
  }
253
- if (active) timer = setTimeout(poll, 300);
254
398
  };
255
399
  nudgeCallbacks.add(poll);
256
400
  poll();
@@ -266,9 +410,11 @@ async function createBrowserDB(dbName, config) {
266
410
  };
267
411
  return opts ? applySchema(wrapped, opts) : wrapped;
268
412
  }
269
- return {
413
+ const handle = {
270
414
  collection: (name, opts) => wrapCollection(name, opts),
271
415
  compact: () => proxy.send("compact"),
416
+ syncStatus: async () => JSON.parse(await proxy.send("syncStatus")),
417
+ flushSync: (timeoutMs = 5e3) => proxy.send("flushSync", { timeoutMs }),
272
418
  close: async () => {
273
419
  channel?.close();
274
420
  try {
@@ -277,13 +423,22 @@ async function createBrowserDB(dbName, config) {
277
423
  worker.terminate();
278
424
  proxy.abort(new Error("taladb worker closed"));
279
425
  }
280
- }
426
+ },
427
+ // All engine work (export scan, LWW merge) runs inside the worker, off the
428
+ // main thread — a sync pass never blocks rendering, whatever its size.
429
+ exportChanges: (collections, sinceMs) => proxy.send("exportChangeset", { collectionsJson: JSON.stringify(collections), sinceMs }),
430
+ importChanges: (changeset) => proxy.send("importChangeset", { changesetJson: changeset }),
431
+ listCollectionNames: async () => JSON.parse(await proxy.send("listCollections")),
432
+ sync: (adapter, options) => runSync(handle, adapter, options)
281
433
  };
434
+ return handle;
282
435
  }
283
- async function createNodeDB(dbName, config) {
284
- const { TalaDBNode } = await import("./node-A4LKRSW5.mjs");
436
+ async function createNodeDB(dbName, config, passphrase) {
437
+ const native = await import("./node-A4LKRSW5.mjs");
438
+ const TalaDBNode = native.TalaDbNode ?? native.TalaDBNode;
439
+ if (!TalaDBNode) throw new Error("@taladb/node loaded but exports no TalaDbNode class \u2014 rebuild the native module");
285
440
  const configJson = config !== void 0 ? JSON.stringify(config) : null;
286
- const db = TalaDBNode.open(dbName, configJson);
441
+ const db = TalaDBNode.open(dbName, configJson, passphrase ?? null);
287
442
  function wrapCollection(name, opts) {
288
443
  const col = db.collection(name);
289
444
  const wrapped = {
@@ -296,8 +451,11 @@ async function createNodeDB(dbName, config) {
296
451
  deleteOne: async (filter) => col.deleteOneAsync ? col.deleteOneAsync(filter) : col.deleteOne(filter),
297
452
  deleteMany: async (filter) => col.deleteManyAsync ? col.deleteManyAsync(filter) : col.deleteMany(filter),
298
453
  count: async (filter) => col.count(filter ?? null),
454
+ aggregate: async (pipeline) => col.aggregate(pipeline),
299
455
  createIndex: async (field) => col.createIndex(field),
300
456
  dropIndex: async (field) => col.dropIndex(field),
457
+ createCompoundIndex: async (fields) => col.createCompoundIndex(fields),
458
+ dropCompoundIndex: async (fields) => col.dropCompoundIndex(fields),
301
459
  createFtsIndex: async (field) => col.createFtsIndex(field),
302
460
  dropFtsIndex: async (field) => col.dropFtsIndex(field),
303
461
  createVectorIndex: async (field, options) => col.createVectorIndex(field, options.dimensions, options.metric ?? null, options.indexType ?? null, options.hnswM ?? null, options.hnswEfConstruction ?? null),
@@ -311,16 +469,21 @@ async function createNodeDB(dbName, config) {
311
469
  const raw = await col.findNearest(field, vector, topK, filter ?? null);
312
470
  return raw;
313
471
  },
314
- subscribe: (filter, callback) => makePoller(async () => col.find(filter ?? null), callback)
472
+ subscribe: (filter, callback, onError) => makePoller(async () => col.find(filter ?? null), callback, onError)
315
473
  };
316
474
  return opts ? applySchema(wrapped, opts) : wrapped;
317
475
  }
318
- return {
476
+ const handle = {
319
477
  collection: (name, opts) => wrapCollection(name, opts),
320
478
  compact: async () => db.compact(),
321
479
  // Releases the native file handle/lock (no-op on older .node binaries).
322
- close: async () => db.close?.()
480
+ close: async () => db.close?.(),
481
+ exportChanges: async (collections, sinceMs) => db.exportChanges(sinceMs, collections),
482
+ importChanges: async (changeset) => db.importChanges(changeset),
483
+ listCollectionNames: async () => db.listCollectionNames(),
484
+ sync: (adapter, options) => runSync(handle, adapter, options)
323
485
  };
486
+ return handle;
324
487
  }
325
488
  async function createNativeDB(_dbName) {
326
489
  const maybeNative = globalThis.__TalaDB__;
@@ -341,8 +504,11 @@ async function createNativeDB(_dbName) {
341
504
  deleteOne: async (filter) => native.deleteOne(name, filter),
342
505
  deleteMany: async (filter) => native.deleteMany(name, filter),
343
506
  count: async (filter) => native.count(name, filter ?? {}),
507
+ aggregate: async (pipeline) => native.aggregate(name, pipeline),
344
508
  createIndex: async (field) => native.createIndex(name, field),
345
509
  dropIndex: async (field) => native.dropIndex(name, field),
510
+ createCompoundIndex: async (fields) => native.createCompoundIndex(name, fields),
511
+ dropCompoundIndex: async (fields) => native.dropCompoundIndex(name, fields),
346
512
  createFtsIndex: async (field) => native.createFtsIndex(name, field),
347
513
  dropFtsIndex: async (field) => native.dropFtsIndex(name, field),
348
514
  createVectorIndex: async (field, options) => {
@@ -362,17 +528,35 @@ async function createNativeDB(_dbName) {
362
528
  const raw = native.findNearest(name, field, vector, topK, filter ?? null);
363
529
  return raw;
364
530
  },
365
- subscribe: (filter, callback) => makePoller(async () => native.find(name, filter ?? {}), callback)
531
+ subscribe: (filter, callback, onError) => makePoller(async () => native.find(name, filter ?? {}), callback, onError)
366
532
  };
367
533
  return opts ? applySchema(wrapped, opts) : wrapped;
368
534
  }
535
+ const syncSurface = typeof native.exportChanges === "function" && typeof native.importChanges === "function" && typeof native.listCollectionNames === "function" ? (() => {
536
+ const handle = {
537
+ collection: (name, opts) => wrapCollection(name, opts),
538
+ exportChanges: async (collections, sinceMs) => native.exportChanges(collections, sinceMs),
539
+ importChanges: async (changeset) => native.importChanges(changeset),
540
+ listCollectionNames: async () => native.listCollectionNames(),
541
+ sync: (adapter, options) => runSync(handle, adapter, options)
542
+ };
543
+ return {
544
+ exportChanges: handle.exportChanges,
545
+ importChanges: handle.importChanges,
546
+ sync: handle.sync
547
+ };
548
+ })() : unsupportedSync("react-native");
369
549
  return {
370
550
  collection: (name, opts) => wrapCollection(name, opts),
371
551
  compact: async () => native.compact(),
372
- close: async () => native.close()
552
+ close: async () => native.close(),
553
+ ...syncSurface
373
554
  };
374
555
  }
375
556
  async function openDB(dbName = "taladb.db", options) {
557
+ if (options?.passphrase !== void 0 && options.passphrase.length === 0) {
558
+ throw new Error("TalaDB encryption passphrase must not be empty");
559
+ }
376
560
  let resolvedConfig;
377
561
  if (options?.config !== void 0) {
378
562
  validateConfig(options.config);
@@ -383,14 +567,18 @@ async function openDB(dbName = "taladb.db", options) {
383
567
  const platform = detectPlatform();
384
568
  switch (platform) {
385
569
  case "browser":
386
- return createBrowserDB(dbName, resolvedConfig);
570
+ return createBrowserDB(dbName, resolvedConfig, options?.passphrase);
387
571
  case "react-native":
572
+ if (options?.passphrase !== void 0) {
573
+ throw new Error("On React Native, pass the passphrase in the config JSON to TalaDBModule.initialize(); refusing to assume the already-open native database is encrypted");
574
+ }
388
575
  return createNativeDB(dbName);
389
576
  case "node":
390
- return createNodeDB(dbName, resolvedConfig);
577
+ return createNodeDB(dbName, resolvedConfig, options?.passphrase);
391
578
  }
392
579
  }
393
580
  export {
581
+ HttpSyncAdapter,
394
582
  TalaDbValidationError,
395
583
  openDB
396
584
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taladb",
3
- "version": "0.8.3",
3
+ "version": "0.9.0",
4
4
  "description": "Local-first document and vector database for React, React Native, and Node.js",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -42,7 +42,7 @@
42
42
  "repository": {
43
43
  "type": "git",
44
44
  "url": "https://github.com/thinkgrid-labs/taladb.git",
45
- "directory": "packages/taladb"
45
+ "directory": "packages/clients/taladb"
46
46
  },
47
47
  "homepage": "https://thinkgrid-labs.github.io/taladb/",
48
48
  "bugs": {