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.
package/dist/index.mjs CHANGED
@@ -78,6 +78,103 @@ async function loadConfig(configPath) {
78
78
  return {};
79
79
  }
80
80
 
81
+ // src/sync.ts
82
+ var CURSOR_COLLECTION = "__taladb_sync";
83
+ async function resolveCollections(handle, options) {
84
+ const base = options.collections ?? await handle.listCollectionNames();
85
+ const excluded = new Set(options.exclude ?? []);
86
+ return base.filter((c) => !excluded.has(c) && !c.startsWith("_"));
87
+ }
88
+ function unsupportedSync(runtime) {
89
+ const err = () => new Error(
90
+ `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.`
91
+ );
92
+ return {
93
+ sync: () => Promise.reject(err()),
94
+ exportChanges: () => Promise.reject(err()),
95
+ importChanges: () => Promise.reject(err())
96
+ };
97
+ }
98
+ async function readCursor(cursorCol, target) {
99
+ const doc = await cursorCol.findOne({ target });
100
+ return { pushMs: doc?.pushMs ?? 0, pullMs: doc?.pullMs ?? 0 };
101
+ }
102
+ async function writeCursor(cursorCol, target, cursor) {
103
+ const updated = await cursorCol.updateOne({ target }, { $set: { ...cursor } });
104
+ if (!updated) {
105
+ await cursorCol.insert({ target, ...cursor });
106
+ }
107
+ }
108
+ async function runSync(handle, adapter, options) {
109
+ const direction = options.direction ?? "both";
110
+ const target = options.target ?? "default";
111
+ const doPush = direction === "push" || direction === "both";
112
+ const doPull = direction === "pull" || direction === "both";
113
+ if (doPull && !adapter.pull) {
114
+ throw new Error(`sync direction '${direction}' requires adapter.pull()`);
115
+ }
116
+ if (doPush && !adapter.push) {
117
+ throw new Error(`sync direction '${direction}' requires adapter.push()`);
118
+ }
119
+ const collections = await resolveCollections(handle, options);
120
+ const cursorCol = handle.collection(CURSOR_COLLECTION);
121
+ const cursor = await readCursor(cursorCol, target);
122
+ const local = doPush ? await handle.exportChanges(collections, 0) : "[]";
123
+ let pulled = 0;
124
+ if (doPull) {
125
+ const remote = await adapter.pull(0);
126
+ if (remote && remote !== "[]") {
127
+ pulled = await handle.importChanges(remote);
128
+ }
129
+ }
130
+ let pushed = 0;
131
+ if (doPush && local !== "[]") {
132
+ pushed = JSON.parse(local).length;
133
+ await adapter.push(local);
134
+ }
135
+ await writeCursor(cursorCol, target, {
136
+ pushMs: cursor.pushMs,
137
+ pullMs: cursor.pullMs
138
+ });
139
+ return { pushed, pulled, cursor: 0 };
140
+ }
141
+
142
+ // src/http-adapter.ts
143
+ var HttpSyncAdapter = class {
144
+ constructor(options) {
145
+ this.endpoint = options.endpoint.replace(/\/$/, "");
146
+ this.headers = options.headers ?? {};
147
+ const f = options.fetch ?? globalThis.fetch?.bind(globalThis);
148
+ if (!f) {
149
+ throw new Error(
150
+ "HttpSyncAdapter: no fetch available. Pass options.fetch on runtimes without a global fetch."
151
+ );
152
+ }
153
+ this.fetchFn = f;
154
+ this.pushPath = options.paths?.push ?? "/push";
155
+ this.pullPath = options.paths?.pull ?? "/pull";
156
+ }
157
+ async push(changeset) {
158
+ const res = await this.fetchFn(`${this.endpoint}${this.pushPath}`, {
159
+ method: "POST",
160
+ headers: { "content-type": "application/json", ...this.headers },
161
+ body: changeset
162
+ });
163
+ if (!res.ok) {
164
+ throw new Error(`HttpSyncAdapter push failed: ${res.status} ${res.statusText}`);
165
+ }
166
+ }
167
+ async pull(sinceMs) {
168
+ const url = `${this.endpoint}${this.pullPath}?since=${encodeURIComponent(String(sinceMs))}`;
169
+ const res = await this.fetchFn(url, { method: "GET", headers: this.headers });
170
+ if (!res.ok) {
171
+ throw new Error(`HttpSyncAdapter pull failed: ${res.status} ${res.statusText}`);
172
+ }
173
+ const body = (await res.text()).trim();
174
+ return body.length === 0 ? "[]" : body;
175
+ }
176
+ };
177
+
81
178
  // src/index.ts
82
179
  var TalaDbValidationError = class extends Error {
83
180
  constructor(cause, context) {
@@ -173,28 +270,44 @@ var WorkerProxy = class {
173
270
  this.pending.clear();
174
271
  }
175
272
  };
176
- function makePoller(findFn, callback) {
273
+ function makePoller(findFn, callback, onError) {
177
274
  let active = true;
178
275
  let lastJson = "";
276
+ let running = false;
277
+ let rerun = false;
179
278
  const poll = async () => {
180
279
  if (!active) return;
280
+ if (running) {
281
+ rerun = true;
282
+ return;
283
+ }
284
+ running = true;
181
285
  try {
182
286
  const docs = await findFn();
287
+ if (!active) return;
183
288
  const json = JSON.stringify(docs);
184
289
  if (json !== lastJson) {
185
290
  lastJson = json;
186
291
  callback(docs);
187
292
  }
188
- } catch {
293
+ } catch (error) {
294
+ if (active) onError?.(error);
295
+ } finally {
296
+ running = false;
297
+ if (active) {
298
+ if (rerun) {
299
+ rerun = false;
300
+ void poll();
301
+ } else setTimeout(poll, 300);
302
+ }
189
303
  }
190
- if (active) setTimeout(poll, 300);
191
304
  };
192
305
  poll();
193
306
  return () => {
194
307
  active = false;
195
308
  };
196
309
  }
197
- async function createBrowserDB(dbName, config) {
310
+ async function createBrowserDB(dbName, config, passphrase) {
198
311
  const workerUrl = new URL("@taladb/web/worker/taladb.worker.js", import.meta.url);
199
312
  const worker = new Worker(workerUrl, { type: "module", name: "taladb" });
200
313
  const proxy = new WorkerProxy(worker);
@@ -202,7 +315,13 @@ async function createBrowserDB(dbName, config) {
202
315
  proxy.abort(new Error(`taladb worker error: ${e.message ?? "unknown"}`));
203
316
  };
204
317
  const configJson = config !== void 0 ? JSON.stringify(config) : void 0;
205
- await proxy.send("init", { dbName, configJson });
318
+ try {
319
+ await proxy.send("init", { dbName, configJson, passphrase });
320
+ } catch (e) {
321
+ proxy.abort(e instanceof Error ? e : new Error(String(e)));
322
+ worker.terminate();
323
+ throw e;
324
+ }
206
325
  const nudgeCallbacks = /* @__PURE__ */ new Set();
207
326
  let channel = null;
208
327
  if (typeof BroadcastChannel !== "undefined") {
@@ -254,8 +373,17 @@ async function createBrowserDB(dbName, config) {
254
373
  collection: name,
255
374
  filterJson: filter ? s(filter) : "null"
256
375
  }),
376
+ aggregate: async (pipeline) => {
377
+ const json = await proxy.send("aggregate", {
378
+ collection: name,
379
+ pipelineJson: s(pipeline)
380
+ });
381
+ return JSON.parse(json);
382
+ },
257
383
  createIndex: (field) => proxy.send("createIndex", { collection: name, field }),
258
384
  dropIndex: (field) => proxy.send("dropIndex", { collection: name, field }),
385
+ createCompoundIndex: (fields) => proxy.send("createCompoundIndex", { collection: name, fieldsJson: JSON.stringify(fields) }),
386
+ dropCompoundIndex: (fields) => proxy.send("dropCompoundIndex", { collection: name, fieldsJson: JSON.stringify(fields) }),
259
387
  createFtsIndex: (field) => proxy.send("createFtsIndex", { collection: name, field }),
260
388
  dropFtsIndex: (field) => proxy.send("dropFtsIndex", { collection: name, field }),
261
389
  createVectorIndex: (field, options) => {
@@ -286,12 +414,19 @@ async function createBrowserDB(dbName, config) {
286
414
  });
287
415
  return JSON.parse(json);
288
416
  },
289
- subscribe: (filter, callback) => {
417
+ subscribe: (filter, callback, onError) => {
290
418
  let active = true;
291
419
  let lastJson = "";
292
420
  let timer = null;
421
+ let running = false;
422
+ let rerun = false;
293
423
  const poll = async () => {
294
424
  if (!active) return;
425
+ if (running) {
426
+ rerun = true;
427
+ return;
428
+ }
429
+ running = true;
295
430
  if (timer !== null) {
296
431
  clearTimeout(timer);
297
432
  timer = null;
@@ -301,13 +436,22 @@ async function createBrowserDB(dbName, config) {
301
436
  collection: name,
302
437
  filterJson: filter ? s(filter) : "null"
303
438
  });
439
+ if (!active) return;
304
440
  if (json !== lastJson) {
305
441
  lastJson = json;
306
442
  callback(JSON.parse(json));
307
443
  }
308
- } catch {
444
+ } catch (error) {
445
+ if (active) onError?.(error);
446
+ } finally {
447
+ running = false;
448
+ }
449
+ if (active) {
450
+ if (rerun) {
451
+ rerun = false;
452
+ void poll();
453
+ } else timer = setTimeout(poll, 300);
309
454
  }
310
- if (active) timer = setTimeout(poll, 300);
311
455
  };
312
456
  nudgeCallbacks.add(poll);
313
457
  poll();
@@ -323,9 +467,11 @@ async function createBrowserDB(dbName, config) {
323
467
  };
324
468
  return opts ? applySchema(wrapped, opts) : wrapped;
325
469
  }
326
- return {
470
+ const handle = {
327
471
  collection: (name, opts) => wrapCollection(name, opts),
328
472
  compact: () => proxy.send("compact"),
473
+ syncStatus: async () => JSON.parse(await proxy.send("syncStatus")),
474
+ flushSync: (timeoutMs = 5e3) => proxy.send("flushSync", { timeoutMs }),
329
475
  close: async () => {
330
476
  channel?.close();
331
477
  try {
@@ -334,13 +480,22 @@ async function createBrowserDB(dbName, config) {
334
480
  worker.terminate();
335
481
  proxy.abort(new Error("taladb worker closed"));
336
482
  }
337
- }
483
+ },
484
+ // All engine work (export scan, LWW merge) runs inside the worker, off the
485
+ // main thread — a sync pass never blocks rendering, whatever its size.
486
+ exportChanges: (collections, sinceMs) => proxy.send("exportChangeset", { collectionsJson: JSON.stringify(collections), sinceMs }),
487
+ importChanges: (changeset) => proxy.send("importChangeset", { changesetJson: changeset }),
488
+ listCollectionNames: async () => JSON.parse(await proxy.send("listCollections")),
489
+ sync: (adapter, options) => runSync(handle, adapter, options)
338
490
  };
491
+ return handle;
339
492
  }
340
- async function createNodeDB(dbName, config) {
341
- const { TalaDBNode } = await import("@taladb/node");
493
+ async function createNodeDB(dbName, config, passphrase) {
494
+ const native = await import("@taladb/node");
495
+ const TalaDBNode = native.TalaDbNode ?? native.TalaDBNode;
496
+ if (!TalaDBNode) throw new Error("@taladb/node loaded but exports no TalaDbNode class \u2014 rebuild the native module");
342
497
  const configJson = config !== void 0 ? JSON.stringify(config) : null;
343
- const db = TalaDBNode.open(dbName, configJson);
498
+ const db = TalaDBNode.open(dbName, configJson, passphrase ?? null);
344
499
  function wrapCollection(name, opts) {
345
500
  const col = db.collection(name);
346
501
  const wrapped = {
@@ -353,8 +508,11 @@ async function createNodeDB(dbName, config) {
353
508
  deleteOne: async (filter) => col.deleteOneAsync ? col.deleteOneAsync(filter) : col.deleteOne(filter),
354
509
  deleteMany: async (filter) => col.deleteManyAsync ? col.deleteManyAsync(filter) : col.deleteMany(filter),
355
510
  count: async (filter) => col.count(filter ?? null),
511
+ aggregate: async (pipeline) => col.aggregate(pipeline),
356
512
  createIndex: async (field) => col.createIndex(field),
357
513
  dropIndex: async (field) => col.dropIndex(field),
514
+ createCompoundIndex: async (fields) => col.createCompoundIndex(fields),
515
+ dropCompoundIndex: async (fields) => col.dropCompoundIndex(fields),
358
516
  createFtsIndex: async (field) => col.createFtsIndex(field),
359
517
  dropFtsIndex: async (field) => col.dropFtsIndex(field),
360
518
  createVectorIndex: async (field, options) => col.createVectorIndex(field, options.dimensions, options.metric ?? null, options.indexType ?? null, options.hnswM ?? null, options.hnswEfConstruction ?? null),
@@ -368,16 +526,21 @@ async function createNodeDB(dbName, config) {
368
526
  const raw = await col.findNearest(field, vector, topK, filter ?? null);
369
527
  return raw;
370
528
  },
371
- subscribe: (filter, callback) => makePoller(async () => col.find(filter ?? null), callback)
529
+ subscribe: (filter, callback, onError) => makePoller(async () => col.find(filter ?? null), callback, onError)
372
530
  };
373
531
  return opts ? applySchema(wrapped, opts) : wrapped;
374
532
  }
375
- return {
533
+ const handle = {
376
534
  collection: (name, opts) => wrapCollection(name, opts),
377
535
  compact: async () => db.compact(),
378
536
  // Releases the native file handle/lock (no-op on older .node binaries).
379
- close: async () => db.close?.()
537
+ close: async () => db.close?.(),
538
+ exportChanges: async (collections, sinceMs) => db.exportChanges(sinceMs, collections),
539
+ importChanges: async (changeset) => db.importChanges(changeset),
540
+ listCollectionNames: async () => db.listCollectionNames(),
541
+ sync: (adapter, options) => runSync(handle, adapter, options)
380
542
  };
543
+ return handle;
381
544
  }
382
545
  async function createNativeDB(_dbName) {
383
546
  const maybeNative = globalThis.__TalaDB__;
@@ -398,8 +561,11 @@ async function createNativeDB(_dbName) {
398
561
  deleteOne: async (filter) => native.deleteOne(name, filter),
399
562
  deleteMany: async (filter) => native.deleteMany(name, filter),
400
563
  count: async (filter) => native.count(name, filter ?? {}),
564
+ aggregate: async (pipeline) => native.aggregate(name, pipeline),
401
565
  createIndex: async (field) => native.createIndex(name, field),
402
566
  dropIndex: async (field) => native.dropIndex(name, field),
567
+ createCompoundIndex: async (fields) => native.createCompoundIndex(name, fields),
568
+ dropCompoundIndex: async (fields) => native.dropCompoundIndex(name, fields),
403
569
  createFtsIndex: async (field) => native.createFtsIndex(name, field),
404
570
  dropFtsIndex: async (field) => native.dropFtsIndex(name, field),
405
571
  createVectorIndex: async (field, options) => {
@@ -419,17 +585,35 @@ async function createNativeDB(_dbName) {
419
585
  const raw = native.findNearest(name, field, vector, topK, filter ?? null);
420
586
  return raw;
421
587
  },
422
- subscribe: (filter, callback) => makePoller(async () => native.find(name, filter ?? {}), callback)
588
+ subscribe: (filter, callback, onError) => makePoller(async () => native.find(name, filter ?? {}), callback, onError)
423
589
  };
424
590
  return opts ? applySchema(wrapped, opts) : wrapped;
425
591
  }
592
+ const syncSurface = typeof native.exportChanges === "function" && typeof native.importChanges === "function" && typeof native.listCollectionNames === "function" ? (() => {
593
+ const handle = {
594
+ collection: (name, opts) => wrapCollection(name, opts),
595
+ exportChanges: async (collections, sinceMs) => native.exportChanges(collections, sinceMs),
596
+ importChanges: async (changeset) => native.importChanges(changeset),
597
+ listCollectionNames: async () => native.listCollectionNames(),
598
+ sync: (adapter, options) => runSync(handle, adapter, options)
599
+ };
600
+ return {
601
+ exportChanges: handle.exportChanges,
602
+ importChanges: handle.importChanges,
603
+ sync: handle.sync
604
+ };
605
+ })() : unsupportedSync("react-native");
426
606
  return {
427
607
  collection: (name, opts) => wrapCollection(name, opts),
428
608
  compact: async () => native.compact(),
429
- close: async () => native.close()
609
+ close: async () => native.close(),
610
+ ...syncSurface
430
611
  };
431
612
  }
432
613
  async function openDB(dbName = "taladb.db", options) {
614
+ if (options?.passphrase !== void 0 && options.passphrase.length === 0) {
615
+ throw new Error("TalaDB encryption passphrase must not be empty");
616
+ }
433
617
  let resolvedConfig;
434
618
  if (options?.config !== void 0) {
435
619
  validateConfig(options.config);
@@ -440,14 +624,18 @@ async function openDB(dbName = "taladb.db", options) {
440
624
  const platform = detectPlatform();
441
625
  switch (platform) {
442
626
  case "browser":
443
- return createBrowserDB(dbName, resolvedConfig);
627
+ return createBrowserDB(dbName, resolvedConfig, options?.passphrase);
444
628
  case "react-native":
629
+ if (options?.passphrase !== void 0) {
630
+ 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");
631
+ }
445
632
  return createNativeDB(dbName);
446
633
  case "node":
447
- return createNodeDB(dbName, resolvedConfig);
634
+ return createNodeDB(dbName, resolvedConfig, options?.passphrase);
448
635
  }
449
636
  }
450
637
  export {
638
+ HttpSyncAdapter,
451
639
  TalaDbValidationError,
452
640
  openDB
453
641
  };