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
|
@@ -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", "react-native://unreachable");
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taladb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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": {
|