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.mjs CHANGED
@@ -105,7 +105,7 @@ async function writeCursor(cursorCol, target, cursor) {
105
105
  await cursorCol.insert({ target, ...cursor });
106
106
  }
107
107
  }
108
- async function runSync(handle, adapter, options) {
108
+ async function runSync(handle, adapter, options, syncSchemas = {}) {
109
109
  const direction = options.direction ?? "both";
110
110
  const target = options.target ?? "default";
111
111
  const doPush = direction === "push" || direction === "both";
@@ -120,11 +120,25 @@ async function runSync(handle, adapter, options) {
120
120
  const cursorCol = handle.collection(CURSOR_COLLECTION);
121
121
  const cursor = await readCursor(cursorCol, target);
122
122
  const local = doPush ? await handle.exportChanges(collections, 0) : "[]";
123
+ const scopedSchemas = {};
124
+ for (const c of collections) {
125
+ if (syncSchemas[c]) scopedSchemas[c] = syncSchemas[c];
126
+ }
127
+ const useValidated = handle.importChangesValidated && Object.keys(scopedSchemas).length > 0;
123
128
  let pulled = 0;
129
+ let skipped = 0;
130
+ let quarantined = 0;
124
131
  if (doPull) {
125
132
  const remote = await adapter.pull(0);
126
133
  if (remote && remote !== "[]") {
127
- pulled = await handle.importChanges(remote);
134
+ if (useValidated) {
135
+ const report = await handle.importChangesValidated(remote, JSON.stringify(scopedSchemas));
136
+ pulled = report.applied;
137
+ skipped = report.skipped;
138
+ quarantined = report.quarantined;
139
+ } else {
140
+ pulled = await handle.importChanges(remote);
141
+ }
128
142
  }
129
143
  }
130
144
  let pushed = 0;
@@ -136,7 +150,7 @@ async function runSync(handle, adapter, options) {
136
150
  pushMs: cursor.pushMs,
137
151
  pullMs: cursor.pullMs
138
152
  });
139
- return { pushed, pulled, cursor: 0 };
153
+ return { pushed, pulled, skipped, quarantined, cursor: 0 };
140
154
  }
141
155
 
142
156
  // src/http-adapter.ts
@@ -185,9 +199,34 @@ var TalaDbValidationError = class extends Error {
185
199
  this.name = "TalaDbValidationError";
186
200
  }
187
201
  };
202
+ function deepEqual(a, b) {
203
+ if (a === b) return true;
204
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
205
+ if (Array.isArray(a) || Array.isArray(b)) {
206
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
207
+ return a.every((x, i) => deepEqual(x, b[i]));
208
+ }
209
+ const ao = a;
210
+ const bo = b;
211
+ const keys = Object.keys(ao);
212
+ if (keys.length !== Object.keys(bo).length) return false;
213
+ return keys.every(
214
+ (k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k])
215
+ );
216
+ }
188
217
  function applySchema(col, options) {
189
- const { schema, validateOnRead = false } = options;
190
- if (!schema) return col;
218
+ const { schema, validateOnRead = false, migrateDocument, syncSchema, persistMigrations = false } = options;
219
+ const targetVersion = syncSchema?.version ?? 0;
220
+ if (migrateDocument && targetVersion < 1) {
221
+ throw new Error("CollectionOptions.migrateDocument requires syncSchema.version (the migration target)");
222
+ }
223
+ if (syncSchema && targetVersion < 1 && (syncSchema.renames || syncSchema.defaults)) {
224
+ throw new Error(
225
+ "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"
226
+ );
227
+ }
228
+ const stampVersion = targetVersion > 0;
229
+ if (!schema && !migrateDocument && !stampVersion) return col;
191
230
  function parseWrite(doc, label) {
192
231
  try {
193
232
  return schema.parse(doc);
@@ -195,31 +234,96 @@ function applySchema(col, options) {
195
234
  throw new TalaDbValidationError(err, label);
196
235
  }
197
236
  }
198
- function parseRead(doc) {
237
+ function stamp(doc) {
238
+ if (!stampVersion || doc._v !== void 0) return doc;
239
+ return { ...doc, _v: targetVersion };
240
+ }
241
+ function diffUpdate(original, migrated) {
242
+ const $set = {};
243
+ const $unset = {};
244
+ for (const k of Object.keys(migrated)) {
245
+ if (k === "_id") continue;
246
+ if (!deepEqual(migrated[k], original[k])) $set[k] = migrated[k];
247
+ }
248
+ for (const k of Object.keys(original)) {
249
+ if (k !== "_id" && !(k in migrated)) $unset[k] = true;
250
+ }
251
+ const update = {};
252
+ if (Object.keys($set).length) update.$set = $set;
253
+ if (Object.keys($unset).length) update.$unset = $unset;
254
+ return Object.keys(update).length ? update : null;
255
+ }
256
+ function migrateRead(doc) {
257
+ if (!migrateDocument) return doc;
258
+ const fromVersion = typeof doc._v === "number" ? doc._v : 0;
259
+ if (fromVersion >= targetVersion) return doc;
260
+ return { ...migrateDocument(doc, fromVersion), _v: targetVersion };
261
+ }
262
+ function validateRead(doc) {
263
+ if (!validateOnRead || !schema) return doc;
199
264
  try {
200
265
  return schema.parse(doc);
201
266
  } catch (err) {
202
267
  throw new TalaDbValidationError(err, "read");
203
268
  }
204
269
  }
270
+ async function persistAll(originals, migrated) {
271
+ if (!persistMigrations) return;
272
+ for (let i = 0; i < originals.length; i++) {
273
+ const original = originals[i];
274
+ if (migrated[i] === original || typeof original._id !== "string") continue;
275
+ const update = diffUpdate(original, migrated[i]);
276
+ if (!update) continue;
277
+ try {
278
+ await col.updateOne({ _id: original._id }, update);
279
+ } catch {
280
+ }
281
+ }
282
+ }
283
+ const wrapReads = Boolean(migrateDocument) || validateOnRead && Boolean(schema);
284
+ const wrapWrites = Boolean(schema) || stampVersion;
205
285
  return {
206
286
  ...col,
207
- insert: async (doc) => {
208
- parseWrite(doc, "insert");
209
- return col.insert(doc);
210
- },
211
- insertMany: async (docs) => {
212
- docs.forEach((doc, i) => parseWrite(doc, `insertMany[${i}]`));
213
- return col.insertMany(docs);
214
- },
215
- find: validateOnRead ? async (filter) => {
287
+ insert: wrapWrites ? async (doc) => {
288
+ if (schema) parseWrite(doc, "insert");
289
+ return col.insert(stamp(doc));
290
+ } : col.insert.bind(col),
291
+ insertMany: wrapWrites ? async (docs) => {
292
+ if (schema) docs.forEach((doc, i) => parseWrite(doc, `insertMany[${i}]`));
293
+ return col.insertMany(docs.map(stamp));
294
+ } : col.insertMany.bind(col),
295
+ find: wrapReads ? async (filter) => {
216
296
  const docs = await col.find(filter);
217
- return docs.map((d) => parseRead(d));
297
+ const migrated = docs.map(migrateRead);
298
+ await persistAll(docs, migrated);
299
+ return migrated.map(validateRead);
218
300
  } : col.find.bind(col),
219
- findOne: validateOnRead ? async (filter) => {
301
+ findOne: wrapReads ? async (filter) => {
220
302
  const doc = await col.findOne(filter);
221
- return doc === null ? null : parseRead(doc);
222
- } : col.findOne.bind(col)
303
+ if (doc === null) return null;
304
+ const migrated = migrateRead(doc);
305
+ await persistAll([doc], [migrated]);
306
+ return validateRead(migrated);
307
+ } : col.findOne.bind(col),
308
+ // Live queries feed every @taladb/react hook (useFind, useFindOne,
309
+ // useQueries). Leaving them unwrapped meant React components received the
310
+ // un-migrated shape while a direct find() returned the migrated one.
311
+ subscribe: wrapReads ? (filter, callback, onError) => col.subscribe(
312
+ filter,
313
+ (docs) => {
314
+ const migrated = docs.map(migrateRead);
315
+ let out;
316
+ try {
317
+ out = migrated.map(validateRead);
318
+ } catch (err) {
319
+ onError?.(err);
320
+ return;
321
+ }
322
+ callback(out);
323
+ void persistAll(docs, migrated);
324
+ },
325
+ onError
326
+ ) : col.subscribe.bind(col)
223
327
  };
224
328
  }
225
329
  function detectPlatform() {
@@ -307,7 +411,7 @@ function makePoller(findFn, callback, onError) {
307
411
  active = false;
308
412
  };
309
413
  }
310
- async function createBrowserDB(dbName, config, passphrase) {
414
+ async function createBrowserDB(dbName, config, passphrase, migrations) {
311
415
  const workerUrl = new URL("@taladb/web/worker/taladb.worker.js", import.meta.url);
312
416
  const worker = new Worker(workerUrl, { type: "module", name: "taladb" });
313
417
  const proxy = new WorkerProxy(worker);
@@ -332,7 +436,9 @@ async function createBrowserDB(dbName, config, passphrase) {
332
436
  }
333
437
  };
334
438
  }
439
+ const syncSchemas = {};
335
440
  function wrapCollection(name, opts) {
441
+ if (opts?.syncSchema) syncSchemas[name] = opts.syncSchema;
336
442
  const s = JSON.stringify;
337
443
  const wrapped = {
338
444
  insert: (doc) => proxy.send("insert", { collection: name, docJson: s(doc) }),
@@ -470,6 +576,9 @@ async function createBrowserDB(dbName, config, passphrase) {
470
576
  const handle = {
471
577
  collection: (name, opts) => wrapCollection(name, opts),
472
578
  compact: () => proxy.send("compact"),
579
+ flush: async () => {
580
+ await proxy.send("flush");
581
+ },
473
582
  syncStatus: async () => JSON.parse(await proxy.send("syncStatus")),
474
583
  flushSync: (timeoutMs = 5e3) => proxy.send("flushSync", { timeoutMs }),
475
584
  close: async () => {
@@ -485,18 +594,32 @@ async function createBrowserDB(dbName, config, passphrase) {
485
594
  // main thread — a sync pass never blocks rendering, whatever its size.
486
595
  exportChanges: (collections, sinceMs) => proxy.send("exportChangeset", { collectionsJson: JSON.stringify(collections), sinceMs }),
487
596
  importChanges: (changeset) => proxy.send("importChangeset", { changesetJson: changeset }),
597
+ importChangesValidated: async (changeset, schemasJson) => JSON.parse(await proxy.send("importChangesetValidated", { changesetJson: changeset, schemasJson })),
488
598
  listCollectionNames: async () => JSON.parse(await proxy.send("listCollections")),
489
- sync: (adapter, options) => runSync(handle, adapter, options)
599
+ quarantined: async (collection) => JSON.parse(await proxy.send("quarantined", { collection })),
600
+ sync: (adapter, options) => runSync(handle, adapter, options, syncSchemas)
490
601
  };
602
+ if (migrations?.length) {
603
+ await runMigrations(
604
+ handle,
605
+ async () => proxy.send("userVersion"),
606
+ async (v) => {
607
+ await proxy.send("setUserVersion", { version: v });
608
+ },
609
+ migrations
610
+ );
611
+ }
491
612
  return handle;
492
613
  }
493
- async function createNodeDB(dbName, config, passphrase) {
614
+ async function createNodeDB(dbName, config, passphrase, migrations) {
494
615
  const native = await import("@taladb/node");
495
616
  const TalaDBNode = native.TalaDbNode ?? native.TalaDBNode;
496
617
  if (!TalaDBNode) throw new Error("@taladb/node loaded but exports no TalaDbNode class \u2014 rebuild the native module");
497
618
  const configJson = config !== void 0 ? JSON.stringify(config) : null;
498
619
  const db = TalaDBNode.open(dbName, configJson, passphrase ?? null);
620
+ const syncSchemas = {};
499
621
  function wrapCollection(name, opts) {
622
+ if (opts?.syncSchema) syncSchemas[name] = opts.syncSchema;
500
623
  const col = db.collection(name);
501
624
  const wrapped = {
502
625
  insert: async (doc) => col.insertAsync ? col.insertAsync(doc) : col.insert(doc),
@@ -535,14 +658,32 @@ async function createNodeDB(dbName, config, passphrase) {
535
658
  compact: async () => db.compact(),
536
659
  // Releases the native file handle/lock (no-op on older .node binaries).
537
660
  close: async () => db.close?.(),
661
+ flush: db.flush ? async () => {
662
+ db.flush();
663
+ } : void 0,
538
664
  exportChanges: async (collections, sinceMs) => db.exportChanges(sinceMs, collections),
539
665
  importChanges: async (changeset) => db.importChanges(changeset),
666
+ // Feature-detected: only present when the loaded .node binary supports it,
667
+ // so older prebuilt binaries fall back to plain importChanges.
668
+ importChangesValidated: db.importChangesValidated ? async (changeset, schemasJson) => db.importChangesValidated(changeset, schemasJson) : void 0,
540
669
  listCollectionNames: async () => db.listCollectionNames(),
541
- sync: (adapter, options) => runSync(handle, adapter, options)
670
+ quarantined: async (collection) => db.quarantined ? db.quarantined(collection) : [],
671
+ sync: (adapter, options) => runSync(handle, adapter, options, syncSchemas)
542
672
  };
673
+ if (migrations?.length) {
674
+ if (typeof db.userVersion !== "function" || typeof db.setUserVersion !== "function") {
675
+ throw new Error("openDB({ migrations }) requires @taladb/node \u2265 0.9.2 \u2014 rebuild the native module");
676
+ }
677
+ await runMigrations(
678
+ handle,
679
+ async () => db.userVersion(),
680
+ async (v) => db.setUserVersion(v),
681
+ migrations
682
+ );
683
+ }
543
684
  return handle;
544
685
  }
545
- async function createNativeDB(_dbName) {
686
+ async function createNativeDB(_dbName, migrations) {
546
687
  const maybeNative = globalThis.__TalaDB__;
547
688
  if (!maybeNative) {
548
689
  throw new Error(
@@ -550,7 +691,9 @@ async function createNativeDB(_dbName) {
550
691
  );
551
692
  }
552
693
  const native = maybeNative;
694
+ const syncSchemas = {};
553
695
  function wrapCollection(name, opts) {
696
+ if (opts?.syncSchema) syncSchemas[name] = opts.syncSchema;
554
697
  const wrapped = {
555
698
  insert: async (doc) => native.insert(name, doc),
556
699
  insertMany: async (docs) => native.insertMany(name, docs),
@@ -590,25 +733,64 @@ async function createNativeDB(_dbName) {
590
733
  return opts ? applySchema(wrapped, opts) : wrapped;
591
734
  }
592
735
  const syncSurface = typeof native.exportChanges === "function" && typeof native.importChanges === "function" && typeof native.listCollectionNames === "function" ? (() => {
593
- const handle = {
736
+ const handle2 = {
594
737
  collection: (name, opts) => wrapCollection(name, opts),
595
738
  exportChanges: async (collections, sinceMs) => native.exportChanges(collections, sinceMs),
596
739
  importChanges: async (changeset) => native.importChanges(changeset),
740
+ // Feature-detected: present on 0.9.2+ JSI HostObjects; when absent,
741
+ // runSync falls back to unvalidated importChanges.
742
+ importChangesValidated: native.importChangesValidated ? async (changeset, schemasJson) => native.importChangesValidated(changeset, schemasJson) : void 0,
597
743
  listCollectionNames: async () => native.listCollectionNames(),
598
- sync: (adapter, options) => runSync(handle, adapter, options)
744
+ sync: (adapter, options) => runSync(handle2, adapter, options, syncSchemas)
599
745
  };
600
746
  return {
601
- exportChanges: handle.exportChanges,
602
- importChanges: handle.importChanges,
603
- sync: handle.sync
747
+ exportChanges: handle2.exportChanges,
748
+ importChanges: handle2.importChanges,
749
+ sync: handle2.sync
604
750
  };
605
751
  })() : unsupportedSync("react-native");
606
- return {
752
+ const handle = {
607
753
  collection: (name, opts) => wrapCollection(name, opts),
608
754
  compact: async () => native.compact(),
609
755
  close: async () => native.close(),
756
+ flush: native.flush ? async () => {
757
+ native.flush();
758
+ } : void 0,
759
+ quarantined: native.quarantined ? async (collection) => native.quarantined(collection) : void 0,
610
760
  ...syncSurface
611
761
  };
762
+ if (migrations?.length) {
763
+ if (typeof native.userVersion !== "function" || typeof native.setUserVersion !== "function") {
764
+ throw new Error(
765
+ "openDB({ migrations }) is not available on this @taladb/react-native binary yet (the JSI HostObject does not expose userVersion/setUserVersion). Update the native module."
766
+ );
767
+ }
768
+ await runMigrations(
769
+ handle,
770
+ async () => native.userVersion(),
771
+ async (v) => native.setUserVersion(v),
772
+ migrations
773
+ );
774
+ }
775
+ return handle;
776
+ }
777
+ async function runMigrations(db, getVersion, setVersion, migrations) {
778
+ const sorted = [...migrations].sort((a, b) => a.version - b.version);
779
+ for (let i = 0; i < sorted.length; i++) {
780
+ const v = sorted[i].version;
781
+ if (!Number.isInteger(v) || v < 1) {
782
+ throw new Error(`TalaDB migration version must be a positive integer, got ${v}`);
783
+ }
784
+ if (i > 0 && v === sorted[i - 1].version) {
785
+ throw new Error(`TalaDB duplicate migration version ${v}`);
786
+ }
787
+ }
788
+ const current = await getVersion();
789
+ for (const m of sorted) {
790
+ if (m.version <= current) continue;
791
+ await m.up(db);
792
+ await setVersion(m.version);
793
+ }
612
794
  }
613
795
  async function openDB(dbName = "taladb.db", options) {
614
796
  if (options?.passphrase !== void 0 && options.passphrase.length === 0) {
@@ -621,21 +803,30 @@ async function openDB(dbName = "taladb.db", options) {
621
803
  } else {
622
804
  resolvedConfig = await loadConfig(options?.configPath);
623
805
  }
806
+ if (options?.durability) {
807
+ resolvedConfig = {
808
+ ...resolvedConfig,
809
+ durability: { ...resolvedConfig?.durability, ...options.durability }
810
+ };
811
+ }
624
812
  const platform = detectPlatform();
813
+ const migrations = options?.migrations;
625
814
  switch (platform) {
626
815
  case "browser":
627
- return createBrowserDB(dbName, resolvedConfig, options?.passphrase);
816
+ return createBrowserDB(dbName, resolvedConfig, options?.passphrase, migrations);
628
817
  case "react-native":
629
818
  if (options?.passphrase !== void 0) {
630
819
  throw new Error("On React Native, pass the passphrase in the config JSON to TalaDBModule.initialize(); refusing to assume the already-open native database is encrypted");
631
820
  }
632
- return createNativeDB(dbName);
821
+ return createNativeDB(dbName, migrations);
633
822
  case "node":
634
- return createNodeDB(dbName, resolvedConfig, options?.passphrase);
823
+ return createNodeDB(dbName, resolvedConfig, options?.passphrase, migrations);
635
824
  }
636
825
  }
637
826
  export {
638
827
  HttpSyncAdapter,
639
828
  TalaDbValidationError,
640
- openDB
829
+ applySchema,
830
+ openDB,
831
+ runMigrations
641
832
  };