taladb 0.9.1 → 0.9.3

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.
@@ -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
- pulled = await handle.importChanges(remote);
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
- if (!schema) return col;
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 parseRead(doc) {
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: validateOnRead ? async (filter) => {
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
- return docs.map((d) => parseRead(d));
240
+ const migrated = docs.map(migrateRead);
241
+ await persistAll(docs, migrated);
242
+ return migrated.map(validateRead);
161
243
  } : col.find.bind(col),
162
- findOne: validateOnRead ? async (filter) => {
244
+ findOne: wrapReads ? async (filter) => {
163
245
  const doc = await col.findOne(filter);
164
- return doc === null ? null : parseRead(doc);
165
- } : col.findOne.bind(col)
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
- sync: (adapter, options) => runSync(handle, adapter, options)
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
- sync: (adapter, options) => runSync(handle, adapter, options)
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 handle = {
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(handle, adapter, options)
687
+ sync: (adapter, options) => runSync(handle2, adapter, options, syncSchemas)
542
688
  };
543
689
  return {
544
- exportChanges: handle.exportChanges,
545
- importChanges: handle.importChanges,
546
- sync: handle.sync
690
+ exportChanges: handle2.exportChanges,
691
+ importChanges: handle2.importChanges,
692
+ sync: handle2.sync
547
693
  };
548
694
  })() : unsupportedSync("react-native");
549
- return {
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
- openDB
772
+ applySchema,
773
+ openDB,
774
+ runMigrations
584
775
  };