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.d.ts CHANGED
@@ -57,6 +57,7 @@ type FieldOps<T> = T extends null | undefined ? {
57
57
  $exists?: boolean;
58
58
  /** Full-text search: matches documents where this string field contains the given token. */
59
59
  $contains?: string;
60
+ $regex?: string;
60
61
  };
61
62
  type Filter<T extends Document = Document> = {
62
63
  [K in keyof T]?: T[K] | FieldOps<T[K]>;
@@ -113,6 +114,26 @@ interface CollectionOptions<T extends Document = Document> {
113
114
  */
114
115
  validateOnRead?: boolean;
115
116
  }
117
+ /** A single MongoDB-style aggregation stage. */
118
+ type AggregateStage<T extends Document = Document> = {
119
+ $match: Filter<T>;
120
+ } | {
121
+ /** `_id` is a `"$field"` reference or `null` (single group); other keys are
122
+ * accumulator outputs, e.g. `total: { $sum: '$amount' }`, `n: { $sum: 1 }`. */
123
+ $group: {
124
+ _id: string | null;
125
+ } & Record<string, unknown>;
126
+ } | {
127
+ $sort: Record<string, 1 | -1>;
128
+ } | {
129
+ $skip: number;
130
+ } | {
131
+ $limit: number;
132
+ } | {
133
+ $project: Record<string, 0 | 1>;
134
+ };
135
+ /** An ordered aggregation pipeline. */
136
+ type AggregatePipeline<T extends Document = Document> = AggregateStage<T>[];
116
137
  interface Collection<T extends Document = Document> {
117
138
  insert(doc: Omit<T, '_id'>): Promise<string>;
118
139
  insertMany(docs: Omit<T, '_id'>[]): Promise<string[]>;
@@ -123,8 +144,35 @@ interface Collection<T extends Document = Document> {
123
144
  deleteOne(filter: Filter<T>): Promise<boolean>;
124
145
  deleteMany(filter: Filter<T>): Promise<number>;
125
146
  count(filter?: Filter<T>): Promise<number>;
147
+ /**
148
+ * Run a MongoDB-style aggregation pipeline (`$match`, `$group`, `$sort`,
149
+ * `$skip`, `$limit`, `$project`) inside the engine. Returns the resulting
150
+ * documents. Currently available on Node.js and the in-memory browser build.
151
+ *
152
+ * @example
153
+ * const byStatus = await orders.aggregate([
154
+ * { $group: { _id: '$status', total: { $sum: '$amount' }, n: { $sum: 1 } } },
155
+ * { $sort: { total: -1 } },
156
+ * ]);
157
+ */
158
+ aggregate<R extends Document = Document>(pipeline: AggregatePipeline<T>): Promise<R[]>;
126
159
  createIndex(field: keyof Omit<T, '_id'> & string): Promise<void>;
127
160
  dropIndex(field: keyof Omit<T, '_id'> & string): Promise<void>;
161
+ /**
162
+ * Create a compound (multi-field) index over an ordered list of fields.
163
+ *
164
+ * The query planner uses it to accelerate an `$and` where **every** field of
165
+ * the index is constrained by equality — e.g. an index on
166
+ * `['userId', 'status']` serves `find({ userId, status })` with a single
167
+ * index scan instead of a full-collection scan. Fields are ascending; a
168
+ * partial-prefix or trailing-range match is not used yet (planned).
169
+ *
170
+ * @example
171
+ * await orders.createCompoundIndex(['userId', 'status'])
172
+ */
173
+ createCompoundIndex(fields: (keyof Omit<T, '_id'> & string)[]): Promise<void>;
174
+ /** Drop a compound index by its ordered field list. */
175
+ dropCompoundIndex(fields: (keyof Omit<T, '_id'> & string)[]): Promise<void>;
128
176
  /**
129
177
  * Create a full-text search index on a string field.
130
178
  *
@@ -196,10 +244,85 @@ interface Collection<T extends Document = Document> {
196
244
  * // later…
197
245
  * unsub();
198
246
  */
199
- subscribe(filter: Filter<T>, callback: (docs: T[]) => void): () => void;
247
+ subscribe(filter: Filter<T>, callback: (docs: T[]) => void, onError?: (error: unknown) => void): () => void;
248
+ }
249
+ /**
250
+ * A JSON-encoded changeset — the opaque payload exchanged between peers. Produced
251
+ * by {@link TalaDB.exportChanges}, transported by a {@link SyncAdapter}, and
252
+ * consumed by {@link TalaDB.importChanges}. Treat it as an opaque string.
253
+ */
254
+ type SerializedChangeset = string;
255
+ /** Direction of a sync pass. `'both'` (default) is fully bidirectional. */
256
+ type SyncDirection = 'push' | 'pull' | 'both';
257
+ /**
258
+ * A transport for {@link TalaDB.sync}. Implement `push` to send local changes to
259
+ * a remote, `pull` to fetch remote changes — or both for bidirectional sync.
260
+ * The changeset is an opaque JSON string; move it over any wire you like.
261
+ */
262
+ interface SyncAdapter {
263
+ /** Send a local changeset to the remote. Required for `'push'` / `'both'`. */
264
+ push?(changeset: SerializedChangeset): Promise<void>;
265
+ /**
266
+ * Fetch remote changes with `changed_at` after `sinceMs` (ms epoch), as a
267
+ * serialized changeset. Return `'[]'` when there is nothing new. Required for
268
+ * `'pull'` / `'both'`.
269
+ */
270
+ pull?(sinceMs: number): Promise<SerializedChangeset>;
271
+ }
272
+ interface SyncOptions {
273
+ /**
274
+ * Collections to sync. Omit to sync **all** user collections (reserved
275
+ * `_`-prefixed collections are always skipped). Provide an array to sync only
276
+ * those.
277
+ */
278
+ collections?: string[];
279
+ /**
280
+ * Collections to skip. Applied after `collections` (or after the
281
+ * all-collections default), so `{ exclude: ['logs'] }` means "sync everything
282
+ * except logs".
283
+ */
284
+ exclude?: string[];
285
+ /** Direction of the pass. Default `'both'` (bidirectional). */
286
+ direction?: SyncDirection;
287
+ /**
288
+ * Names this sync target. Reserved cursor state remains isolated per target
289
+ * for forward compatibility with monotonic server cursors. Default
290
+ * `'default'`.
291
+ */
292
+ target?: string;
293
+ }
294
+ interface SyncResult {
295
+ /** Number of local changes pushed to the remote. */
296
+ pushed: number;
297
+ /** Number of documents changed locally by the pulled remote changeset. */
298
+ pulled: number;
299
+ /** Active sync cursor. Currently `0` because timestamp adapters replay safely. */
300
+ cursor: number;
200
301
  }
201
302
  interface TalaDB {
202
303
  collection<T extends Document = Document>(name: string, options?: CollectionOptions<T>): Collection<T>;
304
+ /**
305
+ * Run one bidirectional sync pass against `adapter`: pull remote changes and
306
+ * merge them (Last-Write-Wins), then push local changes since the last cursor.
307
+ * The cursor is persisted per `target`, so successive calls sync incrementally.
308
+ * Set `direction` to `'push'` or `'pull'` to make it one-way.
309
+ *
310
+ * @example
311
+ * await db.sync(httpAdapter, { collections: ['notes'] }); // bidirectional
312
+ * await db.sync(httpAdapter, { collections: ['logs'], direction: 'push' });
313
+ */
314
+ sync(adapter: SyncAdapter, options: SyncOptions): Promise<SyncResult>;
315
+ /**
316
+ * Low-level: export changes to `collections` with `changed_at` after `sinceMs`
317
+ * (exclusive) as a serialized changeset. Most apps use {@link TalaDB.sync}.
318
+ */
319
+ exportChanges(collections: string[], sinceMs: number): Promise<SerializedChangeset>;
320
+ /**
321
+ * Low-level: merge a serialized changeset into the local database via
322
+ * Last-Write-Wins. Returns the number of documents changed. Idempotent —
323
+ * re-importing the same changeset is a no-op.
324
+ */
325
+ importChanges(changeset: SerializedChangeset): Promise<number>;
203
326
  /**
204
327
  * Compact the underlying storage file, reclaiming space freed by deletes
205
328
  * and updates.
@@ -211,6 +334,14 @@ interface TalaDB {
211
334
  * await db.compact();
212
335
  */
213
336
  compact(): Promise<void>;
337
+ /** Browser HTTP-push queue health, when supported by the active binding. */
338
+ syncStatus?(): Promise<{
339
+ pending: number;
340
+ dropped: number;
341
+ failed: number;
342
+ }>;
343
+ /** Wait for accepted browser HTTP-push events, returning false on timeout. */
344
+ flushSync?(timeoutMs?: number): Promise<boolean>;
214
345
  close(): Promise<void>;
215
346
  }
216
347
 
@@ -252,6 +383,45 @@ interface TalaDbConfig {
252
383
  sync?: SyncConfig;
253
384
  }
254
385
 
386
+ interface HttpSyncAdapterOptions {
387
+ /** Base URL, e.g. `https://api.example.com/sync`. `/push` and `/pull` are appended. */
388
+ endpoint: string;
389
+ /** Extra headers on every request — typically `Authorization`. */
390
+ headers?: Record<string, string>;
391
+ /**
392
+ * `fetch` implementation. Defaults to the global `fetch` (Node 18+, browsers,
393
+ * React Native). Inject a custom one for tests or non-standard environments.
394
+ */
395
+ fetch?: typeof fetch;
396
+ /** Paths appended to `endpoint`. Override to match an existing API. */
397
+ paths?: {
398
+ push?: string;
399
+ pull?: string;
400
+ };
401
+ }
402
+ /**
403
+ * A ready-to-use {@link SyncAdapter} that syncs over plain HTTP. Pair it with
404
+ * {@link TalaDB.sync}:
405
+ *
406
+ * ```ts
407
+ * const adapter = new HttpSyncAdapter({
408
+ * endpoint: 'https://api.example.com/sync',
409
+ * headers: { Authorization: `Bearer ${token}` },
410
+ * });
411
+ * await db.sync(adapter, { collections: ['notes'] });
412
+ * ```
413
+ */
414
+ declare class HttpSyncAdapter implements SyncAdapter {
415
+ private readonly endpoint;
416
+ private readonly headers;
417
+ private readonly fetchFn;
418
+ private readonly pushPath;
419
+ private readonly pullPath;
420
+ constructor(options: HttpSyncAdapterOptions);
421
+ push(changeset: SerializedChangeset): Promise<void>;
422
+ pull(sinceMs: number): Promise<SerializedChangeset>;
423
+ }
424
+
255
425
  /**
256
426
  * Thrown when a document fails schema validation on `insert` or `insertMany`.
257
427
  * The `cause` property holds the original error thrown by the schema library.
@@ -263,6 +433,8 @@ declare class TalaDbValidationError extends Error {
263
433
 
264
434
  /** Options for `openDB`. */
265
435
  interface OpenDBOptions {
436
+ /** Encrypt native database values at rest. Never hard-code this value. */
437
+ passphrase?: string;
266
438
  /**
267
439
  * Explicit path to a `taladb.config.yml` / `taladb.config.json` file.
268
440
  * If omitted, TalaDB auto-discovers the file from `process.cwd()` on Node.js.
@@ -294,4 +466,4 @@ interface OpenDBOptions {
294
466
  */
295
467
  declare function openDB(dbName?: string, options?: OpenDBOptions): Promise<TalaDB>;
296
468
 
297
- export { type Collection, type CollectionIndexInfo, type CollectionOptions, type Document, type Filter, type OpenDBOptions, type Schema, type SyncConfig, type TalaDB, type TalaDbConfig, TalaDbValidationError, type Update, type Value, type VectorIndexOptions, type VectorMetric, type VectorSearchResult, openDB };
469
+ export { type AggregatePipeline, type AggregateStage, type Collection, type CollectionIndexInfo, type CollectionOptions, type Document, type Filter, HttpSyncAdapter, type OpenDBOptions, type Schema, type SerializedChangeset, type SyncAdapter, type SyncConfig, type SyncDirection, type SyncOptions, type SyncResult, type TalaDB, type TalaDbConfig, TalaDbValidationError, type Update, type Value, type VectorIndexOptions, type VectorMetric, type VectorSearchResult, openDB };
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ HttpSyncAdapter: () => HttpSyncAdapter,
33
34
  TalaDbValidationError: () => TalaDbValidationError,
34
35
  openDB: () => openDB
35
36
  });
@@ -115,6 +116,103 @@ async function loadConfig(configPath) {
115
116
  return {};
116
117
  }
117
118
 
119
+ // src/sync.ts
120
+ var CURSOR_COLLECTION = "__taladb_sync";
121
+ async function resolveCollections(handle, options) {
122
+ const base = options.collections ?? await handle.listCollectionNames();
123
+ const excluded = new Set(options.exclude ?? []);
124
+ return base.filter((c) => !excluded.has(c) && !c.startsWith("_"));
125
+ }
126
+ function unsupportedSync(runtime) {
127
+ const err = () => new Error(
128
+ `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.`
129
+ );
130
+ return {
131
+ sync: () => Promise.reject(err()),
132
+ exportChanges: () => Promise.reject(err()),
133
+ importChanges: () => Promise.reject(err())
134
+ };
135
+ }
136
+ async function readCursor(cursorCol, target) {
137
+ const doc = await cursorCol.findOne({ target });
138
+ return { pushMs: doc?.pushMs ?? 0, pullMs: doc?.pullMs ?? 0 };
139
+ }
140
+ async function writeCursor(cursorCol, target, cursor) {
141
+ const updated = await cursorCol.updateOne({ target }, { $set: { ...cursor } });
142
+ if (!updated) {
143
+ await cursorCol.insert({ target, ...cursor });
144
+ }
145
+ }
146
+ async function runSync(handle, adapter, options) {
147
+ const direction = options.direction ?? "both";
148
+ const target = options.target ?? "default";
149
+ const doPush = direction === "push" || direction === "both";
150
+ const doPull = direction === "pull" || direction === "both";
151
+ if (doPull && !adapter.pull) {
152
+ throw new Error(`sync direction '${direction}' requires adapter.pull()`);
153
+ }
154
+ if (doPush && !adapter.push) {
155
+ throw new Error(`sync direction '${direction}' requires adapter.push()`);
156
+ }
157
+ const collections = await resolveCollections(handle, options);
158
+ const cursorCol = handle.collection(CURSOR_COLLECTION);
159
+ const cursor = await readCursor(cursorCol, target);
160
+ const local = doPush ? await handle.exportChanges(collections, 0) : "[]";
161
+ let pulled = 0;
162
+ if (doPull) {
163
+ const remote = await adapter.pull(0);
164
+ if (remote && remote !== "[]") {
165
+ pulled = await handle.importChanges(remote);
166
+ }
167
+ }
168
+ let pushed = 0;
169
+ if (doPush && local !== "[]") {
170
+ pushed = JSON.parse(local).length;
171
+ await adapter.push(local);
172
+ }
173
+ await writeCursor(cursorCol, target, {
174
+ pushMs: cursor.pushMs,
175
+ pullMs: cursor.pullMs
176
+ });
177
+ return { pushed, pulled, cursor: 0 };
178
+ }
179
+
180
+ // src/http-adapter.ts
181
+ var HttpSyncAdapter = class {
182
+ constructor(options) {
183
+ this.endpoint = options.endpoint.replace(/\/$/, "");
184
+ this.headers = options.headers ?? {};
185
+ const f = options.fetch ?? globalThis.fetch?.bind(globalThis);
186
+ if (!f) {
187
+ throw new Error(
188
+ "HttpSyncAdapter: no fetch available. Pass options.fetch on runtimes without a global fetch."
189
+ );
190
+ }
191
+ this.fetchFn = f;
192
+ this.pushPath = options.paths?.push ?? "/push";
193
+ this.pullPath = options.paths?.pull ?? "/pull";
194
+ }
195
+ async push(changeset) {
196
+ const res = await this.fetchFn(`${this.endpoint}${this.pushPath}`, {
197
+ method: "POST",
198
+ headers: { "content-type": "application/json", ...this.headers },
199
+ body: changeset
200
+ });
201
+ if (!res.ok) {
202
+ throw new Error(`HttpSyncAdapter push failed: ${res.status} ${res.statusText}`);
203
+ }
204
+ }
205
+ async pull(sinceMs) {
206
+ const url = `${this.endpoint}${this.pullPath}?since=${encodeURIComponent(String(sinceMs))}`;
207
+ const res = await this.fetchFn(url, { method: "GET", headers: this.headers });
208
+ if (!res.ok) {
209
+ throw new Error(`HttpSyncAdapter pull failed: ${res.status} ${res.statusText}`);
210
+ }
211
+ const body = (await res.text()).trim();
212
+ return body.length === 0 ? "[]" : body;
213
+ }
214
+ };
215
+
118
216
  // src/index.ts
119
217
  var import_meta = {};
120
218
  var TalaDbValidationError = class extends Error {
@@ -211,28 +309,44 @@ var WorkerProxy = class {
211
309
  this.pending.clear();
212
310
  }
213
311
  };
214
- function makePoller(findFn, callback) {
312
+ function makePoller(findFn, callback, onError) {
215
313
  let active = true;
216
314
  let lastJson = "";
315
+ let running = false;
316
+ let rerun = false;
217
317
  const poll = async () => {
218
318
  if (!active) return;
319
+ if (running) {
320
+ rerun = true;
321
+ return;
322
+ }
323
+ running = true;
219
324
  try {
220
325
  const docs = await findFn();
326
+ if (!active) return;
221
327
  const json = JSON.stringify(docs);
222
328
  if (json !== lastJson) {
223
329
  lastJson = json;
224
330
  callback(docs);
225
331
  }
226
- } catch {
332
+ } catch (error) {
333
+ if (active) onError?.(error);
334
+ } finally {
335
+ running = false;
336
+ if (active) {
337
+ if (rerun) {
338
+ rerun = false;
339
+ void poll();
340
+ } else setTimeout(poll, 300);
341
+ }
227
342
  }
228
- if (active) setTimeout(poll, 300);
229
343
  };
230
344
  poll();
231
345
  return () => {
232
346
  active = false;
233
347
  };
234
348
  }
235
- async function createBrowserDB(dbName, config) {
349
+ async function createBrowserDB(dbName, config, passphrase) {
236
350
  const workerUrl = new URL("@taladb/web/worker/taladb.worker.js", import_meta.url);
237
351
  const worker = new Worker(workerUrl, { type: "module", name: "taladb" });
238
352
  const proxy = new WorkerProxy(worker);
@@ -240,7 +354,13 @@ async function createBrowserDB(dbName, config) {
240
354
  proxy.abort(new Error(`taladb worker error: ${e.message ?? "unknown"}`));
241
355
  };
242
356
  const configJson = config !== void 0 ? JSON.stringify(config) : void 0;
243
- await proxy.send("init", { dbName, configJson });
357
+ try {
358
+ await proxy.send("init", { dbName, configJson, passphrase });
359
+ } catch (e) {
360
+ proxy.abort(e instanceof Error ? e : new Error(String(e)));
361
+ worker.terminate();
362
+ throw e;
363
+ }
244
364
  const nudgeCallbacks = /* @__PURE__ */ new Set();
245
365
  let channel = null;
246
366
  if (typeof BroadcastChannel !== "undefined") {
@@ -292,8 +412,17 @@ async function createBrowserDB(dbName, config) {
292
412
  collection: name,
293
413
  filterJson: filter ? s(filter) : "null"
294
414
  }),
415
+ aggregate: async (pipeline) => {
416
+ const json = await proxy.send("aggregate", {
417
+ collection: name,
418
+ pipelineJson: s(pipeline)
419
+ });
420
+ return JSON.parse(json);
421
+ },
295
422
  createIndex: (field) => proxy.send("createIndex", { collection: name, field }),
296
423
  dropIndex: (field) => proxy.send("dropIndex", { collection: name, field }),
424
+ createCompoundIndex: (fields) => proxy.send("createCompoundIndex", { collection: name, fieldsJson: JSON.stringify(fields) }),
425
+ dropCompoundIndex: (fields) => proxy.send("dropCompoundIndex", { collection: name, fieldsJson: JSON.stringify(fields) }),
297
426
  createFtsIndex: (field) => proxy.send("createFtsIndex", { collection: name, field }),
298
427
  dropFtsIndex: (field) => proxy.send("dropFtsIndex", { collection: name, field }),
299
428
  createVectorIndex: (field, options) => {
@@ -324,12 +453,19 @@ async function createBrowserDB(dbName, config) {
324
453
  });
325
454
  return JSON.parse(json);
326
455
  },
327
- subscribe: (filter, callback) => {
456
+ subscribe: (filter, callback, onError) => {
328
457
  let active = true;
329
458
  let lastJson = "";
330
459
  let timer = null;
460
+ let running = false;
461
+ let rerun = false;
331
462
  const poll = async () => {
332
463
  if (!active) return;
464
+ if (running) {
465
+ rerun = true;
466
+ return;
467
+ }
468
+ running = true;
333
469
  if (timer !== null) {
334
470
  clearTimeout(timer);
335
471
  timer = null;
@@ -339,13 +475,22 @@ async function createBrowserDB(dbName, config) {
339
475
  collection: name,
340
476
  filterJson: filter ? s(filter) : "null"
341
477
  });
478
+ if (!active) return;
342
479
  if (json !== lastJson) {
343
480
  lastJson = json;
344
481
  callback(JSON.parse(json));
345
482
  }
346
- } catch {
483
+ } catch (error) {
484
+ if (active) onError?.(error);
485
+ } finally {
486
+ running = false;
487
+ }
488
+ if (active) {
489
+ if (rerun) {
490
+ rerun = false;
491
+ void poll();
492
+ } else timer = setTimeout(poll, 300);
347
493
  }
348
- if (active) timer = setTimeout(poll, 300);
349
494
  };
350
495
  nudgeCallbacks.add(poll);
351
496
  poll();
@@ -361,9 +506,11 @@ async function createBrowserDB(dbName, config) {
361
506
  };
362
507
  return opts ? applySchema(wrapped, opts) : wrapped;
363
508
  }
364
- return {
509
+ const handle = {
365
510
  collection: (name, opts) => wrapCollection(name, opts),
366
511
  compact: () => proxy.send("compact"),
512
+ syncStatus: async () => JSON.parse(await proxy.send("syncStatus")),
513
+ flushSync: (timeoutMs = 5e3) => proxy.send("flushSync", { timeoutMs }),
367
514
  close: async () => {
368
515
  channel?.close();
369
516
  try {
@@ -372,13 +519,22 @@ async function createBrowserDB(dbName, config) {
372
519
  worker.terminate();
373
520
  proxy.abort(new Error("taladb worker closed"));
374
521
  }
375
- }
522
+ },
523
+ // All engine work (export scan, LWW merge) runs inside the worker, off the
524
+ // main thread — a sync pass never blocks rendering, whatever its size.
525
+ exportChanges: (collections, sinceMs) => proxy.send("exportChangeset", { collectionsJson: JSON.stringify(collections), sinceMs }),
526
+ importChanges: (changeset) => proxy.send("importChangeset", { changesetJson: changeset }),
527
+ listCollectionNames: async () => JSON.parse(await proxy.send("listCollections")),
528
+ sync: (adapter, options) => runSync(handle, adapter, options)
376
529
  };
530
+ return handle;
377
531
  }
378
- async function createNodeDB(dbName, config) {
379
- const { TalaDBNode } = await import("@taladb/node");
532
+ async function createNodeDB(dbName, config, passphrase) {
533
+ const native = await import("@taladb/node");
534
+ const TalaDBNode = native.TalaDbNode ?? native.TalaDBNode;
535
+ if (!TalaDBNode) throw new Error("@taladb/node loaded but exports no TalaDbNode class \u2014 rebuild the native module");
380
536
  const configJson = config !== void 0 ? JSON.stringify(config) : null;
381
- const db = TalaDBNode.open(dbName, configJson);
537
+ const db = TalaDBNode.open(dbName, configJson, passphrase ?? null);
382
538
  function wrapCollection(name, opts) {
383
539
  const col = db.collection(name);
384
540
  const wrapped = {
@@ -391,8 +547,11 @@ async function createNodeDB(dbName, config) {
391
547
  deleteOne: async (filter) => col.deleteOneAsync ? col.deleteOneAsync(filter) : col.deleteOne(filter),
392
548
  deleteMany: async (filter) => col.deleteManyAsync ? col.deleteManyAsync(filter) : col.deleteMany(filter),
393
549
  count: async (filter) => col.count(filter ?? null),
550
+ aggregate: async (pipeline) => col.aggregate(pipeline),
394
551
  createIndex: async (field) => col.createIndex(field),
395
552
  dropIndex: async (field) => col.dropIndex(field),
553
+ createCompoundIndex: async (fields) => col.createCompoundIndex(fields),
554
+ dropCompoundIndex: async (fields) => col.dropCompoundIndex(fields),
396
555
  createFtsIndex: async (field) => col.createFtsIndex(field),
397
556
  dropFtsIndex: async (field) => col.dropFtsIndex(field),
398
557
  createVectorIndex: async (field, options) => col.createVectorIndex(field, options.dimensions, options.metric ?? null, options.indexType ?? null, options.hnswM ?? null, options.hnswEfConstruction ?? null),
@@ -406,16 +565,21 @@ async function createNodeDB(dbName, config) {
406
565
  const raw = await col.findNearest(field, vector, topK, filter ?? null);
407
566
  return raw;
408
567
  },
409
- subscribe: (filter, callback) => makePoller(async () => col.find(filter ?? null), callback)
568
+ subscribe: (filter, callback, onError) => makePoller(async () => col.find(filter ?? null), callback, onError)
410
569
  };
411
570
  return opts ? applySchema(wrapped, opts) : wrapped;
412
571
  }
413
- return {
572
+ const handle = {
414
573
  collection: (name, opts) => wrapCollection(name, opts),
415
574
  compact: async () => db.compact(),
416
575
  // Releases the native file handle/lock (no-op on older .node binaries).
417
- close: async () => db.close?.()
576
+ close: async () => db.close?.(),
577
+ exportChanges: async (collections, sinceMs) => db.exportChanges(sinceMs, collections),
578
+ importChanges: async (changeset) => db.importChanges(changeset),
579
+ listCollectionNames: async () => db.listCollectionNames(),
580
+ sync: (adapter, options) => runSync(handle, adapter, options)
418
581
  };
582
+ return handle;
419
583
  }
420
584
  async function createNativeDB(_dbName) {
421
585
  const maybeNative = globalThis.__TalaDB__;
@@ -436,8 +600,11 @@ async function createNativeDB(_dbName) {
436
600
  deleteOne: async (filter) => native.deleteOne(name, filter),
437
601
  deleteMany: async (filter) => native.deleteMany(name, filter),
438
602
  count: async (filter) => native.count(name, filter ?? {}),
603
+ aggregate: async (pipeline) => native.aggregate(name, pipeline),
439
604
  createIndex: async (field) => native.createIndex(name, field),
440
605
  dropIndex: async (field) => native.dropIndex(name, field),
606
+ createCompoundIndex: async (fields) => native.createCompoundIndex(name, fields),
607
+ dropCompoundIndex: async (fields) => native.dropCompoundIndex(name, fields),
441
608
  createFtsIndex: async (field) => native.createFtsIndex(name, field),
442
609
  dropFtsIndex: async (field) => native.dropFtsIndex(name, field),
443
610
  createVectorIndex: async (field, options) => {
@@ -457,17 +624,35 @@ async function createNativeDB(_dbName) {
457
624
  const raw = native.findNearest(name, field, vector, topK, filter ?? null);
458
625
  return raw;
459
626
  },
460
- subscribe: (filter, callback) => makePoller(async () => native.find(name, filter ?? {}), callback)
627
+ subscribe: (filter, callback, onError) => makePoller(async () => native.find(name, filter ?? {}), callback, onError)
461
628
  };
462
629
  return opts ? applySchema(wrapped, opts) : wrapped;
463
630
  }
631
+ const syncSurface = typeof native.exportChanges === "function" && typeof native.importChanges === "function" && typeof native.listCollectionNames === "function" ? (() => {
632
+ const handle = {
633
+ collection: (name, opts) => wrapCollection(name, opts),
634
+ exportChanges: async (collections, sinceMs) => native.exportChanges(collections, sinceMs),
635
+ importChanges: async (changeset) => native.importChanges(changeset),
636
+ listCollectionNames: async () => native.listCollectionNames(),
637
+ sync: (adapter, options) => runSync(handle, adapter, options)
638
+ };
639
+ return {
640
+ exportChanges: handle.exportChanges,
641
+ importChanges: handle.importChanges,
642
+ sync: handle.sync
643
+ };
644
+ })() : unsupportedSync("react-native");
464
645
  return {
465
646
  collection: (name, opts) => wrapCollection(name, opts),
466
647
  compact: async () => native.compact(),
467
- close: async () => native.close()
648
+ close: async () => native.close(),
649
+ ...syncSurface
468
650
  };
469
651
  }
470
652
  async function openDB(dbName = "taladb.db", options) {
653
+ if (options?.passphrase !== void 0 && options.passphrase.length === 0) {
654
+ throw new Error("TalaDB encryption passphrase must not be empty");
655
+ }
471
656
  let resolvedConfig;
472
657
  if (options?.config !== void 0) {
473
658
  validateConfig(options.config);
@@ -478,15 +663,19 @@ async function openDB(dbName = "taladb.db", options) {
478
663
  const platform = detectPlatform();
479
664
  switch (platform) {
480
665
  case "browser":
481
- return createBrowserDB(dbName, resolvedConfig);
666
+ return createBrowserDB(dbName, resolvedConfig, options?.passphrase);
482
667
  case "react-native":
668
+ if (options?.passphrase !== void 0) {
669
+ 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");
670
+ }
483
671
  return createNativeDB(dbName);
484
672
  case "node":
485
- return createNodeDB(dbName, resolvedConfig);
673
+ return createNodeDB(dbName, resolvedConfig, options?.passphrase);
486
674
  }
487
675
  }
488
676
  // Annotate the CommonJS export names for ESM import in node:
489
677
  0 && (module.exports = {
678
+ HttpSyncAdapter,
490
679
  TalaDbValidationError,
491
680
  openDB
492
681
  });