taladb 0.8.3 → 0.8.4

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({ _id: target });
43
+ return doc?.sinceMs ?? 0;
44
+ }
45
+ async function writeCursor(cursorCol, target, sinceMs) {
46
+ const existing = await cursorCol.findOne({ _id: target });
47
+ if (existing) {
48
+ await cursorCol.updateOne({ _id: target }, { $set: { sinceMs } });
49
+ } else {
50
+ await cursorCol.insert({ _id: target, sinceMs });
51
+ }
52
+ }
53
+ async function runSync(handle, adapter, options) {
54
+ const direction = options.direction ?? "both";
55
+ const target = options.target ?? "default";
56
+ const doPush = direction === "push" || direction === "both";
57
+ const doPull = direction === "pull" || direction === "both";
58
+ if (doPull && !adapter.pull) {
59
+ throw new Error(`sync direction '${direction}' requires adapter.pull()`);
60
+ }
61
+ if (doPush && !adapter.push) {
62
+ throw new Error(`sync direction '${direction}' requires adapter.push()`);
63
+ }
64
+ const collections = await resolveCollections(handle, options);
65
+ const cursorCol = handle.collection(CURSOR_COLLECTION);
66
+ const sinceMs = await readCursor(cursorCol, target);
67
+ const startedAt = Date.now();
68
+ const local = doPush ? await handle.exportChanges(collections, sinceMs) : "[]";
69
+ let pulled = 0;
70
+ if (doPull) {
71
+ const remote = await adapter.pull(sinceMs);
72
+ if (remote && remote !== "[]") {
73
+ pulled = await handle.importChanges(remote);
74
+ }
75
+ }
76
+ let pushed = 0;
77
+ if (doPush && local !== "[]") {
78
+ pushed = JSON.parse(local).length;
79
+ await adapter.push(local);
80
+ }
81
+ await writeCursor(cursorCol, target, startedAt);
82
+ return { pushed, pulled, cursor: startedAt };
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;
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) {
@@ -197,6 +294,13 @@ async function createBrowserDB(dbName, config) {
197
294
  collection: name,
198
295
  filterJson: filter ? s(filter) : "null"
199
296
  }),
297
+ aggregate: async (pipeline) => {
298
+ const json = await proxy.send("aggregate", {
299
+ collection: name,
300
+ pipelineJson: s(pipeline)
301
+ });
302
+ return JSON.parse(json);
303
+ },
200
304
  createIndex: (field) => proxy.send("createIndex", { collection: name, field }),
201
305
  dropIndex: (field) => proxy.send("dropIndex", { collection: name, field }),
202
306
  createFtsIndex: (field) => proxy.send("createFtsIndex", { collection: name, field }),
@@ -277,7 +381,8 @@ async function createBrowserDB(dbName, config) {
277
381
  worker.terminate();
278
382
  proxy.abort(new Error("taladb worker closed"));
279
383
  }
280
- }
384
+ },
385
+ ...unsupportedSync("browser (OPFS worker)")
281
386
  };
282
387
  }
283
388
  async function createNodeDB(dbName, config) {
@@ -296,6 +401,7 @@ async function createNodeDB(dbName, config) {
296
401
  deleteOne: async (filter) => col.deleteOneAsync ? col.deleteOneAsync(filter) : col.deleteOne(filter),
297
402
  deleteMany: async (filter) => col.deleteManyAsync ? col.deleteManyAsync(filter) : col.deleteMany(filter),
298
403
  count: async (filter) => col.count(filter ?? null),
404
+ aggregate: async (pipeline) => col.aggregate(pipeline),
299
405
  createIndex: async (field) => col.createIndex(field),
300
406
  dropIndex: async (field) => col.dropIndex(field),
301
407
  createFtsIndex: async (field) => col.createFtsIndex(field),
@@ -315,12 +421,17 @@ async function createNodeDB(dbName, config) {
315
421
  };
316
422
  return opts ? applySchema(wrapped, opts) : wrapped;
317
423
  }
318
- return {
424
+ const handle = {
319
425
  collection: (name, opts) => wrapCollection(name, opts),
320
426
  compact: async () => db.compact(),
321
427
  // Releases the native file handle/lock (no-op on older .node binaries).
322
- close: async () => db.close?.()
428
+ close: async () => db.close?.(),
429
+ exportChanges: async (collections, sinceMs) => db.exportChanges(sinceMs, collections),
430
+ importChanges: async (changeset) => db.importChanges(changeset),
431
+ listCollectionNames: async () => db.listCollectionNames(),
432
+ sync: (adapter, options) => runSync(handle, adapter, options)
323
433
  };
434
+ return handle;
324
435
  }
325
436
  async function createNativeDB(_dbName) {
326
437
  const maybeNative = globalThis.__TalaDB__;
@@ -341,6 +452,7 @@ async function createNativeDB(_dbName) {
341
452
  deleteOne: async (filter) => native.deleteOne(name, filter),
342
453
  deleteMany: async (filter) => native.deleteMany(name, filter),
343
454
  count: async (filter) => native.count(name, filter ?? {}),
455
+ aggregate: async (pipeline) => native.aggregate(name, pipeline),
344
456
  createIndex: async (field) => native.createIndex(name, field),
345
457
  dropIndex: async (field) => native.dropIndex(name, field),
346
458
  createFtsIndex: async (field) => native.createFtsIndex(name, field),
@@ -369,7 +481,8 @@ async function createNativeDB(_dbName) {
369
481
  return {
370
482
  collection: (name, opts) => wrapCollection(name, opts),
371
483
  compact: async () => native.compact(),
372
- close: async () => native.close()
484
+ close: async () => native.close(),
485
+ ...unsupportedSync("react-native")
373
486
  };
374
487
  }
375
488
  async function openDB(dbName = "taladb.db", options) {
@@ -391,6 +504,7 @@ async function openDB(dbName = "taladb.db", options) {
391
504
  }
392
505
  }
393
506
  export {
507
+ HttpSyncAdapter,
394
508
  TalaDbValidationError,
395
509
  openDB
396
510
  };
package/dist/index.d.mts CHANGED
@@ -113,6 +113,26 @@ interface CollectionOptions<T extends Document = Document> {
113
113
  */
114
114
  validateOnRead?: boolean;
115
115
  }
116
+ /** A single MongoDB-style aggregation stage. */
117
+ type AggregateStage<T extends Document = Document> = {
118
+ $match: Filter<T>;
119
+ } | {
120
+ /** `_id` is a `"$field"` reference or `null` (single group); other keys are
121
+ * accumulator outputs, e.g. `total: { $sum: '$amount' }`, `n: { $sum: 1 }`. */
122
+ $group: {
123
+ _id: string | null;
124
+ } & Record<string, unknown>;
125
+ } | {
126
+ $sort: Record<string, 1 | -1>;
127
+ } | {
128
+ $skip: number;
129
+ } | {
130
+ $limit: number;
131
+ } | {
132
+ $project: Record<string, 0 | 1>;
133
+ };
134
+ /** An ordered aggregation pipeline. */
135
+ type AggregatePipeline<T extends Document = Document> = AggregateStage<T>[];
116
136
  interface Collection<T extends Document = Document> {
117
137
  insert(doc: Omit<T, '_id'>): Promise<string>;
118
138
  insertMany(docs: Omit<T, '_id'>[]): Promise<string[]>;
@@ -123,6 +143,18 @@ interface Collection<T extends Document = Document> {
123
143
  deleteOne(filter: Filter<T>): Promise<boolean>;
124
144
  deleteMany(filter: Filter<T>): Promise<number>;
125
145
  count(filter?: Filter<T>): Promise<number>;
146
+ /**
147
+ * Run a MongoDB-style aggregation pipeline (`$match`, `$group`, `$sort`,
148
+ * `$skip`, `$limit`, `$project`) inside the engine. Returns the resulting
149
+ * documents. Currently available on Node.js and the in-memory browser build.
150
+ *
151
+ * @example
152
+ * const byStatus = await orders.aggregate([
153
+ * { $group: { _id: '$status', total: { $sum: '$amount' }, n: { $sum: 1 } } },
154
+ * { $sort: { total: -1 } },
155
+ * ]);
156
+ */
157
+ aggregate<R extends Document = Document>(pipeline: AggregatePipeline<T>): Promise<R[]>;
126
158
  createIndex(field: keyof Omit<T, '_id'> & string): Promise<void>;
127
159
  dropIndex(field: keyof Omit<T, '_id'> & string): Promise<void>;
128
160
  /**
@@ -198,8 +230,82 @@ interface Collection<T extends Document = Document> {
198
230
  */
199
231
  subscribe(filter: Filter<T>, callback: (docs: T[]) => void): () => void;
200
232
  }
233
+ /**
234
+ * A JSON-encoded changeset — the opaque payload exchanged between peers. Produced
235
+ * by {@link TalaDB.exportChanges}, transported by a {@link SyncAdapter}, and
236
+ * consumed by {@link TalaDB.importChanges}. Treat it as an opaque string.
237
+ */
238
+ type SerializedChangeset = string;
239
+ /** Direction of a sync pass. `'both'` (default) is fully bidirectional. */
240
+ type SyncDirection = 'push' | 'pull' | 'both';
241
+ /**
242
+ * A transport for {@link TalaDB.sync}. Implement `push` to send local changes to
243
+ * a remote, `pull` to fetch remote changes — or both for bidirectional sync.
244
+ * The changeset is an opaque JSON string; move it over any wire you like.
245
+ */
246
+ interface SyncAdapter {
247
+ /** Send a local changeset to the remote. Required for `'push'` / `'both'`. */
248
+ push?(changeset: SerializedChangeset): Promise<void>;
249
+ /**
250
+ * Fetch remote changes with `changed_at` after `sinceMs` (ms epoch), as a
251
+ * serialized changeset. Return `'[]'` when there is nothing new. Required for
252
+ * `'pull'` / `'both'`.
253
+ */
254
+ pull?(sinceMs: number): Promise<SerializedChangeset>;
255
+ }
256
+ interface SyncOptions {
257
+ /**
258
+ * Collections to sync. Omit to sync **all** user collections (reserved
259
+ * `_`-prefixed collections are always skipped). Provide an array to sync only
260
+ * those.
261
+ */
262
+ collections?: string[];
263
+ /**
264
+ * Collections to skip. Applied after `collections` (or after the
265
+ * all-collections default), so `{ exclude: ['logs'] }` means "sync everything
266
+ * except logs".
267
+ */
268
+ exclude?: string[];
269
+ /** Direction of the pass. Default `'both'` (bidirectional). */
270
+ direction?: SyncDirection;
271
+ /**
272
+ * Names this sync target for cursor persistence, so multiple remotes each
273
+ * keep their own watermark. Default `'default'`.
274
+ */
275
+ target?: string;
276
+ }
277
+ interface SyncResult {
278
+ /** Number of local changes pushed to the remote. */
279
+ pushed: number;
280
+ /** Number of documents changed locally by the pulled remote changeset. */
281
+ pulled: number;
282
+ /** New sync cursor (ms epoch) persisted for the next pass. */
283
+ cursor: number;
284
+ }
201
285
  interface TalaDB {
202
286
  collection<T extends Document = Document>(name: string, options?: CollectionOptions<T>): Collection<T>;
287
+ /**
288
+ * Run one bidirectional sync pass against `adapter`: pull remote changes and
289
+ * merge them (Last-Write-Wins), then push local changes since the last cursor.
290
+ * The cursor is persisted per `target`, so successive calls sync incrementally.
291
+ * Set `direction` to `'push'` or `'pull'` to make it one-way.
292
+ *
293
+ * @example
294
+ * await db.sync(httpAdapter, { collections: ['notes'] }); // bidirectional
295
+ * await db.sync(httpAdapter, { collections: ['logs'], direction: 'push' });
296
+ */
297
+ sync(adapter: SyncAdapter, options: SyncOptions): Promise<SyncResult>;
298
+ /**
299
+ * Low-level: export changes to `collections` with `changed_at` after `sinceMs`
300
+ * (exclusive) as a serialized changeset. Most apps use {@link TalaDB.sync}.
301
+ */
302
+ exportChanges(collections: string[], sinceMs: number): Promise<SerializedChangeset>;
303
+ /**
304
+ * Low-level: merge a serialized changeset into the local database via
305
+ * Last-Write-Wins. Returns the number of documents changed. Idempotent —
306
+ * re-importing the same changeset is a no-op.
307
+ */
308
+ importChanges(changeset: SerializedChangeset): Promise<number>;
203
309
  /**
204
310
  * Compact the underlying storage file, reclaiming space freed by deletes
205
311
  * and updates.
@@ -252,6 +358,45 @@ interface TalaDbConfig {
252
358
  sync?: SyncConfig;
253
359
  }
254
360
 
361
+ interface HttpSyncAdapterOptions {
362
+ /** Base URL, e.g. `https://api.example.com/sync`. `/push` and `/pull` are appended. */
363
+ endpoint: string;
364
+ /** Extra headers on every request — typically `Authorization`. */
365
+ headers?: Record<string, string>;
366
+ /**
367
+ * `fetch` implementation. Defaults to the global `fetch` (Node 18+, browsers,
368
+ * React Native). Inject a custom one for tests or non-standard environments.
369
+ */
370
+ fetch?: typeof fetch;
371
+ /** Paths appended to `endpoint`. Override to match an existing API. */
372
+ paths?: {
373
+ push?: string;
374
+ pull?: string;
375
+ };
376
+ }
377
+ /**
378
+ * A ready-to-use {@link SyncAdapter} that syncs over plain HTTP. Pair it with
379
+ * {@link TalaDB.sync}:
380
+ *
381
+ * ```ts
382
+ * const adapter = new HttpSyncAdapter({
383
+ * endpoint: 'https://api.example.com/sync',
384
+ * headers: { Authorization: `Bearer ${token}` },
385
+ * });
386
+ * await db.sync(adapter, { collections: ['notes'] });
387
+ * ```
388
+ */
389
+ declare class HttpSyncAdapter implements SyncAdapter {
390
+ private readonly endpoint;
391
+ private readonly headers;
392
+ private readonly fetchFn;
393
+ private readonly pushPath;
394
+ private readonly pullPath;
395
+ constructor(options: HttpSyncAdapterOptions);
396
+ push(changeset: SerializedChangeset): Promise<void>;
397
+ pull(sinceMs: number): Promise<SerializedChangeset>;
398
+ }
399
+
255
400
  /**
256
401
  * Thrown when a document fails schema validation on `insert` or `insertMany`.
257
402
  * The `cause` property holds the original error thrown by the schema library.
@@ -294,4 +439,4 @@ interface OpenDBOptions {
294
439
  */
295
440
  declare function openDB(dbName?: string, options?: OpenDBOptions): Promise<TalaDB>;
296
441
 
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 };
442
+ 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.d.ts CHANGED
@@ -113,6 +113,26 @@ interface CollectionOptions<T extends Document = Document> {
113
113
  */
114
114
  validateOnRead?: boolean;
115
115
  }
116
+ /** A single MongoDB-style aggregation stage. */
117
+ type AggregateStage<T extends Document = Document> = {
118
+ $match: Filter<T>;
119
+ } | {
120
+ /** `_id` is a `"$field"` reference or `null` (single group); other keys are
121
+ * accumulator outputs, e.g. `total: { $sum: '$amount' }`, `n: { $sum: 1 }`. */
122
+ $group: {
123
+ _id: string | null;
124
+ } & Record<string, unknown>;
125
+ } | {
126
+ $sort: Record<string, 1 | -1>;
127
+ } | {
128
+ $skip: number;
129
+ } | {
130
+ $limit: number;
131
+ } | {
132
+ $project: Record<string, 0 | 1>;
133
+ };
134
+ /** An ordered aggregation pipeline. */
135
+ type AggregatePipeline<T extends Document = Document> = AggregateStage<T>[];
116
136
  interface Collection<T extends Document = Document> {
117
137
  insert(doc: Omit<T, '_id'>): Promise<string>;
118
138
  insertMany(docs: Omit<T, '_id'>[]): Promise<string[]>;
@@ -123,6 +143,18 @@ interface Collection<T extends Document = Document> {
123
143
  deleteOne(filter: Filter<T>): Promise<boolean>;
124
144
  deleteMany(filter: Filter<T>): Promise<number>;
125
145
  count(filter?: Filter<T>): Promise<number>;
146
+ /**
147
+ * Run a MongoDB-style aggregation pipeline (`$match`, `$group`, `$sort`,
148
+ * `$skip`, `$limit`, `$project`) inside the engine. Returns the resulting
149
+ * documents. Currently available on Node.js and the in-memory browser build.
150
+ *
151
+ * @example
152
+ * const byStatus = await orders.aggregate([
153
+ * { $group: { _id: '$status', total: { $sum: '$amount' }, n: { $sum: 1 } } },
154
+ * { $sort: { total: -1 } },
155
+ * ]);
156
+ */
157
+ aggregate<R extends Document = Document>(pipeline: AggregatePipeline<T>): Promise<R[]>;
126
158
  createIndex(field: keyof Omit<T, '_id'> & string): Promise<void>;
127
159
  dropIndex(field: keyof Omit<T, '_id'> & string): Promise<void>;
128
160
  /**
@@ -198,8 +230,82 @@ interface Collection<T extends Document = Document> {
198
230
  */
199
231
  subscribe(filter: Filter<T>, callback: (docs: T[]) => void): () => void;
200
232
  }
233
+ /**
234
+ * A JSON-encoded changeset — the opaque payload exchanged between peers. Produced
235
+ * by {@link TalaDB.exportChanges}, transported by a {@link SyncAdapter}, and
236
+ * consumed by {@link TalaDB.importChanges}. Treat it as an opaque string.
237
+ */
238
+ type SerializedChangeset = string;
239
+ /** Direction of a sync pass. `'both'` (default) is fully bidirectional. */
240
+ type SyncDirection = 'push' | 'pull' | 'both';
241
+ /**
242
+ * A transport for {@link TalaDB.sync}. Implement `push` to send local changes to
243
+ * a remote, `pull` to fetch remote changes — or both for bidirectional sync.
244
+ * The changeset is an opaque JSON string; move it over any wire you like.
245
+ */
246
+ interface SyncAdapter {
247
+ /** Send a local changeset to the remote. Required for `'push'` / `'both'`. */
248
+ push?(changeset: SerializedChangeset): Promise<void>;
249
+ /**
250
+ * Fetch remote changes with `changed_at` after `sinceMs` (ms epoch), as a
251
+ * serialized changeset. Return `'[]'` when there is nothing new. Required for
252
+ * `'pull'` / `'both'`.
253
+ */
254
+ pull?(sinceMs: number): Promise<SerializedChangeset>;
255
+ }
256
+ interface SyncOptions {
257
+ /**
258
+ * Collections to sync. Omit to sync **all** user collections (reserved
259
+ * `_`-prefixed collections are always skipped). Provide an array to sync only
260
+ * those.
261
+ */
262
+ collections?: string[];
263
+ /**
264
+ * Collections to skip. Applied after `collections` (or after the
265
+ * all-collections default), so `{ exclude: ['logs'] }` means "sync everything
266
+ * except logs".
267
+ */
268
+ exclude?: string[];
269
+ /** Direction of the pass. Default `'both'` (bidirectional). */
270
+ direction?: SyncDirection;
271
+ /**
272
+ * Names this sync target for cursor persistence, so multiple remotes each
273
+ * keep their own watermark. Default `'default'`.
274
+ */
275
+ target?: string;
276
+ }
277
+ interface SyncResult {
278
+ /** Number of local changes pushed to the remote. */
279
+ pushed: number;
280
+ /** Number of documents changed locally by the pulled remote changeset. */
281
+ pulled: number;
282
+ /** New sync cursor (ms epoch) persisted for the next pass. */
283
+ cursor: number;
284
+ }
201
285
  interface TalaDB {
202
286
  collection<T extends Document = Document>(name: string, options?: CollectionOptions<T>): Collection<T>;
287
+ /**
288
+ * Run one bidirectional sync pass against `adapter`: pull remote changes and
289
+ * merge them (Last-Write-Wins), then push local changes since the last cursor.
290
+ * The cursor is persisted per `target`, so successive calls sync incrementally.
291
+ * Set `direction` to `'push'` or `'pull'` to make it one-way.
292
+ *
293
+ * @example
294
+ * await db.sync(httpAdapter, { collections: ['notes'] }); // bidirectional
295
+ * await db.sync(httpAdapter, { collections: ['logs'], direction: 'push' });
296
+ */
297
+ sync(adapter: SyncAdapter, options: SyncOptions): Promise<SyncResult>;
298
+ /**
299
+ * Low-level: export changes to `collections` with `changed_at` after `sinceMs`
300
+ * (exclusive) as a serialized changeset. Most apps use {@link TalaDB.sync}.
301
+ */
302
+ exportChanges(collections: string[], sinceMs: number): Promise<SerializedChangeset>;
303
+ /**
304
+ * Low-level: merge a serialized changeset into the local database via
305
+ * Last-Write-Wins. Returns the number of documents changed. Idempotent —
306
+ * re-importing the same changeset is a no-op.
307
+ */
308
+ importChanges(changeset: SerializedChangeset): Promise<number>;
203
309
  /**
204
310
  * Compact the underlying storage file, reclaiming space freed by deletes
205
311
  * and updates.
@@ -252,6 +358,45 @@ interface TalaDbConfig {
252
358
  sync?: SyncConfig;
253
359
  }
254
360
 
361
+ interface HttpSyncAdapterOptions {
362
+ /** Base URL, e.g. `https://api.example.com/sync`. `/push` and `/pull` are appended. */
363
+ endpoint: string;
364
+ /** Extra headers on every request — typically `Authorization`. */
365
+ headers?: Record<string, string>;
366
+ /**
367
+ * `fetch` implementation. Defaults to the global `fetch` (Node 18+, browsers,
368
+ * React Native). Inject a custom one for tests or non-standard environments.
369
+ */
370
+ fetch?: typeof fetch;
371
+ /** Paths appended to `endpoint`. Override to match an existing API. */
372
+ paths?: {
373
+ push?: string;
374
+ pull?: string;
375
+ };
376
+ }
377
+ /**
378
+ * A ready-to-use {@link SyncAdapter} that syncs over plain HTTP. Pair it with
379
+ * {@link TalaDB.sync}:
380
+ *
381
+ * ```ts
382
+ * const adapter = new HttpSyncAdapter({
383
+ * endpoint: 'https://api.example.com/sync',
384
+ * headers: { Authorization: `Bearer ${token}` },
385
+ * });
386
+ * await db.sync(adapter, { collections: ['notes'] });
387
+ * ```
388
+ */
389
+ declare class HttpSyncAdapter implements SyncAdapter {
390
+ private readonly endpoint;
391
+ private readonly headers;
392
+ private readonly fetchFn;
393
+ private readonly pushPath;
394
+ private readonly pullPath;
395
+ constructor(options: HttpSyncAdapterOptions);
396
+ push(changeset: SerializedChangeset): Promise<void>;
397
+ pull(sinceMs: number): Promise<SerializedChangeset>;
398
+ }
399
+
255
400
  /**
256
401
  * Thrown when a document fails schema validation on `insert` or `insertMany`.
257
402
  * The `cause` property holds the original error thrown by the schema library.
@@ -294,4 +439,4 @@ interface OpenDBOptions {
294
439
  */
295
440
  declare function openDB(dbName?: string, options?: OpenDBOptions): Promise<TalaDB>;
296
441
 
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 };
442
+ 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({ _id: target });
138
+ return doc?.sinceMs ?? 0;
139
+ }
140
+ async function writeCursor(cursorCol, target, sinceMs) {
141
+ const existing = await cursorCol.findOne({ _id: target });
142
+ if (existing) {
143
+ await cursorCol.updateOne({ _id: target }, { $set: { sinceMs } });
144
+ } else {
145
+ await cursorCol.insert({ _id: target, sinceMs });
146
+ }
147
+ }
148
+ async function runSync(handle, adapter, options) {
149
+ const direction = options.direction ?? "both";
150
+ const target = options.target ?? "default";
151
+ const doPush = direction === "push" || direction === "both";
152
+ const doPull = direction === "pull" || direction === "both";
153
+ if (doPull && !adapter.pull) {
154
+ throw new Error(`sync direction '${direction}' requires adapter.pull()`);
155
+ }
156
+ if (doPush && !adapter.push) {
157
+ throw new Error(`sync direction '${direction}' requires adapter.push()`);
158
+ }
159
+ const collections = await resolveCollections(handle, options);
160
+ const cursorCol = handle.collection(CURSOR_COLLECTION);
161
+ const sinceMs = await readCursor(cursorCol, target);
162
+ const startedAt = Date.now();
163
+ const local = doPush ? await handle.exportChanges(collections, sinceMs) : "[]";
164
+ let pulled = 0;
165
+ if (doPull) {
166
+ const remote = await adapter.pull(sinceMs);
167
+ if (remote && remote !== "[]") {
168
+ pulled = await handle.importChanges(remote);
169
+ }
170
+ }
171
+ let pushed = 0;
172
+ if (doPush && local !== "[]") {
173
+ pushed = JSON.parse(local).length;
174
+ await adapter.push(local);
175
+ }
176
+ await writeCursor(cursorCol, target, startedAt);
177
+ return { pushed, pulled, cursor: startedAt };
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;
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 {
@@ -292,6 +390,13 @@ async function createBrowserDB(dbName, config) {
292
390
  collection: name,
293
391
  filterJson: filter ? s(filter) : "null"
294
392
  }),
393
+ aggregate: async (pipeline) => {
394
+ const json = await proxy.send("aggregate", {
395
+ collection: name,
396
+ pipelineJson: s(pipeline)
397
+ });
398
+ return JSON.parse(json);
399
+ },
295
400
  createIndex: (field) => proxy.send("createIndex", { collection: name, field }),
296
401
  dropIndex: (field) => proxy.send("dropIndex", { collection: name, field }),
297
402
  createFtsIndex: (field) => proxy.send("createFtsIndex", { collection: name, field }),
@@ -372,7 +477,8 @@ async function createBrowserDB(dbName, config) {
372
477
  worker.terminate();
373
478
  proxy.abort(new Error("taladb worker closed"));
374
479
  }
375
- }
480
+ },
481
+ ...unsupportedSync("browser (OPFS worker)")
376
482
  };
377
483
  }
378
484
  async function createNodeDB(dbName, config) {
@@ -391,6 +497,7 @@ async function createNodeDB(dbName, config) {
391
497
  deleteOne: async (filter) => col.deleteOneAsync ? col.deleteOneAsync(filter) : col.deleteOne(filter),
392
498
  deleteMany: async (filter) => col.deleteManyAsync ? col.deleteManyAsync(filter) : col.deleteMany(filter),
393
499
  count: async (filter) => col.count(filter ?? null),
500
+ aggregate: async (pipeline) => col.aggregate(pipeline),
394
501
  createIndex: async (field) => col.createIndex(field),
395
502
  dropIndex: async (field) => col.dropIndex(field),
396
503
  createFtsIndex: async (field) => col.createFtsIndex(field),
@@ -410,12 +517,17 @@ async function createNodeDB(dbName, config) {
410
517
  };
411
518
  return opts ? applySchema(wrapped, opts) : wrapped;
412
519
  }
413
- return {
520
+ const handle = {
414
521
  collection: (name, opts) => wrapCollection(name, opts),
415
522
  compact: async () => db.compact(),
416
523
  // Releases the native file handle/lock (no-op on older .node binaries).
417
- close: async () => db.close?.()
524
+ close: async () => db.close?.(),
525
+ exportChanges: async (collections, sinceMs) => db.exportChanges(sinceMs, collections),
526
+ importChanges: async (changeset) => db.importChanges(changeset),
527
+ listCollectionNames: async () => db.listCollectionNames(),
528
+ sync: (adapter, options) => runSync(handle, adapter, options)
418
529
  };
530
+ return handle;
419
531
  }
420
532
  async function createNativeDB(_dbName) {
421
533
  const maybeNative = globalThis.__TalaDB__;
@@ -436,6 +548,7 @@ async function createNativeDB(_dbName) {
436
548
  deleteOne: async (filter) => native.deleteOne(name, filter),
437
549
  deleteMany: async (filter) => native.deleteMany(name, filter),
438
550
  count: async (filter) => native.count(name, filter ?? {}),
551
+ aggregate: async (pipeline) => native.aggregate(name, pipeline),
439
552
  createIndex: async (field) => native.createIndex(name, field),
440
553
  dropIndex: async (field) => native.dropIndex(name, field),
441
554
  createFtsIndex: async (field) => native.createFtsIndex(name, field),
@@ -464,7 +577,8 @@ async function createNativeDB(_dbName) {
464
577
  return {
465
578
  collection: (name, opts) => wrapCollection(name, opts),
466
579
  compact: async () => native.compact(),
467
- close: async () => native.close()
580
+ close: async () => native.close(),
581
+ ...unsupportedSync("react-native")
468
582
  };
469
583
  }
470
584
  async function openDB(dbName = "taladb.db", options) {
@@ -487,6 +601,7 @@ async function openDB(dbName = "taladb.db", options) {
487
601
  }
488
602
  // Annotate the CommonJS export names for ESM import in node:
489
603
  0 && (module.exports = {
604
+ HttpSyncAdapter,
490
605
  TalaDbValidationError,
491
606
  openDB
492
607
  });
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({ _id: target });
100
+ return doc?.sinceMs ?? 0;
101
+ }
102
+ async function writeCursor(cursorCol, target, sinceMs) {
103
+ const existing = await cursorCol.findOne({ _id: target });
104
+ if (existing) {
105
+ await cursorCol.updateOne({ _id: target }, { $set: { sinceMs } });
106
+ } else {
107
+ await cursorCol.insert({ _id: target, sinceMs });
108
+ }
109
+ }
110
+ async function runSync(handle, adapter, options) {
111
+ const direction = options.direction ?? "both";
112
+ const target = options.target ?? "default";
113
+ const doPush = direction === "push" || direction === "both";
114
+ const doPull = direction === "pull" || direction === "both";
115
+ if (doPull && !adapter.pull) {
116
+ throw new Error(`sync direction '${direction}' requires adapter.pull()`);
117
+ }
118
+ if (doPush && !adapter.push) {
119
+ throw new Error(`sync direction '${direction}' requires adapter.push()`);
120
+ }
121
+ const collections = await resolveCollections(handle, options);
122
+ const cursorCol = handle.collection(CURSOR_COLLECTION);
123
+ const sinceMs = await readCursor(cursorCol, target);
124
+ const startedAt = Date.now();
125
+ const local = doPush ? await handle.exportChanges(collections, sinceMs) : "[]";
126
+ let pulled = 0;
127
+ if (doPull) {
128
+ const remote = await adapter.pull(sinceMs);
129
+ if (remote && remote !== "[]") {
130
+ pulled = await handle.importChanges(remote);
131
+ }
132
+ }
133
+ let pushed = 0;
134
+ if (doPush && local !== "[]") {
135
+ pushed = JSON.parse(local).length;
136
+ await adapter.push(local);
137
+ }
138
+ await writeCursor(cursorCol, target, startedAt);
139
+ return { pushed, pulled, cursor: startedAt };
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;
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) {
@@ -254,6 +351,13 @@ async function createBrowserDB(dbName, config) {
254
351
  collection: name,
255
352
  filterJson: filter ? s(filter) : "null"
256
353
  }),
354
+ aggregate: async (pipeline) => {
355
+ const json = await proxy.send("aggregate", {
356
+ collection: name,
357
+ pipelineJson: s(pipeline)
358
+ });
359
+ return JSON.parse(json);
360
+ },
257
361
  createIndex: (field) => proxy.send("createIndex", { collection: name, field }),
258
362
  dropIndex: (field) => proxy.send("dropIndex", { collection: name, field }),
259
363
  createFtsIndex: (field) => proxy.send("createFtsIndex", { collection: name, field }),
@@ -334,7 +438,8 @@ async function createBrowserDB(dbName, config) {
334
438
  worker.terminate();
335
439
  proxy.abort(new Error("taladb worker closed"));
336
440
  }
337
- }
441
+ },
442
+ ...unsupportedSync("browser (OPFS worker)")
338
443
  };
339
444
  }
340
445
  async function createNodeDB(dbName, config) {
@@ -353,6 +458,7 @@ async function createNodeDB(dbName, config) {
353
458
  deleteOne: async (filter) => col.deleteOneAsync ? col.deleteOneAsync(filter) : col.deleteOne(filter),
354
459
  deleteMany: async (filter) => col.deleteManyAsync ? col.deleteManyAsync(filter) : col.deleteMany(filter),
355
460
  count: async (filter) => col.count(filter ?? null),
461
+ aggregate: async (pipeline) => col.aggregate(pipeline),
356
462
  createIndex: async (field) => col.createIndex(field),
357
463
  dropIndex: async (field) => col.dropIndex(field),
358
464
  createFtsIndex: async (field) => col.createFtsIndex(field),
@@ -372,12 +478,17 @@ async function createNodeDB(dbName, config) {
372
478
  };
373
479
  return opts ? applySchema(wrapped, opts) : wrapped;
374
480
  }
375
- return {
481
+ const handle = {
376
482
  collection: (name, opts) => wrapCollection(name, opts),
377
483
  compact: async () => db.compact(),
378
484
  // Releases the native file handle/lock (no-op on older .node binaries).
379
- close: async () => db.close?.()
485
+ close: async () => db.close?.(),
486
+ exportChanges: async (collections, sinceMs) => db.exportChanges(sinceMs, collections),
487
+ importChanges: async (changeset) => db.importChanges(changeset),
488
+ listCollectionNames: async () => db.listCollectionNames(),
489
+ sync: (adapter, options) => runSync(handle, adapter, options)
380
490
  };
491
+ return handle;
381
492
  }
382
493
  async function createNativeDB(_dbName) {
383
494
  const maybeNative = globalThis.__TalaDB__;
@@ -398,6 +509,7 @@ async function createNativeDB(_dbName) {
398
509
  deleteOne: async (filter) => native.deleteOne(name, filter),
399
510
  deleteMany: async (filter) => native.deleteMany(name, filter),
400
511
  count: async (filter) => native.count(name, filter ?? {}),
512
+ aggregate: async (pipeline) => native.aggregate(name, pipeline),
401
513
  createIndex: async (field) => native.createIndex(name, field),
402
514
  dropIndex: async (field) => native.dropIndex(name, field),
403
515
  createFtsIndex: async (field) => native.createFtsIndex(name, field),
@@ -426,7 +538,8 @@ async function createNativeDB(_dbName) {
426
538
  return {
427
539
  collection: (name, opts) => wrapCollection(name, opts),
428
540
  compact: async () => native.compact(),
429
- close: async () => native.close()
541
+ close: async () => native.close(),
542
+ ...unsupportedSync("react-native")
430
543
  };
431
544
  }
432
545
  async function openDB(dbName = "taladb.db", options) {
@@ -448,6 +561,7 @@ async function openDB(dbName = "taladb.db", options) {
448
561
  }
449
562
  }
450
563
  export {
564
+ HttpSyncAdapter,
451
565
  TalaDbValidationError,
452
566
  openDB
453
567
  };
@@ -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({ _id: target });
43
+ return doc?.sinceMs ?? 0;
44
+ }
45
+ async function writeCursor(cursorCol, target, sinceMs) {
46
+ const existing = await cursorCol.findOne({ _id: target });
47
+ if (existing) {
48
+ await cursorCol.updateOne({ _id: target }, { $set: { sinceMs } });
49
+ } else {
50
+ await cursorCol.insert({ _id: target, sinceMs });
51
+ }
52
+ }
53
+ async function runSync(handle, adapter, options) {
54
+ const direction = options.direction ?? "both";
55
+ const target = options.target ?? "default";
56
+ const doPush = direction === "push" || direction === "both";
57
+ const doPull = direction === "pull" || direction === "both";
58
+ if (doPull && !adapter.pull) {
59
+ throw new Error(`sync direction '${direction}' requires adapter.pull()`);
60
+ }
61
+ if (doPush && !adapter.push) {
62
+ throw new Error(`sync direction '${direction}' requires adapter.push()`);
63
+ }
64
+ const collections = await resolveCollections(handle, options);
65
+ const cursorCol = handle.collection(CURSOR_COLLECTION);
66
+ const sinceMs = await readCursor(cursorCol, target);
67
+ const startedAt = Date.now();
68
+ const local = doPush ? await handle.exportChanges(collections, sinceMs) : "[]";
69
+ let pulled = 0;
70
+ if (doPull) {
71
+ const remote = await adapter.pull(sinceMs);
72
+ if (remote && remote !== "[]") {
73
+ pulled = await handle.importChanges(remote);
74
+ }
75
+ }
76
+ let pushed = 0;
77
+ if (doPush && local !== "[]") {
78
+ pushed = JSON.parse(local).length;
79
+ await adapter.push(local);
80
+ }
81
+ await writeCursor(cursorCol, target, startedAt);
82
+ return { pushed, pulled, cursor: startedAt };
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;
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) {
@@ -197,6 +294,13 @@ async function createBrowserDB(dbName, config) {
197
294
  collection: name,
198
295
  filterJson: filter ? s(filter) : "null"
199
296
  }),
297
+ aggregate: async (pipeline) => {
298
+ const json = await proxy.send("aggregate", {
299
+ collection: name,
300
+ pipelineJson: s(pipeline)
301
+ });
302
+ return JSON.parse(json);
303
+ },
200
304
  createIndex: (field) => proxy.send("createIndex", { collection: name, field }),
201
305
  dropIndex: (field) => proxy.send("dropIndex", { collection: name, field }),
202
306
  createFtsIndex: (field) => proxy.send("createFtsIndex", { collection: name, field }),
@@ -277,7 +381,8 @@ async function createBrowserDB(dbName, config) {
277
381
  worker.terminate();
278
382
  proxy.abort(new Error("taladb worker closed"));
279
383
  }
280
- }
384
+ },
385
+ ...unsupportedSync("browser (OPFS worker)")
281
386
  };
282
387
  }
283
388
  async function createNodeDB(dbName, config) {
@@ -296,6 +401,7 @@ async function createNodeDB(dbName, config) {
296
401
  deleteOne: async (filter) => col.deleteOneAsync ? col.deleteOneAsync(filter) : col.deleteOne(filter),
297
402
  deleteMany: async (filter) => col.deleteManyAsync ? col.deleteManyAsync(filter) : col.deleteMany(filter),
298
403
  count: async (filter) => col.count(filter ?? null),
404
+ aggregate: async (pipeline) => col.aggregate(pipeline),
299
405
  createIndex: async (field) => col.createIndex(field),
300
406
  dropIndex: async (field) => col.dropIndex(field),
301
407
  createFtsIndex: async (field) => col.createFtsIndex(field),
@@ -315,12 +421,17 @@ async function createNodeDB(dbName, config) {
315
421
  };
316
422
  return opts ? applySchema(wrapped, opts) : wrapped;
317
423
  }
318
- return {
424
+ const handle = {
319
425
  collection: (name, opts) => wrapCollection(name, opts),
320
426
  compact: async () => db.compact(),
321
427
  // Releases the native file handle/lock (no-op on older .node binaries).
322
- close: async () => db.close?.()
428
+ close: async () => db.close?.(),
429
+ exportChanges: async (collections, sinceMs) => db.exportChanges(sinceMs, collections),
430
+ importChanges: async (changeset) => db.importChanges(changeset),
431
+ listCollectionNames: async () => db.listCollectionNames(),
432
+ sync: (adapter, options) => runSync(handle, adapter, options)
323
433
  };
434
+ return handle;
324
435
  }
325
436
  async function createNativeDB(_dbName) {
326
437
  const maybeNative = globalThis.__TalaDB__;
@@ -341,6 +452,7 @@ async function createNativeDB(_dbName) {
341
452
  deleteOne: async (filter) => native.deleteOne(name, filter),
342
453
  deleteMany: async (filter) => native.deleteMany(name, filter),
343
454
  count: async (filter) => native.count(name, filter ?? {}),
455
+ aggregate: async (pipeline) => native.aggregate(name, pipeline),
344
456
  createIndex: async (field) => native.createIndex(name, field),
345
457
  dropIndex: async (field) => native.dropIndex(name, field),
346
458
  createFtsIndex: async (field) => native.createFtsIndex(name, field),
@@ -369,7 +481,8 @@ async function createNativeDB(_dbName) {
369
481
  return {
370
482
  collection: (name, opts) => wrapCollection(name, opts),
371
483
  compact: async () => native.compact(),
372
- close: async () => native.close()
484
+ close: async () => native.close(),
485
+ ...unsupportedSync("react-native")
373
486
  };
374
487
  }
375
488
  async function openDB(dbName = "taladb.db", options) {
@@ -391,6 +504,7 @@ async function openDB(dbName = "taladb.db", options) {
391
504
  }
392
505
  }
393
506
  export {
507
+ HttpSyncAdapter,
394
508
  TalaDbValidationError,
395
509
  openDB
396
510
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taladb",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
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": {