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.browser.mjs +208 -20
- package/dist/index.d.mts +174 -2
- package/dist/index.d.ts +174 -2
- package/dist/index.js +209 -20
- package/dist/index.mjs +208 -20
- package/dist/index.react-native.mjs +208 -20
- package/package.json +2 -2
package/dist/index.browser.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 };
|