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.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({ target });
|
|
100
|
+
return { pushMs: doc?.pushMs ?? 0, pullMs: doc?.pullMs ?? 0 };
|
|
101
|
+
}
|
|
102
|
+
async function writeCursor(cursorCol, target, cursor) {
|
|
103
|
+
const updated = await cursorCol.updateOne({ target }, { $set: { ...cursor } });
|
|
104
|
+
if (!updated) {
|
|
105
|
+
await cursorCol.insert({ target, ...cursor });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function runSync(handle, adapter, options) {
|
|
109
|
+
const direction = options.direction ?? "both";
|
|
110
|
+
const target = options.target ?? "default";
|
|
111
|
+
const doPush = direction === "push" || direction === "both";
|
|
112
|
+
const doPull = direction === "pull" || direction === "both";
|
|
113
|
+
if (doPull && !adapter.pull) {
|
|
114
|
+
throw new Error(`sync direction '${direction}' requires adapter.pull()`);
|
|
115
|
+
}
|
|
116
|
+
if (doPush && !adapter.push) {
|
|
117
|
+
throw new Error(`sync direction '${direction}' requires adapter.push()`);
|
|
118
|
+
}
|
|
119
|
+
const collections = await resolveCollections(handle, options);
|
|
120
|
+
const cursorCol = handle.collection(CURSOR_COLLECTION);
|
|
121
|
+
const cursor = await readCursor(cursorCol, target);
|
|
122
|
+
const local = doPush ? await handle.exportChanges(collections, 0) : "[]";
|
|
123
|
+
let pulled = 0;
|
|
124
|
+
if (doPull) {
|
|
125
|
+
const remote = await adapter.pull(0);
|
|
126
|
+
if (remote && remote !== "[]") {
|
|
127
|
+
pulled = await handle.importChanges(remote);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
let pushed = 0;
|
|
131
|
+
if (doPush && local !== "[]") {
|
|
132
|
+
pushed = JSON.parse(local).length;
|
|
133
|
+
await adapter.push(local);
|
|
134
|
+
}
|
|
135
|
+
await writeCursor(cursorCol, target, {
|
|
136
|
+
pushMs: cursor.pushMs,
|
|
137
|
+
pullMs: cursor.pullMs
|
|
138
|
+
});
|
|
139
|
+
return { pushed, pulled, cursor: 0 };
|
|
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?.bind(globalThis);
|
|
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) {
|
|
@@ -173,28 +270,44 @@ var WorkerProxy = class {
|
|
|
173
270
|
this.pending.clear();
|
|
174
271
|
}
|
|
175
272
|
};
|
|
176
|
-
function makePoller(findFn, callback) {
|
|
273
|
+
function makePoller(findFn, callback, onError) {
|
|
177
274
|
let active = true;
|
|
178
275
|
let lastJson = "";
|
|
276
|
+
let running = false;
|
|
277
|
+
let rerun = false;
|
|
179
278
|
const poll = async () => {
|
|
180
279
|
if (!active) return;
|
|
280
|
+
if (running) {
|
|
281
|
+
rerun = true;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
running = true;
|
|
181
285
|
try {
|
|
182
286
|
const docs = await findFn();
|
|
287
|
+
if (!active) return;
|
|
183
288
|
const json = JSON.stringify(docs);
|
|
184
289
|
if (json !== lastJson) {
|
|
185
290
|
lastJson = json;
|
|
186
291
|
callback(docs);
|
|
187
292
|
}
|
|
188
|
-
} catch {
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (active) onError?.(error);
|
|
295
|
+
} finally {
|
|
296
|
+
running = false;
|
|
297
|
+
if (active) {
|
|
298
|
+
if (rerun) {
|
|
299
|
+
rerun = false;
|
|
300
|
+
void poll();
|
|
301
|
+
} else setTimeout(poll, 300);
|
|
302
|
+
}
|
|
189
303
|
}
|
|
190
|
-
if (active) setTimeout(poll, 300);
|
|
191
304
|
};
|
|
192
305
|
poll();
|
|
193
306
|
return () => {
|
|
194
307
|
active = false;
|
|
195
308
|
};
|
|
196
309
|
}
|
|
197
|
-
async function createBrowserDB(dbName, config) {
|
|
310
|
+
async function createBrowserDB(dbName, config, passphrase) {
|
|
198
311
|
const workerUrl = new URL("@taladb/web/worker/taladb.worker.js", import.meta.url);
|
|
199
312
|
const worker = new Worker(workerUrl, { type: "module", name: "taladb" });
|
|
200
313
|
const proxy = new WorkerProxy(worker);
|
|
@@ -202,7 +315,13 @@ async function createBrowserDB(dbName, config) {
|
|
|
202
315
|
proxy.abort(new Error(`taladb worker error: ${e.message ?? "unknown"}`));
|
|
203
316
|
};
|
|
204
317
|
const configJson = config !== void 0 ? JSON.stringify(config) : void 0;
|
|
205
|
-
|
|
318
|
+
try {
|
|
319
|
+
await proxy.send("init", { dbName, configJson, passphrase });
|
|
320
|
+
} catch (e) {
|
|
321
|
+
proxy.abort(e instanceof Error ? e : new Error(String(e)));
|
|
322
|
+
worker.terminate();
|
|
323
|
+
throw e;
|
|
324
|
+
}
|
|
206
325
|
const nudgeCallbacks = /* @__PURE__ */ new Set();
|
|
207
326
|
let channel = null;
|
|
208
327
|
if (typeof BroadcastChannel !== "undefined") {
|
|
@@ -254,8 +373,17 @@ async function createBrowserDB(dbName, config) {
|
|
|
254
373
|
collection: name,
|
|
255
374
|
filterJson: filter ? s(filter) : "null"
|
|
256
375
|
}),
|
|
376
|
+
aggregate: async (pipeline) => {
|
|
377
|
+
const json = await proxy.send("aggregate", {
|
|
378
|
+
collection: name,
|
|
379
|
+
pipelineJson: s(pipeline)
|
|
380
|
+
});
|
|
381
|
+
return JSON.parse(json);
|
|
382
|
+
},
|
|
257
383
|
createIndex: (field) => proxy.send("createIndex", { collection: name, field }),
|
|
258
384
|
dropIndex: (field) => proxy.send("dropIndex", { collection: name, field }),
|
|
385
|
+
createCompoundIndex: (fields) => proxy.send("createCompoundIndex", { collection: name, fieldsJson: JSON.stringify(fields) }),
|
|
386
|
+
dropCompoundIndex: (fields) => proxy.send("dropCompoundIndex", { collection: name, fieldsJson: JSON.stringify(fields) }),
|
|
259
387
|
createFtsIndex: (field) => proxy.send("createFtsIndex", { collection: name, field }),
|
|
260
388
|
dropFtsIndex: (field) => proxy.send("dropFtsIndex", { collection: name, field }),
|
|
261
389
|
createVectorIndex: (field, options) => {
|
|
@@ -286,12 +414,19 @@ async function createBrowserDB(dbName, config) {
|
|
|
286
414
|
});
|
|
287
415
|
return JSON.parse(json);
|
|
288
416
|
},
|
|
289
|
-
subscribe: (filter, callback) => {
|
|
417
|
+
subscribe: (filter, callback, onError) => {
|
|
290
418
|
let active = true;
|
|
291
419
|
let lastJson = "";
|
|
292
420
|
let timer = null;
|
|
421
|
+
let running = false;
|
|
422
|
+
let rerun = false;
|
|
293
423
|
const poll = async () => {
|
|
294
424
|
if (!active) return;
|
|
425
|
+
if (running) {
|
|
426
|
+
rerun = true;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
running = true;
|
|
295
430
|
if (timer !== null) {
|
|
296
431
|
clearTimeout(timer);
|
|
297
432
|
timer = null;
|
|
@@ -301,13 +436,22 @@ async function createBrowserDB(dbName, config) {
|
|
|
301
436
|
collection: name,
|
|
302
437
|
filterJson: filter ? s(filter) : "null"
|
|
303
438
|
});
|
|
439
|
+
if (!active) return;
|
|
304
440
|
if (json !== lastJson) {
|
|
305
441
|
lastJson = json;
|
|
306
442
|
callback(JSON.parse(json));
|
|
307
443
|
}
|
|
308
|
-
} catch {
|
|
444
|
+
} catch (error) {
|
|
445
|
+
if (active) onError?.(error);
|
|
446
|
+
} finally {
|
|
447
|
+
running = false;
|
|
448
|
+
}
|
|
449
|
+
if (active) {
|
|
450
|
+
if (rerun) {
|
|
451
|
+
rerun = false;
|
|
452
|
+
void poll();
|
|
453
|
+
} else timer = setTimeout(poll, 300);
|
|
309
454
|
}
|
|
310
|
-
if (active) timer = setTimeout(poll, 300);
|
|
311
455
|
};
|
|
312
456
|
nudgeCallbacks.add(poll);
|
|
313
457
|
poll();
|
|
@@ -323,9 +467,11 @@ async function createBrowserDB(dbName, config) {
|
|
|
323
467
|
};
|
|
324
468
|
return opts ? applySchema(wrapped, opts) : wrapped;
|
|
325
469
|
}
|
|
326
|
-
|
|
470
|
+
const handle = {
|
|
327
471
|
collection: (name, opts) => wrapCollection(name, opts),
|
|
328
472
|
compact: () => proxy.send("compact"),
|
|
473
|
+
syncStatus: async () => JSON.parse(await proxy.send("syncStatus")),
|
|
474
|
+
flushSync: (timeoutMs = 5e3) => proxy.send("flushSync", { timeoutMs }),
|
|
329
475
|
close: async () => {
|
|
330
476
|
channel?.close();
|
|
331
477
|
try {
|
|
@@ -334,13 +480,22 @@ async function createBrowserDB(dbName, config) {
|
|
|
334
480
|
worker.terminate();
|
|
335
481
|
proxy.abort(new Error("taladb worker closed"));
|
|
336
482
|
}
|
|
337
|
-
}
|
|
483
|
+
},
|
|
484
|
+
// All engine work (export scan, LWW merge) runs inside the worker, off the
|
|
485
|
+
// main thread — a sync pass never blocks rendering, whatever its size.
|
|
486
|
+
exportChanges: (collections, sinceMs) => proxy.send("exportChangeset", { collectionsJson: JSON.stringify(collections), sinceMs }),
|
|
487
|
+
importChanges: (changeset) => proxy.send("importChangeset", { changesetJson: changeset }),
|
|
488
|
+
listCollectionNames: async () => JSON.parse(await proxy.send("listCollections")),
|
|
489
|
+
sync: (adapter, options) => runSync(handle, adapter, options)
|
|
338
490
|
};
|
|
491
|
+
return handle;
|
|
339
492
|
}
|
|
340
|
-
async function createNodeDB(dbName, config) {
|
|
341
|
-
const
|
|
493
|
+
async function createNodeDB(dbName, config, passphrase) {
|
|
494
|
+
const native = await import("@taladb/node");
|
|
495
|
+
const TalaDBNode = native.TalaDbNode ?? native.TalaDBNode;
|
|
496
|
+
if (!TalaDBNode) throw new Error("@taladb/node loaded but exports no TalaDbNode class \u2014 rebuild the native module");
|
|
342
497
|
const configJson = config !== void 0 ? JSON.stringify(config) : null;
|
|
343
|
-
const db = TalaDBNode.open(dbName, configJson);
|
|
498
|
+
const db = TalaDBNode.open(dbName, configJson, passphrase ?? null);
|
|
344
499
|
function wrapCollection(name, opts) {
|
|
345
500
|
const col = db.collection(name);
|
|
346
501
|
const wrapped = {
|
|
@@ -353,8 +508,11 @@ async function createNodeDB(dbName, config) {
|
|
|
353
508
|
deleteOne: async (filter) => col.deleteOneAsync ? col.deleteOneAsync(filter) : col.deleteOne(filter),
|
|
354
509
|
deleteMany: async (filter) => col.deleteManyAsync ? col.deleteManyAsync(filter) : col.deleteMany(filter),
|
|
355
510
|
count: async (filter) => col.count(filter ?? null),
|
|
511
|
+
aggregate: async (pipeline) => col.aggregate(pipeline),
|
|
356
512
|
createIndex: async (field) => col.createIndex(field),
|
|
357
513
|
dropIndex: async (field) => col.dropIndex(field),
|
|
514
|
+
createCompoundIndex: async (fields) => col.createCompoundIndex(fields),
|
|
515
|
+
dropCompoundIndex: async (fields) => col.dropCompoundIndex(fields),
|
|
358
516
|
createFtsIndex: async (field) => col.createFtsIndex(field),
|
|
359
517
|
dropFtsIndex: async (field) => col.dropFtsIndex(field),
|
|
360
518
|
createVectorIndex: async (field, options) => col.createVectorIndex(field, options.dimensions, options.metric ?? null, options.indexType ?? null, options.hnswM ?? null, options.hnswEfConstruction ?? null),
|
|
@@ -368,16 +526,21 @@ async function createNodeDB(dbName, config) {
|
|
|
368
526
|
const raw = await col.findNearest(field, vector, topK, filter ?? null);
|
|
369
527
|
return raw;
|
|
370
528
|
},
|
|
371
|
-
subscribe: (filter, callback) => makePoller(async () => col.find(filter ?? null), callback)
|
|
529
|
+
subscribe: (filter, callback, onError) => makePoller(async () => col.find(filter ?? null), callback, onError)
|
|
372
530
|
};
|
|
373
531
|
return opts ? applySchema(wrapped, opts) : wrapped;
|
|
374
532
|
}
|
|
375
|
-
|
|
533
|
+
const handle = {
|
|
376
534
|
collection: (name, opts) => wrapCollection(name, opts),
|
|
377
535
|
compact: async () => db.compact(),
|
|
378
536
|
// Releases the native file handle/lock (no-op on older .node binaries).
|
|
379
|
-
close: async () => db.close?.()
|
|
537
|
+
close: async () => db.close?.(),
|
|
538
|
+
exportChanges: async (collections, sinceMs) => db.exportChanges(sinceMs, collections),
|
|
539
|
+
importChanges: async (changeset) => db.importChanges(changeset),
|
|
540
|
+
listCollectionNames: async () => db.listCollectionNames(),
|
|
541
|
+
sync: (adapter, options) => runSync(handle, adapter, options)
|
|
380
542
|
};
|
|
543
|
+
return handle;
|
|
381
544
|
}
|
|
382
545
|
async function createNativeDB(_dbName) {
|
|
383
546
|
const maybeNative = globalThis.__TalaDB__;
|
|
@@ -398,8 +561,11 @@ async function createNativeDB(_dbName) {
|
|
|
398
561
|
deleteOne: async (filter) => native.deleteOne(name, filter),
|
|
399
562
|
deleteMany: async (filter) => native.deleteMany(name, filter),
|
|
400
563
|
count: async (filter) => native.count(name, filter ?? {}),
|
|
564
|
+
aggregate: async (pipeline) => native.aggregate(name, pipeline),
|
|
401
565
|
createIndex: async (field) => native.createIndex(name, field),
|
|
402
566
|
dropIndex: async (field) => native.dropIndex(name, field),
|
|
567
|
+
createCompoundIndex: async (fields) => native.createCompoundIndex(name, fields),
|
|
568
|
+
dropCompoundIndex: async (fields) => native.dropCompoundIndex(name, fields),
|
|
403
569
|
createFtsIndex: async (field) => native.createFtsIndex(name, field),
|
|
404
570
|
dropFtsIndex: async (field) => native.dropFtsIndex(name, field),
|
|
405
571
|
createVectorIndex: async (field, options) => {
|
|
@@ -419,17 +585,35 @@ async function createNativeDB(_dbName) {
|
|
|
419
585
|
const raw = native.findNearest(name, field, vector, topK, filter ?? null);
|
|
420
586
|
return raw;
|
|
421
587
|
},
|
|
422
|
-
subscribe: (filter, callback) => makePoller(async () => native.find(name, filter ?? {}), callback)
|
|
588
|
+
subscribe: (filter, callback, onError) => makePoller(async () => native.find(name, filter ?? {}), callback, onError)
|
|
423
589
|
};
|
|
424
590
|
return opts ? applySchema(wrapped, opts) : wrapped;
|
|
425
591
|
}
|
|
592
|
+
const syncSurface = typeof native.exportChanges === "function" && typeof native.importChanges === "function" && typeof native.listCollectionNames === "function" ? (() => {
|
|
593
|
+
const handle = {
|
|
594
|
+
collection: (name, opts) => wrapCollection(name, opts),
|
|
595
|
+
exportChanges: async (collections, sinceMs) => native.exportChanges(collections, sinceMs),
|
|
596
|
+
importChanges: async (changeset) => native.importChanges(changeset),
|
|
597
|
+
listCollectionNames: async () => native.listCollectionNames(),
|
|
598
|
+
sync: (adapter, options) => runSync(handle, adapter, options)
|
|
599
|
+
};
|
|
600
|
+
return {
|
|
601
|
+
exportChanges: handle.exportChanges,
|
|
602
|
+
importChanges: handle.importChanges,
|
|
603
|
+
sync: handle.sync
|
|
604
|
+
};
|
|
605
|
+
})() : unsupportedSync("react-native");
|
|
426
606
|
return {
|
|
427
607
|
collection: (name, opts) => wrapCollection(name, opts),
|
|
428
608
|
compact: async () => native.compact(),
|
|
429
|
-
close: async () => native.close()
|
|
609
|
+
close: async () => native.close(),
|
|
610
|
+
...syncSurface
|
|
430
611
|
};
|
|
431
612
|
}
|
|
432
613
|
async function openDB(dbName = "taladb.db", options) {
|
|
614
|
+
if (options?.passphrase !== void 0 && options.passphrase.length === 0) {
|
|
615
|
+
throw new Error("TalaDB encryption passphrase must not be empty");
|
|
616
|
+
}
|
|
433
617
|
let resolvedConfig;
|
|
434
618
|
if (options?.config !== void 0) {
|
|
435
619
|
validateConfig(options.config);
|
|
@@ -440,14 +624,18 @@ async function openDB(dbName = "taladb.db", options) {
|
|
|
440
624
|
const platform = detectPlatform();
|
|
441
625
|
switch (platform) {
|
|
442
626
|
case "browser":
|
|
443
|
-
return createBrowserDB(dbName, resolvedConfig);
|
|
627
|
+
return createBrowserDB(dbName, resolvedConfig, options?.passphrase);
|
|
444
628
|
case "react-native":
|
|
629
|
+
if (options?.passphrase !== void 0) {
|
|
630
|
+
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");
|
|
631
|
+
}
|
|
445
632
|
return createNativeDB(dbName);
|
|
446
633
|
case "node":
|
|
447
|
-
return createNodeDB(dbName, resolvedConfig);
|
|
634
|
+
return createNodeDB(dbName, resolvedConfig, options?.passphrase);
|
|
448
635
|
}
|
|
449
636
|
}
|
|
450
637
|
export {
|
|
638
|
+
HttpSyncAdapter,
|
|
451
639
|
TalaDbValidationError,
|
|
452
640
|
openDB
|
|
453
641
|
};
|