@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.cjs
CHANGED
|
@@ -20,6 +20,91 @@ function isObjectId(value) {
|
|
|
20
20
|
return typeof value === "string" && /^[0-9a-f]{24}$/i.test(value);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// src/reactive.ts
|
|
24
|
+
var RELEVANCE_PROBE_LIMIT = 500;
|
|
25
|
+
var LiveQuery = class {
|
|
26
|
+
constructor(source, args, cb) {
|
|
27
|
+
this.source = source;
|
|
28
|
+
this.args = args;
|
|
29
|
+
this.cb = cb;
|
|
30
|
+
this.recompute("init");
|
|
31
|
+
}
|
|
32
|
+
source;
|
|
33
|
+
args;
|
|
34
|
+
cb;
|
|
35
|
+
results = [];
|
|
36
|
+
stopped = false;
|
|
37
|
+
ids = /* @__PURE__ */ new Set();
|
|
38
|
+
/** Called by the Reactor with the ids that changed this tick. */
|
|
39
|
+
notify(changedIds) {
|
|
40
|
+
if (this.stopped) return;
|
|
41
|
+
if (!this.isRelevant(changedIds)) return;
|
|
42
|
+
this.recompute("change", changedIds);
|
|
43
|
+
}
|
|
44
|
+
isRelevant(changedIds) {
|
|
45
|
+
for (const id of changedIds) {
|
|
46
|
+
if (this.ids.has(id)) return true;
|
|
47
|
+
}
|
|
48
|
+
if (changedIds.size > RELEVANCE_PROBE_LIMIT) return true;
|
|
49
|
+
const idIn = {
|
|
50
|
+
_id: { in: [...changedIds] }
|
|
51
|
+
};
|
|
52
|
+
const where = this.args.where ? { AND: [this.args.where, idIn] } : idIn;
|
|
53
|
+
return this.source.existsCore(where);
|
|
54
|
+
}
|
|
55
|
+
recompute(type, changedIds) {
|
|
56
|
+
const next = this.source.findManyCore(this.args);
|
|
57
|
+
const nextById = new Map(next.map((d) => [d._id, d]));
|
|
58
|
+
const added = next.filter((d) => !this.ids.has(d._id));
|
|
59
|
+
const removed = this.results.filter((d) => !nextById.has(d._id));
|
|
60
|
+
const changed = changedIds ? next.filter((d) => this.ids.has(d._id) && changedIds.has(d._id)) : [];
|
|
61
|
+
this.results = next;
|
|
62
|
+
this.ids = new Set(nextById.keys());
|
|
63
|
+
this.cb({ type, results: next, added, removed, changed });
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var Reactor = class {
|
|
67
|
+
byCollection = /* @__PURE__ */ new Map();
|
|
68
|
+
pending = /* @__PURE__ */ new Map();
|
|
69
|
+
flushScheduled = false;
|
|
70
|
+
hasWatchers(collection) {
|
|
71
|
+
return this.byCollection.has(collection);
|
|
72
|
+
}
|
|
73
|
+
register(collection, lq) {
|
|
74
|
+
let set = this.byCollection.get(collection);
|
|
75
|
+
if (!set) this.byCollection.set(collection, set = /* @__PURE__ */ new Set());
|
|
76
|
+
set.add(lq);
|
|
77
|
+
}
|
|
78
|
+
unregister(collection, lq) {
|
|
79
|
+
const set = this.byCollection.get(collection);
|
|
80
|
+
if (!set) return;
|
|
81
|
+
set.delete(lq);
|
|
82
|
+
if (set.size === 0) this.byCollection.delete(collection);
|
|
83
|
+
}
|
|
84
|
+
/** Record that documents changed; schedule a notification flush. */
|
|
85
|
+
emit(collection, ids) {
|
|
86
|
+
const set = this.byCollection.get(collection);
|
|
87
|
+
if (!set || set.size === 0 || ids.length === 0) return;
|
|
88
|
+
let p = this.pending.get(collection);
|
|
89
|
+
if (!p) this.pending.set(collection, p = /* @__PURE__ */ new Set());
|
|
90
|
+
for (const id of ids) p.add(id);
|
|
91
|
+
if (!this.flushScheduled) {
|
|
92
|
+
this.flushScheduled = true;
|
|
93
|
+
queueMicrotask(() => this.flush());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
flush() {
|
|
97
|
+
this.flushScheduled = false;
|
|
98
|
+
const work = [...this.pending];
|
|
99
|
+
this.pending.clear();
|
|
100
|
+
for (const [collection, ids] of work) {
|
|
101
|
+
const set = this.byCollection.get(collection);
|
|
102
|
+
if (!set) continue;
|
|
103
|
+
for (const lq of [...set]) lq.notify(ids);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
23
108
|
// src/errors.ts
|
|
24
109
|
var MonliteError = class extends Error {
|
|
25
110
|
constructor(message, options) {
|
|
@@ -239,9 +324,7 @@ function cmp(expr, op, v, ctx) {
|
|
|
239
324
|
}
|
|
240
325
|
function inExpr(expr, arr, ctx, negate) {
|
|
241
326
|
if (!Array.isArray(arr)) {
|
|
242
|
-
throw new MonliteQueryError(
|
|
243
|
-
`${negate ? "notIn" : "in"} expects an array`
|
|
244
|
-
);
|
|
327
|
+
throw new MonliteQueryError(`${negate ? "notIn" : "in"} expects an array`);
|
|
245
328
|
}
|
|
246
329
|
if (arr.length === 0) return negate ? "1" : "0";
|
|
247
330
|
const placeholders = arr.map((v) => {
|
|
@@ -371,7 +454,8 @@ function applyUpdate(doc, data) {
|
|
|
371
454
|
}
|
|
372
455
|
const ops = data;
|
|
373
456
|
if (ops.$set) {
|
|
374
|
-
for (const [path, value] of Object.entries(ops.$set))
|
|
457
|
+
for (const [path, value] of Object.entries(ops.$set))
|
|
458
|
+
setPath(next, path, value);
|
|
375
459
|
}
|
|
376
460
|
if (ops.$inc) {
|
|
377
461
|
for (const [path, by] of Object.entries(ops.$inc)) {
|
|
@@ -492,7 +576,11 @@ function buildHaving(having, params, columns) {
|
|
|
492
576
|
if (!selection) continue;
|
|
493
577
|
for (const field of Object.keys(selection)) {
|
|
494
578
|
parts.push(
|
|
495
|
-
...comparisonSql(
|
|
579
|
+
...comparisonSql(
|
|
580
|
+
`${fn}(${fieldExpr(field, columns)})`,
|
|
581
|
+
selection[field],
|
|
582
|
+
params
|
|
583
|
+
)
|
|
496
584
|
);
|
|
497
585
|
}
|
|
498
586
|
}
|
|
@@ -520,7 +608,11 @@ function groupBy(ctx, args) {
|
|
|
520
608
|
groupCols.push({ alias, field });
|
|
521
609
|
});
|
|
522
610
|
selects.push(`COUNT(*) AS agg_count`);
|
|
523
|
-
const { selects: accSelects, cols } = buildAccumulators(
|
|
611
|
+
const { selects: accSelects, cols } = buildAccumulators(
|
|
612
|
+
args,
|
|
613
|
+
ctx.onPath,
|
|
614
|
+
ctx.columns
|
|
615
|
+
);
|
|
524
616
|
selects.push(...accSelects);
|
|
525
617
|
let sql = `SELECT ${selects.join(", ")} FROM "${ctx.table}" WHERE ${where} GROUP BY ${groupExprs.join(", ")}`;
|
|
526
618
|
if (args.having) {
|
|
@@ -598,6 +690,7 @@ var Collection = class {
|
|
|
598
690
|
this.columns.add(field);
|
|
599
691
|
if (normalized.type === "JSON") this.jsonColumns.add(field);
|
|
600
692
|
}
|
|
693
|
+
this.ensureTable();
|
|
601
694
|
}
|
|
602
695
|
}
|
|
603
696
|
mon;
|
|
@@ -642,22 +735,15 @@ var Collection = class {
|
|
|
642
735
|
`_id TEXT PRIMARY KEY`,
|
|
643
736
|
`created_at INTEGER NOT NULL`,
|
|
644
737
|
`updated_at INTEGER NOT NULL`,
|
|
645
|
-
`data TEXT NOT NULL DEFAULT '{}'
|
|
738
|
+
`data TEXT NOT NULL DEFAULT '{}'`,
|
|
739
|
+
...this.columnOrder.map((f) => this.columnDdl(f, false))
|
|
646
740
|
];
|
|
647
|
-
for (const field of this.columnOrder) {
|
|
648
|
-
const def = this.columnDefs[field];
|
|
649
|
-
let line = `"${field}" ${sqliteType(def.type)}`;
|
|
650
|
-
if (def.notNull) line += " NOT NULL";
|
|
651
|
-
if (def.unique) line += " UNIQUE";
|
|
652
|
-
if (def.default !== void 0) line += ` DEFAULT ${formatDefault(def.default)}`;
|
|
653
|
-
if (def.references) line += ` REFERENCES ${def.references}`;
|
|
654
|
-
lines.push(line);
|
|
655
|
-
}
|
|
656
741
|
this.db.exec(
|
|
657
742
|
`CREATE TABLE IF NOT EXISTS "${this.name}" (
|
|
658
743
|
${lines.join(",\n ")}
|
|
659
744
|
)`
|
|
660
745
|
);
|
|
746
|
+
this.migrateColumns();
|
|
661
747
|
for (const field of this.columnOrder) {
|
|
662
748
|
if (this.columnDefs[field].index) {
|
|
663
749
|
this.db.exec(
|
|
@@ -668,6 +754,39 @@ var Collection = class {
|
|
|
668
754
|
}
|
|
669
755
|
this.initialized = true;
|
|
670
756
|
}
|
|
757
|
+
columnDdl(field, forAlter) {
|
|
758
|
+
const def = this.columnDefs[field];
|
|
759
|
+
let line = `"${field}" ${sqliteType(def.type)}`;
|
|
760
|
+
if (def.notNull) line += " NOT NULL";
|
|
761
|
+
if (def.unique && !forAlter) line += " UNIQUE";
|
|
762
|
+
if (def.default !== void 0)
|
|
763
|
+
line += ` DEFAULT ${formatDefault(def.default)}`;
|
|
764
|
+
if (def.references) line += ` REFERENCES ${def.references}`;
|
|
765
|
+
return line;
|
|
766
|
+
}
|
|
767
|
+
/** Auto-additive migration: add declared columns missing from an existing table. */
|
|
768
|
+
migrateColumns() {
|
|
769
|
+
const existing = new Set(
|
|
770
|
+
this.db.prepare(`PRAGMA table_info("${this.name}")`).all().map((r) => r.name)
|
|
771
|
+
);
|
|
772
|
+
for (const field of this.columnOrder) {
|
|
773
|
+
if (existing.has(field)) continue;
|
|
774
|
+
try {
|
|
775
|
+
this.db.exec(
|
|
776
|
+
`ALTER TABLE "${this.name}" ADD COLUMN ${this.columnDdl(field, true)}`
|
|
777
|
+
);
|
|
778
|
+
} catch (err) {
|
|
779
|
+
throw new MonliteError(
|
|
780
|
+
`Failed to add column "${field}" to "${this.name}": ${err.message}. NOT NULL columns need a default when added to an existing table.`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
if (this.columnDefs[field].unique) {
|
|
784
|
+
this.db.exec(
|
|
785
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "uq_${this.name}_${field}" ON "${this.name}"("${field}")`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
671
790
|
/* --------------------------- row <-> doc -------------------------- */
|
|
672
791
|
rowToDoc(row) {
|
|
673
792
|
const doc = this.mode === "document" ? JSON.parse(row.data) : JSON.parse(row.data ?? "{}");
|
|
@@ -713,7 +832,12 @@ var Collection = class {
|
|
|
713
832
|
const now = Date.now();
|
|
714
833
|
const id = input._id != null ? String(input._id) : objectId();
|
|
715
834
|
const doc = stripSystem(input);
|
|
716
|
-
const returned = {
|
|
835
|
+
const returned = {
|
|
836
|
+
...doc,
|
|
837
|
+
_id: id,
|
|
838
|
+
created_at: now,
|
|
839
|
+
updated_at: now
|
|
840
|
+
};
|
|
717
841
|
if (this.mode === "document") {
|
|
718
842
|
return {
|
|
719
843
|
_id: id,
|
|
@@ -765,9 +889,72 @@ var Collection = class {
|
|
|
765
889
|
];
|
|
766
890
|
return { setSql: setParts.join(", "), values };
|
|
767
891
|
}
|
|
768
|
-
/** Sync store
|
|
892
|
+
/** Sync store for recording local changes (both document and structured). */
|
|
769
893
|
get recorder() {
|
|
770
|
-
return this.
|
|
894
|
+
return this.mon.$sync;
|
|
895
|
+
}
|
|
896
|
+
/** @internal Read a full document by id (mode-aware), synchronously. */
|
|
897
|
+
getRaw(id) {
|
|
898
|
+
this.ensureTable();
|
|
899
|
+
const row = this.db.prepare(`SELECT * FROM "${this.name}" WHERE _id = ?`).get(id);
|
|
900
|
+
return row ? this.rowToDoc(row) : null;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* @internal Apply a remote change to storage WITHOUT recording it to the
|
|
904
|
+
* change feed (the sync store records the `remote` feed row itself). Used by
|
|
905
|
+
* `@monlite/sync` so structured collections sync correctly through the same
|
|
906
|
+
* column/overflow split as local writes.
|
|
907
|
+
*/
|
|
908
|
+
applyRemoteWrite(op, id, doc, ts) {
|
|
909
|
+
this.ensureTable();
|
|
910
|
+
if (op === "delete") {
|
|
911
|
+
this.db.prepare(`DELETE FROM "${this.name}" WHERE _id = ?`).run(id);
|
|
912
|
+
this.afterWrite([id]);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const clean = stripSystem(doc ?? {});
|
|
916
|
+
const createdAt = typeof doc?.created_at === "number" ? doc.created_at : ts;
|
|
917
|
+
if (this.mode === "document") {
|
|
918
|
+
this.db.prepare(
|
|
919
|
+
`INSERT INTO "${this.name}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
920
|
+
ON CONFLICT(_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
|
|
921
|
+
).run(id, JSON.stringify(clean), createdAt, ts);
|
|
922
|
+
this.afterWrite([id]);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const overflow = {};
|
|
926
|
+
const colValues = {};
|
|
927
|
+
for (const [k, v] of Object.entries(clean)) {
|
|
928
|
+
if (this.columns.has(k)) colValues[k] = v;
|
|
929
|
+
else overflow[k] = v;
|
|
930
|
+
}
|
|
931
|
+
const cols = [
|
|
932
|
+
"_id",
|
|
933
|
+
"created_at",
|
|
934
|
+
"updated_at",
|
|
935
|
+
"data",
|
|
936
|
+
...this.columnOrder
|
|
937
|
+
];
|
|
938
|
+
const values = [
|
|
939
|
+
id,
|
|
940
|
+
createdAt,
|
|
941
|
+
ts,
|
|
942
|
+
JSON.stringify(overflow),
|
|
943
|
+
...this.columnOrder.map(
|
|
944
|
+
(c) => c in colValues ? this.encodeColumn(c, colValues[c]) : null
|
|
945
|
+
)
|
|
946
|
+
];
|
|
947
|
+
const colList = cols.map((c) => `"${c}"`).join(", ");
|
|
948
|
+
const placeholders = cols.map(() => "?").join(", ");
|
|
949
|
+
const updateSet = cols.filter((c) => c !== "_id" && c !== "created_at").map((c) => `"${c}" = excluded."${c}"`).join(", ");
|
|
950
|
+
this.db.prepare(
|
|
951
|
+
`INSERT INTO "${this.name}" (${colList}) VALUES (${placeholders}) ON CONFLICT(_id) DO UPDATE SET ${updateSet}`
|
|
952
|
+
).run(...values);
|
|
953
|
+
this.afterWrite([id]);
|
|
954
|
+
}
|
|
955
|
+
/** @internal Notify reactivity watchers that documents changed. */
|
|
956
|
+
afterWrite(ids) {
|
|
957
|
+
this.mon.reactor.emit(this.name, ids);
|
|
771
958
|
}
|
|
772
959
|
/* ----------------------------- create ----------------------------- */
|
|
773
960
|
async create(args) {
|
|
@@ -779,26 +966,29 @@ var Collection = class {
|
|
|
779
966
|
recorder?.recordLocal(this.name, row._id, "upsert", row.created_at);
|
|
780
967
|
};
|
|
781
968
|
this.guard(() => recorder ? this.db.transaction(write) : write());
|
|
969
|
+
this.afterWrite([row._id]);
|
|
782
970
|
return row.returned;
|
|
783
971
|
}
|
|
784
972
|
async createMany(args) {
|
|
785
973
|
this.ensureTable();
|
|
786
974
|
const stmt = this.db.prepare(this.insertSql());
|
|
787
975
|
const recorder = this.recorder;
|
|
976
|
+
const ids = [];
|
|
788
977
|
this.guard(
|
|
789
978
|
() => this.db.transaction(() => {
|
|
790
979
|
for (const item of args.data) {
|
|
791
980
|
const row = this.buildInsert(item);
|
|
792
981
|
stmt.run(...row.values);
|
|
793
982
|
recorder?.recordLocal(this.name, row._id, "upsert", row.created_at);
|
|
983
|
+
ids.push(row._id);
|
|
794
984
|
}
|
|
795
985
|
})
|
|
796
986
|
);
|
|
987
|
+
this.afterWrite(ids);
|
|
797
988
|
return { count: args.data.length };
|
|
798
989
|
}
|
|
799
990
|
/* ------------------------------ read ------------------------------ */
|
|
800
|
-
|
|
801
|
-
this.ensureTable();
|
|
991
|
+
buildFindSql(args) {
|
|
802
992
|
const params = [];
|
|
803
993
|
const where = buildWhere(args.where, {
|
|
804
994
|
params,
|
|
@@ -816,9 +1006,29 @@ var Collection = class {
|
|
|
816
1006
|
sql += (args.take != null ? "" : " LIMIT -1") + " OFFSET ?";
|
|
817
1007
|
params.push(args.skip);
|
|
818
1008
|
}
|
|
1009
|
+
return { sql, params };
|
|
1010
|
+
}
|
|
1011
|
+
/** @internal Synchronous core of findMany (used by reactivity). */
|
|
1012
|
+
findManyCore(args = {}) {
|
|
1013
|
+
this.ensureTable();
|
|
1014
|
+
const { sql, params } = this.buildFindSql(args);
|
|
819
1015
|
const rows = this.db.prepare(sql).all(...params);
|
|
820
1016
|
return rows.map((r) => project(this.rowToDoc(r), args.select));
|
|
821
1017
|
}
|
|
1018
|
+
/** @internal Synchronous core of exists (used by reactivity). */
|
|
1019
|
+
existsCore(where) {
|
|
1020
|
+
this.ensureTable();
|
|
1021
|
+
const params = [];
|
|
1022
|
+
const clause = buildWhere(where, {
|
|
1023
|
+
params,
|
|
1024
|
+
onPath: this.trackPath,
|
|
1025
|
+
columns: this.columns
|
|
1026
|
+
});
|
|
1027
|
+
return this.db.prepare(`SELECT 1 FROM "${this.name}" WHERE ${clause} LIMIT 1`).get(...params) != null;
|
|
1028
|
+
}
|
|
1029
|
+
async findMany(args = {}) {
|
|
1030
|
+
return this.findManyCore(args);
|
|
1031
|
+
}
|
|
822
1032
|
async findFirst(args = {}) {
|
|
823
1033
|
const rows = await this.findMany({ ...args, take: 1 });
|
|
824
1034
|
return rows[0] ?? null;
|
|
@@ -835,15 +1045,39 @@ var Collection = class {
|
|
|
835
1045
|
}
|
|
836
1046
|
/** True if at least one document matches. */
|
|
837
1047
|
async exists(where) {
|
|
1048
|
+
return this.existsCore(where);
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Subscribe to a live query. The callback fires immediately with the current
|
|
1052
|
+
* results (`type: "init"`) and again whenever a change affects the result set
|
|
1053
|
+
* (row-level: only relevant changes trigger a recompute). Includes changes
|
|
1054
|
+
* applied by `@monlite/sync`.
|
|
1055
|
+
*/
|
|
1056
|
+
watch(args = {}, cb) {
|
|
838
1057
|
this.ensureTable();
|
|
839
|
-
const
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1058
|
+
const lq = new LiveQuery(this, args, cb);
|
|
1059
|
+
const reactor = this.mon.reactor;
|
|
1060
|
+
const name = this.name;
|
|
1061
|
+
reactor.register(name, lq);
|
|
1062
|
+
return {
|
|
1063
|
+
get results() {
|
|
1064
|
+
return lq.results;
|
|
1065
|
+
},
|
|
1066
|
+
stop() {
|
|
1067
|
+
lq.stopped = true;
|
|
1068
|
+
reactor.unregister(name, lq);
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
/** Show SQLite's query plan for a `findMany`, and whether it uses an index. */
|
|
1073
|
+
async explain(args = {}) {
|
|
1074
|
+
this.ensureTable();
|
|
1075
|
+
const { sql, params } = this.buildFindSql(args);
|
|
1076
|
+
const plan = this.db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(...params);
|
|
1077
|
+
const usesIndex = plan.some(
|
|
1078
|
+
(r) => /USING (COVERING )?INDEX/i.test(r.detail)
|
|
1079
|
+
);
|
|
1080
|
+
return { sql, usesIndex, plan };
|
|
847
1081
|
}
|
|
848
1082
|
async findById(id) {
|
|
849
1083
|
this.ensureTable();
|
|
@@ -900,25 +1134,27 @@ var Collection = class {
|
|
|
900
1134
|
if (!rows.length) return [];
|
|
901
1135
|
const now = Date.now();
|
|
902
1136
|
const recorder = this.recorder;
|
|
903
|
-
|
|
1137
|
+
const out = this.guard(
|
|
904
1138
|
() => this.db.transaction(() => {
|
|
905
|
-
const
|
|
1139
|
+
const result = [];
|
|
906
1140
|
for (const row of rows) {
|
|
907
1141
|
const current = stripSystem(this.rowToDoc(row));
|
|
908
1142
|
const updated = stripSystem(applyUpdate(current, data));
|
|
909
1143
|
const { setSql, values } = this.buildUpdateSet(updated, now);
|
|
910
1144
|
this.db.prepare(`UPDATE "${this.name}" SET ${setSql} WHERE _id = ?`).run(...values, row._id);
|
|
911
1145
|
recorder?.recordLocal(this.name, row._id, "upsert", now);
|
|
912
|
-
|
|
1146
|
+
result.push({
|
|
913
1147
|
...updated,
|
|
914
1148
|
_id: row._id,
|
|
915
1149
|
created_at: row.created_at,
|
|
916
1150
|
updated_at: now
|
|
917
1151
|
});
|
|
918
1152
|
}
|
|
919
|
-
return
|
|
1153
|
+
return result;
|
|
920
1154
|
})
|
|
921
1155
|
);
|
|
1156
|
+
this.afterWrite(out.map((d) => d._id));
|
|
1157
|
+
return out;
|
|
922
1158
|
}
|
|
923
1159
|
async update(args) {
|
|
924
1160
|
return this.runUpdate(args.where, args.data, true)[0] ?? null;
|
|
@@ -928,7 +1164,7 @@ var Collection = class {
|
|
|
928
1164
|
}
|
|
929
1165
|
async upsert(args) {
|
|
930
1166
|
this.ensureTable();
|
|
931
|
-
|
|
1167
|
+
const result = this.guard(
|
|
932
1168
|
() => this.db.transaction(() => {
|
|
933
1169
|
const params = [];
|
|
934
1170
|
const clause = buildWhere(args.where, {
|
|
@@ -958,6 +1194,8 @@ var Collection = class {
|
|
|
958
1194
|
return ins.returned;
|
|
959
1195
|
})
|
|
960
1196
|
);
|
|
1197
|
+
this.afterWrite([result._id]);
|
|
1198
|
+
return result;
|
|
961
1199
|
}
|
|
962
1200
|
/* ----------------------------- delete ----------------------------- */
|
|
963
1201
|
runDelete(where, single) {
|
|
@@ -983,6 +1221,7 @@ var Collection = class {
|
|
|
983
1221
|
}
|
|
984
1222
|
})
|
|
985
1223
|
);
|
|
1224
|
+
this.afterWrite(rows.map((r) => r._id));
|
|
986
1225
|
return rows.map((r) => this.rowToDoc(r));
|
|
987
1226
|
}
|
|
988
1227
|
async delete(args) {
|
|
@@ -996,7 +1235,12 @@ var Collection = class {
|
|
|
996
1235
|
this.ensureTable();
|
|
997
1236
|
return this.guard(
|
|
998
1237
|
() => aggregate(
|
|
999
|
-
{
|
|
1238
|
+
{
|
|
1239
|
+
db: this.db,
|
|
1240
|
+
table: this.name,
|
|
1241
|
+
onPath: this.trackPath,
|
|
1242
|
+
columns: this.columns
|
|
1243
|
+
},
|
|
1000
1244
|
args
|
|
1001
1245
|
)
|
|
1002
1246
|
);
|
|
@@ -1005,7 +1249,12 @@ var Collection = class {
|
|
|
1005
1249
|
this.ensureTable();
|
|
1006
1250
|
return this.guard(
|
|
1007
1251
|
() => groupBy(
|
|
1008
|
-
{
|
|
1252
|
+
{
|
|
1253
|
+
db: this.db,
|
|
1254
|
+
table: this.name,
|
|
1255
|
+
onPath: this.trackPath,
|
|
1256
|
+
columns: this.columns
|
|
1257
|
+
},
|
|
1009
1258
|
args
|
|
1010
1259
|
)
|
|
1011
1260
|
);
|
|
@@ -1253,12 +1502,14 @@ function stripSystem2(obj) {
|
|
|
1253
1502
|
return rest;
|
|
1254
1503
|
}
|
|
1255
1504
|
var SyncStore = class {
|
|
1256
|
-
constructor(db, nodeId) {
|
|
1505
|
+
constructor(db, nodeId, mon) {
|
|
1257
1506
|
this.db = db;
|
|
1507
|
+
this.mon = mon;
|
|
1258
1508
|
this.init();
|
|
1259
1509
|
this.nodeId = this.resolveNodeId(nodeId);
|
|
1260
1510
|
}
|
|
1261
1511
|
db;
|
|
1512
|
+
mon;
|
|
1262
1513
|
nodeId;
|
|
1263
1514
|
versionSeq = 0;
|
|
1264
1515
|
init() {
|
|
@@ -1293,7 +1544,9 @@ var SyncStore = class {
|
|
|
1293
1544
|
}
|
|
1294
1545
|
resolveNodeId(explicit) {
|
|
1295
1546
|
if (explicit) {
|
|
1296
|
-
this.db.prepare(
|
|
1547
|
+
this.db.prepare(
|
|
1548
|
+
`INSERT OR REPLACE INTO _monlite_meta (key, value) VALUES ('nodeId', ?)`
|
|
1549
|
+
).run(explicit);
|
|
1297
1550
|
return explicit;
|
|
1298
1551
|
}
|
|
1299
1552
|
const row = this.db.prepare(`SELECT value FROM _monlite_meta WHERE key = 'nodeId'`).get();
|
|
@@ -1408,6 +1661,9 @@ var SyncStore = class {
|
|
|
1408
1661
|
);
|
|
1409
1662
|
}
|
|
1410
1663
|
if (winner !== "remote") {
|
|
1664
|
+
if (localVersion !== null) {
|
|
1665
|
+
this.recordLocal(change.collection, change._id, "upsert", Date.now());
|
|
1666
|
+
}
|
|
1411
1667
|
return { applied: false, conflict: localVersion !== null, winner };
|
|
1412
1668
|
}
|
|
1413
1669
|
this.applyData(change);
|
|
@@ -1421,24 +1677,31 @@ var SyncStore = class {
|
|
|
1421
1677
|
change.version,
|
|
1422
1678
|
versionTs(change.version)
|
|
1423
1679
|
);
|
|
1424
|
-
return {
|
|
1680
|
+
return {
|
|
1681
|
+
applied: true,
|
|
1682
|
+
conflict: localVersion !== null,
|
|
1683
|
+
winner: "remote"
|
|
1684
|
+
};
|
|
1425
1685
|
});
|
|
1426
1686
|
}
|
|
1427
1687
|
applyData(change) {
|
|
1428
|
-
const
|
|
1688
|
+
const ts = versionTs(change.version);
|
|
1689
|
+
if (this.mon) {
|
|
1690
|
+
this.mon.collection(change.collection).applyRemoteWrite(change.op, change._id, change.doc, ts);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
const coll = change.collection;
|
|
1429
1694
|
this.ensureCollTable(coll);
|
|
1430
|
-
if (op === "delete") {
|
|
1431
|
-
this.db.prepare(`DELETE FROM "${coll}" WHERE _id = ?`).run(_id);
|
|
1695
|
+
if (change.op === "delete") {
|
|
1696
|
+
this.db.prepare(`DELETE FROM "${coll}" WHERE _id = ?`).run(change._id);
|
|
1432
1697
|
return;
|
|
1433
1698
|
}
|
|
1434
1699
|
const doc = change.doc ?? {};
|
|
1435
|
-
const data = JSON.stringify(stripSystem2(doc));
|
|
1436
|
-
const ts = versionTs(change.version);
|
|
1437
1700
|
const createdAt = typeof doc.created_at === "number" ? doc.created_at : ts;
|
|
1438
1701
|
this.db.prepare(
|
|
1439
1702
|
`INSERT INTO "${coll}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
1440
1703
|
ON CONFLICT(_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
|
|
1441
|
-
).run(_id,
|
|
1704
|
+
).run(change._id, JSON.stringify(stripSystem2(doc)), createdAt, ts);
|
|
1442
1705
|
}
|
|
1443
1706
|
/**
|
|
1444
1707
|
* Latest change per document with `seq` greater than the given watermark,
|
|
@@ -1495,6 +1758,7 @@ var SyncStore = class {
|
|
|
1495
1758
|
this.db.transaction(() => {
|
|
1496
1759
|
for (const coll of collections) {
|
|
1497
1760
|
assertName(coll);
|
|
1761
|
+
if (!this.tableExists(coll)) continue;
|
|
1498
1762
|
const docs = this.db.prepare(`SELECT _id, updated_at FROM "${coll}"`).all();
|
|
1499
1763
|
for (const d of docs) {
|
|
1500
1764
|
if (this.currentVersion(coll, d._id) !== null) continue;
|
|
@@ -1540,7 +1804,14 @@ var SyncStore = class {
|
|
|
1540
1804
|
this.db.prepare(
|
|
1541
1805
|
`INSERT INTO _monlite_conflicts (coll, doc_id, local_version, remote_version, winner, ts)
|
|
1542
1806
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1543
|
-
).run(
|
|
1807
|
+
).run(
|
|
1808
|
+
coll,
|
|
1809
|
+
id,
|
|
1810
|
+
localVersion,
|
|
1811
|
+
remoteVersion,
|
|
1812
|
+
winner,
|
|
1813
|
+
versionTs(remoteVersion)
|
|
1814
|
+
);
|
|
1544
1815
|
}
|
|
1545
1816
|
conflicts() {
|
|
1546
1817
|
const rows = this.db.prepare(
|
|
@@ -1559,6 +1830,8 @@ var SyncStore = class {
|
|
|
1559
1830
|
/* ------------------------------ helpers ------------------------------- */
|
|
1560
1831
|
readDoc(coll, id) {
|
|
1561
1832
|
assertName(coll);
|
|
1833
|
+
if (this.mon) return this.mon.collection(coll).getRaw(id);
|
|
1834
|
+
if (!this.tableExists(coll)) return null;
|
|
1562
1835
|
const row = this.db.prepare(
|
|
1563
1836
|
`SELECT _id, data, created_at, updated_at FROM "${coll}" WHERE _id = ?`
|
|
1564
1837
|
).get(id);
|
|
@@ -1569,6 +1842,9 @@ var SyncStore = class {
|
|
|
1569
1842
|
doc.updated_at = row.updated_at;
|
|
1570
1843
|
return doc;
|
|
1571
1844
|
}
|
|
1845
|
+
tableExists(name) {
|
|
1846
|
+
return this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?`).get(name) != null;
|
|
1847
|
+
}
|
|
1572
1848
|
ensureCollTable(coll) {
|
|
1573
1849
|
assertName(coll);
|
|
1574
1850
|
this.db.exec(
|
|
@@ -1607,6 +1883,8 @@ var Monlite = class {
|
|
|
1607
1883
|
driver;
|
|
1608
1884
|
/** @internal */
|
|
1609
1885
|
autoIndexer;
|
|
1886
|
+
/** @internal Reactivity hub for `collection.watch()`. */
|
|
1887
|
+
reactor = new Reactor();
|
|
1610
1888
|
/** @internal Sync metadata store; present only when `{ sync: true }`. */
|
|
1611
1889
|
$sync;
|
|
1612
1890
|
collections = /* @__PURE__ */ new Map();
|
|
@@ -1625,7 +1903,7 @@ var Monlite = class {
|
|
|
1625
1903
|
options.autoIndexAfter ?? 10
|
|
1626
1904
|
);
|
|
1627
1905
|
if (options.sync) {
|
|
1628
|
-
this.$sync = new SyncStore(this.driver, options.nodeId);
|
|
1906
|
+
this.$sync = new SyncStore(this.driver, options.nodeId, this);
|
|
1629
1907
|
}
|
|
1630
1908
|
}
|
|
1631
1909
|
/** Stable node id for LWW tie-breaking (only when sync is enabled). */
|
|
@@ -1741,6 +2019,15 @@ var Monlite = class {
|
|
|
1741
2019
|
async $dropAll() {
|
|
1742
2020
|
for (const name of await this.$collections()) await this.$drop(name);
|
|
1743
2021
|
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Write a consistent on-disk snapshot of the database to `path` (via
|
|
2024
|
+
* `VACUUM INTO`). The destination file must not already exist.
|
|
2025
|
+
*/
|
|
2026
|
+
backup(path) {
|
|
2027
|
+
this.assertOpen();
|
|
2028
|
+
this.driver.exec(`VACUUM INTO '${path.replace(/'/g, "''")}'`);
|
|
2029
|
+
return Promise.resolve();
|
|
2030
|
+
}
|
|
1744
2031
|
/** Close the underlying SQLite connection. */
|
|
1745
2032
|
$disconnect() {
|
|
1746
2033
|
if (!this.closed) {
|