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", import.meta.url);
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("@taladb/node");
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/dist/index.d.mts 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 };