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