taladb 0.9.1 → 0.9.2
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 +225 -34
- package/dist/index.d.mts +219 -1
- package/dist/index.d.ts +219 -1
- package/dist/index.js +228 -35
- package/dist/index.mjs +225 -34
- package/dist/index.react-native.mjs +225 -34
- package/package.json +1 -1
package/dist/index.browser.mjs
CHANGED
|
@@ -48,7 +48,7 @@ async function writeCursor(cursorCol, target, cursor) {
|
|
|
48
48
|
await cursorCol.insert({ target, ...cursor });
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
async function runSync(handle, adapter, options) {
|
|
51
|
+
async function runSync(handle, adapter, options, syncSchemas = {}) {
|
|
52
52
|
const direction = options.direction ?? "both";
|
|
53
53
|
const target = options.target ?? "default";
|
|
54
54
|
const doPush = direction === "push" || direction === "both";
|
|
@@ -63,11 +63,25 @@ async function runSync(handle, adapter, options) {
|
|
|
63
63
|
const cursorCol = handle.collection(CURSOR_COLLECTION);
|
|
64
64
|
const cursor = await readCursor(cursorCol, target);
|
|
65
65
|
const local = doPush ? await handle.exportChanges(collections, 0) : "[]";
|
|
66
|
+
const scopedSchemas = {};
|
|
67
|
+
for (const c of collections) {
|
|
68
|
+
if (syncSchemas[c]) scopedSchemas[c] = syncSchemas[c];
|
|
69
|
+
}
|
|
70
|
+
const useValidated = handle.importChangesValidated && Object.keys(scopedSchemas).length > 0;
|
|
66
71
|
let pulled = 0;
|
|
72
|
+
let skipped = 0;
|
|
73
|
+
let quarantined = 0;
|
|
67
74
|
if (doPull) {
|
|
68
75
|
const remote = await adapter.pull(0);
|
|
69
76
|
if (remote && remote !== "[]") {
|
|
70
|
-
|
|
77
|
+
if (useValidated) {
|
|
78
|
+
const report = await handle.importChangesValidated(remote, JSON.stringify(scopedSchemas));
|
|
79
|
+
pulled = report.applied;
|
|
80
|
+
skipped = report.skipped;
|
|
81
|
+
quarantined = report.quarantined;
|
|
82
|
+
} else {
|
|
83
|
+
pulled = await handle.importChanges(remote);
|
|
84
|
+
}
|
|
71
85
|
}
|
|
72
86
|
}
|
|
73
87
|
let pushed = 0;
|
|
@@ -79,7 +93,7 @@ async function runSync(handle, adapter, options) {
|
|
|
79
93
|
pushMs: cursor.pushMs,
|
|
80
94
|
pullMs: cursor.pullMs
|
|
81
95
|
});
|
|
82
|
-
return { pushed, pulled, cursor: 0 };
|
|
96
|
+
return { pushed, pulled, skipped, quarantined, cursor: 0 };
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
// src/http-adapter.ts
|
|
@@ -128,9 +142,34 @@ var TalaDbValidationError = class extends Error {
|
|
|
128
142
|
this.name = "TalaDbValidationError";
|
|
129
143
|
}
|
|
130
144
|
};
|
|
145
|
+
function deepEqual(a, b) {
|
|
146
|
+
if (a === b) return true;
|
|
147
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
148
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
149
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
150
|
+
return a.every((x, i) => deepEqual(x, b[i]));
|
|
151
|
+
}
|
|
152
|
+
const ao = a;
|
|
153
|
+
const bo = b;
|
|
154
|
+
const keys = Object.keys(ao);
|
|
155
|
+
if (keys.length !== Object.keys(bo).length) return false;
|
|
156
|
+
return keys.every(
|
|
157
|
+
(k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k])
|
|
158
|
+
);
|
|
159
|
+
}
|
|
131
160
|
function applySchema(col, options) {
|
|
132
|
-
const { schema, validateOnRead = false } = options;
|
|
133
|
-
|
|
161
|
+
const { schema, validateOnRead = false, migrateDocument, syncSchema, persistMigrations = false } = options;
|
|
162
|
+
const targetVersion = syncSchema?.version ?? 0;
|
|
163
|
+
if (migrateDocument && targetVersion < 1) {
|
|
164
|
+
throw new Error("CollectionOptions.migrateDocument requires syncSchema.version (the migration target)");
|
|
165
|
+
}
|
|
166
|
+
if (syncSchema && targetVersion < 1 && (syncSchema.renames || syncSchema.defaults)) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
"CollectionOptions.syncSchema.renames/defaults require syncSchema.version >= 1 \u2014 without a version the import migration step never runs and documents missing the renamed/defaulted fields are quarantined instead of upgraded"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const stampVersion = targetVersion > 0;
|
|
172
|
+
if (!schema && !migrateDocument && !stampVersion) return col;
|
|
134
173
|
function parseWrite(doc, label) {
|
|
135
174
|
try {
|
|
136
175
|
return schema.parse(doc);
|
|
@@ -138,31 +177,96 @@ function applySchema(col, options) {
|
|
|
138
177
|
throw new TalaDbValidationError(err, label);
|
|
139
178
|
}
|
|
140
179
|
}
|
|
141
|
-
function
|
|
180
|
+
function stamp(doc) {
|
|
181
|
+
if (!stampVersion || doc._v !== void 0) return doc;
|
|
182
|
+
return { ...doc, _v: targetVersion };
|
|
183
|
+
}
|
|
184
|
+
function diffUpdate(original, migrated) {
|
|
185
|
+
const $set = {};
|
|
186
|
+
const $unset = {};
|
|
187
|
+
for (const k of Object.keys(migrated)) {
|
|
188
|
+
if (k === "_id") continue;
|
|
189
|
+
if (!deepEqual(migrated[k], original[k])) $set[k] = migrated[k];
|
|
190
|
+
}
|
|
191
|
+
for (const k of Object.keys(original)) {
|
|
192
|
+
if (k !== "_id" && !(k in migrated)) $unset[k] = true;
|
|
193
|
+
}
|
|
194
|
+
const update = {};
|
|
195
|
+
if (Object.keys($set).length) update.$set = $set;
|
|
196
|
+
if (Object.keys($unset).length) update.$unset = $unset;
|
|
197
|
+
return Object.keys(update).length ? update : null;
|
|
198
|
+
}
|
|
199
|
+
function migrateRead(doc) {
|
|
200
|
+
if (!migrateDocument) return doc;
|
|
201
|
+
const fromVersion = typeof doc._v === "number" ? doc._v : 0;
|
|
202
|
+
if (fromVersion >= targetVersion) return doc;
|
|
203
|
+
return { ...migrateDocument(doc, fromVersion), _v: targetVersion };
|
|
204
|
+
}
|
|
205
|
+
function validateRead(doc) {
|
|
206
|
+
if (!validateOnRead || !schema) return doc;
|
|
142
207
|
try {
|
|
143
208
|
return schema.parse(doc);
|
|
144
209
|
} catch (err) {
|
|
145
210
|
throw new TalaDbValidationError(err, "read");
|
|
146
211
|
}
|
|
147
212
|
}
|
|
213
|
+
async function persistAll(originals, migrated) {
|
|
214
|
+
if (!persistMigrations) return;
|
|
215
|
+
for (let i = 0; i < originals.length; i++) {
|
|
216
|
+
const original = originals[i];
|
|
217
|
+
if (migrated[i] === original || typeof original._id !== "string") continue;
|
|
218
|
+
const update = diffUpdate(original, migrated[i]);
|
|
219
|
+
if (!update) continue;
|
|
220
|
+
try {
|
|
221
|
+
await col.updateOne({ _id: original._id }, update);
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const wrapReads = Boolean(migrateDocument) || validateOnRead && Boolean(schema);
|
|
227
|
+
const wrapWrites = Boolean(schema) || stampVersion;
|
|
148
228
|
return {
|
|
149
229
|
...col,
|
|
150
|
-
insert: async (doc) => {
|
|
151
|
-
parseWrite(doc, "insert");
|
|
152
|
-
return col.insert(doc);
|
|
153
|
-
},
|
|
154
|
-
insertMany: async (docs) => {
|
|
155
|
-
docs.forEach((doc, i) => parseWrite(doc, `insertMany[${i}]`));
|
|
156
|
-
return col.insertMany(docs);
|
|
157
|
-
},
|
|
158
|
-
find:
|
|
230
|
+
insert: wrapWrites ? async (doc) => {
|
|
231
|
+
if (schema) parseWrite(doc, "insert");
|
|
232
|
+
return col.insert(stamp(doc));
|
|
233
|
+
} : col.insert.bind(col),
|
|
234
|
+
insertMany: wrapWrites ? async (docs) => {
|
|
235
|
+
if (schema) docs.forEach((doc, i) => parseWrite(doc, `insertMany[${i}]`));
|
|
236
|
+
return col.insertMany(docs.map(stamp));
|
|
237
|
+
} : col.insertMany.bind(col),
|
|
238
|
+
find: wrapReads ? async (filter) => {
|
|
159
239
|
const docs = await col.find(filter);
|
|
160
|
-
|
|
240
|
+
const migrated = docs.map(migrateRead);
|
|
241
|
+
await persistAll(docs, migrated);
|
|
242
|
+
return migrated.map(validateRead);
|
|
161
243
|
} : col.find.bind(col),
|
|
162
|
-
findOne:
|
|
244
|
+
findOne: wrapReads ? async (filter) => {
|
|
163
245
|
const doc = await col.findOne(filter);
|
|
164
|
-
|
|
165
|
-
|
|
246
|
+
if (doc === null) return null;
|
|
247
|
+
const migrated = migrateRead(doc);
|
|
248
|
+
await persistAll([doc], [migrated]);
|
|
249
|
+
return validateRead(migrated);
|
|
250
|
+
} : col.findOne.bind(col),
|
|
251
|
+
// Live queries feed every @taladb/react hook (useFind, useFindOne,
|
|
252
|
+
// useQueries). Leaving them unwrapped meant React components received the
|
|
253
|
+
// un-migrated shape while a direct find() returned the migrated one.
|
|
254
|
+
subscribe: wrapReads ? (filter, callback, onError) => col.subscribe(
|
|
255
|
+
filter,
|
|
256
|
+
(docs) => {
|
|
257
|
+
const migrated = docs.map(migrateRead);
|
|
258
|
+
let out;
|
|
259
|
+
try {
|
|
260
|
+
out = migrated.map(validateRead);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
onError?.(err);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
callback(out);
|
|
266
|
+
void persistAll(docs, migrated);
|
|
267
|
+
},
|
|
268
|
+
onError
|
|
269
|
+
) : col.subscribe.bind(col)
|
|
166
270
|
};
|
|
167
271
|
}
|
|
168
272
|
function detectPlatform() {
|
|
@@ -250,7 +354,7 @@ function makePoller(findFn, callback, onError) {
|
|
|
250
354
|
active = false;
|
|
251
355
|
};
|
|
252
356
|
}
|
|
253
|
-
async function createBrowserDB(dbName, config, passphrase) {
|
|
357
|
+
async function createBrowserDB(dbName, config, passphrase, migrations) {
|
|
254
358
|
const workerUrl = new URL("@taladb/web/worker/taladb.worker.js", import.meta.url);
|
|
255
359
|
const worker = new Worker(workerUrl, { type: "module", name: "taladb" });
|
|
256
360
|
const proxy = new WorkerProxy(worker);
|
|
@@ -275,7 +379,9 @@ async function createBrowserDB(dbName, config, passphrase) {
|
|
|
275
379
|
}
|
|
276
380
|
};
|
|
277
381
|
}
|
|
382
|
+
const syncSchemas = {};
|
|
278
383
|
function wrapCollection(name, opts) {
|
|
384
|
+
if (opts?.syncSchema) syncSchemas[name] = opts.syncSchema;
|
|
279
385
|
const s = JSON.stringify;
|
|
280
386
|
const wrapped = {
|
|
281
387
|
insert: (doc) => proxy.send("insert", { collection: name, docJson: s(doc) }),
|
|
@@ -413,6 +519,9 @@ async function createBrowserDB(dbName, config, passphrase) {
|
|
|
413
519
|
const handle = {
|
|
414
520
|
collection: (name, opts) => wrapCollection(name, opts),
|
|
415
521
|
compact: () => proxy.send("compact"),
|
|
522
|
+
flush: async () => {
|
|
523
|
+
await proxy.send("flush");
|
|
524
|
+
},
|
|
416
525
|
syncStatus: async () => JSON.parse(await proxy.send("syncStatus")),
|
|
417
526
|
flushSync: (timeoutMs = 5e3) => proxy.send("flushSync", { timeoutMs }),
|
|
418
527
|
close: async () => {
|
|
@@ -428,18 +537,32 @@ async function createBrowserDB(dbName, config, passphrase) {
|
|
|
428
537
|
// main thread — a sync pass never blocks rendering, whatever its size.
|
|
429
538
|
exportChanges: (collections, sinceMs) => proxy.send("exportChangeset", { collectionsJson: JSON.stringify(collections), sinceMs }),
|
|
430
539
|
importChanges: (changeset) => proxy.send("importChangeset", { changesetJson: changeset }),
|
|
540
|
+
importChangesValidated: async (changeset, schemasJson) => JSON.parse(await proxy.send("importChangesetValidated", { changesetJson: changeset, schemasJson })),
|
|
431
541
|
listCollectionNames: async () => JSON.parse(await proxy.send("listCollections")),
|
|
432
|
-
|
|
542
|
+
quarantined: async (collection) => JSON.parse(await proxy.send("quarantined", { collection })),
|
|
543
|
+
sync: (adapter, options) => runSync(handle, adapter, options, syncSchemas)
|
|
433
544
|
};
|
|
545
|
+
if (migrations?.length) {
|
|
546
|
+
await runMigrations(
|
|
547
|
+
handle,
|
|
548
|
+
async () => proxy.send("userVersion"),
|
|
549
|
+
async (v) => {
|
|
550
|
+
await proxy.send("setUserVersion", { version: v });
|
|
551
|
+
},
|
|
552
|
+
migrations
|
|
553
|
+
);
|
|
554
|
+
}
|
|
434
555
|
return handle;
|
|
435
556
|
}
|
|
436
|
-
async function createNodeDB(dbName, config, passphrase) {
|
|
557
|
+
async function createNodeDB(dbName, config, passphrase, migrations) {
|
|
437
558
|
const native = await import("./node-A4LKRSW5.mjs");
|
|
438
559
|
const TalaDBNode = native.TalaDbNode ?? native.TalaDBNode;
|
|
439
560
|
if (!TalaDBNode) throw new Error("@taladb/node loaded but exports no TalaDbNode class \u2014 rebuild the native module");
|
|
440
561
|
const configJson = config !== void 0 ? JSON.stringify(config) : null;
|
|
441
562
|
const db = TalaDBNode.open(dbName, configJson, passphrase ?? null);
|
|
563
|
+
const syncSchemas = {};
|
|
442
564
|
function wrapCollection(name, opts) {
|
|
565
|
+
if (opts?.syncSchema) syncSchemas[name] = opts.syncSchema;
|
|
443
566
|
const col = db.collection(name);
|
|
444
567
|
const wrapped = {
|
|
445
568
|
insert: async (doc) => col.insertAsync ? col.insertAsync(doc) : col.insert(doc),
|
|
@@ -478,14 +601,32 @@ async function createNodeDB(dbName, config, passphrase) {
|
|
|
478
601
|
compact: async () => db.compact(),
|
|
479
602
|
// Releases the native file handle/lock (no-op on older .node binaries).
|
|
480
603
|
close: async () => db.close?.(),
|
|
604
|
+
flush: db.flush ? async () => {
|
|
605
|
+
db.flush();
|
|
606
|
+
} : void 0,
|
|
481
607
|
exportChanges: async (collections, sinceMs) => db.exportChanges(sinceMs, collections),
|
|
482
608
|
importChanges: async (changeset) => db.importChanges(changeset),
|
|
609
|
+
// Feature-detected: only present when the loaded .node binary supports it,
|
|
610
|
+
// so older prebuilt binaries fall back to plain importChanges.
|
|
611
|
+
importChangesValidated: db.importChangesValidated ? async (changeset, schemasJson) => db.importChangesValidated(changeset, schemasJson) : void 0,
|
|
483
612
|
listCollectionNames: async () => db.listCollectionNames(),
|
|
484
|
-
|
|
613
|
+
quarantined: async (collection) => db.quarantined ? db.quarantined(collection) : [],
|
|
614
|
+
sync: (adapter, options) => runSync(handle, adapter, options, syncSchemas)
|
|
485
615
|
};
|
|
616
|
+
if (migrations?.length) {
|
|
617
|
+
if (typeof db.userVersion !== "function" || typeof db.setUserVersion !== "function") {
|
|
618
|
+
throw new Error("openDB({ migrations }) requires @taladb/node \u2265 0.9.2 \u2014 rebuild the native module");
|
|
619
|
+
}
|
|
620
|
+
await runMigrations(
|
|
621
|
+
handle,
|
|
622
|
+
async () => db.userVersion(),
|
|
623
|
+
async (v) => db.setUserVersion(v),
|
|
624
|
+
migrations
|
|
625
|
+
);
|
|
626
|
+
}
|
|
486
627
|
return handle;
|
|
487
628
|
}
|
|
488
|
-
async function createNativeDB(_dbName) {
|
|
629
|
+
async function createNativeDB(_dbName, migrations) {
|
|
489
630
|
const maybeNative = globalThis.__TalaDB__;
|
|
490
631
|
if (!maybeNative) {
|
|
491
632
|
throw new Error(
|
|
@@ -493,7 +634,9 @@ async function createNativeDB(_dbName) {
|
|
|
493
634
|
);
|
|
494
635
|
}
|
|
495
636
|
const native = maybeNative;
|
|
637
|
+
const syncSchemas = {};
|
|
496
638
|
function wrapCollection(name, opts) {
|
|
639
|
+
if (opts?.syncSchema) syncSchemas[name] = opts.syncSchema;
|
|
497
640
|
const wrapped = {
|
|
498
641
|
insert: async (doc) => native.insert(name, doc),
|
|
499
642
|
insertMany: async (docs) => native.insertMany(name, docs),
|
|
@@ -533,25 +676,64 @@ async function createNativeDB(_dbName) {
|
|
|
533
676
|
return opts ? applySchema(wrapped, opts) : wrapped;
|
|
534
677
|
}
|
|
535
678
|
const syncSurface = typeof native.exportChanges === "function" && typeof native.importChanges === "function" && typeof native.listCollectionNames === "function" ? (() => {
|
|
536
|
-
const
|
|
679
|
+
const handle2 = {
|
|
537
680
|
collection: (name, opts) => wrapCollection(name, opts),
|
|
538
681
|
exportChanges: async (collections, sinceMs) => native.exportChanges(collections, sinceMs),
|
|
539
682
|
importChanges: async (changeset) => native.importChanges(changeset),
|
|
683
|
+
// Feature-detected: present on 0.9.2+ JSI HostObjects; when absent,
|
|
684
|
+
// runSync falls back to unvalidated importChanges.
|
|
685
|
+
importChangesValidated: native.importChangesValidated ? async (changeset, schemasJson) => native.importChangesValidated(changeset, schemasJson) : void 0,
|
|
540
686
|
listCollectionNames: async () => native.listCollectionNames(),
|
|
541
|
-
sync: (adapter, options) => runSync(
|
|
687
|
+
sync: (adapter, options) => runSync(handle2, adapter, options, syncSchemas)
|
|
542
688
|
};
|
|
543
689
|
return {
|
|
544
|
-
exportChanges:
|
|
545
|
-
importChanges:
|
|
546
|
-
sync:
|
|
690
|
+
exportChanges: handle2.exportChanges,
|
|
691
|
+
importChanges: handle2.importChanges,
|
|
692
|
+
sync: handle2.sync
|
|
547
693
|
};
|
|
548
694
|
})() : unsupportedSync("react-native");
|
|
549
|
-
|
|
695
|
+
const handle = {
|
|
550
696
|
collection: (name, opts) => wrapCollection(name, opts),
|
|
551
697
|
compact: async () => native.compact(),
|
|
552
698
|
close: async () => native.close(),
|
|
699
|
+
flush: native.flush ? async () => {
|
|
700
|
+
native.flush();
|
|
701
|
+
} : void 0,
|
|
702
|
+
quarantined: native.quarantined ? async (collection) => native.quarantined(collection) : void 0,
|
|
553
703
|
...syncSurface
|
|
554
704
|
};
|
|
705
|
+
if (migrations?.length) {
|
|
706
|
+
if (typeof native.userVersion !== "function" || typeof native.setUserVersion !== "function") {
|
|
707
|
+
throw new Error(
|
|
708
|
+
"openDB({ migrations }) is not available on this @taladb/react-native binary yet (the JSI HostObject does not expose userVersion/setUserVersion). Update the native module."
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
await runMigrations(
|
|
712
|
+
handle,
|
|
713
|
+
async () => native.userVersion(),
|
|
714
|
+
async (v) => native.setUserVersion(v),
|
|
715
|
+
migrations
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
return handle;
|
|
719
|
+
}
|
|
720
|
+
async function runMigrations(db, getVersion, setVersion, migrations) {
|
|
721
|
+
const sorted = [...migrations].sort((a, b) => a.version - b.version);
|
|
722
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
723
|
+
const v = sorted[i].version;
|
|
724
|
+
if (!Number.isInteger(v) || v < 1) {
|
|
725
|
+
throw new Error(`TalaDB migration version must be a positive integer, got ${v}`);
|
|
726
|
+
}
|
|
727
|
+
if (i > 0 && v === sorted[i - 1].version) {
|
|
728
|
+
throw new Error(`TalaDB duplicate migration version ${v}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const current = await getVersion();
|
|
732
|
+
for (const m of sorted) {
|
|
733
|
+
if (m.version <= current) continue;
|
|
734
|
+
await m.up(db);
|
|
735
|
+
await setVersion(m.version);
|
|
736
|
+
}
|
|
555
737
|
}
|
|
556
738
|
async function openDB(dbName = "taladb.db", options) {
|
|
557
739
|
if (options?.passphrase !== void 0 && options.passphrase.length === 0) {
|
|
@@ -564,21 +746,30 @@ async function openDB(dbName = "taladb.db", options) {
|
|
|
564
746
|
} else {
|
|
565
747
|
resolvedConfig = await loadConfig(options?.configPath);
|
|
566
748
|
}
|
|
749
|
+
if (options?.durability) {
|
|
750
|
+
resolvedConfig = {
|
|
751
|
+
...resolvedConfig,
|
|
752
|
+
durability: { ...resolvedConfig?.durability, ...options.durability }
|
|
753
|
+
};
|
|
754
|
+
}
|
|
567
755
|
const platform = detectPlatform();
|
|
756
|
+
const migrations = options?.migrations;
|
|
568
757
|
switch (platform) {
|
|
569
758
|
case "browser":
|
|
570
|
-
return createBrowserDB(dbName, resolvedConfig, options?.passphrase);
|
|
759
|
+
return createBrowserDB(dbName, resolvedConfig, options?.passphrase, migrations);
|
|
571
760
|
case "react-native":
|
|
572
761
|
if (options?.passphrase !== void 0) {
|
|
573
762
|
throw new Error("On React Native, pass the passphrase in the config JSON to TalaDBModule.initialize(); refusing to assume the already-open native database is encrypted");
|
|
574
763
|
}
|
|
575
|
-
return createNativeDB(dbName);
|
|
764
|
+
return createNativeDB(dbName, migrations);
|
|
576
765
|
case "node":
|
|
577
|
-
return createNodeDB(dbName, resolvedConfig, options?.passphrase);
|
|
766
|
+
return createNodeDB(dbName, resolvedConfig, options?.passphrase, migrations);
|
|
578
767
|
}
|
|
579
768
|
}
|
|
580
769
|
export {
|
|
581
770
|
HttpSyncAdapter,
|
|
582
771
|
TalaDbValidationError,
|
|
583
|
-
|
|
772
|
+
applySchema,
|
|
773
|
+
openDB,
|
|
774
|
+
runMigrations
|
|
584
775
|
};
|
package/dist/index.d.mts
CHANGED
|
@@ -96,6 +96,57 @@ type Update<T extends Document = Document> = {
|
|
|
96
96
|
interface Schema<T> {
|
|
97
97
|
parse(data: unknown): T;
|
|
98
98
|
}
|
|
99
|
+
/** Primitive field type for a {@link SyncSchema}. `'any'` requires presence
|
|
100
|
+
* without constraining the value's type. */
|
|
101
|
+
type SyncFieldType = 'bool' | 'int' | 'float' | 'str' | 'bytes' | 'array' | 'object' | 'any';
|
|
102
|
+
/**
|
|
103
|
+
* A tolerant, structural schema applied to documents **arriving via sync**
|
|
104
|
+
* (`db.sync()` pull). Distinct from {@link Schema} (Zod/Valibot), which is
|
|
105
|
+
* strict and runs on the *local* `insert` path: sync import is the boundary you
|
|
106
|
+
* don't control, so it validates structurally and never hard-rejects.
|
|
107
|
+
*
|
|
108
|
+
* On import, per document:
|
|
109
|
+
* - `_v` **below** `version` → upgraded in place (missing `defaults` filled,
|
|
110
|
+
* `_v` stamped) — additive-only migration.
|
|
111
|
+
* - `_v` **above** `version` → accepted untouched (the peer is ahead).
|
|
112
|
+
* - a missing/`null` `required` field or a `types` mismatch → **quarantined**
|
|
113
|
+
* (set aside, recoverable via {@link TalaDB.quarantined}), never dropped and
|
|
114
|
+
* never aborting the batch.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* const users = db.collection<User>('users', {
|
|
118
|
+
* schema: User, // strict, on insert
|
|
119
|
+
* syncSchema: { // tolerant, on import
|
|
120
|
+
* version: 1,
|
|
121
|
+
* required: ['name'],
|
|
122
|
+
* types: { name: 'str', age: 'int' },
|
|
123
|
+
* defaults: { age: 0 },
|
|
124
|
+
* },
|
|
125
|
+
* });
|
|
126
|
+
*/
|
|
127
|
+
interface SyncSchema {
|
|
128
|
+
/**
|
|
129
|
+
* Current document shape version. Omit or `0` to disable the migration step
|
|
130
|
+
* entirely — in which case {@link renames} and {@link defaults} never run, so
|
|
131
|
+
* declaring either without a `version` is rejected at `db.collection()` rather
|
|
132
|
+
* than silently quarantining the documents they were meant to upgrade.
|
|
133
|
+
*/
|
|
134
|
+
version?: number;
|
|
135
|
+
/** Fields that must be present and non-null, or the document is quarantined. */
|
|
136
|
+
required?: string[];
|
|
137
|
+
/** Expected primitive type per field. Fields absent here accept any type. */
|
|
138
|
+
types?: Record<string, SyncFieldType>;
|
|
139
|
+
/** Values applied to missing fields when upgrading a below-`version` document. */
|
|
140
|
+
defaults?: Record<string, Value>;
|
|
141
|
+
/**
|
|
142
|
+
* Field renames applied when upgrading a below-`version` document, as
|
|
143
|
+
* `{ oldName: newName }`. If the old field is present and the new one absent,
|
|
144
|
+
* the value moves. Applied before {@link defaults}. Structural (runs in the
|
|
145
|
+
* engine at import) — for renames that need computation, use
|
|
146
|
+
* {@link CollectionOptions.migrateDocument}.
|
|
147
|
+
*/
|
|
148
|
+
renames?: Record<string, string>;
|
|
149
|
+
}
|
|
99
150
|
/** Options passed to `db.collection()`. */
|
|
100
151
|
interface CollectionOptions<T extends Document = Document> {
|
|
101
152
|
/**
|
|
@@ -113,6 +164,53 @@ interface CollectionOptions<T extends Document = Document> {
|
|
|
113
164
|
* Defaults to `false`.
|
|
114
165
|
*/
|
|
115
166
|
validateOnRead?: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* Tolerant structural schema applied to documents arriving via `db.sync()`.
|
|
169
|
+
* See {@link SyncSchema}. Enables validate-on-import ("validate, never cast")
|
|
170
|
+
* in the core sync path, with `_v` migration and quarantine of bad shapes.
|
|
171
|
+
* Wired on browser (OPFS worker), Node.js, and React Native; a binding whose
|
|
172
|
+
* native module predates 0.9.2 falls back to unvalidated import.
|
|
173
|
+
*
|
|
174
|
+
* Declaring a `version` also makes locally-inserted documents carry that `_v`,
|
|
175
|
+
* so they are never mistaken for legacy documents on read.
|
|
176
|
+
*/
|
|
177
|
+
syncSchema?: SyncSchema;
|
|
178
|
+
/**
|
|
179
|
+
* Lazy, read-time document migration — the arbitrary-JS complement to the
|
|
180
|
+
* structural {@link SyncSchema}. When set, every document returned by `find`
|
|
181
|
+
* / `findOne` whose `_v` is **below** `syncSchema.version` is passed through
|
|
182
|
+
* `migrateDocument(doc, fromVersion)` and stamped to the current version
|
|
183
|
+
* before you see it, so application code always reads the current shape even
|
|
184
|
+
* for documents that predate the schema (renames, computed/derived fields,
|
|
185
|
+
* splits/merges). Runs on **every runtime** (it's a pure read transform in
|
|
186
|
+
* the client — no binding support needed).
|
|
187
|
+
*
|
|
188
|
+
* Requires `syncSchema.version` (the migration target). The transform is
|
|
189
|
+
* applied to the returned value only; it is not persisted back to storage —
|
|
190
|
+
* pair with `openDB({ migrations })` or a `syncSchema` rename to rewrite
|
|
191
|
+
* stored documents eagerly. Must be pure and deterministic.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* const users = db.collection<User>('users', {
|
|
195
|
+
* syncSchema: { version: 2 },
|
|
196
|
+
* migrateDocument: (doc, from) =>
|
|
197
|
+
* from < 2 ? { ...doc, fullName: `${doc.first} ${doc.last}` } : doc,
|
|
198
|
+
* });
|
|
199
|
+
*/
|
|
200
|
+
migrateDocument?: (doc: T, fromVersion: number) => T;
|
|
201
|
+
/**
|
|
202
|
+
* When `true`, a document upgraded by {@link migrateDocument} on read is
|
|
203
|
+
* **written back** to storage (a best-effort `updateOne` computing the
|
|
204
|
+
* `$set`/`$unset` diff) so the migration becomes permanent — after which
|
|
205
|
+
* filters and indexes on the new shape match it. Default `false` (the
|
|
206
|
+
* migrated shape is returned but not persisted).
|
|
207
|
+
*
|
|
208
|
+
* Trade-offs: reads that encounter un-migrated documents now issue writes
|
|
209
|
+
* (which fire live-query and sync-hook notifications like any other write);
|
|
210
|
+
* a failed write is swallowed and simply retried on the next read. For a
|
|
211
|
+
* one-shot eager rewrite instead, prefer `openDB({ migrations })`.
|
|
212
|
+
*/
|
|
213
|
+
persistMigrations?: boolean;
|
|
116
214
|
}
|
|
117
215
|
/** A single MongoDB-style aggregation stage. */
|
|
118
216
|
type AggregateStage<T extends Document = Document> = {
|
|
@@ -296,9 +394,30 @@ interface SyncResult {
|
|
|
296
394
|
pushed: number;
|
|
297
395
|
/** Number of documents changed locally by the pulled remote changeset. */
|
|
298
396
|
pulled: number;
|
|
397
|
+
/**
|
|
398
|
+
* Documents in the pulled changeset skipped by an import validator (a
|
|
399
|
+
* collection this client does not model). Always `0` when no `syncSchema`
|
|
400
|
+
* applied to the pass.
|
|
401
|
+
*/
|
|
402
|
+
skipped?: number;
|
|
403
|
+
/**
|
|
404
|
+
* Documents in the pulled changeset set aside by an import validator because
|
|
405
|
+
* they failed structural validation. Recoverable via {@link TalaDB.quarantined}.
|
|
406
|
+
* Always `0` when no `syncSchema` applied to the pass.
|
|
407
|
+
*/
|
|
408
|
+
quarantined?: number;
|
|
299
409
|
/** Active sync cursor. Currently `0` because timestamp adapters replay safely. */
|
|
300
410
|
cursor: number;
|
|
301
411
|
}
|
|
412
|
+
/** A document set aside during a validated sync import, with its rejection reason. */
|
|
413
|
+
interface QuarantinedDocument<T extends Document = Document> {
|
|
414
|
+
/** The rejected document, retained verbatim. */
|
|
415
|
+
document: T;
|
|
416
|
+
/** Human-readable reason the document was quarantined. */
|
|
417
|
+
reason: string;
|
|
418
|
+
/** The `changed_at` (ms epoch) the rejected change carried. */
|
|
419
|
+
changedAt: number;
|
|
420
|
+
}
|
|
302
421
|
interface TalaDB {
|
|
303
422
|
collection<T extends Document = Document>(name: string, options?: CollectionOptions<T>): Collection<T>;
|
|
304
423
|
/**
|
|
@@ -334,6 +453,20 @@ interface TalaDB {
|
|
|
334
453
|
* await db.compact();
|
|
335
454
|
*/
|
|
336
455
|
compact(): Promise<void>;
|
|
456
|
+
/**
|
|
457
|
+
* Return the documents set aside in `collection`'s quarantine table by a
|
|
458
|
+
* validated sync import (see {@link SyncSchema}). Empty when nothing was
|
|
459
|
+
* quarantined. Wired on browser and Node.js; resolves to `[]` on runtimes
|
|
460
|
+
* without support.
|
|
461
|
+
*/
|
|
462
|
+
quarantined?<T extends Document = Document>(collection: string): Promise<QuarantinedDocument<T>[]>;
|
|
463
|
+
/**
|
|
464
|
+
* Force any batched (eventual-durability) writes to durable storage, and on
|
|
465
|
+
* the browser also write the IndexedDB fallback snapshot immediately. A
|
|
466
|
+
* no-op under the default `flush_every_write: true` durability. Use for
|
|
467
|
+
* "save now" moments (before checkout, on `visibilitychange`).
|
|
468
|
+
*/
|
|
469
|
+
flush?(): Promise<void>;
|
|
337
470
|
/** Browser HTTP-push queue health, when supported by the active binding. */
|
|
338
471
|
syncStatus?(): Promise<{
|
|
339
472
|
pending: number;
|
|
@@ -377,10 +510,28 @@ interface SyncConfig {
|
|
|
377
510
|
*/
|
|
378
511
|
exclude_fields?: string[];
|
|
379
512
|
}
|
|
513
|
+
/** Storage durability settings. */
|
|
514
|
+
interface DurabilityConfig {
|
|
515
|
+
/**
|
|
516
|
+
* When `true` (default), every write commit is fsync'd immediately — a crash
|
|
517
|
+
* never loses an acknowledged write. When `false`, commits are batched for
|
|
518
|
+
* higher write throughput; call `db.flush()` to force a durable sync. Applies
|
|
519
|
+
* to Node (file) and browser OPFS storage; in-memory ignores it.
|
|
520
|
+
*/
|
|
521
|
+
flush_every_write?: boolean;
|
|
522
|
+
/**
|
|
523
|
+
* Browser IndexedDB-fallback snapshot debounce, in milliseconds (default
|
|
524
|
+
* 500). Only affects the non-OPFS browser fallback path — the OPFS and Node
|
|
525
|
+
* paths use `flush_every_write`.
|
|
526
|
+
*/
|
|
527
|
+
flush_ms?: number;
|
|
528
|
+
}
|
|
380
529
|
/** Top-level TalaDB configuration. */
|
|
381
530
|
interface TalaDbConfig {
|
|
382
531
|
/** HTTP push sync configuration. Disabled by default. */
|
|
383
532
|
sync?: SyncConfig;
|
|
533
|
+
/** Storage durability configuration. */
|
|
534
|
+
durability?: DurabilityConfig;
|
|
384
535
|
}
|
|
385
536
|
|
|
386
537
|
interface HttpSyncAdapterOptions {
|
|
@@ -430,11 +581,70 @@ declare class TalaDbValidationError extends Error {
|
|
|
430
581
|
readonly cause: unknown;
|
|
431
582
|
constructor(cause: unknown, context?: string);
|
|
432
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* Wraps a `Collection<T>` to intercept writes through a schema validator
|
|
586
|
+
* (`schema`), stamp `_v` on insert when a `syncSchema.version` is declared, and
|
|
587
|
+
* normalize reads through a lazy `migrateDocument`. Returns the collection
|
|
588
|
+
* unchanged when none of those apply.
|
|
589
|
+
*
|
|
590
|
+
* Read normalization covers `find`, `findOne`, and `subscribe` (live queries).
|
|
591
|
+
* It cannot cover `aggregate`, `findNearest`, or FTS `search`: those run inside
|
|
592
|
+
* the engine against the *stored* shape, so a below-version document is matched
|
|
593
|
+
* and projected as it sits on disk. Use `persistMigrations` or
|
|
594
|
+
* `openDB({ migrations })` to rewrite storage if you query old documents that way.
|
|
595
|
+
*
|
|
596
|
+
* @internal Exported for unit testing; not part of the public API surface.
|
|
597
|
+
*/
|
|
598
|
+
declare function applySchema<T extends Document>(col: Collection<T>, options: CollectionOptions<T>): Collection<T>;
|
|
433
599
|
|
|
600
|
+
/**
|
|
601
|
+
* A single application schema migration, run once at `openDB` when its
|
|
602
|
+
* `version` is greater than the database's stored migration version.
|
|
603
|
+
*/
|
|
604
|
+
interface Migration {
|
|
605
|
+
/** Monotonic version. Must be a positive integer, unique across the array. */
|
|
606
|
+
version: number;
|
|
607
|
+
/** Optional human-readable label for logs. */
|
|
608
|
+
description?: string;
|
|
609
|
+
/**
|
|
610
|
+
* The migration body. Receives the open database and may use the full
|
|
611
|
+
* collection API. Runs to completion before the version is advanced.
|
|
612
|
+
*
|
|
613
|
+
* **Write migrations idempotently.** TalaDB checkpoints per version (the
|
|
614
|
+
* stored version advances only after `up` fully resolves), but a single `up`
|
|
615
|
+
* is not wrapped in one atomic transaction — if it throws partway, the writes
|
|
616
|
+
* it already made persist and `up` re-runs from the start on the next open.
|
|
617
|
+
*/
|
|
618
|
+
up: (db: TalaDB) => Promise<void> | void;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Runtime-agnostic migration runner. Each binding supplies `getVersion` /
|
|
622
|
+
* `setVersion` (its own persisted counter); the loop is identical everywhere.
|
|
623
|
+
*
|
|
624
|
+
* Runs pending migrations (`version` > stored) in ascending order, advancing
|
|
625
|
+
* the stored version after each `up` resolves — checkpoint per version. If an
|
|
626
|
+
* `up` throws, the loop stops and the error propagates; the stored version
|
|
627
|
+
* reflects the last fully-applied migration, so the next open resumes there.
|
|
628
|
+
*
|
|
629
|
+
* @internal Exported for unit testing; not part of the public API surface.
|
|
630
|
+
*/
|
|
631
|
+
declare function runMigrations(db: TalaDB, getVersion: () => Promise<number>, setVersion: (v: number) => Promise<void>, migrations: Migration[]): Promise<void>;
|
|
434
632
|
/** Options for `openDB`. */
|
|
435
633
|
interface OpenDBOptions {
|
|
436
634
|
/** Encrypt native database values at rest. Never hard-code this value. */
|
|
437
635
|
passphrase?: string;
|
|
636
|
+
/**
|
|
637
|
+
* Ordered application schema migrations, run once each at open in ascending
|
|
638
|
+
* `version` order (only those newer than the stored migration version). The
|
|
639
|
+
* stored version advances after each migration succeeds — checkpoint per
|
|
640
|
+
* version, resuming from the last applied one on the next open.
|
|
641
|
+
*
|
|
642
|
+
* Supported on Node.js, the browser (via the OPFS worker), and React Native.
|
|
643
|
+
* On a binding whose native module predates 0.9.2 (no `userVersion` /
|
|
644
|
+
* `setUserVersion`), `openDB` throws rather than silently skipping the
|
|
645
|
+
* migrations — rebuild or update the native module.
|
|
646
|
+
*/
|
|
647
|
+
migrations?: Migration[];
|
|
438
648
|
/**
|
|
439
649
|
* Explicit path to a `taladb.config.yml` / `taladb.config.json` file.
|
|
440
650
|
* If omitted, TalaDB auto-discovers the file from `process.cwd()` on Node.js.
|
|
@@ -448,6 +658,14 @@ interface OpenDBOptions {
|
|
|
448
658
|
* Useful for passing config programmatically without a config file on disk.
|
|
449
659
|
*/
|
|
450
660
|
config?: TalaDbConfig;
|
|
661
|
+
/**
|
|
662
|
+
* Storage durability, e.g. `{ flush_every_write: false }` to batch commits
|
|
663
|
+
* for write throughput (call `db.flush()` to force a sync), or `{ flush_ms }`
|
|
664
|
+
* to tune the browser IndexedDB-fallback snapshot debounce. Merged into
|
|
665
|
+
* `config.durability`. Node + browser; on React Native pass it in the config
|
|
666
|
+
* JSON to `TalaDBModule.initialize`.
|
|
667
|
+
*/
|
|
668
|
+
durability?: DurabilityConfig;
|
|
451
669
|
}
|
|
452
670
|
/**
|
|
453
671
|
* Open a TalaDB database.
|
|
@@ -466,4 +684,4 @@ interface OpenDBOptions {
|
|
|
466
684
|
*/
|
|
467
685
|
declare function openDB(dbName?: string, options?: OpenDBOptions): Promise<TalaDB>;
|
|
468
686
|
|
|
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 };
|
|
687
|
+
export { type AggregatePipeline, type AggregateStage, type Collection, type CollectionIndexInfo, type CollectionOptions, type Document, type DurabilityConfig, type Filter, HttpSyncAdapter, type Migration, 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, applySchema, openDB, runMigrations };
|