@monlite/core 0.6.0 → 0.7.0

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.d.cts CHANGED
@@ -265,8 +265,17 @@ declare class Collection<T = Doc> {
265
265
  private buildInsert;
266
266
  /** Build the `SET` clause + values to persist an updated document. */
267
267
  private buildUpdateSet;
268
- /** Sync store, but only for document collections (structured sync is future work). */
268
+ /** Sync store for recording local changes (both document and structured). */
269
269
  private get recorder();
270
+ /** @internal Read a full document by id (mode-aware), synchronously. */
271
+ getRaw(id: string): WithId<T> | null;
272
+ /**
273
+ * @internal Apply a remote change to storage WITHOUT recording it to the
274
+ * change feed (the sync store records the `remote` feed row itself). Used by
275
+ * `@monlite/sync` so structured collections sync correctly through the same
276
+ * column/overflow split as local writes.
277
+ */
278
+ applyRemoteWrite(op: "upsert" | "delete", id: string, doc: Record<string, any> | undefined, ts: number): void;
270
279
  create(args: CreateArgs<T>): Promise<WithId<T>>;
271
280
  createMany(args: CreateManyArgs<T>): Promise<{
272
281
  count: number;
@@ -421,9 +430,10 @@ interface ConflictRow {
421
430
  */
422
431
  declare class SyncStore {
423
432
  private readonly db;
433
+ private readonly mon?;
424
434
  readonly nodeId: string;
425
435
  private versionSeq;
426
- constructor(db: Driver, nodeId?: string);
436
+ constructor(db: Driver, nodeId?: string, mon?: Monlite | undefined);
427
437
  private init;
428
438
  private resolveNodeId;
429
439
  /** True if this database tracks sync metadata (always, once constructed). */
@@ -462,6 +472,7 @@ declare class SyncStore {
462
472
  private recordConflict;
463
473
  conflicts(): ConflictRow[];
464
474
  private readDoc;
475
+ private tableExists;
465
476
  private ensureCollTable;
466
477
  }
467
478
 
package/dist/index.d.ts CHANGED
@@ -265,8 +265,17 @@ declare class Collection<T = Doc> {
265
265
  private buildInsert;
266
266
  /** Build the `SET` clause + values to persist an updated document. */
267
267
  private buildUpdateSet;
268
- /** Sync store, but only for document collections (structured sync is future work). */
268
+ /** Sync store for recording local changes (both document and structured). */
269
269
  private get recorder();
270
+ /** @internal Read a full document by id (mode-aware), synchronously. */
271
+ getRaw(id: string): WithId<T> | null;
272
+ /**
273
+ * @internal Apply a remote change to storage WITHOUT recording it to the
274
+ * change feed (the sync store records the `remote` feed row itself). Used by
275
+ * `@monlite/sync` so structured collections sync correctly through the same
276
+ * column/overflow split as local writes.
277
+ */
278
+ applyRemoteWrite(op: "upsert" | "delete", id: string, doc: Record<string, any> | undefined, ts: number): void;
270
279
  create(args: CreateArgs<T>): Promise<WithId<T>>;
271
280
  createMany(args: CreateManyArgs<T>): Promise<{
272
281
  count: number;
@@ -421,9 +430,10 @@ interface ConflictRow {
421
430
  */
422
431
  declare class SyncStore {
423
432
  private readonly db;
433
+ private readonly mon?;
424
434
  readonly nodeId: string;
425
435
  private versionSeq;
426
- constructor(db: Driver, nodeId?: string);
436
+ constructor(db: Driver, nodeId?: string, mon?: Monlite | undefined);
427
437
  private init;
428
438
  private resolveNodeId;
429
439
  /** True if this database tracks sync metadata (always, once constructed). */
@@ -462,6 +472,7 @@ declare class SyncStore {
462
472
  private recordConflict;
463
473
  conflicts(): ConflictRow[];
464
474
  private readDoc;
475
+ private tableExists;
465
476
  private ensureCollTable;
466
477
  }
467
478
 
package/dist/index.js CHANGED
@@ -236,9 +236,7 @@ function cmp(expr, op, v, ctx) {
236
236
  }
237
237
  function inExpr(expr, arr, ctx, negate) {
238
238
  if (!Array.isArray(arr)) {
239
- throw new MonliteQueryError(
240
- `${negate ? "notIn" : "in"} expects an array`
241
- );
239
+ throw new MonliteQueryError(`${negate ? "notIn" : "in"} expects an array`);
242
240
  }
243
241
  if (arr.length === 0) return negate ? "1" : "0";
244
242
  const placeholders = arr.map((v) => {
@@ -368,7 +366,8 @@ function applyUpdate(doc, data) {
368
366
  }
369
367
  const ops = data;
370
368
  if (ops.$set) {
371
- for (const [path, value] of Object.entries(ops.$set)) setPath(next, path, value);
369
+ for (const [path, value] of Object.entries(ops.$set))
370
+ setPath(next, path, value);
372
371
  }
373
372
  if (ops.$inc) {
374
373
  for (const [path, by] of Object.entries(ops.$inc)) {
@@ -489,7 +488,11 @@ function buildHaving(having, params, columns) {
489
488
  if (!selection) continue;
490
489
  for (const field of Object.keys(selection)) {
491
490
  parts.push(
492
- ...comparisonSql(`${fn}(${fieldExpr(field, columns)})`, selection[field], params)
491
+ ...comparisonSql(
492
+ `${fn}(${fieldExpr(field, columns)})`,
493
+ selection[field],
494
+ params
495
+ )
493
496
  );
494
497
  }
495
498
  }
@@ -517,7 +520,11 @@ function groupBy(ctx, args) {
517
520
  groupCols.push({ alias, field });
518
521
  });
519
522
  selects.push(`COUNT(*) AS agg_count`);
520
- const { selects: accSelects, cols } = buildAccumulators(args, ctx.onPath, ctx.columns);
523
+ const { selects: accSelects, cols } = buildAccumulators(
524
+ args,
525
+ ctx.onPath,
526
+ ctx.columns
527
+ );
521
528
  selects.push(...accSelects);
522
529
  let sql = `SELECT ${selects.join(", ")} FROM "${ctx.table}" WHERE ${where} GROUP BY ${groupExprs.join(", ")}`;
523
530
  if (args.having) {
@@ -646,7 +653,8 @@ var Collection = class {
646
653
  let line = `"${field}" ${sqliteType(def.type)}`;
647
654
  if (def.notNull) line += " NOT NULL";
648
655
  if (def.unique) line += " UNIQUE";
649
- if (def.default !== void 0) line += ` DEFAULT ${formatDefault(def.default)}`;
656
+ if (def.default !== void 0)
657
+ line += ` DEFAULT ${formatDefault(def.default)}`;
650
658
  if (def.references) line += ` REFERENCES ${def.references}`;
651
659
  lines.push(line);
652
660
  }
@@ -710,7 +718,12 @@ var Collection = class {
710
718
  const now = Date.now();
711
719
  const id = input._id != null ? String(input._id) : objectId();
712
720
  const doc = stripSystem(input);
713
- const returned = { ...doc, _id: id, created_at: now, updated_at: now };
721
+ const returned = {
722
+ ...doc,
723
+ _id: id,
724
+ created_at: now,
725
+ updated_at: now
726
+ };
714
727
  if (this.mode === "document") {
715
728
  return {
716
729
  _id: id,
@@ -762,9 +775,65 @@ var Collection = class {
762
775
  ];
763
776
  return { setSql: setParts.join(", "), values };
764
777
  }
765
- /** Sync store, but only for document collections (structured sync is future work). */
778
+ /** Sync store for recording local changes (both document and structured). */
766
779
  get recorder() {
767
- return this.mode === "document" ? this.mon.$sync : void 0;
780
+ return this.mon.$sync;
781
+ }
782
+ /** @internal Read a full document by id (mode-aware), synchronously. */
783
+ getRaw(id) {
784
+ this.ensureTable();
785
+ const row = this.db.prepare(`SELECT * FROM "${this.name}" WHERE _id = ?`).get(id);
786
+ return row ? this.rowToDoc(row) : null;
787
+ }
788
+ /**
789
+ * @internal Apply a remote change to storage WITHOUT recording it to the
790
+ * change feed (the sync store records the `remote` feed row itself). Used by
791
+ * `@monlite/sync` so structured collections sync correctly through the same
792
+ * column/overflow split as local writes.
793
+ */
794
+ applyRemoteWrite(op, id, doc, ts) {
795
+ this.ensureTable();
796
+ if (op === "delete") {
797
+ this.db.prepare(`DELETE FROM "${this.name}" WHERE _id = ?`).run(id);
798
+ return;
799
+ }
800
+ const clean = stripSystem(doc ?? {});
801
+ const createdAt = typeof doc?.created_at === "number" ? doc.created_at : ts;
802
+ if (this.mode === "document") {
803
+ this.db.prepare(
804
+ `INSERT INTO "${this.name}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)
805
+ ON CONFLICT(_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
806
+ ).run(id, JSON.stringify(clean), createdAt, ts);
807
+ return;
808
+ }
809
+ const overflow = {};
810
+ const colValues = {};
811
+ for (const [k, v] of Object.entries(clean)) {
812
+ if (this.columns.has(k)) colValues[k] = v;
813
+ else overflow[k] = v;
814
+ }
815
+ const cols = [
816
+ "_id",
817
+ "created_at",
818
+ "updated_at",
819
+ "data",
820
+ ...this.columnOrder
821
+ ];
822
+ const values = [
823
+ id,
824
+ createdAt,
825
+ ts,
826
+ JSON.stringify(overflow),
827
+ ...this.columnOrder.map(
828
+ (c) => c in colValues ? this.encodeColumn(c, colValues[c]) : null
829
+ )
830
+ ];
831
+ const colList = cols.map((c) => `"${c}"`).join(", ");
832
+ const placeholders = cols.map(() => "?").join(", ");
833
+ const updateSet = cols.filter((c) => c !== "_id" && c !== "created_at").map((c) => `"${c}" = excluded."${c}"`).join(", ");
834
+ this.db.prepare(
835
+ `INSERT INTO "${this.name}" (${colList}) VALUES (${placeholders}) ON CONFLICT(_id) DO UPDATE SET ${updateSet}`
836
+ ).run(...values);
768
837
  }
769
838
  /* ----------------------------- create ----------------------------- */
770
839
  async create(args) {
@@ -993,7 +1062,12 @@ var Collection = class {
993
1062
  this.ensureTable();
994
1063
  return this.guard(
995
1064
  () => aggregate(
996
- { db: this.db, table: this.name, onPath: this.trackPath, columns: this.columns },
1065
+ {
1066
+ db: this.db,
1067
+ table: this.name,
1068
+ onPath: this.trackPath,
1069
+ columns: this.columns
1070
+ },
997
1071
  args
998
1072
  )
999
1073
  );
@@ -1002,7 +1076,12 @@ var Collection = class {
1002
1076
  this.ensureTable();
1003
1077
  return this.guard(
1004
1078
  () => groupBy(
1005
- { db: this.db, table: this.name, onPath: this.trackPath, columns: this.columns },
1079
+ {
1080
+ db: this.db,
1081
+ table: this.name,
1082
+ onPath: this.trackPath,
1083
+ columns: this.columns
1084
+ },
1006
1085
  args
1007
1086
  )
1008
1087
  );
@@ -1250,12 +1329,14 @@ function stripSystem2(obj) {
1250
1329
  return rest;
1251
1330
  }
1252
1331
  var SyncStore = class {
1253
- constructor(db, nodeId) {
1332
+ constructor(db, nodeId, mon) {
1254
1333
  this.db = db;
1334
+ this.mon = mon;
1255
1335
  this.init();
1256
1336
  this.nodeId = this.resolveNodeId(nodeId);
1257
1337
  }
1258
1338
  db;
1339
+ mon;
1259
1340
  nodeId;
1260
1341
  versionSeq = 0;
1261
1342
  init() {
@@ -1290,7 +1371,9 @@ var SyncStore = class {
1290
1371
  }
1291
1372
  resolveNodeId(explicit) {
1292
1373
  if (explicit) {
1293
- this.db.prepare(`INSERT OR REPLACE INTO _monlite_meta (key, value) VALUES ('nodeId', ?)`).run(explicit);
1374
+ this.db.prepare(
1375
+ `INSERT OR REPLACE INTO _monlite_meta (key, value) VALUES ('nodeId', ?)`
1376
+ ).run(explicit);
1294
1377
  return explicit;
1295
1378
  }
1296
1379
  const row = this.db.prepare(`SELECT value FROM _monlite_meta WHERE key = 'nodeId'`).get();
@@ -1405,6 +1488,9 @@ var SyncStore = class {
1405
1488
  );
1406
1489
  }
1407
1490
  if (winner !== "remote") {
1491
+ if (localVersion !== null) {
1492
+ this.recordLocal(change.collection, change._id, "upsert", Date.now());
1493
+ }
1408
1494
  return { applied: false, conflict: localVersion !== null, winner };
1409
1495
  }
1410
1496
  this.applyData(change);
@@ -1418,24 +1504,31 @@ var SyncStore = class {
1418
1504
  change.version,
1419
1505
  versionTs(change.version)
1420
1506
  );
1421
- return { applied: true, conflict: localVersion !== null, winner: "remote" };
1507
+ return {
1508
+ applied: true,
1509
+ conflict: localVersion !== null,
1510
+ winner: "remote"
1511
+ };
1422
1512
  });
1423
1513
  }
1424
1514
  applyData(change) {
1425
- const { collection: coll, _id, op } = change;
1515
+ const ts = versionTs(change.version);
1516
+ if (this.mon) {
1517
+ this.mon.collection(change.collection).applyRemoteWrite(change.op, change._id, change.doc, ts);
1518
+ return;
1519
+ }
1520
+ const coll = change.collection;
1426
1521
  this.ensureCollTable(coll);
1427
- if (op === "delete") {
1428
- this.db.prepare(`DELETE FROM "${coll}" WHERE _id = ?`).run(_id);
1522
+ if (change.op === "delete") {
1523
+ this.db.prepare(`DELETE FROM "${coll}" WHERE _id = ?`).run(change._id);
1429
1524
  return;
1430
1525
  }
1431
1526
  const doc = change.doc ?? {};
1432
- const data = JSON.stringify(stripSystem2(doc));
1433
- const ts = versionTs(change.version);
1434
1527
  const createdAt = typeof doc.created_at === "number" ? doc.created_at : ts;
1435
1528
  this.db.prepare(
1436
1529
  `INSERT INTO "${coll}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)
1437
1530
  ON CONFLICT(_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
1438
- ).run(_id, data, createdAt, ts);
1531
+ ).run(change._id, JSON.stringify(stripSystem2(doc)), createdAt, ts);
1439
1532
  }
1440
1533
  /**
1441
1534
  * Latest change per document with `seq` greater than the given watermark,
@@ -1492,6 +1585,7 @@ var SyncStore = class {
1492
1585
  this.db.transaction(() => {
1493
1586
  for (const coll of collections) {
1494
1587
  assertName(coll);
1588
+ if (!this.tableExists(coll)) continue;
1495
1589
  const docs = this.db.prepare(`SELECT _id, updated_at FROM "${coll}"`).all();
1496
1590
  for (const d of docs) {
1497
1591
  if (this.currentVersion(coll, d._id) !== null) continue;
@@ -1537,7 +1631,14 @@ var SyncStore = class {
1537
1631
  this.db.prepare(
1538
1632
  `INSERT INTO _monlite_conflicts (coll, doc_id, local_version, remote_version, winner, ts)
1539
1633
  VALUES (?, ?, ?, ?, ?, ?)`
1540
- ).run(coll, id, localVersion, remoteVersion, winner, versionTs(remoteVersion));
1634
+ ).run(
1635
+ coll,
1636
+ id,
1637
+ localVersion,
1638
+ remoteVersion,
1639
+ winner,
1640
+ versionTs(remoteVersion)
1641
+ );
1541
1642
  }
1542
1643
  conflicts() {
1543
1644
  const rows = this.db.prepare(
@@ -1556,6 +1657,8 @@ var SyncStore = class {
1556
1657
  /* ------------------------------ helpers ------------------------------- */
1557
1658
  readDoc(coll, id) {
1558
1659
  assertName(coll);
1660
+ if (this.mon) return this.mon.collection(coll).getRaw(id);
1661
+ if (!this.tableExists(coll)) return null;
1559
1662
  const row = this.db.prepare(
1560
1663
  `SELECT _id, data, created_at, updated_at FROM "${coll}" WHERE _id = ?`
1561
1664
  ).get(id);
@@ -1566,6 +1669,9 @@ var SyncStore = class {
1566
1669
  doc.updated_at = row.updated_at;
1567
1670
  return doc;
1568
1671
  }
1672
+ tableExists(name) {
1673
+ return this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?`).get(name) != null;
1674
+ }
1569
1675
  ensureCollTable(coll) {
1570
1676
  assertName(coll);
1571
1677
  this.db.exec(
@@ -1622,7 +1728,7 @@ var Monlite = class {
1622
1728
  options.autoIndexAfter ?? 10
1623
1729
  );
1624
1730
  if (options.sync) {
1625
- this.$sync = new SyncStore(this.driver, options.nodeId);
1731
+ this.$sync = new SyncStore(this.driver, options.nodeId, this);
1626
1732
  }
1627
1733
  }
1628
1734
  /** Stable node id for LWW tie-breaking (only when sync is enabled). */