taladb 0.9.0 → 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.js CHANGED
@@ -32,7 +32,9 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  HttpSyncAdapter: () => HttpSyncAdapter,
34
34
  TalaDbValidationError: () => TalaDbValidationError,
35
- openDB: () => openDB
35
+ applySchema: () => applySchema,
36
+ openDB: () => openDB,
37
+ runMigrations: () => runMigrations
36
38
  });
37
39
  module.exports = __toCommonJS(index_exports);
38
40
 
@@ -143,7 +145,7 @@ async function writeCursor(cursorCol, target, cursor) {
143
145
  await cursorCol.insert({ target, ...cursor });
144
146
  }
145
147
  }
146
- async function runSync(handle, adapter, options) {
148
+ async function runSync(handle, adapter, options, syncSchemas = {}) {
147
149
  const direction = options.direction ?? "both";
148
150
  const target = options.target ?? "default";
149
151
  const doPush = direction === "push" || direction === "both";
@@ -158,11 +160,25 @@ async function runSync(handle, adapter, options) {
158
160
  const cursorCol = handle.collection(CURSOR_COLLECTION);
159
161
  const cursor = await readCursor(cursorCol, target);
160
162
  const local = doPush ? await handle.exportChanges(collections, 0) : "[]";
163
+ const scopedSchemas = {};
164
+ for (const c of collections) {
165
+ if (syncSchemas[c]) scopedSchemas[c] = syncSchemas[c];
166
+ }
167
+ const useValidated = handle.importChangesValidated && Object.keys(scopedSchemas).length > 0;
161
168
  let pulled = 0;
169
+ let skipped = 0;
170
+ let quarantined = 0;
162
171
  if (doPull) {
163
172
  const remote = await adapter.pull(0);
164
173
  if (remote && remote !== "[]") {
165
- pulled = await handle.importChanges(remote);
174
+ if (useValidated) {
175
+ const report = await handle.importChangesValidated(remote, JSON.stringify(scopedSchemas));
176
+ pulled = report.applied;
177
+ skipped = report.skipped;
178
+ quarantined = report.quarantined;
179
+ } else {
180
+ pulled = await handle.importChanges(remote);
181
+ }
166
182
  }
167
183
  }
168
184
  let pushed = 0;
@@ -174,7 +190,7 @@ async function runSync(handle, adapter, options) {
174
190
  pushMs: cursor.pushMs,
175
191
  pullMs: cursor.pullMs
176
192
  });
177
- return { pushed, pulled, cursor: 0 };
193
+ return { pushed, pulled, skipped, quarantined, cursor: 0 };
178
194
  }
179
195
 
180
196
  // src/http-adapter.ts
@@ -224,9 +240,34 @@ var TalaDbValidationError = class extends Error {
224
240
  this.name = "TalaDbValidationError";
225
241
  }
226
242
  };
243
+ function deepEqual(a, b) {
244
+ if (a === b) return true;
245
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
246
+ if (Array.isArray(a) || Array.isArray(b)) {
247
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
248
+ return a.every((x, i) => deepEqual(x, b[i]));
249
+ }
250
+ const ao = a;
251
+ const bo = b;
252
+ const keys = Object.keys(ao);
253
+ if (keys.length !== Object.keys(bo).length) return false;
254
+ return keys.every(
255
+ (k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k])
256
+ );
257
+ }
227
258
  function applySchema(col, options) {
228
- const { schema, validateOnRead = false } = options;
229
- if (!schema) return col;
259
+ const { schema, validateOnRead = false, migrateDocument, syncSchema, persistMigrations = false } = options;
260
+ const targetVersion = syncSchema?.version ?? 0;
261
+ if (migrateDocument && targetVersion < 1) {
262
+ throw new Error("CollectionOptions.migrateDocument requires syncSchema.version (the migration target)");
263
+ }
264
+ if (syncSchema && targetVersion < 1 && (syncSchema.renames || syncSchema.defaults)) {
265
+ throw new Error(
266
+ "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"
267
+ );
268
+ }
269
+ const stampVersion = targetVersion > 0;
270
+ if (!schema && !migrateDocument && !stampVersion) return col;
230
271
  function parseWrite(doc, label) {
231
272
  try {
232
273
  return schema.parse(doc);
@@ -234,31 +275,96 @@ function applySchema(col, options) {
234
275
  throw new TalaDbValidationError(err, label);
235
276
  }
236
277
  }
237
- function parseRead(doc) {
278
+ function stamp(doc) {
279
+ if (!stampVersion || doc._v !== void 0) return doc;
280
+ return { ...doc, _v: targetVersion };
281
+ }
282
+ function diffUpdate(original, migrated) {
283
+ const $set = {};
284
+ const $unset = {};
285
+ for (const k of Object.keys(migrated)) {
286
+ if (k === "_id") continue;
287
+ if (!deepEqual(migrated[k], original[k])) $set[k] = migrated[k];
288
+ }
289
+ for (const k of Object.keys(original)) {
290
+ if (k !== "_id" && !(k in migrated)) $unset[k] = true;
291
+ }
292
+ const update = {};
293
+ if (Object.keys($set).length) update.$set = $set;
294
+ if (Object.keys($unset).length) update.$unset = $unset;
295
+ return Object.keys(update).length ? update : null;
296
+ }
297
+ function migrateRead(doc) {
298
+ if (!migrateDocument) return doc;
299
+ const fromVersion = typeof doc._v === "number" ? doc._v : 0;
300
+ if (fromVersion >= targetVersion) return doc;
301
+ return { ...migrateDocument(doc, fromVersion), _v: targetVersion };
302
+ }
303
+ function validateRead(doc) {
304
+ if (!validateOnRead || !schema) return doc;
238
305
  try {
239
306
  return schema.parse(doc);
240
307
  } catch (err) {
241
308
  throw new TalaDbValidationError(err, "read");
242
309
  }
243
310
  }
311
+ async function persistAll(originals, migrated) {
312
+ if (!persistMigrations) return;
313
+ for (let i = 0; i < originals.length; i++) {
314
+ const original = originals[i];
315
+ if (migrated[i] === original || typeof original._id !== "string") continue;
316
+ const update = diffUpdate(original, migrated[i]);
317
+ if (!update) continue;
318
+ try {
319
+ await col.updateOne({ _id: original._id }, update);
320
+ } catch {
321
+ }
322
+ }
323
+ }
324
+ const wrapReads = Boolean(migrateDocument) || validateOnRead && Boolean(schema);
325
+ const wrapWrites = Boolean(schema) || stampVersion;
244
326
  return {
245
327
  ...col,
246
- insert: async (doc) => {
247
- parseWrite(doc, "insert");
248
- return col.insert(doc);
249
- },
250
- insertMany: async (docs) => {
251
- docs.forEach((doc, i) => parseWrite(doc, `insertMany[${i}]`));
252
- return col.insertMany(docs);
253
- },
254
- find: validateOnRead ? async (filter) => {
328
+ insert: wrapWrites ? async (doc) => {
329
+ if (schema) parseWrite(doc, "insert");
330
+ return col.insert(stamp(doc));
331
+ } : col.insert.bind(col),
332
+ insertMany: wrapWrites ? async (docs) => {
333
+ if (schema) docs.forEach((doc, i) => parseWrite(doc, `insertMany[${i}]`));
334
+ return col.insertMany(docs.map(stamp));
335
+ } : col.insertMany.bind(col),
336
+ find: wrapReads ? async (filter) => {
255
337
  const docs = await col.find(filter);
256
- return docs.map((d) => parseRead(d));
338
+ const migrated = docs.map(migrateRead);
339
+ await persistAll(docs, migrated);
340
+ return migrated.map(validateRead);
257
341
  } : col.find.bind(col),
258
- findOne: validateOnRead ? async (filter) => {
342
+ findOne: wrapReads ? async (filter) => {
259
343
  const doc = await col.findOne(filter);
260
- return doc === null ? null : parseRead(doc);
261
- } : col.findOne.bind(col)
344
+ if (doc === null) return null;
345
+ const migrated = migrateRead(doc);
346
+ await persistAll([doc], [migrated]);
347
+ return validateRead(migrated);
348
+ } : col.findOne.bind(col),
349
+ // Live queries feed every @taladb/react hook (useFind, useFindOne,
350
+ // useQueries). Leaving them unwrapped meant React components received the
351
+ // un-migrated shape while a direct find() returned the migrated one.
352
+ subscribe: wrapReads ? (filter, callback, onError) => col.subscribe(
353
+ filter,
354
+ (docs) => {
355
+ const migrated = docs.map(migrateRead);
356
+ let out;
357
+ try {
358
+ out = migrated.map(validateRead);
359
+ } catch (err) {
360
+ onError?.(err);
361
+ return;
362
+ }
363
+ callback(out);
364
+ void persistAll(docs, migrated);
365
+ },
366
+ onError
367
+ ) : col.subscribe.bind(col)
262
368
  };
263
369
  }
264
370
  function detectPlatform() {
@@ -346,7 +452,7 @@ function makePoller(findFn, callback, onError) {
346
452
  active = false;
347
453
  };
348
454
  }
349
- async function createBrowserDB(dbName, config, passphrase) {
455
+ async function createBrowserDB(dbName, config, passphrase, migrations) {
350
456
  const workerUrl = new URL("@taladb/web/worker/taladb.worker.js", import_meta.url);
351
457
  const worker = new Worker(workerUrl, { type: "module", name: "taladb" });
352
458
  const proxy = new WorkerProxy(worker);
@@ -371,7 +477,9 @@ async function createBrowserDB(dbName, config, passphrase) {
371
477
  }
372
478
  };
373
479
  }
480
+ const syncSchemas = {};
374
481
  function wrapCollection(name, opts) {
482
+ if (opts?.syncSchema) syncSchemas[name] = opts.syncSchema;
375
483
  const s = JSON.stringify;
376
484
  const wrapped = {
377
485
  insert: (doc) => proxy.send("insert", { collection: name, docJson: s(doc) }),
@@ -509,6 +617,9 @@ async function createBrowserDB(dbName, config, passphrase) {
509
617
  const handle = {
510
618
  collection: (name, opts) => wrapCollection(name, opts),
511
619
  compact: () => proxy.send("compact"),
620
+ flush: async () => {
621
+ await proxy.send("flush");
622
+ },
512
623
  syncStatus: async () => JSON.parse(await proxy.send("syncStatus")),
513
624
  flushSync: (timeoutMs = 5e3) => proxy.send("flushSync", { timeoutMs }),
514
625
  close: async () => {
@@ -524,18 +635,32 @@ async function createBrowserDB(dbName, config, passphrase) {
524
635
  // main thread — a sync pass never blocks rendering, whatever its size.
525
636
  exportChanges: (collections, sinceMs) => proxy.send("exportChangeset", { collectionsJson: JSON.stringify(collections), sinceMs }),
526
637
  importChanges: (changeset) => proxy.send("importChangeset", { changesetJson: changeset }),
638
+ importChangesValidated: async (changeset, schemasJson) => JSON.parse(await proxy.send("importChangesetValidated", { changesetJson: changeset, schemasJson })),
527
639
  listCollectionNames: async () => JSON.parse(await proxy.send("listCollections")),
528
- sync: (adapter, options) => runSync(handle, adapter, options)
640
+ quarantined: async (collection) => JSON.parse(await proxy.send("quarantined", { collection })),
641
+ sync: (adapter, options) => runSync(handle, adapter, options, syncSchemas)
529
642
  };
643
+ if (migrations?.length) {
644
+ await runMigrations(
645
+ handle,
646
+ async () => proxy.send("userVersion"),
647
+ async (v) => {
648
+ await proxy.send("setUserVersion", { version: v });
649
+ },
650
+ migrations
651
+ );
652
+ }
530
653
  return handle;
531
654
  }
532
- async function createNodeDB(dbName, config, passphrase) {
655
+ async function createNodeDB(dbName, config, passphrase, migrations) {
533
656
  const native = await import("@taladb/node");
534
657
  const TalaDBNode = native.TalaDbNode ?? native.TalaDBNode;
535
658
  if (!TalaDBNode) throw new Error("@taladb/node loaded but exports no TalaDbNode class \u2014 rebuild the native module");
536
659
  const configJson = config !== void 0 ? JSON.stringify(config) : null;
537
660
  const db = TalaDBNode.open(dbName, configJson, passphrase ?? null);
661
+ const syncSchemas = {};
538
662
  function wrapCollection(name, opts) {
663
+ if (opts?.syncSchema) syncSchemas[name] = opts.syncSchema;
539
664
  const col = db.collection(name);
540
665
  const wrapped = {
541
666
  insert: async (doc) => col.insertAsync ? col.insertAsync(doc) : col.insert(doc),
@@ -574,14 +699,32 @@ async function createNodeDB(dbName, config, passphrase) {
574
699
  compact: async () => db.compact(),
575
700
  // Releases the native file handle/lock (no-op on older .node binaries).
576
701
  close: async () => db.close?.(),
702
+ flush: db.flush ? async () => {
703
+ db.flush();
704
+ } : void 0,
577
705
  exportChanges: async (collections, sinceMs) => db.exportChanges(sinceMs, collections),
578
706
  importChanges: async (changeset) => db.importChanges(changeset),
707
+ // Feature-detected: only present when the loaded .node binary supports it,
708
+ // so older prebuilt binaries fall back to plain importChanges.
709
+ importChangesValidated: db.importChangesValidated ? async (changeset, schemasJson) => db.importChangesValidated(changeset, schemasJson) : void 0,
579
710
  listCollectionNames: async () => db.listCollectionNames(),
580
- sync: (adapter, options) => runSync(handle, adapter, options)
711
+ quarantined: async (collection) => db.quarantined ? db.quarantined(collection) : [],
712
+ sync: (adapter, options) => runSync(handle, adapter, options, syncSchemas)
581
713
  };
714
+ if (migrations?.length) {
715
+ if (typeof db.userVersion !== "function" || typeof db.setUserVersion !== "function") {
716
+ throw new Error("openDB({ migrations }) requires @taladb/node \u2265 0.9.2 \u2014 rebuild the native module");
717
+ }
718
+ await runMigrations(
719
+ handle,
720
+ async () => db.userVersion(),
721
+ async (v) => db.setUserVersion(v),
722
+ migrations
723
+ );
724
+ }
582
725
  return handle;
583
726
  }
584
- async function createNativeDB(_dbName) {
727
+ async function createNativeDB(_dbName, migrations) {
585
728
  const maybeNative = globalThis.__TalaDB__;
586
729
  if (!maybeNative) {
587
730
  throw new Error(
@@ -589,7 +732,9 @@ async function createNativeDB(_dbName) {
589
732
  );
590
733
  }
591
734
  const native = maybeNative;
735
+ const syncSchemas = {};
592
736
  function wrapCollection(name, opts) {
737
+ if (opts?.syncSchema) syncSchemas[name] = opts.syncSchema;
593
738
  const wrapped = {
594
739
  insert: async (doc) => native.insert(name, doc),
595
740
  insertMany: async (docs) => native.insertMany(name, docs),
@@ -629,25 +774,64 @@ async function createNativeDB(_dbName) {
629
774
  return opts ? applySchema(wrapped, opts) : wrapped;
630
775
  }
631
776
  const syncSurface = typeof native.exportChanges === "function" && typeof native.importChanges === "function" && typeof native.listCollectionNames === "function" ? (() => {
632
- const handle = {
777
+ const handle2 = {
633
778
  collection: (name, opts) => wrapCollection(name, opts),
634
779
  exportChanges: async (collections, sinceMs) => native.exportChanges(collections, sinceMs),
635
780
  importChanges: async (changeset) => native.importChanges(changeset),
781
+ // Feature-detected: present on 0.9.2+ JSI HostObjects; when absent,
782
+ // runSync falls back to unvalidated importChanges.
783
+ importChangesValidated: native.importChangesValidated ? async (changeset, schemasJson) => native.importChangesValidated(changeset, schemasJson) : void 0,
636
784
  listCollectionNames: async () => native.listCollectionNames(),
637
- sync: (adapter, options) => runSync(handle, adapter, options)
785
+ sync: (adapter, options) => runSync(handle2, adapter, options, syncSchemas)
638
786
  };
639
787
  return {
640
- exportChanges: handle.exportChanges,
641
- importChanges: handle.importChanges,
642
- sync: handle.sync
788
+ exportChanges: handle2.exportChanges,
789
+ importChanges: handle2.importChanges,
790
+ sync: handle2.sync
643
791
  };
644
792
  })() : unsupportedSync("react-native");
645
- return {
793
+ const handle = {
646
794
  collection: (name, opts) => wrapCollection(name, opts),
647
795
  compact: async () => native.compact(),
648
796
  close: async () => native.close(),
797
+ flush: native.flush ? async () => {
798
+ native.flush();
799
+ } : void 0,
800
+ quarantined: native.quarantined ? async (collection) => native.quarantined(collection) : void 0,
649
801
  ...syncSurface
650
802
  };
803
+ if (migrations?.length) {
804
+ if (typeof native.userVersion !== "function" || typeof native.setUserVersion !== "function") {
805
+ throw new Error(
806
+ "openDB({ migrations }) is not available on this @taladb/react-native binary yet (the JSI HostObject does not expose userVersion/setUserVersion). Update the native module."
807
+ );
808
+ }
809
+ await runMigrations(
810
+ handle,
811
+ async () => native.userVersion(),
812
+ async (v) => native.setUserVersion(v),
813
+ migrations
814
+ );
815
+ }
816
+ return handle;
817
+ }
818
+ async function runMigrations(db, getVersion, setVersion, migrations) {
819
+ const sorted = [...migrations].sort((a, b) => a.version - b.version);
820
+ for (let i = 0; i < sorted.length; i++) {
821
+ const v = sorted[i].version;
822
+ if (!Number.isInteger(v) || v < 1) {
823
+ throw new Error(`TalaDB migration version must be a positive integer, got ${v}`);
824
+ }
825
+ if (i > 0 && v === sorted[i - 1].version) {
826
+ throw new Error(`TalaDB duplicate migration version ${v}`);
827
+ }
828
+ }
829
+ const current = await getVersion();
830
+ for (const m of sorted) {
831
+ if (m.version <= current) continue;
832
+ await m.up(db);
833
+ await setVersion(m.version);
834
+ }
651
835
  }
652
836
  async function openDB(dbName = "taladb.db", options) {
653
837
  if (options?.passphrase !== void 0 && options.passphrase.length === 0) {
@@ -660,22 +844,31 @@ async function openDB(dbName = "taladb.db", options) {
660
844
  } else {
661
845
  resolvedConfig = await loadConfig(options?.configPath);
662
846
  }
847
+ if (options?.durability) {
848
+ resolvedConfig = {
849
+ ...resolvedConfig,
850
+ durability: { ...resolvedConfig?.durability, ...options.durability }
851
+ };
852
+ }
663
853
  const platform = detectPlatform();
854
+ const migrations = options?.migrations;
664
855
  switch (platform) {
665
856
  case "browser":
666
- return createBrowserDB(dbName, resolvedConfig, options?.passphrase);
857
+ return createBrowserDB(dbName, resolvedConfig, options?.passphrase, migrations);
667
858
  case "react-native":
668
859
  if (options?.passphrase !== void 0) {
669
860
  throw new Error("On React Native, pass the passphrase in the config JSON to TalaDBModule.initialize(); refusing to assume the already-open native database is encrypted");
670
861
  }
671
- return createNativeDB(dbName);
862
+ return createNativeDB(dbName, migrations);
672
863
  case "node":
673
- return createNodeDB(dbName, resolvedConfig, options?.passphrase);
864
+ return createNodeDB(dbName, resolvedConfig, options?.passphrase, migrations);
674
865
  }
675
866
  }
676
867
  // Annotate the CommonJS export names for ESM import in node:
677
868
  0 && (module.exports = {
678
869
  HttpSyncAdapter,
679
870
  TalaDbValidationError,
680
- openDB
871
+ applySchema,
872
+ openDB,
873
+ runMigrations
681
874
  });