@monlite/core 0.6.0 → 0.8.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/README.md +42 -2
- package/dist/index.cjs +334 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -3
- package/dist/index.d.ts +112 -3
- package/dist/index.js +334 -47
- package/dist/index.js.map +1 -1
- package/package.json +8 -1
package/dist/index.js
CHANGED
|
@@ -17,6 +17,91 @@ function isObjectId(value) {
|
|
|
17
17
|
return typeof value === "string" && /^[0-9a-f]{24}$/i.test(value);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// src/reactive.ts
|
|
21
|
+
var RELEVANCE_PROBE_LIMIT = 500;
|
|
22
|
+
var LiveQuery = class {
|
|
23
|
+
constructor(source, args, cb) {
|
|
24
|
+
this.source = source;
|
|
25
|
+
this.args = args;
|
|
26
|
+
this.cb = cb;
|
|
27
|
+
this.recompute("init");
|
|
28
|
+
}
|
|
29
|
+
source;
|
|
30
|
+
args;
|
|
31
|
+
cb;
|
|
32
|
+
results = [];
|
|
33
|
+
stopped = false;
|
|
34
|
+
ids = /* @__PURE__ */ new Set();
|
|
35
|
+
/** Called by the Reactor with the ids that changed this tick. */
|
|
36
|
+
notify(changedIds) {
|
|
37
|
+
if (this.stopped) return;
|
|
38
|
+
if (!this.isRelevant(changedIds)) return;
|
|
39
|
+
this.recompute("change", changedIds);
|
|
40
|
+
}
|
|
41
|
+
isRelevant(changedIds) {
|
|
42
|
+
for (const id of changedIds) {
|
|
43
|
+
if (this.ids.has(id)) return true;
|
|
44
|
+
}
|
|
45
|
+
if (changedIds.size > RELEVANCE_PROBE_LIMIT) return true;
|
|
46
|
+
const idIn = {
|
|
47
|
+
_id: { in: [...changedIds] }
|
|
48
|
+
};
|
|
49
|
+
const where = this.args.where ? { AND: [this.args.where, idIn] } : idIn;
|
|
50
|
+
return this.source.existsCore(where);
|
|
51
|
+
}
|
|
52
|
+
recompute(type, changedIds) {
|
|
53
|
+
const next = this.source.findManyCore(this.args);
|
|
54
|
+
const nextById = new Map(next.map((d) => [d._id, d]));
|
|
55
|
+
const added = next.filter((d) => !this.ids.has(d._id));
|
|
56
|
+
const removed = this.results.filter((d) => !nextById.has(d._id));
|
|
57
|
+
const changed = changedIds ? next.filter((d) => this.ids.has(d._id) && changedIds.has(d._id)) : [];
|
|
58
|
+
this.results = next;
|
|
59
|
+
this.ids = new Set(nextById.keys());
|
|
60
|
+
this.cb({ type, results: next, added, removed, changed });
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var Reactor = class {
|
|
64
|
+
byCollection = /* @__PURE__ */ new Map();
|
|
65
|
+
pending = /* @__PURE__ */ new Map();
|
|
66
|
+
flushScheduled = false;
|
|
67
|
+
hasWatchers(collection) {
|
|
68
|
+
return this.byCollection.has(collection);
|
|
69
|
+
}
|
|
70
|
+
register(collection, lq) {
|
|
71
|
+
let set = this.byCollection.get(collection);
|
|
72
|
+
if (!set) this.byCollection.set(collection, set = /* @__PURE__ */ new Set());
|
|
73
|
+
set.add(lq);
|
|
74
|
+
}
|
|
75
|
+
unregister(collection, lq) {
|
|
76
|
+
const set = this.byCollection.get(collection);
|
|
77
|
+
if (!set) return;
|
|
78
|
+
set.delete(lq);
|
|
79
|
+
if (set.size === 0) this.byCollection.delete(collection);
|
|
80
|
+
}
|
|
81
|
+
/** Record that documents changed; schedule a notification flush. */
|
|
82
|
+
emit(collection, ids) {
|
|
83
|
+
const set = this.byCollection.get(collection);
|
|
84
|
+
if (!set || set.size === 0 || ids.length === 0) return;
|
|
85
|
+
let p = this.pending.get(collection);
|
|
86
|
+
if (!p) this.pending.set(collection, p = /* @__PURE__ */ new Set());
|
|
87
|
+
for (const id of ids) p.add(id);
|
|
88
|
+
if (!this.flushScheduled) {
|
|
89
|
+
this.flushScheduled = true;
|
|
90
|
+
queueMicrotask(() => this.flush());
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
flush() {
|
|
94
|
+
this.flushScheduled = false;
|
|
95
|
+
const work = [...this.pending];
|
|
96
|
+
this.pending.clear();
|
|
97
|
+
for (const [collection, ids] of work) {
|
|
98
|
+
const set = this.byCollection.get(collection);
|
|
99
|
+
if (!set) continue;
|
|
100
|
+
for (const lq of [...set]) lq.notify(ids);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
20
105
|
// src/errors.ts
|
|
21
106
|
var MonliteError = class extends Error {
|
|
22
107
|
constructor(message, options) {
|
|
@@ -236,9 +321,7 @@ function cmp(expr, op, v, ctx) {
|
|
|
236
321
|
}
|
|
237
322
|
function inExpr(expr, arr, ctx, negate) {
|
|
238
323
|
if (!Array.isArray(arr)) {
|
|
239
|
-
throw new MonliteQueryError(
|
|
240
|
-
`${negate ? "notIn" : "in"} expects an array`
|
|
241
|
-
);
|
|
324
|
+
throw new MonliteQueryError(`${negate ? "notIn" : "in"} expects an array`);
|
|
242
325
|
}
|
|
243
326
|
if (arr.length === 0) return negate ? "1" : "0";
|
|
244
327
|
const placeholders = arr.map((v) => {
|
|
@@ -368,7 +451,8 @@ function applyUpdate(doc, data) {
|
|
|
368
451
|
}
|
|
369
452
|
const ops = data;
|
|
370
453
|
if (ops.$set) {
|
|
371
|
-
for (const [path, value] of Object.entries(ops.$set))
|
|
454
|
+
for (const [path, value] of Object.entries(ops.$set))
|
|
455
|
+
setPath(next, path, value);
|
|
372
456
|
}
|
|
373
457
|
if (ops.$inc) {
|
|
374
458
|
for (const [path, by] of Object.entries(ops.$inc)) {
|
|
@@ -489,7 +573,11 @@ function buildHaving(having, params, columns) {
|
|
|
489
573
|
if (!selection) continue;
|
|
490
574
|
for (const field of Object.keys(selection)) {
|
|
491
575
|
parts.push(
|
|
492
|
-
...comparisonSql(
|
|
576
|
+
...comparisonSql(
|
|
577
|
+
`${fn}(${fieldExpr(field, columns)})`,
|
|
578
|
+
selection[field],
|
|
579
|
+
params
|
|
580
|
+
)
|
|
493
581
|
);
|
|
494
582
|
}
|
|
495
583
|
}
|
|
@@ -517,7 +605,11 @@ function groupBy(ctx, args) {
|
|
|
517
605
|
groupCols.push({ alias, field });
|
|
518
606
|
});
|
|
519
607
|
selects.push(`COUNT(*) AS agg_count`);
|
|
520
|
-
const { selects: accSelects, cols } = buildAccumulators(
|
|
608
|
+
const { selects: accSelects, cols } = buildAccumulators(
|
|
609
|
+
args,
|
|
610
|
+
ctx.onPath,
|
|
611
|
+
ctx.columns
|
|
612
|
+
);
|
|
521
613
|
selects.push(...accSelects);
|
|
522
614
|
let sql = `SELECT ${selects.join(", ")} FROM "${ctx.table}" WHERE ${where} GROUP BY ${groupExprs.join(", ")}`;
|
|
523
615
|
if (args.having) {
|
|
@@ -595,6 +687,7 @@ var Collection = class {
|
|
|
595
687
|
this.columns.add(field);
|
|
596
688
|
if (normalized.type === "JSON") this.jsonColumns.add(field);
|
|
597
689
|
}
|
|
690
|
+
this.ensureTable();
|
|
598
691
|
}
|
|
599
692
|
}
|
|
600
693
|
mon;
|
|
@@ -639,22 +732,15 @@ var Collection = class {
|
|
|
639
732
|
`_id TEXT PRIMARY KEY`,
|
|
640
733
|
`created_at INTEGER NOT NULL`,
|
|
641
734
|
`updated_at INTEGER NOT NULL`,
|
|
642
|
-
`data TEXT NOT NULL DEFAULT '{}'
|
|
735
|
+
`data TEXT NOT NULL DEFAULT '{}'`,
|
|
736
|
+
...this.columnOrder.map((f) => this.columnDdl(f, false))
|
|
643
737
|
];
|
|
644
|
-
for (const field of this.columnOrder) {
|
|
645
|
-
const def = this.columnDefs[field];
|
|
646
|
-
let line = `"${field}" ${sqliteType(def.type)}`;
|
|
647
|
-
if (def.notNull) line += " NOT NULL";
|
|
648
|
-
if (def.unique) line += " UNIQUE";
|
|
649
|
-
if (def.default !== void 0) line += ` DEFAULT ${formatDefault(def.default)}`;
|
|
650
|
-
if (def.references) line += ` REFERENCES ${def.references}`;
|
|
651
|
-
lines.push(line);
|
|
652
|
-
}
|
|
653
738
|
this.db.exec(
|
|
654
739
|
`CREATE TABLE IF NOT EXISTS "${this.name}" (
|
|
655
740
|
${lines.join(",\n ")}
|
|
656
741
|
)`
|
|
657
742
|
);
|
|
743
|
+
this.migrateColumns();
|
|
658
744
|
for (const field of this.columnOrder) {
|
|
659
745
|
if (this.columnDefs[field].index) {
|
|
660
746
|
this.db.exec(
|
|
@@ -665,6 +751,39 @@ var Collection = class {
|
|
|
665
751
|
}
|
|
666
752
|
this.initialized = true;
|
|
667
753
|
}
|
|
754
|
+
columnDdl(field, forAlter) {
|
|
755
|
+
const def = this.columnDefs[field];
|
|
756
|
+
let line = `"${field}" ${sqliteType(def.type)}`;
|
|
757
|
+
if (def.notNull) line += " NOT NULL";
|
|
758
|
+
if (def.unique && !forAlter) line += " UNIQUE";
|
|
759
|
+
if (def.default !== void 0)
|
|
760
|
+
line += ` DEFAULT ${formatDefault(def.default)}`;
|
|
761
|
+
if (def.references) line += ` REFERENCES ${def.references}`;
|
|
762
|
+
return line;
|
|
763
|
+
}
|
|
764
|
+
/** Auto-additive migration: add declared columns missing from an existing table. */
|
|
765
|
+
migrateColumns() {
|
|
766
|
+
const existing = new Set(
|
|
767
|
+
this.db.prepare(`PRAGMA table_info("${this.name}")`).all().map((r) => r.name)
|
|
768
|
+
);
|
|
769
|
+
for (const field of this.columnOrder) {
|
|
770
|
+
if (existing.has(field)) continue;
|
|
771
|
+
try {
|
|
772
|
+
this.db.exec(
|
|
773
|
+
`ALTER TABLE "${this.name}" ADD COLUMN ${this.columnDdl(field, true)}`
|
|
774
|
+
);
|
|
775
|
+
} catch (err) {
|
|
776
|
+
throw new MonliteError(
|
|
777
|
+
`Failed to add column "${field}" to "${this.name}": ${err.message}. NOT NULL columns need a default when added to an existing table.`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
if (this.columnDefs[field].unique) {
|
|
781
|
+
this.db.exec(
|
|
782
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "uq_${this.name}_${field}" ON "${this.name}"("${field}")`
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
668
787
|
/* --------------------------- row <-> doc -------------------------- */
|
|
669
788
|
rowToDoc(row) {
|
|
670
789
|
const doc = this.mode === "document" ? JSON.parse(row.data) : JSON.parse(row.data ?? "{}");
|
|
@@ -710,7 +829,12 @@ var Collection = class {
|
|
|
710
829
|
const now = Date.now();
|
|
711
830
|
const id = input._id != null ? String(input._id) : objectId();
|
|
712
831
|
const doc = stripSystem(input);
|
|
713
|
-
const returned = {
|
|
832
|
+
const returned = {
|
|
833
|
+
...doc,
|
|
834
|
+
_id: id,
|
|
835
|
+
created_at: now,
|
|
836
|
+
updated_at: now
|
|
837
|
+
};
|
|
714
838
|
if (this.mode === "document") {
|
|
715
839
|
return {
|
|
716
840
|
_id: id,
|
|
@@ -762,9 +886,72 @@ var Collection = class {
|
|
|
762
886
|
];
|
|
763
887
|
return { setSql: setParts.join(", "), values };
|
|
764
888
|
}
|
|
765
|
-
/** Sync store
|
|
889
|
+
/** Sync store for recording local changes (both document and structured). */
|
|
766
890
|
get recorder() {
|
|
767
|
-
return this.
|
|
891
|
+
return this.mon.$sync;
|
|
892
|
+
}
|
|
893
|
+
/** @internal Read a full document by id (mode-aware), synchronously. */
|
|
894
|
+
getRaw(id) {
|
|
895
|
+
this.ensureTable();
|
|
896
|
+
const row = this.db.prepare(`SELECT * FROM "${this.name}" WHERE _id = ?`).get(id);
|
|
897
|
+
return row ? this.rowToDoc(row) : null;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* @internal Apply a remote change to storage WITHOUT recording it to the
|
|
901
|
+
* change feed (the sync store records the `remote` feed row itself). Used by
|
|
902
|
+
* `@monlite/sync` so structured collections sync correctly through the same
|
|
903
|
+
* column/overflow split as local writes.
|
|
904
|
+
*/
|
|
905
|
+
applyRemoteWrite(op, id, doc, ts) {
|
|
906
|
+
this.ensureTable();
|
|
907
|
+
if (op === "delete") {
|
|
908
|
+
this.db.prepare(`DELETE FROM "${this.name}" WHERE _id = ?`).run(id);
|
|
909
|
+
this.afterWrite([id]);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const clean = stripSystem(doc ?? {});
|
|
913
|
+
const createdAt = typeof doc?.created_at === "number" ? doc.created_at : ts;
|
|
914
|
+
if (this.mode === "document") {
|
|
915
|
+
this.db.prepare(
|
|
916
|
+
`INSERT INTO "${this.name}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
917
|
+
ON CONFLICT(_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
|
|
918
|
+
).run(id, JSON.stringify(clean), createdAt, ts);
|
|
919
|
+
this.afterWrite([id]);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const overflow = {};
|
|
923
|
+
const colValues = {};
|
|
924
|
+
for (const [k, v] of Object.entries(clean)) {
|
|
925
|
+
if (this.columns.has(k)) colValues[k] = v;
|
|
926
|
+
else overflow[k] = v;
|
|
927
|
+
}
|
|
928
|
+
const cols = [
|
|
929
|
+
"_id",
|
|
930
|
+
"created_at",
|
|
931
|
+
"updated_at",
|
|
932
|
+
"data",
|
|
933
|
+
...this.columnOrder
|
|
934
|
+
];
|
|
935
|
+
const values = [
|
|
936
|
+
id,
|
|
937
|
+
createdAt,
|
|
938
|
+
ts,
|
|
939
|
+
JSON.stringify(overflow),
|
|
940
|
+
...this.columnOrder.map(
|
|
941
|
+
(c) => c in colValues ? this.encodeColumn(c, colValues[c]) : null
|
|
942
|
+
)
|
|
943
|
+
];
|
|
944
|
+
const colList = cols.map((c) => `"${c}"`).join(", ");
|
|
945
|
+
const placeholders = cols.map(() => "?").join(", ");
|
|
946
|
+
const updateSet = cols.filter((c) => c !== "_id" && c !== "created_at").map((c) => `"${c}" = excluded."${c}"`).join(", ");
|
|
947
|
+
this.db.prepare(
|
|
948
|
+
`INSERT INTO "${this.name}" (${colList}) VALUES (${placeholders}) ON CONFLICT(_id) DO UPDATE SET ${updateSet}`
|
|
949
|
+
).run(...values);
|
|
950
|
+
this.afterWrite([id]);
|
|
951
|
+
}
|
|
952
|
+
/** @internal Notify reactivity watchers that documents changed. */
|
|
953
|
+
afterWrite(ids) {
|
|
954
|
+
this.mon.reactor.emit(this.name, ids);
|
|
768
955
|
}
|
|
769
956
|
/* ----------------------------- create ----------------------------- */
|
|
770
957
|
async create(args) {
|
|
@@ -776,26 +963,29 @@ var Collection = class {
|
|
|
776
963
|
recorder?.recordLocal(this.name, row._id, "upsert", row.created_at);
|
|
777
964
|
};
|
|
778
965
|
this.guard(() => recorder ? this.db.transaction(write) : write());
|
|
966
|
+
this.afterWrite([row._id]);
|
|
779
967
|
return row.returned;
|
|
780
968
|
}
|
|
781
969
|
async createMany(args) {
|
|
782
970
|
this.ensureTable();
|
|
783
971
|
const stmt = this.db.prepare(this.insertSql());
|
|
784
972
|
const recorder = this.recorder;
|
|
973
|
+
const ids = [];
|
|
785
974
|
this.guard(
|
|
786
975
|
() => this.db.transaction(() => {
|
|
787
976
|
for (const item of args.data) {
|
|
788
977
|
const row = this.buildInsert(item);
|
|
789
978
|
stmt.run(...row.values);
|
|
790
979
|
recorder?.recordLocal(this.name, row._id, "upsert", row.created_at);
|
|
980
|
+
ids.push(row._id);
|
|
791
981
|
}
|
|
792
982
|
})
|
|
793
983
|
);
|
|
984
|
+
this.afterWrite(ids);
|
|
794
985
|
return { count: args.data.length };
|
|
795
986
|
}
|
|
796
987
|
/* ------------------------------ read ------------------------------ */
|
|
797
|
-
|
|
798
|
-
this.ensureTable();
|
|
988
|
+
buildFindSql(args) {
|
|
799
989
|
const params = [];
|
|
800
990
|
const where = buildWhere(args.where, {
|
|
801
991
|
params,
|
|
@@ -813,9 +1003,29 @@ var Collection = class {
|
|
|
813
1003
|
sql += (args.take != null ? "" : " LIMIT -1") + " OFFSET ?";
|
|
814
1004
|
params.push(args.skip);
|
|
815
1005
|
}
|
|
1006
|
+
return { sql, params };
|
|
1007
|
+
}
|
|
1008
|
+
/** @internal Synchronous core of findMany (used by reactivity). */
|
|
1009
|
+
findManyCore(args = {}) {
|
|
1010
|
+
this.ensureTable();
|
|
1011
|
+
const { sql, params } = this.buildFindSql(args);
|
|
816
1012
|
const rows = this.db.prepare(sql).all(...params);
|
|
817
1013
|
return rows.map((r) => project(this.rowToDoc(r), args.select));
|
|
818
1014
|
}
|
|
1015
|
+
/** @internal Synchronous core of exists (used by reactivity). */
|
|
1016
|
+
existsCore(where) {
|
|
1017
|
+
this.ensureTable();
|
|
1018
|
+
const params = [];
|
|
1019
|
+
const clause = buildWhere(where, {
|
|
1020
|
+
params,
|
|
1021
|
+
onPath: this.trackPath,
|
|
1022
|
+
columns: this.columns
|
|
1023
|
+
});
|
|
1024
|
+
return this.db.prepare(`SELECT 1 FROM "${this.name}" WHERE ${clause} LIMIT 1`).get(...params) != null;
|
|
1025
|
+
}
|
|
1026
|
+
async findMany(args = {}) {
|
|
1027
|
+
return this.findManyCore(args);
|
|
1028
|
+
}
|
|
819
1029
|
async findFirst(args = {}) {
|
|
820
1030
|
const rows = await this.findMany({ ...args, take: 1 });
|
|
821
1031
|
return rows[0] ?? null;
|
|
@@ -832,15 +1042,39 @@ var Collection = class {
|
|
|
832
1042
|
}
|
|
833
1043
|
/** True if at least one document matches. */
|
|
834
1044
|
async exists(where) {
|
|
1045
|
+
return this.existsCore(where);
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Subscribe to a live query. The callback fires immediately with the current
|
|
1049
|
+
* results (`type: "init"`) and again whenever a change affects the result set
|
|
1050
|
+
* (row-level: only relevant changes trigger a recompute). Includes changes
|
|
1051
|
+
* applied by `@monlite/sync`.
|
|
1052
|
+
*/
|
|
1053
|
+
watch(args = {}, cb) {
|
|
835
1054
|
this.ensureTable();
|
|
836
|
-
const
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1055
|
+
const lq = new LiveQuery(this, args, cb);
|
|
1056
|
+
const reactor = this.mon.reactor;
|
|
1057
|
+
const name = this.name;
|
|
1058
|
+
reactor.register(name, lq);
|
|
1059
|
+
return {
|
|
1060
|
+
get results() {
|
|
1061
|
+
return lq.results;
|
|
1062
|
+
},
|
|
1063
|
+
stop() {
|
|
1064
|
+
lq.stopped = true;
|
|
1065
|
+
reactor.unregister(name, lq);
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
/** Show SQLite's query plan for a `findMany`, and whether it uses an index. */
|
|
1070
|
+
async explain(args = {}) {
|
|
1071
|
+
this.ensureTable();
|
|
1072
|
+
const { sql, params } = this.buildFindSql(args);
|
|
1073
|
+
const plan = this.db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(...params);
|
|
1074
|
+
const usesIndex = plan.some(
|
|
1075
|
+
(r) => /USING (COVERING )?INDEX/i.test(r.detail)
|
|
1076
|
+
);
|
|
1077
|
+
return { sql, usesIndex, plan };
|
|
844
1078
|
}
|
|
845
1079
|
async findById(id) {
|
|
846
1080
|
this.ensureTable();
|
|
@@ -897,25 +1131,27 @@ var Collection = class {
|
|
|
897
1131
|
if (!rows.length) return [];
|
|
898
1132
|
const now = Date.now();
|
|
899
1133
|
const recorder = this.recorder;
|
|
900
|
-
|
|
1134
|
+
const out = this.guard(
|
|
901
1135
|
() => this.db.transaction(() => {
|
|
902
|
-
const
|
|
1136
|
+
const result = [];
|
|
903
1137
|
for (const row of rows) {
|
|
904
1138
|
const current = stripSystem(this.rowToDoc(row));
|
|
905
1139
|
const updated = stripSystem(applyUpdate(current, data));
|
|
906
1140
|
const { setSql, values } = this.buildUpdateSet(updated, now);
|
|
907
1141
|
this.db.prepare(`UPDATE "${this.name}" SET ${setSql} WHERE _id = ?`).run(...values, row._id);
|
|
908
1142
|
recorder?.recordLocal(this.name, row._id, "upsert", now);
|
|
909
|
-
|
|
1143
|
+
result.push({
|
|
910
1144
|
...updated,
|
|
911
1145
|
_id: row._id,
|
|
912
1146
|
created_at: row.created_at,
|
|
913
1147
|
updated_at: now
|
|
914
1148
|
});
|
|
915
1149
|
}
|
|
916
|
-
return
|
|
1150
|
+
return result;
|
|
917
1151
|
})
|
|
918
1152
|
);
|
|
1153
|
+
this.afterWrite(out.map((d) => d._id));
|
|
1154
|
+
return out;
|
|
919
1155
|
}
|
|
920
1156
|
async update(args) {
|
|
921
1157
|
return this.runUpdate(args.where, args.data, true)[0] ?? null;
|
|
@@ -925,7 +1161,7 @@ var Collection = class {
|
|
|
925
1161
|
}
|
|
926
1162
|
async upsert(args) {
|
|
927
1163
|
this.ensureTable();
|
|
928
|
-
|
|
1164
|
+
const result = this.guard(
|
|
929
1165
|
() => this.db.transaction(() => {
|
|
930
1166
|
const params = [];
|
|
931
1167
|
const clause = buildWhere(args.where, {
|
|
@@ -955,6 +1191,8 @@ var Collection = class {
|
|
|
955
1191
|
return ins.returned;
|
|
956
1192
|
})
|
|
957
1193
|
);
|
|
1194
|
+
this.afterWrite([result._id]);
|
|
1195
|
+
return result;
|
|
958
1196
|
}
|
|
959
1197
|
/* ----------------------------- delete ----------------------------- */
|
|
960
1198
|
runDelete(where, single) {
|
|
@@ -980,6 +1218,7 @@ var Collection = class {
|
|
|
980
1218
|
}
|
|
981
1219
|
})
|
|
982
1220
|
);
|
|
1221
|
+
this.afterWrite(rows.map((r) => r._id));
|
|
983
1222
|
return rows.map((r) => this.rowToDoc(r));
|
|
984
1223
|
}
|
|
985
1224
|
async delete(args) {
|
|
@@ -993,7 +1232,12 @@ var Collection = class {
|
|
|
993
1232
|
this.ensureTable();
|
|
994
1233
|
return this.guard(
|
|
995
1234
|
() => aggregate(
|
|
996
|
-
{
|
|
1235
|
+
{
|
|
1236
|
+
db: this.db,
|
|
1237
|
+
table: this.name,
|
|
1238
|
+
onPath: this.trackPath,
|
|
1239
|
+
columns: this.columns
|
|
1240
|
+
},
|
|
997
1241
|
args
|
|
998
1242
|
)
|
|
999
1243
|
);
|
|
@@ -1002,7 +1246,12 @@ var Collection = class {
|
|
|
1002
1246
|
this.ensureTable();
|
|
1003
1247
|
return this.guard(
|
|
1004
1248
|
() => groupBy(
|
|
1005
|
-
{
|
|
1249
|
+
{
|
|
1250
|
+
db: this.db,
|
|
1251
|
+
table: this.name,
|
|
1252
|
+
onPath: this.trackPath,
|
|
1253
|
+
columns: this.columns
|
|
1254
|
+
},
|
|
1006
1255
|
args
|
|
1007
1256
|
)
|
|
1008
1257
|
);
|
|
@@ -1250,12 +1499,14 @@ function stripSystem2(obj) {
|
|
|
1250
1499
|
return rest;
|
|
1251
1500
|
}
|
|
1252
1501
|
var SyncStore = class {
|
|
1253
|
-
constructor(db, nodeId) {
|
|
1502
|
+
constructor(db, nodeId, mon) {
|
|
1254
1503
|
this.db = db;
|
|
1504
|
+
this.mon = mon;
|
|
1255
1505
|
this.init();
|
|
1256
1506
|
this.nodeId = this.resolveNodeId(nodeId);
|
|
1257
1507
|
}
|
|
1258
1508
|
db;
|
|
1509
|
+
mon;
|
|
1259
1510
|
nodeId;
|
|
1260
1511
|
versionSeq = 0;
|
|
1261
1512
|
init() {
|
|
@@ -1290,7 +1541,9 @@ var SyncStore = class {
|
|
|
1290
1541
|
}
|
|
1291
1542
|
resolveNodeId(explicit) {
|
|
1292
1543
|
if (explicit) {
|
|
1293
|
-
this.db.prepare(
|
|
1544
|
+
this.db.prepare(
|
|
1545
|
+
`INSERT OR REPLACE INTO _monlite_meta (key, value) VALUES ('nodeId', ?)`
|
|
1546
|
+
).run(explicit);
|
|
1294
1547
|
return explicit;
|
|
1295
1548
|
}
|
|
1296
1549
|
const row = this.db.prepare(`SELECT value FROM _monlite_meta WHERE key = 'nodeId'`).get();
|
|
@@ -1405,6 +1658,9 @@ var SyncStore = class {
|
|
|
1405
1658
|
);
|
|
1406
1659
|
}
|
|
1407
1660
|
if (winner !== "remote") {
|
|
1661
|
+
if (localVersion !== null) {
|
|
1662
|
+
this.recordLocal(change.collection, change._id, "upsert", Date.now());
|
|
1663
|
+
}
|
|
1408
1664
|
return { applied: false, conflict: localVersion !== null, winner };
|
|
1409
1665
|
}
|
|
1410
1666
|
this.applyData(change);
|
|
@@ -1418,24 +1674,31 @@ var SyncStore = class {
|
|
|
1418
1674
|
change.version,
|
|
1419
1675
|
versionTs(change.version)
|
|
1420
1676
|
);
|
|
1421
|
-
return {
|
|
1677
|
+
return {
|
|
1678
|
+
applied: true,
|
|
1679
|
+
conflict: localVersion !== null,
|
|
1680
|
+
winner: "remote"
|
|
1681
|
+
};
|
|
1422
1682
|
});
|
|
1423
1683
|
}
|
|
1424
1684
|
applyData(change) {
|
|
1425
|
-
const
|
|
1685
|
+
const ts = versionTs(change.version);
|
|
1686
|
+
if (this.mon) {
|
|
1687
|
+
this.mon.collection(change.collection).applyRemoteWrite(change.op, change._id, change.doc, ts);
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
const coll = change.collection;
|
|
1426
1691
|
this.ensureCollTable(coll);
|
|
1427
|
-
if (op === "delete") {
|
|
1428
|
-
this.db.prepare(`DELETE FROM "${coll}" WHERE _id = ?`).run(_id);
|
|
1692
|
+
if (change.op === "delete") {
|
|
1693
|
+
this.db.prepare(`DELETE FROM "${coll}" WHERE _id = ?`).run(change._id);
|
|
1429
1694
|
return;
|
|
1430
1695
|
}
|
|
1431
1696
|
const doc = change.doc ?? {};
|
|
1432
|
-
const data = JSON.stringify(stripSystem2(doc));
|
|
1433
|
-
const ts = versionTs(change.version);
|
|
1434
1697
|
const createdAt = typeof doc.created_at === "number" ? doc.created_at : ts;
|
|
1435
1698
|
this.db.prepare(
|
|
1436
1699
|
`INSERT INTO "${coll}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
1437
1700
|
ON CONFLICT(_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
|
|
1438
|
-
).run(_id,
|
|
1701
|
+
).run(change._id, JSON.stringify(stripSystem2(doc)), createdAt, ts);
|
|
1439
1702
|
}
|
|
1440
1703
|
/**
|
|
1441
1704
|
* Latest change per document with `seq` greater than the given watermark,
|
|
@@ -1492,6 +1755,7 @@ var SyncStore = class {
|
|
|
1492
1755
|
this.db.transaction(() => {
|
|
1493
1756
|
for (const coll of collections) {
|
|
1494
1757
|
assertName(coll);
|
|
1758
|
+
if (!this.tableExists(coll)) continue;
|
|
1495
1759
|
const docs = this.db.prepare(`SELECT _id, updated_at FROM "${coll}"`).all();
|
|
1496
1760
|
for (const d of docs) {
|
|
1497
1761
|
if (this.currentVersion(coll, d._id) !== null) continue;
|
|
@@ -1537,7 +1801,14 @@ var SyncStore = class {
|
|
|
1537
1801
|
this.db.prepare(
|
|
1538
1802
|
`INSERT INTO _monlite_conflicts (coll, doc_id, local_version, remote_version, winner, ts)
|
|
1539
1803
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1540
|
-
).run(
|
|
1804
|
+
).run(
|
|
1805
|
+
coll,
|
|
1806
|
+
id,
|
|
1807
|
+
localVersion,
|
|
1808
|
+
remoteVersion,
|
|
1809
|
+
winner,
|
|
1810
|
+
versionTs(remoteVersion)
|
|
1811
|
+
);
|
|
1541
1812
|
}
|
|
1542
1813
|
conflicts() {
|
|
1543
1814
|
const rows = this.db.prepare(
|
|
@@ -1556,6 +1827,8 @@ var SyncStore = class {
|
|
|
1556
1827
|
/* ------------------------------ helpers ------------------------------- */
|
|
1557
1828
|
readDoc(coll, id) {
|
|
1558
1829
|
assertName(coll);
|
|
1830
|
+
if (this.mon) return this.mon.collection(coll).getRaw(id);
|
|
1831
|
+
if (!this.tableExists(coll)) return null;
|
|
1559
1832
|
const row = this.db.prepare(
|
|
1560
1833
|
`SELECT _id, data, created_at, updated_at FROM "${coll}" WHERE _id = ?`
|
|
1561
1834
|
).get(id);
|
|
@@ -1566,6 +1839,9 @@ var SyncStore = class {
|
|
|
1566
1839
|
doc.updated_at = row.updated_at;
|
|
1567
1840
|
return doc;
|
|
1568
1841
|
}
|
|
1842
|
+
tableExists(name) {
|
|
1843
|
+
return this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?`).get(name) != null;
|
|
1844
|
+
}
|
|
1569
1845
|
ensureCollTable(coll) {
|
|
1570
1846
|
assertName(coll);
|
|
1571
1847
|
this.db.exec(
|
|
@@ -1604,6 +1880,8 @@ var Monlite = class {
|
|
|
1604
1880
|
driver;
|
|
1605
1881
|
/** @internal */
|
|
1606
1882
|
autoIndexer;
|
|
1883
|
+
/** @internal Reactivity hub for `collection.watch()`. */
|
|
1884
|
+
reactor = new Reactor();
|
|
1607
1885
|
/** @internal Sync metadata store; present only when `{ sync: true }`. */
|
|
1608
1886
|
$sync;
|
|
1609
1887
|
collections = /* @__PURE__ */ new Map();
|
|
@@ -1622,7 +1900,7 @@ var Monlite = class {
|
|
|
1622
1900
|
options.autoIndexAfter ?? 10
|
|
1623
1901
|
);
|
|
1624
1902
|
if (options.sync) {
|
|
1625
|
-
this.$sync = new SyncStore(this.driver, options.nodeId);
|
|
1903
|
+
this.$sync = new SyncStore(this.driver, options.nodeId, this);
|
|
1626
1904
|
}
|
|
1627
1905
|
}
|
|
1628
1906
|
/** Stable node id for LWW tie-breaking (only when sync is enabled). */
|
|
@@ -1738,6 +2016,15 @@ var Monlite = class {
|
|
|
1738
2016
|
async $dropAll() {
|
|
1739
2017
|
for (const name of await this.$collections()) await this.$drop(name);
|
|
1740
2018
|
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Write a consistent on-disk snapshot of the database to `path` (via
|
|
2021
|
+
* `VACUUM INTO`). The destination file must not already exist.
|
|
2022
|
+
*/
|
|
2023
|
+
backup(path) {
|
|
2024
|
+
this.assertOpen();
|
|
2025
|
+
this.driver.exec(`VACUUM INTO '${path.replace(/'/g, "''")}'`);
|
|
2026
|
+
return Promise.resolve();
|
|
2027
|
+
}
|
|
1741
2028
|
/** Close the underlying SQLite connection. */
|
|
1742
2029
|
$disconnect() {
|
|
1743
2030
|
if (!this.closed) {
|