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.d.ts
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 };
|
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({ target });
|
|
138
|
+
return { pushMs: doc?.pushMs ?? 0, pullMs: doc?.pullMs ?? 0 };
|
|
139
|
+
}
|
|
140
|
+
async function writeCursor(cursorCol, target, cursor) {
|
|
141
|
+
const updated = await cursorCol.updateOne({ target }, { $set: { ...cursor } });
|
|
142
|
+
if (!updated) {
|
|
143
|
+
await cursorCol.insert({ target, ...cursor });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function runSync(handle, adapter, options) {
|
|
147
|
+
const direction = options.direction ?? "both";
|
|
148
|
+
const target = options.target ?? "default";
|
|
149
|
+
const doPush = direction === "push" || direction === "both";
|
|
150
|
+
const doPull = direction === "pull" || direction === "both";
|
|
151
|
+
if (doPull && !adapter.pull) {
|
|
152
|
+
throw new Error(`sync direction '${direction}' requires adapter.pull()`);
|
|
153
|
+
}
|
|
154
|
+
if (doPush && !adapter.push) {
|
|
155
|
+
throw new Error(`sync direction '${direction}' requires adapter.push()`);
|
|
156
|
+
}
|
|
157
|
+
const collections = await resolveCollections(handle, options);
|
|
158
|
+
const cursorCol = handle.collection(CURSOR_COLLECTION);
|
|
159
|
+
const cursor = await readCursor(cursorCol, target);
|
|
160
|
+
const local = doPush ? await handle.exportChanges(collections, 0) : "[]";
|
|
161
|
+
let pulled = 0;
|
|
162
|
+
if (doPull) {
|
|
163
|
+
const remote = await adapter.pull(0);
|
|
164
|
+
if (remote && remote !== "[]") {
|
|
165
|
+
pulled = await handle.importChanges(remote);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
let pushed = 0;
|
|
169
|
+
if (doPush && local !== "[]") {
|
|
170
|
+
pushed = JSON.parse(local).length;
|
|
171
|
+
await adapter.push(local);
|
|
172
|
+
}
|
|
173
|
+
await writeCursor(cursorCol, target, {
|
|
174
|
+
pushMs: cursor.pushMs,
|
|
175
|
+
pullMs: cursor.pullMs
|
|
176
|
+
});
|
|
177
|
+
return { pushed, pulled, cursor: 0 };
|
|
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?.bind(globalThis);
|
|
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 {
|
|
@@ -211,28 +309,44 @@ var WorkerProxy = class {
|
|
|
211
309
|
this.pending.clear();
|
|
212
310
|
}
|
|
213
311
|
};
|
|
214
|
-
function makePoller(findFn, callback) {
|
|
312
|
+
function makePoller(findFn, callback, onError) {
|
|
215
313
|
let active = true;
|
|
216
314
|
let lastJson = "";
|
|
315
|
+
let running = false;
|
|
316
|
+
let rerun = false;
|
|
217
317
|
const poll = async () => {
|
|
218
318
|
if (!active) return;
|
|
319
|
+
if (running) {
|
|
320
|
+
rerun = true;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
running = true;
|
|
219
324
|
try {
|
|
220
325
|
const docs = await findFn();
|
|
326
|
+
if (!active) return;
|
|
221
327
|
const json = JSON.stringify(docs);
|
|
222
328
|
if (json !== lastJson) {
|
|
223
329
|
lastJson = json;
|
|
224
330
|
callback(docs);
|
|
225
331
|
}
|
|
226
|
-
} catch {
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (active) onError?.(error);
|
|
334
|
+
} finally {
|
|
335
|
+
running = false;
|
|
336
|
+
if (active) {
|
|
337
|
+
if (rerun) {
|
|
338
|
+
rerun = false;
|
|
339
|
+
void poll();
|
|
340
|
+
} else setTimeout(poll, 300);
|
|
341
|
+
}
|
|
227
342
|
}
|
|
228
|
-
if (active) setTimeout(poll, 300);
|
|
229
343
|
};
|
|
230
344
|
poll();
|
|
231
345
|
return () => {
|
|
232
346
|
active = false;
|
|
233
347
|
};
|
|
234
348
|
}
|
|
235
|
-
async function createBrowserDB(dbName, config) {
|
|
349
|
+
async function createBrowserDB(dbName, config, passphrase) {
|
|
236
350
|
const workerUrl = new URL("@taladb/web/worker/taladb.worker.js", import_meta.url);
|
|
237
351
|
const worker = new Worker(workerUrl, { type: "module", name: "taladb" });
|
|
238
352
|
const proxy = new WorkerProxy(worker);
|
|
@@ -240,7 +354,13 @@ async function createBrowserDB(dbName, config) {
|
|
|
240
354
|
proxy.abort(new Error(`taladb worker error: ${e.message ?? "unknown"}`));
|
|
241
355
|
};
|
|
242
356
|
const configJson = config !== void 0 ? JSON.stringify(config) : void 0;
|
|
243
|
-
|
|
357
|
+
try {
|
|
358
|
+
await proxy.send("init", { dbName, configJson, passphrase });
|
|
359
|
+
} catch (e) {
|
|
360
|
+
proxy.abort(e instanceof Error ? e : new Error(String(e)));
|
|
361
|
+
worker.terminate();
|
|
362
|
+
throw e;
|
|
363
|
+
}
|
|
244
364
|
const nudgeCallbacks = /* @__PURE__ */ new Set();
|
|
245
365
|
let channel = null;
|
|
246
366
|
if (typeof BroadcastChannel !== "undefined") {
|
|
@@ -292,8 +412,17 @@ async function createBrowserDB(dbName, config) {
|
|
|
292
412
|
collection: name,
|
|
293
413
|
filterJson: filter ? s(filter) : "null"
|
|
294
414
|
}),
|
|
415
|
+
aggregate: async (pipeline) => {
|
|
416
|
+
const json = await proxy.send("aggregate", {
|
|
417
|
+
collection: name,
|
|
418
|
+
pipelineJson: s(pipeline)
|
|
419
|
+
});
|
|
420
|
+
return JSON.parse(json);
|
|
421
|
+
},
|
|
295
422
|
createIndex: (field) => proxy.send("createIndex", { collection: name, field }),
|
|
296
423
|
dropIndex: (field) => proxy.send("dropIndex", { collection: name, field }),
|
|
424
|
+
createCompoundIndex: (fields) => proxy.send("createCompoundIndex", { collection: name, fieldsJson: JSON.stringify(fields) }),
|
|
425
|
+
dropCompoundIndex: (fields) => proxy.send("dropCompoundIndex", { collection: name, fieldsJson: JSON.stringify(fields) }),
|
|
297
426
|
createFtsIndex: (field) => proxy.send("createFtsIndex", { collection: name, field }),
|
|
298
427
|
dropFtsIndex: (field) => proxy.send("dropFtsIndex", { collection: name, field }),
|
|
299
428
|
createVectorIndex: (field, options) => {
|
|
@@ -324,12 +453,19 @@ async function createBrowserDB(dbName, config) {
|
|
|
324
453
|
});
|
|
325
454
|
return JSON.parse(json);
|
|
326
455
|
},
|
|
327
|
-
subscribe: (filter, callback) => {
|
|
456
|
+
subscribe: (filter, callback, onError) => {
|
|
328
457
|
let active = true;
|
|
329
458
|
let lastJson = "";
|
|
330
459
|
let timer = null;
|
|
460
|
+
let running = false;
|
|
461
|
+
let rerun = false;
|
|
331
462
|
const poll = async () => {
|
|
332
463
|
if (!active) return;
|
|
464
|
+
if (running) {
|
|
465
|
+
rerun = true;
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
running = true;
|
|
333
469
|
if (timer !== null) {
|
|
334
470
|
clearTimeout(timer);
|
|
335
471
|
timer = null;
|
|
@@ -339,13 +475,22 @@ async function createBrowserDB(dbName, config) {
|
|
|
339
475
|
collection: name,
|
|
340
476
|
filterJson: filter ? s(filter) : "null"
|
|
341
477
|
});
|
|
478
|
+
if (!active) return;
|
|
342
479
|
if (json !== lastJson) {
|
|
343
480
|
lastJson = json;
|
|
344
481
|
callback(JSON.parse(json));
|
|
345
482
|
}
|
|
346
|
-
} catch {
|
|
483
|
+
} catch (error) {
|
|
484
|
+
if (active) onError?.(error);
|
|
485
|
+
} finally {
|
|
486
|
+
running = false;
|
|
487
|
+
}
|
|
488
|
+
if (active) {
|
|
489
|
+
if (rerun) {
|
|
490
|
+
rerun = false;
|
|
491
|
+
void poll();
|
|
492
|
+
} else timer = setTimeout(poll, 300);
|
|
347
493
|
}
|
|
348
|
-
if (active) timer = setTimeout(poll, 300);
|
|
349
494
|
};
|
|
350
495
|
nudgeCallbacks.add(poll);
|
|
351
496
|
poll();
|
|
@@ -361,9 +506,11 @@ async function createBrowserDB(dbName, config) {
|
|
|
361
506
|
};
|
|
362
507
|
return opts ? applySchema(wrapped, opts) : wrapped;
|
|
363
508
|
}
|
|
364
|
-
|
|
509
|
+
const handle = {
|
|
365
510
|
collection: (name, opts) => wrapCollection(name, opts),
|
|
366
511
|
compact: () => proxy.send("compact"),
|
|
512
|
+
syncStatus: async () => JSON.parse(await proxy.send("syncStatus")),
|
|
513
|
+
flushSync: (timeoutMs = 5e3) => proxy.send("flushSync", { timeoutMs }),
|
|
367
514
|
close: async () => {
|
|
368
515
|
channel?.close();
|
|
369
516
|
try {
|
|
@@ -372,13 +519,22 @@ async function createBrowserDB(dbName, config) {
|
|
|
372
519
|
worker.terminate();
|
|
373
520
|
proxy.abort(new Error("taladb worker closed"));
|
|
374
521
|
}
|
|
375
|
-
}
|
|
522
|
+
},
|
|
523
|
+
// All engine work (export scan, LWW merge) runs inside the worker, off the
|
|
524
|
+
// main thread — a sync pass never blocks rendering, whatever its size.
|
|
525
|
+
exportChanges: (collections, sinceMs) => proxy.send("exportChangeset", { collectionsJson: JSON.stringify(collections), sinceMs }),
|
|
526
|
+
importChanges: (changeset) => proxy.send("importChangeset", { changesetJson: changeset }),
|
|
527
|
+
listCollectionNames: async () => JSON.parse(await proxy.send("listCollections")),
|
|
528
|
+
sync: (adapter, options) => runSync(handle, adapter, options)
|
|
376
529
|
};
|
|
530
|
+
return handle;
|
|
377
531
|
}
|
|
378
|
-
async function createNodeDB(dbName, config) {
|
|
379
|
-
const
|
|
532
|
+
async function createNodeDB(dbName, config, passphrase) {
|
|
533
|
+
const native = await import("@taladb/node");
|
|
534
|
+
const TalaDBNode = native.TalaDbNode ?? native.TalaDBNode;
|
|
535
|
+
if (!TalaDBNode) throw new Error("@taladb/node loaded but exports no TalaDbNode class \u2014 rebuild the native module");
|
|
380
536
|
const configJson = config !== void 0 ? JSON.stringify(config) : null;
|
|
381
|
-
const db = TalaDBNode.open(dbName, configJson);
|
|
537
|
+
const db = TalaDBNode.open(dbName, configJson, passphrase ?? null);
|
|
382
538
|
function wrapCollection(name, opts) {
|
|
383
539
|
const col = db.collection(name);
|
|
384
540
|
const wrapped = {
|
|
@@ -391,8 +547,11 @@ async function createNodeDB(dbName, config) {
|
|
|
391
547
|
deleteOne: async (filter) => col.deleteOneAsync ? col.deleteOneAsync(filter) : col.deleteOne(filter),
|
|
392
548
|
deleteMany: async (filter) => col.deleteManyAsync ? col.deleteManyAsync(filter) : col.deleteMany(filter),
|
|
393
549
|
count: async (filter) => col.count(filter ?? null),
|
|
550
|
+
aggregate: async (pipeline) => col.aggregate(pipeline),
|
|
394
551
|
createIndex: async (field) => col.createIndex(field),
|
|
395
552
|
dropIndex: async (field) => col.dropIndex(field),
|
|
553
|
+
createCompoundIndex: async (fields) => col.createCompoundIndex(fields),
|
|
554
|
+
dropCompoundIndex: async (fields) => col.dropCompoundIndex(fields),
|
|
396
555
|
createFtsIndex: async (field) => col.createFtsIndex(field),
|
|
397
556
|
dropFtsIndex: async (field) => col.dropFtsIndex(field),
|
|
398
557
|
createVectorIndex: async (field, options) => col.createVectorIndex(field, options.dimensions, options.metric ?? null, options.indexType ?? null, options.hnswM ?? null, options.hnswEfConstruction ?? null),
|
|
@@ -406,16 +565,21 @@ async function createNodeDB(dbName, config) {
|
|
|
406
565
|
const raw = await col.findNearest(field, vector, topK, filter ?? null);
|
|
407
566
|
return raw;
|
|
408
567
|
},
|
|
409
|
-
subscribe: (filter, callback) => makePoller(async () => col.find(filter ?? null), callback)
|
|
568
|
+
subscribe: (filter, callback, onError) => makePoller(async () => col.find(filter ?? null), callback, onError)
|
|
410
569
|
};
|
|
411
570
|
return opts ? applySchema(wrapped, opts) : wrapped;
|
|
412
571
|
}
|
|
413
|
-
|
|
572
|
+
const handle = {
|
|
414
573
|
collection: (name, opts) => wrapCollection(name, opts),
|
|
415
574
|
compact: async () => db.compact(),
|
|
416
575
|
// Releases the native file handle/lock (no-op on older .node binaries).
|
|
417
|
-
close: async () => db.close?.()
|
|
576
|
+
close: async () => db.close?.(),
|
|
577
|
+
exportChanges: async (collections, sinceMs) => db.exportChanges(sinceMs, collections),
|
|
578
|
+
importChanges: async (changeset) => db.importChanges(changeset),
|
|
579
|
+
listCollectionNames: async () => db.listCollectionNames(),
|
|
580
|
+
sync: (adapter, options) => runSync(handle, adapter, options)
|
|
418
581
|
};
|
|
582
|
+
return handle;
|
|
419
583
|
}
|
|
420
584
|
async function createNativeDB(_dbName) {
|
|
421
585
|
const maybeNative = globalThis.__TalaDB__;
|
|
@@ -436,8 +600,11 @@ async function createNativeDB(_dbName) {
|
|
|
436
600
|
deleteOne: async (filter) => native.deleteOne(name, filter),
|
|
437
601
|
deleteMany: async (filter) => native.deleteMany(name, filter),
|
|
438
602
|
count: async (filter) => native.count(name, filter ?? {}),
|
|
603
|
+
aggregate: async (pipeline) => native.aggregate(name, pipeline),
|
|
439
604
|
createIndex: async (field) => native.createIndex(name, field),
|
|
440
605
|
dropIndex: async (field) => native.dropIndex(name, field),
|
|
606
|
+
createCompoundIndex: async (fields) => native.createCompoundIndex(name, fields),
|
|
607
|
+
dropCompoundIndex: async (fields) => native.dropCompoundIndex(name, fields),
|
|
441
608
|
createFtsIndex: async (field) => native.createFtsIndex(name, field),
|
|
442
609
|
dropFtsIndex: async (field) => native.dropFtsIndex(name, field),
|
|
443
610
|
createVectorIndex: async (field, options) => {
|
|
@@ -457,17 +624,35 @@ async function createNativeDB(_dbName) {
|
|
|
457
624
|
const raw = native.findNearest(name, field, vector, topK, filter ?? null);
|
|
458
625
|
return raw;
|
|
459
626
|
},
|
|
460
|
-
subscribe: (filter, callback) => makePoller(async () => native.find(name, filter ?? {}), callback)
|
|
627
|
+
subscribe: (filter, callback, onError) => makePoller(async () => native.find(name, filter ?? {}), callback, onError)
|
|
461
628
|
};
|
|
462
629
|
return opts ? applySchema(wrapped, opts) : wrapped;
|
|
463
630
|
}
|
|
631
|
+
const syncSurface = typeof native.exportChanges === "function" && typeof native.importChanges === "function" && typeof native.listCollectionNames === "function" ? (() => {
|
|
632
|
+
const handle = {
|
|
633
|
+
collection: (name, opts) => wrapCollection(name, opts),
|
|
634
|
+
exportChanges: async (collections, sinceMs) => native.exportChanges(collections, sinceMs),
|
|
635
|
+
importChanges: async (changeset) => native.importChanges(changeset),
|
|
636
|
+
listCollectionNames: async () => native.listCollectionNames(),
|
|
637
|
+
sync: (adapter, options) => runSync(handle, adapter, options)
|
|
638
|
+
};
|
|
639
|
+
return {
|
|
640
|
+
exportChanges: handle.exportChanges,
|
|
641
|
+
importChanges: handle.importChanges,
|
|
642
|
+
sync: handle.sync
|
|
643
|
+
};
|
|
644
|
+
})() : unsupportedSync("react-native");
|
|
464
645
|
return {
|
|
465
646
|
collection: (name, opts) => wrapCollection(name, opts),
|
|
466
647
|
compact: async () => native.compact(),
|
|
467
|
-
close: async () => native.close()
|
|
648
|
+
close: async () => native.close(),
|
|
649
|
+
...syncSurface
|
|
468
650
|
};
|
|
469
651
|
}
|
|
470
652
|
async function openDB(dbName = "taladb.db", options) {
|
|
653
|
+
if (options?.passphrase !== void 0 && options.passphrase.length === 0) {
|
|
654
|
+
throw new Error("TalaDB encryption passphrase must not be empty");
|
|
655
|
+
}
|
|
471
656
|
let resolvedConfig;
|
|
472
657
|
if (options?.config !== void 0) {
|
|
473
658
|
validateConfig(options.config);
|
|
@@ -478,15 +663,19 @@ async function openDB(dbName = "taladb.db", options) {
|
|
|
478
663
|
const platform = detectPlatform();
|
|
479
664
|
switch (platform) {
|
|
480
665
|
case "browser":
|
|
481
|
-
return createBrowserDB(dbName, resolvedConfig);
|
|
666
|
+
return createBrowserDB(dbName, resolvedConfig, options?.passphrase);
|
|
482
667
|
case "react-native":
|
|
668
|
+
if (options?.passphrase !== void 0) {
|
|
669
|
+
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");
|
|
670
|
+
}
|
|
483
671
|
return createNativeDB(dbName);
|
|
484
672
|
case "node":
|
|
485
|
-
return createNodeDB(dbName, resolvedConfig);
|
|
673
|
+
return createNodeDB(dbName, resolvedConfig, options?.passphrase);
|
|
486
674
|
}
|
|
487
675
|
}
|
|
488
676
|
// Annotate the CommonJS export names for ESM import in node:
|
|
489
677
|
0 && (module.exports = {
|
|
678
|
+
HttpSyncAdapter,
|
|
490
679
|
TalaDbValidationError,
|
|
491
680
|
openDB
|
|
492
681
|
});
|