datanotes 3.4.0 → 3.4.1

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.
@@ -26440,7 +26440,9 @@ var init_hints = __esm({
26440
26440
  manageTokens: "/guides/security/tokens.md#manage-your-tokens",
26441
26441
  tokenAutolink: "/guides/security/tokens.md#turn-automatic-links-off-for-a-sync-program",
26442
26442
  tokenRead: "/guides/security/tokens.md#limit-what-a-token-can-read",
26443
+ tokenWrite: "/guides/security/tokens.md#a-write-into-another-table-needs-write-too",
26443
26444
  reportAgents: "/guides/security/who-did-what.md#report-what-an-agent-does",
26445
+ sessionOwner: "/guides/security/who-did-what.md#a-session-belongs-to-one-token",
26444
26446
  auth: "/develop/http-api.md#authenticate-your-requests",
26445
26447
  routes: "/develop/http-api.md#call-the-engines-routes",
26446
26448
  store: "/administration/run-the-service.md#where-the-engine-keeps-its-store",
@@ -29755,418 +29757,1131 @@ var init_inverse = __esm({
29755
29757
  }
29756
29758
  });
29757
29759
 
29758
- // src/core/unreadable.ts
29759
- function frontmatterError(text5) {
29760
+ // src/core/changeset.ts
29761
+ function createdHash(text5) {
29760
29762
  try {
29761
- readOrderedFromMarkdown(text5);
29762
- return null;
29763
- } catch (e) {
29764
- return messageOf(e);
29765
- }
29766
- }
29767
- function rawFrontmatterValue(text5, key2) {
29768
- const split2 = splitFrontmatter(text5);
29769
- if (!split2) return void 0;
29770
- for (const line of split2.yamlText.split(/\r?\n/)) {
29771
- const head = [`${key2}:`, `"${key2}":`, `'${key2}':`].find((h) => line.startsWith(h));
29772
- if (!head) continue;
29773
- const rest = line.slice(head.length).trim();
29774
- try {
29775
- return (0, import_yaml3.parse)(rest);
29776
- } catch {
29777
- return rest;
29778
- }
29779
- }
29780
- return void 0;
29781
- }
29782
- function unreadableMessage(error2) {
29783
- return `the front matter cannot be read, so the row is left out of every read: ${error2.split("\n")[0].trim()}`;
29784
- }
29785
- function excludedFolders(model, settings) {
29786
- const out2 = /* @__PURE__ */ new Map();
29787
- for (const { ref: ref2, entry } of catalogTables(model)) {
29788
- try {
29789
- const options = parseTableOptions(entryToOrdered(ref2, entry, settings).find(([k]) => k === TABLE_KEY)?.[1]);
29790
- out2.set(ref2.full, [...options.exclude ?? [], catalogLayout(normalizeDatabaseFolder(settings.databaseFolder), ref2, options).archive]);
29791
- } catch {
29792
- }
29793
- }
29794
- return out2;
29795
- }
29796
- function unreadableRowOf(model, settings, path23, text5, excluded) {
29797
- if (!text5.startsWith("---")) return null;
29798
- const error2 = frontmatterError(text5);
29799
- if (error2 === null) return null;
29800
- const table = rowTableFull(model, rawFrontmatterValue(text5, settings.tableKey));
29801
- if (!table) return null;
29802
- const folders = (excluded ?? excludedFolders(model, settings)).get(table) ?? [];
29803
- if (folders.some((dir) => path23.startsWith(`${dir}/`))) return null;
29804
- return { path: path23, table, error: error2 };
29805
- }
29806
- async function unreadableRows(app, settings, model, table) {
29807
- const excluded = excludedFolders(model, settings);
29808
- const out2 = [];
29809
- const idx = vaultIndex(app, settings);
29810
- const notes = idx ? [...idx.allNotes()].filter((n) => n.bare).map((n) => n.file) : app.vault.getMarkdownFiles();
29811
- for (const f of notes) {
29812
- let cached2;
29813
- try {
29814
- cached2 = idx ? void 0 : app.metadataCache.getFileCache(f)?.frontmatter;
29815
- } catch {
29816
- cached2 = void 0;
29817
- }
29818
- if (cached2) continue;
29819
- let text5;
29820
- try {
29821
- text5 = (await readHashed(app, f)).content;
29822
- } catch {
29823
- continue;
29824
- }
29825
- const u = unreadableRowOf(model, settings, f.path, text5, excluded);
29826
- if (u && (table === void 0 || u.table === table)) out2.push(u);
29763
+ const { ordered, body } = readOrderedFromMarkdown(text5);
29764
+ return contentHash(JSON.stringify([ordered, body]));
29765
+ } catch {
29766
+ return contentHash(text5);
29827
29767
  }
29828
- return out2.sort((a, b) => a.path.localeCompare(b.path));
29829
29768
  }
29830
- var import_yaml3;
29831
- var init_unreadable = __esm({
29832
- "src/core/unreadable.ts"() {
29833
- "use strict";
29834
- init_file_cache();
29835
- import_yaml3 = __toESM(require_dist(), 1);
29836
- init_frontmatter();
29837
- init_metastore();
29838
- init_table_loader();
29839
- init_types_table();
29840
- init_message();
29841
- init_db_paths();
29842
- init_vault_index();
29843
- }
29844
- });
29845
-
29846
- // src/core/log-files.ts
29847
- function holdsVersions(path23) {
29848
- return COMMIT_RE.test(path23) || SEGMENT_RE.test(path23);
29769
+ function bodyChanges(e) {
29770
+ return e.bodyAfter !== void 0 && e.bodyAfter !== e.bodyBefore;
29849
29771
  }
29850
- function tocsOf(app) {
29851
- let m = TOCS.get(app);
29852
- if (!m) TOCS.set(app, m = /* @__PURE__ */ new Map());
29853
- return m;
29772
+ function bodyOf(text5) {
29773
+ const split2 = splitFrontmatter(text5);
29774
+ return split2 ? split2.body : text5;
29854
29775
  }
29855
- function latestOf(toc) {
29856
- return toc.versions.length > 0 ? toc.versions[toc.versions.length - 1] : -1;
29776
+ function replaceBody(text5, body) {
29777
+ const split2 = splitFrontmatter(text5);
29778
+ if (!split2) return body;
29779
+ return text5.slice(0, text5.length - split2.body.length) + body;
29857
29780
  }
29858
- function parseSegment(text5) {
29859
- const commits = [];
29860
- let pos = 0;
29861
- while (pos < text5.length) {
29862
- const nl = text5.indexOf("\n", pos);
29863
- if (nl < 0) break;
29864
- let head;
29865
- try {
29866
- head = JSON.parse(text5.slice(pos, nl)).commit;
29867
- } catch {
29868
- break;
29869
- }
29870
- if (!head || !Number.isInteger(head.version) || !Number.isInteger(head.lines) || head.lines < 1) break;
29871
- let end = nl + 1;
29872
- let whole = true;
29873
- for (let i = 0; i < head.lines; i++) {
29874
- const e = text5.indexOf("\n", end);
29875
- if (e < 0) {
29876
- whole = false;
29877
- break;
29878
- }
29879
- end = e + 1;
29880
- }
29881
- if (!whole) break;
29882
- if (typeof head.hash === "string" && contentHash(text5.slice(nl + 1, end)) !== head.hash) break;
29883
- const prev = commits[commits.length - 1];
29884
- if (prev && head.version !== prev.version + 1) break;
29885
- commits.push({ version: head.version, start: nl + 1, end });
29886
- pos = end;
29887
- }
29888
- return { commits, validEnd: pos };
29781
+ async function writeBody(app, file, expected, next) {
29782
+ await app.vault.process(file, (text5) => {
29783
+ if (bodyOf(text5) !== expected) throw new Error("note body changed since it was read");
29784
+ return replaceBody(text5, next);
29785
+ });
29889
29786
  }
29890
- function framed(version2, text5) {
29891
- const lines = text5.split("\n").length - 1;
29892
- return `${JSON.stringify({ commit: { version: version2, lines, hash: contentHash(text5) } })}
29893
- ${text5}`;
29787
+ function emptyChangeSet(op2, summary, schema) {
29788
+ return {
29789
+ op: op2,
29790
+ summary,
29791
+ ...schema !== void 0 ? { schema } : {},
29792
+ edits: [],
29793
+ creates: [],
29794
+ moves: [],
29795
+ deletes: [],
29796
+ texts: []
29797
+ };
29894
29798
  }
29895
- async function segmentText(app, seg) {
29896
- let cache = SEGMENT_TEXTS.get(app);
29897
- if (!cache) SEGMENT_TEXTS.set(app, cache = /* @__PURE__ */ new Map());
29898
- const key2 = `${seg.path}\0${seg.sealed ? "" : `${seg.chars}/${seg.last}`}\0${storeGeneration(app, seg.path)}`;
29899
- const hit = cache.get(key2);
29900
- if (hit) {
29901
- cache.delete(key2);
29902
- cache.set(key2, hit);
29903
- return hit;
29904
- }
29905
- const text5 = await storeReadFile(app, seg.path);
29906
- const index = /* @__PURE__ */ new Map();
29907
- for (const c of parseSegment(text5).commits) index.set(c.version, [c.start, c.end]);
29908
- const out2 = { text: text5, index };
29909
- cache.set(key2, out2);
29910
- for (const k of cache.keys()) {
29911
- if (cache.size <= SEGMENT_TEXTS_KEPT) break;
29912
- cache.delete(k);
29913
- }
29914
- return out2;
29799
+ function same(a, b) {
29800
+ return JSON.stringify(a) === JSON.stringify(b);
29915
29801
  }
29916
- async function buildToc(app, dir) {
29917
- const gen = storeGeneration(app, dir);
29918
- const stat = await storeStat(app, dir);
29919
- const toc = { dir, versions: [], files: /* @__PURE__ */ new Map(), segments: [], checkpoints: [], torn: null, gen, mtime: stat ? stat.mtime : null };
29920
- for (const f of (await storeList(app, dir)).files) {
29921
- let m;
29922
- if (m = COMMIT_RE.exec(f)) toc.files.set(Number(m[1]), f);
29923
- else if (m = CHECKPOINT_RE.exec(f)) toc.checkpoints.push({ version: Number(m[1]), kind: 1, path: f });
29924
- else if (m = MANIFEST_RE.exec(f)) toc.checkpoints.push({ version: Number(m[1]), kind: 2, path: f });
29925
- else if (m = SEGMENT_RE.exec(f)) toc.segments.push({ path: f, first: Number(m[1]), last: m[2] !== void 0 ? Number(m[2]) : Number(m[1]) - 1, sealed: m[2] !== void 0 });
29926
- }
29927
- toc.segments.sort((a, b) => a.first - b.first);
29928
- toc.checkpoints.sort((a, b) => a.version - b.version || b.kind - a.kind);
29929
- for (const seg of toc.segments) {
29930
- if (seg.sealed) continue;
29931
- const text5 = await storeReadFile(app, seg.path);
29932
- const { commits, validEnd } = parseSegment(text5);
29933
- seg.last = commits.length > 0 ? commits[commits.length - 1].version : seg.first - 1;
29934
- seg.chars = text5.length;
29935
- if (validEnd < text5.length) seg.tornAt = validEnd;
29936
- const st = await storeStat(app, seg.path);
29937
- if (st) seg.bytes = st.size;
29938
- }
29939
- const versions = new Set(toc.files.keys());
29940
- for (const seg of toc.segments) for (let v = seg.first; v <= seg.last; v++) versions.add(v);
29941
- toc.versions = [...versions].sort((a, b) => a - b);
29942
- const newest = latestOf(toc);
29943
- const file = toc.files.get(newest);
29944
- if (file !== void 0 && !inSegment(toc, newest)) {
29945
- const text5 = await storeRead(app, file);
29946
- if (text5 === null || text5.trim() === "" || !wholeVersion(text5)) {
29947
- toc.torn = file;
29948
- toc.files.delete(newest);
29949
- toc.versions.pop();
29802
+ function diffOrdered(before, after) {
29803
+ const b = new Map(before);
29804
+ const a = new Map(after);
29805
+ const d = {
29806
+ set: [],
29807
+ remove: [],
29808
+ beforeValues: {},
29809
+ beforeAbsent: [],
29810
+ afterValues: {},
29811
+ afterAbsent: []
29812
+ };
29813
+ const keys = /* @__PURE__ */ new Set([...b.keys(), ...a.keys()]);
29814
+ for (const k of keys) {
29815
+ const inB = b.has(k);
29816
+ const inA = a.has(k);
29817
+ if (inB && inA && same(b.get(k), a.get(k))) continue;
29818
+ if (inB) d.beforeValues[k] = b.get(k);
29819
+ else d.beforeAbsent.push(k);
29820
+ if (inA) {
29821
+ d.afterValues[k] = a.get(k);
29822
+ d.set.push([k, a.get(k)]);
29823
+ } else {
29824
+ d.afterAbsent.push(k);
29825
+ d.remove.push(k);
29950
29826
  }
29951
29827
  }
29952
- return toc;
29953
- }
29954
- function wholeVersion(text5) {
29955
- try {
29956
- const lines = text5.split("\n").filter((l) => l.trim() !== "");
29957
- if (lines.length === 0 || !JSON.parse(lines[0]).commitInfo) return false;
29958
- for (const l of lines.slice(1)) JSON.parse(l);
29959
- return true;
29960
- } catch {
29961
- return false;
29962
- }
29963
- }
29964
- function inSegment(toc, v) {
29965
- for (let i = toc.segments.length - 1; i >= 0; i--) {
29966
- const s = toc.segments[i];
29967
- if (v >= s.first && v <= s.last) return s;
29968
- }
29969
- return void 0;
29970
- }
29971
- async function logToc(app, dir) {
29972
- const tocs = tocsOf(app);
29973
- const kept2 = tocs.get(dir);
29974
- if (kept2 && kept2.gen === storeGeneration(app, dir)) {
29975
- const stat = await storeStat(app, dir);
29976
- if (stat !== void 0 ? (stat ? stat.mtime : null) === kept2.mtime : await stillNewest(app, kept2)) return kept2;
29977
- }
29978
- const toc = await buildToc(app, dir);
29979
- tocs.set(dir, toc);
29980
- return toc;
29981
- }
29982
- async function stillNewest(app, toc) {
29983
- const latest = latestOf(toc);
29984
- const where = latest < 0 ? null : inSegment(toc, latest)?.path ?? toc.files.get(latest);
29985
- if (where !== null && !await storeExists(app, where)) return false;
29986
- const next = commitPath(toc.dir, latest + 1);
29987
- if (next !== toc.torn && await storeExists(app, next)) return false;
29988
- return !await storeExists(app, segmentPath(toc.dir, latest + 1));
29989
- }
29990
- async function settled(app, toc) {
29991
- toc.gen = storeGeneration(app, toc.dir);
29992
- const stat = await storeStat(app, toc.dir);
29993
- toc.mtime = stat ? stat.mtime : null;
29828
+ return d;
29994
29829
  }
29995
- async function versionText(app, toc, v) {
29996
- const segments = segmentsWith(toc, v);
29997
- for (const seg of segments) {
29998
- const s = await segmentText(app, seg);
29999
- const at = s.index.get(v);
30000
- if (at) return s.text.slice(at[0], at[1]);
30001
- }
30002
- const file = toc.files.get(v);
30003
- if (file !== void 0) return storeReadFile(app, file);
30004
- if (segments.length === 0) throw new HistoryError(`${toc.dir} has no version ${v}`);
30005
- throw new HistoryError(
30006
- `${segments[0].path} does not hold version ${v} whole`,
30007
- guide(`A crash lost version ${v}: no file of the log holds it whole. Restore the store from its backup (datanotes store restore --from <backup url> --database <folder> --force), then run the command again.`, "restoreStore")
29830
+ function editIsNoop(e) {
29831
+ const d = diffOrdered(e.before, e.after);
29832
+ const orderChanged = !same(
29833
+ e.before.map(([k]) => k),
29834
+ e.after.map(([k]) => k)
30008
29835
  );
29836
+ return d.set.length === 0 && d.remove.length === 0 && !orderChanged && !bodyChanges(e);
30009
29837
  }
30010
- async function wholeVersions(app, seg) {
30011
- return new Set((await segmentText(app, seg)).index.keys());
29838
+ function applyDiff(current2, set, remove3, order) {
29839
+ let out2 = current2;
29840
+ for (const k of remove3) out2 = fmDelete(out2, k);
29841
+ for (const [k, v] of set) out2 = fmSet(out2, k, v);
29842
+ return fmReorder(out2, order);
30012
29843
  }
30013
- function segmentsWith(toc, v) {
30014
- return toc.segments.filter((s) => v >= s.first && v <= s.last).reverse();
29844
+ function fileOrNull(app, path23) {
29845
+ const f = app.vault.getAbstractFileByPath(path23);
29846
+ return f != null && "extension" in f ? f : null;
30015
29847
  }
30016
- async function versionTexts(app, toc, versions) {
30017
- const out2 = new Array(versions.length);
30018
- const files = [];
30019
- for (let i = 0; i < versions.length; i++) {
30020
- const v = versions[i];
30021
- if (inSegment(toc, v)) out2[i] = await versionText(app, toc, v);
30022
- else files.push(i);
30023
- }
30024
- for (let i = 0; i < files.length; i += READ_AHEAD) {
30025
- const chunk = files.slice(i, i + READ_AHEAD);
30026
- const texts = await Promise.all(chunk.map((j) => versionText(app, toc, versions[j])));
30027
- texts.forEach((t, k) => out2[chunk[k]] = t);
30028
- }
30029
- return out2;
29848
+ function parentFolder(path23) {
29849
+ const i = path23.lastIndexOf("/");
29850
+ return i < 0 ? "" : path23.slice(0, i);
30030
29851
  }
30031
- async function setAside(app, path23) {
30032
- const to = `${path23}.torn-${Date.now()}`;
30033
- await storeRename(app, path23, to);
30034
- return to;
29852
+ async function ensureFolder(app, folder) {
29853
+ if (folder === "") return;
29854
+ if (app.vault.getAbstractFileByPath(folder) != null) return;
29855
+ await ensureFolder(app, parentFolder(folder));
29856
+ await app.vault.createFolder(folder);
30035
29857
  }
30036
- async function appendVersion(app, dir, v, text5, format) {
30037
- const toc = await logToc(app, dir);
30038
- if (latestOf(toc) !== v - 1) {
30039
- const file = toc.files.get(v);
30040
- throw new HistoryError(file !== void 0 ? `log file exists: ${file}` : `log moved on: ${dir} is at version ${latestOf(toc)}, not ${v - 1}`);
30041
- }
30042
- const repaired = [];
30043
- if (toc.torn) {
30044
- repaired.push(`${toc.torn} set aside as ${await setAside(app, toc.torn)}`);
30045
- toc.torn = null;
30046
- }
30047
- for (const seg of toc.segments) {
30048
- if (seg.sealed || seg.tornAt === void 0 && seg.chars > 0) continue;
30049
- const whole = (await storeReadFile(app, seg.path)).slice(0, seg.tornAt ?? 0);
30050
- repaired.push(`${seg.path}: ${seg.chars - whole.length} characters of a torn version cut${whole === "" ? ", the file removed" : ""}`);
30051
- if (whole === "") await storeRemove(app, seg.path);
30052
- else await storeWrite(app, seg.path, whole);
30053
- seg.chars = whole.length;
30054
- delete seg.tornAt;
30055
- const st = await storeStat(app, seg.path);
30056
- if (st) seg.bytes = st.size;
30057
- }
30058
- toc.segments = toc.segments.filter((s) => s.sealed || s.chars > 0);
30059
- if (format === "files") {
30060
- const path23 = commitPath(dir, v);
30061
- if (await storeExists(app, path23)) throw new HistoryError(`log file exists: ${path23}`);
30062
- await storeWrite(app, path23, text5);
30063
- toc.files.set(v, path23);
30064
- } else await appendToSegment(app, toc, v, text5);
30065
- toc.versions.push(v);
30066
- await settled(app, toc);
30067
- return repaired;
29858
+ async function readOrderedFor(app, file) {
29859
+ return readOrderedFromMarkdown(await app.vault.read(file)).ordered;
30068
29860
  }
30069
- async function appendToSegment(app, toc, v, text5) {
30070
- const chunk = framed(v, text5);
30071
- const open2 = toc.segments.filter((s) => !s.sealed);
30072
- const active = open2[open2.length - 1];
30073
- const fits = active && active.last === v - 1 && active.chars < SEGMENT_POLICY.maxBytes && active.last - active.first + 1 < SEGMENT_POLICY.maxVersions;
30074
- for (const seg of open2) if (!(fits && seg === active)) await seal(app, toc, seg);
30075
- if (fits) {
30076
- const st2 = await storeStat(app, active.path);
30077
- if (st2 && active.bytes !== void 0 && st2.size !== active.bytes) throw new HistoryError(`log moved on: ${active.path} changed`);
30078
- await storeAppend(app, active.path, chunk);
30079
- active.last = v;
30080
- active.chars += chunk.length;
30081
- const after = await storeStat(app, active.path);
30082
- if (after) active.bytes = after.size;
30083
- return;
30084
- }
30085
- const path23 = segmentPath(toc.dir, v);
30086
- if (await storeExists(app, path23)) throw new HistoryError(`log file exists: ${path23}`);
30087
- await storeWrite(app, path23, chunk);
30088
- const st = await storeStat(app, path23);
30089
- toc.segments.push({ path: path23, first: v, last: v, sealed: false, chars: chunk.length, ...st ? { bytes: st.size } : {} });
29861
+ async function fileExists(app, path23) {
29862
+ return inStore(path23) ? storeExists(app, path23) : fileOrNull(app, path23) != null;
30090
29863
  }
30091
- async function seal(app, toc, seg) {
30092
- const to = segmentPath(toc.dir, seg.first, seg.last);
30093
- await storeRename(app, seg.path, to);
30094
- seg.path = to;
30095
- seg.sealed = true;
30096
- delete seg.bytes;
30097
- delete seg.chars;
29864
+ async function occupied(app, path23) {
29865
+ return inStore(path23) ? storeExists(app, path23) : app.vault.getAbstractFileByPath(path23) != null;
30098
29866
  }
30099
- async function writeSealedSegments(app, dir, versions) {
29867
+ async function storeFilesSince(app, moves) {
29868
+ const folders = /* @__PURE__ */ new Map();
29869
+ for (const m of moves) if (inStore(m.from) && inStore(m.to)) folders.set(parentFolder(m.to), parentFolder(m.from));
29870
+ const moved = new Set(moves.map((m) => m.to));
30100
29871
  const out2 = [];
30101
- let run2 = [];
30102
- let chars = 0;
30103
- const flush2 = async () => {
30104
- if (run2.length === 0) return;
30105
- const path23 = segmentPath(dir, run2[0].version, run2[run2.length - 1].version);
30106
- if (await holdsWhole(app, path23, run2.map((r) => r.version))) storeSyncNext(app, path23);
30107
- else await storeWrite(app, path23, run2.map((r) => framed(r.version, r.text)).join(""));
30108
- out2.push(path23);
30109
- run2 = [];
30110
- chars = 0;
30111
- };
30112
- for (const r of versions) {
30113
- const last = run2[run2.length - 1];
30114
- if (last && (r.version !== last.version + 1 || chars >= SEGMENT_POLICY.maxBytes || run2.length >= SEGMENT_POLICY.maxVersions)) await flush2();
30115
- run2.push(r);
30116
- chars += r.text.length;
29872
+ for (const [to, from] of folders) {
29873
+ for (const f of (await storeList(app, to)).files) {
29874
+ if (!moved.has(f)) out2.push({ from: `${from}/${f.slice(to.length + 1)}`, to: f });
29875
+ }
30117
29876
  }
30118
- await flush2();
30119
29877
  return out2;
30120
29878
  }
30121
- async function holdsWhole(app, path23, versions) {
30122
- const text5 = await storeRead(app, path23);
30123
- if (text5 === null) return false;
30124
- const { commits, validEnd } = parseSegment(text5);
30125
- return validEnd === text5.length && commits.length === versions.length && commits.every((c, i) => c.version === versions[i]);
30126
- }
30127
- async function gzip(text5) {
30128
- const stream = new Blob([text5]).stream().pipeThrough(new CompressionStream("gzip"));
30129
- return new Response(stream).arrayBuffer();
30130
- }
30131
- async function gunzip(data) {
30132
- const stream = new Blob([data]).stream().pipeThrough(new DecompressionStream("gzip"));
30133
- return new Response(stream).text();
29879
+ function atOnceFor(count2) {
29880
+ return count2 >= WRITE_CONCURRENCY.from ? WRITE_CONCURRENCY.atOnce : 1;
30134
29881
  }
30135
- function chunked(lines) {
30136
- const out2 = [];
30137
- let cur = [];
30138
- let chars = 0;
30139
- for (const l of lines) {
30140
- if (cur.length > 0 && chars + l.length + 1 > CHECKPOINT_PART.maxChars) {
30141
- out2.push(cur);
30142
- cur = [];
30143
- chars = 0;
29882
+ async function inOrder(items3, pathOf, fn, limit = atOnceFor(items3.length)) {
29883
+ const results = new Array(items3.length);
29884
+ let next = 0;
29885
+ let stop = false;
29886
+ const worker = async () => {
29887
+ while (!stop && next < items3.length) {
29888
+ const i = next++;
29889
+ try {
29890
+ results[i] = { ok: true, written: await fn(items3[i]) };
29891
+ } catch (e) {
29892
+ results[i] = { ok: false, error: messageOf(e) };
29893
+ stop = true;
29894
+ }
29895
+ }
29896
+ };
29897
+ await Promise.all(Array.from({ length: Math.min(limit, items3.length) }, worker));
29898
+ const first2 = results.findIndex((r) => r !== void 0 && !r.ok);
29899
+ const kept2 = /* @__PURE__ */ new Set();
29900
+ if (first2 >= 0) {
29901
+ for (let i = results.length - 1; i > first2; i--) {
29902
+ const r = results[i];
29903
+ if (!r?.ok) continue;
29904
+ try {
29905
+ await r.written.undo();
29906
+ } catch {
29907
+ kept2.add(i);
29908
+ }
30144
29909
  }
30145
- cur.push(l);
30146
- chars += l.length + 1;
30147
29910
  }
30148
- if (cur.length > 0 || out2.length === 0) out2.push(cur);
30149
- return out2;
29911
+ const done = [];
29912
+ results.forEach((r, i) => {
29913
+ if (r?.ok && (first2 < 0 || i < first2 || kept2.has(i))) done.push(r.written.value);
29914
+ });
29915
+ const f = first2 >= 0 ? results[first2] : null;
29916
+ return { done, failed: f ? { path: pathOf(items3[first2]), error: f.error } : null };
30150
29917
  }
30151
- async function writeManifestCheckpoint(app, dir, head, rows) {
30152
- const main6 = manifestPath(dir, head.version);
30153
- if (await wholeManifest(app, dir, main6)) return [];
30154
- const binary = storeHoldsBinary(app);
30155
- const written2 = [];
30156
- const textLines = rows.map((r) => JSON.stringify([r.path, r.content]));
30157
- const partOf = [];
30158
- const parts = chunked(textLines);
30159
- let row = 0;
30160
- const partNames = [];
30161
- for (const [i, lines] of parts.entries()) {
30162
- const name = `${pad2(head.version)}.part-${i}.jsonl${binary ? ".gz" : ""}`;
30163
- const text5 = lines.length > 0 ? `${lines.join("\n")}
30164
- ` : "";
30165
- if (binary) await storeWriteBinary(app, `${dir}/${name}`, await gzip(text5));
30166
- else await storeWrite(app, `${dir}/${name}`, text5);
30167
- written2.push(`${dir}/${name}`);
30168
- partNames.push(name);
30169
- for (let k = 0; k < lines.length; k++) partOf[row++] = i;
29918
+ async function moveFile(app, from, to) {
29919
+ if (inStore(from)) return storeRename(app, from, to);
29920
+ const f = fileOrNull(app, from);
29921
+ if (!f) throw new Error(`file not found: ${from}`);
29922
+ if (app.vault.getAbstractFileByPath(to) != null) throw new Error(`target exists: ${to}`);
29923
+ await ensureFolder(app, parentFolder(to));
29924
+ await app.fileManager.renameFile(f, to);
29925
+ }
29926
+ async function applyChangeSetImpl(app, settings, cs, now = /* @__PURE__ */ new Date()) {
29927
+ const rows = [];
29928
+ const creates = [];
29929
+ const moves = [];
29930
+ const references = [];
29931
+ const deleted = [];
29932
+ const failed2 = [];
29933
+ let referencesUpdated = 0;
29934
+ const fail2 = (path23, e) => {
29935
+ failed2.push({ path: path23, error: messageOf(e) });
29936
+ };
29937
+ run: {
29938
+ const folders = /* @__PURE__ */ new Map();
29939
+ const folderOnce = (folder) => {
29940
+ if (folder === "") return Promise.resolve();
29941
+ let p = folders.get(folder);
29942
+ if (!p) {
29943
+ p = folderOnce(parentFolder(folder)).then(async () => {
29944
+ if (app.vault.getAbstractFileByPath(folder) == null) await app.vault.createFolder(folder);
29945
+ });
29946
+ folders.set(folder, p);
29947
+ }
29948
+ return p;
29949
+ };
29950
+ const created = await inOrder(cs.creates, (c) => c.path, async (c) => {
29951
+ if (app.vault.getAbstractFileByPath(c.path) != null) throw new Error(`file exists: ${c.path}`);
29952
+ await folderOnce(parentFolder(c.path));
29953
+ const file = await app.vault.create(c.path, c.content);
29954
+ return { value: c.path, undo: () => app.vault.delete(file, true) };
29955
+ });
29956
+ creates.push(...created.done);
29957
+ if (created.failed) {
29958
+ failed2.push(created.failed);
29959
+ break run;
29960
+ }
29961
+ for (const t of cs.texts) {
29962
+ try {
29963
+ const existing = app.vault.getAbstractFileByPath(t.path);
29964
+ if (existing == null) {
29965
+ if (t.before !== "") throw new Error("file not found");
29966
+ if (t.path === metastorePath(settings)) {
29967
+ t.after = withNewDatabaseConfig(t.after);
29968
+ if (settings.databaseId) t.after = withMetastoreId(t.after, settings.databaseId);
29969
+ }
29970
+ await ensureFolder(app, parentFolder(t.path));
29971
+ await app.vault.create(t.path, t.after);
29972
+ } else {
29973
+ await app.vault.process(existing, (current2) => {
29974
+ if (current2 !== t.before) throw new Error("file changed since it was read");
29975
+ return t.after;
29976
+ });
29977
+ }
29978
+ references.push({ path: t.path, before_text: t.before, after_text: t.after });
29979
+ } catch (err) {
29980
+ fail2(t.path, err);
29981
+ break run;
29982
+ }
29983
+ }
29984
+ const edits = cs.edits.filter((e) => !editIsNoop(e));
29985
+ const distinct = new Set(edits.map((e) => e.path)).size === edits.length;
29986
+ const atOnce = distinct ? atOnceFor(edits.length) : 1;
29987
+ const edited = await inOrder(edits, (e) => e.path, async (e) => {
29988
+ const file = fileOrNull(app, e.path);
29989
+ if (!file) throw new Error("file not found");
29990
+ const was = atOnce > 1 ? (await readHashed(app, file)).content : "";
29991
+ const d = diffOrdered(e.before, e.after);
29992
+ const order = e.after.map(([k]) => k);
29993
+ const fmChanged = d.set.length > 0 || d.remove.length > 0 || !same(e.before.map(([k]) => k), order);
29994
+ if (fmChanged) {
29995
+ await writeFrontmatter(app, file, (fm) => applyDiff(fm, d.set, d.remove, order));
29996
+ }
29997
+ if (bodyChanges(e)) await writeBody(app, file, e.bodyBefore, e.bodyAfter);
29998
+ const row = {
29999
+ path: e.path,
30000
+ before: d.beforeValues,
30001
+ before_absent: d.beforeAbsent,
30002
+ before_order: e.before.map(([k]) => k),
30003
+ after: d.afterValues,
30004
+ after_absent: d.afterAbsent,
30005
+ ...bodyChanges(e) ? { body_before: e.bodyBefore, body_after: e.bodyAfter } : {}
30006
+ };
30007
+ return { value: row, undo: async () => void await app.vault.process(file, () => was) };
30008
+ }, atOnce);
30009
+ rows.push(...edited.done);
30010
+ if (edited.failed) {
30011
+ failed2.push(edited.failed);
30012
+ break run;
30013
+ }
30014
+ for (const m of cs.moves) {
30015
+ try {
30016
+ await moveFile(app, m.from, m.to);
30017
+ moves.push(m);
30018
+ } catch (err) {
30019
+ fail2(m.from, err);
30020
+ break run;
30021
+ }
30022
+ }
30023
+ for (const p of cs.deletes) {
30024
+ const file = fileOrNull(app, p);
30025
+ try {
30026
+ if (!file) throw new Error("file not found");
30027
+ const content = await app.vault.read(file);
30028
+ await trash(app, file);
30029
+ deleted.push({ path: p, content });
30030
+ } catch (err) {
30031
+ fail2(p, err);
30032
+ break run;
30033
+ }
30034
+ }
30035
+ }
30036
+ const createdHashes = [];
30037
+ const movedTo = new Map(moves.map((m) => [m.from, m.to]));
30038
+ for (const p of creates) {
30039
+ const f = fileOrNull(app, movedTo.get(p) ?? p);
30040
+ if (!f) continue;
30041
+ try {
30042
+ createdHashes.push({ path: p, hash: createdHash(await app.vault.read(f)) });
30043
+ } catch {
30044
+ }
30045
+ }
30046
+ const journal = await writeJournal(
30047
+ app,
30048
+ settings,
30049
+ {
30050
+ op: cs.op,
30051
+ ...cs.schema !== void 0 ? { schema: cs.schema } : {},
30052
+ ...currentActor() ? { actor: currentActor() } : {},
30053
+ summary: cs.summary,
30054
+ executed_at: now.toISOString(),
30055
+ rows,
30056
+ creates,
30057
+ ...createdHashes.length > 0 ? { created_hashes: createdHashes } : {},
30058
+ moves,
30059
+ references,
30060
+ deleted,
30061
+ failed: failed2
30062
+ },
30063
+ now
30064
+ );
30065
+ return {
30066
+ ok: failed2.length === 0,
30067
+ migrationId: journal.id,
30068
+ journalPath: journal.path,
30069
+ notesChanged: rows.length,
30070
+ filesCreated: creates.length,
30071
+ filesMoved: moves.length,
30072
+ filesDeleted: deleted.length,
30073
+ referencesUpdated,
30074
+ failed: failed2
30075
+ };
30076
+ }
30077
+ async function trash(app, file) {
30078
+ const fm = app.fileManager;
30079
+ if (typeof fm.trashFile === "function") {
30080
+ await fm.trashFile(file);
30081
+ } else {
30082
+ await app.vault.trash(file, true);
30083
+ }
30084
+ }
30085
+ async function undoChangeSet(app, entries, summary = `undo ${entries.map((e) => e.migration_id).join(", ")}`) {
30086
+ const state = /* @__PURE__ */ new Map();
30087
+ const texts = /* @__PURE__ */ new Map();
30088
+ const origin = /* @__PURE__ */ new Map();
30089
+ const vaultNote = async (path23) => {
30090
+ const f = fileOrNull(app, path23);
30091
+ if (!f) return null;
30092
+ const text5 = await app.vault.read(f);
30093
+ return { ordered: readOrderedFromMarkdown(text5).ordered, body: bodyOf(text5) };
30094
+ };
30095
+ const read3 = async (path23) => state.has(path23) ? state.get(path23) : vaultNote(path23);
30096
+ for (const entry of entries) {
30097
+ const movedTo = new Map(entry.moves.map((m) => [m.from, m.to]));
30098
+ const currentPath = (p) => movedTo.get(p) ?? p;
30099
+ for (const ref2 of entry.references) {
30100
+ const path23 = currentPath(ref2.path);
30101
+ if (/\.md$/i.test(path23)) {
30102
+ const { ordered } = readOrderedFromMarkdown(ref2.before_text);
30103
+ state.set(path23, { ordered, body: bodyOf(ref2.before_text) });
30104
+ } else {
30105
+ texts.set(path23, { before: texts.get(path23)?.before ?? ref2.after_text, after: ref2.before_text });
30106
+ }
30107
+ }
30108
+ for (const r of entry.rows) {
30109
+ const path23 = currentPath(r.path);
30110
+ const cur = await read3(path23);
30111
+ if (!cur) continue;
30112
+ state.set(path23, { ...cur, ordered: applyDiff(cur.ordered, Object.entries(r.before), r.before_absent, r.before_order) });
30113
+ }
30114
+ for (const m of [...entry.moves].reverse()) {
30115
+ state.set(m.from, await read3(m.to));
30116
+ state.set(m.to, null);
30117
+ origin.set(m.from, origin.get(m.to) ?? m.to);
30118
+ origin.delete(m.to);
30119
+ }
30120
+ for (const c of entry.creates) state.set(c, null);
30121
+ for (const d of entry.deleted) state.set(d.path, { ordered: readOrderedFromMarkdown(d.content).ordered, body: bodyOf(d.content) });
30122
+ }
30123
+ const cs = emptyChangeSet("undo_migration", summary);
30124
+ const moved = /* @__PURE__ */ new Set();
30125
+ for (const [to, from] of origin) {
30126
+ const final = state.get(to);
30127
+ if (to === from || !final || !fileOrNull(app, from)) continue;
30128
+ moved.add(to).add(from);
30129
+ cs.moves.push({ from, to });
30130
+ cs.edits.push({ path: from, before: (await vaultNote(from)).ordered, after: final.ordered });
30131
+ }
30132
+ for (const [path23, final] of state) {
30133
+ if (moved.has(path23)) continue;
30134
+ const now = fileOrNull(app, path23) ? await vaultNote(path23) : null;
30135
+ if (now && final) cs.edits.push({ path: path23, before: now.ordered, after: final.ordered });
30136
+ else if (now) cs.deletes.push(path23);
30137
+ else if (final) cs.creates.push({ path: path23, content: buildMarkdownWithFrontmatter(final.ordered, final.body) });
30138
+ }
30139
+ for (const [path23, t] of texts) if (t.before !== t.after) cs.texts.push({ path: path23, ...t });
30140
+ return cs;
30141
+ }
30142
+ async function undoMigration(app, settings, migrationId, now = /* @__PURE__ */ new Date(), check2, op2 = "undo_migration") {
30143
+ const { entry, path: journalPath, undone } = await readJournal(app, settings, migrationId);
30144
+ const conflicts = [];
30145
+ const partial2 = entry.failed.length > 0;
30146
+ const back = /* @__PURE__ */ new Set();
30147
+ const why = RECORD_ONLY_OPS[entry.op];
30148
+ if (why) return { ok: false, undoes: migrationId, conflicts: [{ path: journalPath, message: why }], failed: [] };
30149
+ if (undone) {
30150
+ return {
30151
+ ok: false,
30152
+ undoes: migrationId,
30153
+ conflicts: [{ path: journalPath, message: "migration already undone" }],
30154
+ failed: []
30155
+ };
30156
+ }
30157
+ const movedTo = new Map(entry.moves.map((m) => [m.from, m.to]));
30158
+ const currentPath = (p) => movedTo.get(p) ?? p;
30159
+ for (const r of entry.rows) {
30160
+ const f = fileOrNull(app, currentPath(r.path));
30161
+ if (!f) {
30162
+ conflicts.push({ path: r.path, message: "note no longer exists" });
30163
+ continue;
30164
+ }
30165
+ const cur = new Map(await readOrderedFor(app, f));
30166
+ if (partial2 && Object.entries(r.before).every(([k, v]) => cur.has(k) && same(cur.get(k), v)) && r.before_absent.every((k) => !cur.has(k)) && (r.body_before === void 0 || bodyOf(await app.vault.read(f)) === r.body_before)) {
30167
+ back.add(r);
30168
+ continue;
30169
+ }
30170
+ for (const [k, v] of Object.entries(r.after)) {
30171
+ if (!cur.has(k) || !same(cur.get(k), v)) {
30172
+ conflicts.push({ path: r.path, message: `"${k}" changed since the operation` });
30173
+ }
30174
+ }
30175
+ for (const k of r.after_absent) {
30176
+ if (cur.has(k)) conflicts.push({ path: r.path, message: `"${k}" was added since the operation` });
30177
+ }
30178
+ if (r.body_after !== void 0 && bodyOf(await app.vault.read(f)) !== r.body_after) {
30179
+ conflicts.push({ path: r.path, message: "body changed since the operation" });
30180
+ }
30181
+ }
30182
+ for (const ref2 of entry.references) {
30183
+ const f = fileOrNull(app, currentPath(ref2.path));
30184
+ const text5 = f ? await app.vault.read(f) : null;
30185
+ if (partial2 && text5 === ref2.before_text) back.add(ref2);
30186
+ else if (text5 !== ref2.after_text) {
30187
+ conflicts.push({ path: ref2.path, message: "text changed since the operation" });
30188
+ }
30189
+ }
30190
+ const since = await storeFilesSince(app, entry.moves);
30191
+ const vacuumed = /* @__PURE__ */ new Set();
30192
+ for (const m of entry.moves) {
30193
+ if (!await fileExists(app, m.to)) {
30194
+ if (inStore(m.to)) vacuumed.add(m.to);
30195
+ else if (partial2 && await fileExists(app, m.from)) back.add(m);
30196
+ else conflicts.push({ path: m.to, message: "moved file no longer exists" });
30197
+ } else if (await occupied(app, m.from)) {
30198
+ conflicts.push({ path: m.from, message: "original path is occupied" });
30199
+ }
30200
+ }
30201
+ for (const m of since) {
30202
+ if (await occupied(app, m.from)) conflicts.push({ path: m.from, message: "original path is occupied" });
30203
+ }
30204
+ const hashOfCreated = new Map((entry.created_hashes ?? []).map((h) => [h.path, h.hash]));
30205
+ for (const c of entry.creates) {
30206
+ const f = fileOrNull(app, currentPath(c));
30207
+ if (f) {
30208
+ const was = hashOfCreated.get(c);
30209
+ if (!partial2 && was !== void 0 && createdHash(await app.vault.read(f)) !== was) {
30210
+ conflicts.push({ path: currentPath(c), message: "created note changed since the operation" });
30211
+ }
30212
+ continue;
30213
+ }
30214
+ if (partial2) back.add(c);
30215
+ else conflicts.push({ path: c, message: "created file no longer exists" });
30216
+ }
30217
+ for (const d of entry.deleted) {
30218
+ const f = fileOrNull(app, d.path);
30219
+ if (partial2 && f && await app.vault.read(f) === d.content) back.add(d);
30220
+ else if (app.vault.getAbstractFileByPath(d.path) != null) {
30221
+ conflicts.push({ path: d.path, message: "deleted path is occupied" });
30222
+ }
30223
+ }
30224
+ if (conflicts.length > 0) return { ok: false, undoes: migrationId, conflicts, failed: [] };
30225
+ if (check2) {
30226
+ const refused2 = await check2(await undoChangeSet(app, [entry], `undo ${migrationId}`));
30227
+ if (refused2.length > 0) return { ok: false, undoes: migrationId, conflicts: refused2, failed: [] };
30228
+ }
30229
+ const failed2 = [];
30230
+ const undoRows = [];
30231
+ const undoMoves = [];
30232
+ const undoReferences = [];
30233
+ const undoCreates = [];
30234
+ const undoCreatedHashes = [];
30235
+ const undoDeleted = [];
30236
+ const steps = [];
30237
+ for (const ref2 of entry.references) {
30238
+ if (back.has(ref2)) continue;
30239
+ steps.push({
30240
+ path: currentPath(ref2.path),
30241
+ run: async () => {
30242
+ const f = fileOrNull(app, currentPath(ref2.path));
30243
+ await app.vault.process(f, () => ref2.before_text);
30244
+ undoReferences.push({ path: currentPath(ref2.path), before_text: ref2.after_text, after_text: ref2.before_text });
30245
+ }
30246
+ });
30247
+ }
30248
+ for (const r of entry.rows) {
30249
+ if (back.has(r)) continue;
30250
+ steps.push({
30251
+ path: currentPath(r.path),
30252
+ run: async () => {
30253
+ const f = fileOrNull(app, currentPath(r.path));
30254
+ const order = (await readOrderedFor(app, f)).map(([k]) => k);
30255
+ if (Object.keys(r.after).length > 0 || r.after_absent.length > 0 || Object.keys(r.before).length > 0) {
30256
+ await writeFrontmatter(
30257
+ app,
30258
+ f,
30259
+ (fm) => applyDiff(fm, Object.entries(r.before), r.before_absent, r.before_order)
30260
+ );
30261
+ }
30262
+ if (r.body_after !== void 0) await writeBody(app, f, r.body_after, r.body_before);
30263
+ undoRows.push({
30264
+ path: currentPath(r.path),
30265
+ before: r.after,
30266
+ before_absent: r.after_absent,
30267
+ before_order: order,
30268
+ after: r.before,
30269
+ after_absent: r.before_absent,
30270
+ ...r.body_after !== void 0 ? { body_before: r.body_after, body_after: r.body_before } : {}
30271
+ });
30272
+ }
30273
+ });
30274
+ }
30275
+ for (const m of [...[...entry.moves].reverse().filter((x) => !vacuumed.has(x.to) && !back.has(x)), ...since]) {
30276
+ steps.push({
30277
+ path: m.to,
30278
+ run: async () => {
30279
+ await moveFile(app, m.to, m.from);
30280
+ undoMoves.push({ from: m.to, to: m.from });
30281
+ }
30282
+ });
30283
+ }
30284
+ for (const c of entry.creates) {
30285
+ if (back.has(c)) continue;
30286
+ steps.push({
30287
+ path: c,
30288
+ run: async () => {
30289
+ const f = fileOrNull(app, c);
30290
+ const content = await app.vault.read(f);
30291
+ await trash(app, f);
30292
+ undoDeleted.push({ path: c, content });
30293
+ }
30294
+ });
30295
+ }
30296
+ for (const d of entry.deleted) {
30297
+ if (back.has(d)) continue;
30298
+ steps.push({
30299
+ path: d.path,
30300
+ run: async () => {
30301
+ await ensureFolder(app, parentFolder(d.path));
30302
+ await app.vault.create(d.path, d.content);
30303
+ undoCreates.push(d.path);
30304
+ undoCreatedHashes.push({ path: d.path, hash: createdHash(d.content) });
30305
+ }
30306
+ });
30307
+ }
30308
+ let done = 0;
30309
+ try {
30310
+ for (const s of steps) {
30311
+ await s.run();
30312
+ done++;
30313
+ }
30314
+ } catch (e) {
30315
+ failed2.push({ path: "undo", error: messageOf(e) });
30316
+ }
30317
+ const journal = await writeJournal(
30318
+ app,
30319
+ settings,
30320
+ {
30321
+ op: op2,
30322
+ ...entry.schema !== void 0 ? { schema: entry.schema } : {},
30323
+ ...currentActor() ? { actor: currentActor() } : {},
30324
+ summary: `undo ${migrationId}`,
30325
+ executed_at: now.toISOString(),
30326
+ rows: undoRows,
30327
+ creates: undoCreates,
30328
+ ...undoCreatedHashes.length > 0 ? { created_hashes: undoCreatedHashes } : {},
30329
+ moves: undoMoves,
30330
+ references: undoReferences,
30331
+ deleted: undoDeleted,
30332
+ failed: failed2,
30333
+ undoes: migrationId
30334
+ },
30335
+ now
30336
+ );
30337
+ if (failed2.length === 0) {
30338
+ await markUndone(app, settings, migrationId, journal.id, now);
30339
+ }
30340
+ return {
30341
+ ok: failed2.length === 0,
30342
+ undoes: migrationId,
30343
+ migration_id: journal.id,
30344
+ conflicts: [],
30345
+ failed: failed2,
30346
+ ...failed2.length > 0 ? { left: [...new Set(steps.slice(done).map((s) => s.path))] } : {},
30347
+ touched: [
30348
+ ...entry.rows.map((r) => r.path),
30349
+ ...entry.creates,
30350
+ ...[...entry.moves, ...since].flatMap((m) => [m.from, m.to]),
30351
+ ...entry.references.map((r) => r.path),
30352
+ ...entry.deleted.map((d) => d.path)
30353
+ ]
30354
+ };
30355
+ }
30356
+ function applyChangeSet(...args) {
30357
+ const cs = args[2];
30358
+ return phase("engine.apply", async (span) => {
30359
+ const r = await applyChangeSetImpl(...args);
30360
+ span.setAttributes({ "datanotes.notes_changed": r.notesChanged, "datanotes.files_created": r.filesCreated, "datanotes.files_moved": r.filesMoved, "datanotes.files_deleted": r.filesDeleted, "datanotes.failed": r.failed.length });
30361
+ if (!r.ok) span.setAttribute("error.type", "write_failed").setStatus("error", "the write failed partway");
30362
+ return r;
30363
+ }, { "datanotes.edits": cs.edits.length, "datanotes.creates": cs.creates.length, "datanotes.moves": cs.moves.length, "datanotes.deletes": cs.deletes.length });
30364
+ }
30365
+ var inStore, WRITE_CONCURRENCY, RECORD_ONLY_OPS;
30366
+ var init_changeset = __esm({
30367
+ "src/core/changeset.ts"() {
30368
+ "use strict";
30369
+ init_frontmatter();
30370
+ init_actor();
30371
+ init_journal();
30372
+ init_store();
30373
+ init_metastore();
30374
+ init_database_id();
30375
+ init_message();
30376
+ init_file_cache();
30377
+ init_tracing();
30378
+ init_journal();
30379
+ inStore = isStorePath;
30380
+ WRITE_CONCURRENCY = { atOnce: 16, from: 32 };
30381
+ RECORD_ONLY_OPS = {
30382
+ move_database: "a move of the database folder cannot be undone: move it back with configure_database (root)",
30383
+ set_database_id: "the database's id cannot be taken back: the engine finds its store by it",
30384
+ rollback: "the rollback of a failed write cannot be undone: it put the database back as it was; run the write again instead"
30385
+ };
30386
+ }
30387
+ });
30388
+
30389
+ // src/core/write-scope.ts
30390
+ function runWithWriteScope(scope, fn) {
30391
+ if (!storage3) {
30392
+ if (scope) throw new Error("write permissions need Node's AsyncLocalStorage");
30393
+ return fn();
30394
+ }
30395
+ if (!scope && !storage3.getStore()) return fn();
30396
+ return storage3.run({ scope }, fn);
30397
+ }
30398
+ function currentWriteScope() {
30399
+ return storage3?.getStore()?.scope ?? null;
30400
+ }
30401
+ function tableOfFrontmatter(model, settings, fm) {
30402
+ return rowTableFull(model, fmGet(fm, settings.tableKey));
30403
+ }
30404
+ function changeSetTables(app, settings, model, cs) {
30405
+ const out2 = /* @__PURE__ */ new Set();
30406
+ const add = (full) => {
30407
+ if (full !== null) out2.add(full);
30408
+ };
30409
+ const keep = /* @__PURE__ */ new Set([settings.tableKey]);
30410
+ for (const e of cs.edits) {
30411
+ if (editIsNoop(e)) continue;
30412
+ add(tableOfFrontmatter(model, settings, e.before) ?? tableOfPath(app, settings, e.path));
30413
+ add(tableOfFrontmatter(model, settings, e.after));
30414
+ }
30415
+ for (const c of cs.creates) add(tableOfFrontmatter(model, settings, readOrderedFromMarkdown(c.content, keep).ordered));
30416
+ for (const m of cs.moves) add(tableOfPath(app, settings, m.from));
30417
+ for (const d of cs.deletes) add(tableOfPath(app, settings, d));
30418
+ return out2;
30419
+ }
30420
+ async function assertChangeSetWritable(app, settings, cs) {
30421
+ const scope = currentWriteScope();
30422
+ if (!scope || !ROW_WRITE_OPS.has(cs.op)) return;
30423
+ const model = (await loadMetastore(app, settings))?.model;
30424
+ if (!model) return;
30425
+ const denied = [...changeSetTables(app, settings, model, cs)].filter((t) => !matchesTablePattern(t, scope.patterns)).sort();
30426
+ if (denied.length === 0) return;
30427
+ const named = denied.find((t) => tableReadable(t));
30428
+ throw new WriteDeniedError(scope.token, named ?? null);
30429
+ }
30430
+ var storage3, ROW_WRITE_OPS, WriteDeniedError;
30431
+ var init_write_scope = __esm({
30432
+ "src/core/write-scope.ts"() {
30433
+ "use strict";
30434
+ init_changeset();
30435
+ init_frontmatter();
30436
+ init_hints();
30437
+ init_metastore();
30438
+ init_read_scope();
30439
+ storage3 = null;
30440
+ try {
30441
+ const { AsyncLocalStorage: AsyncLocalStorage2 } = require("node:async_hooks");
30442
+ storage3 = new AsyncLocalStorage2();
30443
+ } catch {
30444
+ storage3 = null;
30445
+ }
30446
+ ROW_WRITE_OPS = /* @__PURE__ */ new Set([
30447
+ "insert_row",
30448
+ "update_row",
30449
+ "update_rows",
30450
+ "delete_rows",
30451
+ "move_rows",
30452
+ "adopt_rows",
30453
+ "merge_rows",
30454
+ "recompute_columns",
30455
+ "align_relations"
30456
+ ]);
30457
+ WriteDeniedError = class extends GuidedError {
30458
+ constructor(token, table) {
30459
+ super(
30460
+ table === null ? `token ${token} may not write a table it may not read: this write changes rows of it` : `token ${token} may not write ${table}: this write changes rows of it`,
30461
+ guide(
30462
+ table === null ? `Nothing was written. The write changes rows of a table this token may not read (set_null, an inverse link or align_relations). The owner can add that table to the token's "read" and "write" with tokens/update/. Or change the write so it leaves that table alone.` : `Nothing was written. The write changes rows of ${table} (set_null, an inverse link or align_relations), so the token needs write on it too: the owner adds "${table}" (or a wider pattern) to the token's "write" with tokens/update/. Or change the write so it leaves that table alone.`,
30463
+ "tokenWrite"
30464
+ )
30465
+ );
30466
+ this.name = "WriteDeniedError";
30467
+ this.table = table;
30468
+ }
30469
+ };
30470
+ }
30471
+ });
30472
+
30473
+ // src/core/unreadable.ts
30474
+ function frontmatterError(text5) {
30475
+ try {
30476
+ readOrderedFromMarkdown(text5);
30477
+ return null;
30478
+ } catch (e) {
30479
+ return messageOf(e);
30480
+ }
30481
+ }
30482
+ function rawFrontmatterValue(text5, key2) {
30483
+ const split2 = splitFrontmatter(text5);
30484
+ if (!split2) return void 0;
30485
+ for (const line of split2.yamlText.split(/\r?\n/)) {
30486
+ const head = [`${key2}:`, `"${key2}":`, `'${key2}':`].find((h) => line.startsWith(h));
30487
+ if (!head) continue;
30488
+ const rest = line.slice(head.length).trim();
30489
+ try {
30490
+ return (0, import_yaml3.parse)(rest);
30491
+ } catch {
30492
+ return rest;
30493
+ }
30494
+ }
30495
+ return void 0;
30496
+ }
30497
+ function unreadableMessage(error2) {
30498
+ return `the front matter cannot be read, so the row is left out of every read: ${error2.split("\n")[0].trim()}`;
30499
+ }
30500
+ function excludedFolders(model, settings) {
30501
+ const out2 = /* @__PURE__ */ new Map();
30502
+ for (const { ref: ref2, entry } of catalogTables(model)) {
30503
+ try {
30504
+ const options = parseTableOptions(entryToOrdered(ref2, entry, settings).find(([k]) => k === TABLE_KEY)?.[1]);
30505
+ out2.set(ref2.full, [...options.exclude ?? [], catalogLayout(normalizeDatabaseFolder(settings.databaseFolder), ref2, options).archive]);
30506
+ } catch {
30507
+ }
30508
+ }
30509
+ return out2;
30510
+ }
30511
+ function unreadableRowOf(model, settings, path23, text5, excluded) {
30512
+ if (!text5.startsWith("---")) return null;
30513
+ const error2 = frontmatterError(text5);
30514
+ if (error2 === null) return null;
30515
+ const table = rowTableFull(model, rawFrontmatterValue(text5, settings.tableKey));
30516
+ if (!table) return null;
30517
+ const folders = (excluded ?? excludedFolders(model, settings)).get(table) ?? [];
30518
+ if (folders.some((dir) => path23.startsWith(`${dir}/`))) return null;
30519
+ return { path: path23, table, error: error2 };
30520
+ }
30521
+ async function unreadableRows(app, settings, model, table) {
30522
+ const excluded = excludedFolders(model, settings);
30523
+ const out2 = [];
30524
+ const idx = vaultIndex(app, settings);
30525
+ const notes = idx ? [...idx.allNotes()].filter((n) => n.bare).map((n) => n.file) : app.vault.getMarkdownFiles();
30526
+ for (const f of notes) {
30527
+ let cached2;
30528
+ try {
30529
+ cached2 = idx ? void 0 : app.metadataCache.getFileCache(f)?.frontmatter;
30530
+ } catch {
30531
+ cached2 = void 0;
30532
+ }
30533
+ if (cached2) continue;
30534
+ let text5;
30535
+ try {
30536
+ text5 = (await readHashed(app, f)).content;
30537
+ } catch {
30538
+ continue;
30539
+ }
30540
+ const u = unreadableRowOf(model, settings, f.path, text5, excluded);
30541
+ if (u && (table === void 0 || u.table === table)) out2.push(u);
30542
+ }
30543
+ return out2.sort((a, b) => a.path.localeCompare(b.path));
30544
+ }
30545
+ var import_yaml3;
30546
+ var init_unreadable = __esm({
30547
+ "src/core/unreadable.ts"() {
30548
+ "use strict";
30549
+ init_file_cache();
30550
+ import_yaml3 = __toESM(require_dist(), 1);
30551
+ init_frontmatter();
30552
+ init_metastore();
30553
+ init_table_loader();
30554
+ init_types_table();
30555
+ init_message();
30556
+ init_db_paths();
30557
+ init_vault_index();
30558
+ }
30559
+ });
30560
+
30561
+ // src/core/log-files.ts
30562
+ function holdsVersions(path23) {
30563
+ return COMMIT_RE.test(path23) || SEGMENT_RE.test(path23);
30564
+ }
30565
+ function tocsOf(app) {
30566
+ let m = TOCS.get(app);
30567
+ if (!m) TOCS.set(app, m = /* @__PURE__ */ new Map());
30568
+ return m;
30569
+ }
30570
+ function latestOf(toc) {
30571
+ return toc.versions.length > 0 ? toc.versions[toc.versions.length - 1] : -1;
30572
+ }
30573
+ function parseSegment(text5) {
30574
+ const commits = [];
30575
+ let pos = 0;
30576
+ while (pos < text5.length) {
30577
+ const nl = text5.indexOf("\n", pos);
30578
+ if (nl < 0) break;
30579
+ let head;
30580
+ try {
30581
+ head = JSON.parse(text5.slice(pos, nl)).commit;
30582
+ } catch {
30583
+ break;
30584
+ }
30585
+ if (!head || !Number.isInteger(head.version) || !Number.isInteger(head.lines) || head.lines < 1) break;
30586
+ let end = nl + 1;
30587
+ let whole = true;
30588
+ for (let i = 0; i < head.lines; i++) {
30589
+ const e = text5.indexOf("\n", end);
30590
+ if (e < 0) {
30591
+ whole = false;
30592
+ break;
30593
+ }
30594
+ end = e + 1;
30595
+ }
30596
+ if (!whole) break;
30597
+ if (typeof head.hash === "string" && contentHash(text5.slice(nl + 1, end)) !== head.hash) break;
30598
+ const prev = commits[commits.length - 1];
30599
+ if (prev && head.version !== prev.version + 1) break;
30600
+ commits.push({ version: head.version, start: nl + 1, end });
30601
+ pos = end;
30602
+ }
30603
+ return { commits, validEnd: pos };
30604
+ }
30605
+ function framed(version2, text5) {
30606
+ const lines = text5.split("\n").length - 1;
30607
+ return `${JSON.stringify({ commit: { version: version2, lines, hash: contentHash(text5) } })}
30608
+ ${text5}`;
30609
+ }
30610
+ async function segmentText(app, seg) {
30611
+ let cache = SEGMENT_TEXTS.get(app);
30612
+ if (!cache) SEGMENT_TEXTS.set(app, cache = /* @__PURE__ */ new Map());
30613
+ const key2 = `${seg.path}\0${seg.sealed ? "" : `${seg.chars}/${seg.last}`}\0${storeGeneration(app, seg.path)}`;
30614
+ const hit = cache.get(key2);
30615
+ if (hit) {
30616
+ cache.delete(key2);
30617
+ cache.set(key2, hit);
30618
+ return hit;
30619
+ }
30620
+ const text5 = await storeReadFile(app, seg.path);
30621
+ const index = /* @__PURE__ */ new Map();
30622
+ for (const c of parseSegment(text5).commits) index.set(c.version, [c.start, c.end]);
30623
+ const out2 = { text: text5, index };
30624
+ cache.set(key2, out2);
30625
+ for (const k of cache.keys()) {
30626
+ if (cache.size <= SEGMENT_TEXTS_KEPT) break;
30627
+ cache.delete(k);
30628
+ }
30629
+ return out2;
30630
+ }
30631
+ async function buildToc(app, dir) {
30632
+ const gen = storeGeneration(app, dir);
30633
+ const stat = await storeStat(app, dir);
30634
+ const toc = { dir, versions: [], files: /* @__PURE__ */ new Map(), segments: [], checkpoints: [], torn: null, gen, mtime: stat ? stat.mtime : null };
30635
+ for (const f of (await storeList(app, dir)).files) {
30636
+ let m;
30637
+ if (m = COMMIT_RE.exec(f)) toc.files.set(Number(m[1]), f);
30638
+ else if (m = CHECKPOINT_RE.exec(f)) toc.checkpoints.push({ version: Number(m[1]), kind: 1, path: f });
30639
+ else if (m = MANIFEST_RE.exec(f)) toc.checkpoints.push({ version: Number(m[1]), kind: 2, path: f });
30640
+ else if (m = SEGMENT_RE.exec(f)) toc.segments.push({ path: f, first: Number(m[1]), last: m[2] !== void 0 ? Number(m[2]) : Number(m[1]) - 1, sealed: m[2] !== void 0 });
30641
+ }
30642
+ toc.segments.sort((a, b) => a.first - b.first);
30643
+ toc.checkpoints.sort((a, b) => a.version - b.version || b.kind - a.kind);
30644
+ for (const seg of toc.segments) {
30645
+ if (seg.sealed) continue;
30646
+ const text5 = await storeReadFile(app, seg.path);
30647
+ const { commits, validEnd } = parseSegment(text5);
30648
+ seg.last = commits.length > 0 ? commits[commits.length - 1].version : seg.first - 1;
30649
+ seg.chars = text5.length;
30650
+ if (validEnd < text5.length) seg.tornAt = validEnd;
30651
+ const st = await storeStat(app, seg.path);
30652
+ if (st) seg.bytes = st.size;
30653
+ }
30654
+ const versions = new Set(toc.files.keys());
30655
+ for (const seg of toc.segments) for (let v = seg.first; v <= seg.last; v++) versions.add(v);
30656
+ toc.versions = [...versions].sort((a, b) => a - b);
30657
+ const newest = latestOf(toc);
30658
+ const file = toc.files.get(newest);
30659
+ if (file !== void 0 && !inSegment(toc, newest)) {
30660
+ const text5 = await storeRead(app, file);
30661
+ if (text5 === null || text5.trim() === "" || !wholeVersion(text5)) {
30662
+ toc.torn = file;
30663
+ toc.files.delete(newest);
30664
+ toc.versions.pop();
30665
+ }
30666
+ }
30667
+ return toc;
30668
+ }
30669
+ function wholeVersion(text5) {
30670
+ try {
30671
+ const lines = text5.split("\n").filter((l) => l.trim() !== "");
30672
+ if (lines.length === 0 || !JSON.parse(lines[0]).commitInfo) return false;
30673
+ for (const l of lines.slice(1)) JSON.parse(l);
30674
+ return true;
30675
+ } catch {
30676
+ return false;
30677
+ }
30678
+ }
30679
+ function inSegment(toc, v) {
30680
+ for (let i = toc.segments.length - 1; i >= 0; i--) {
30681
+ const s = toc.segments[i];
30682
+ if (v >= s.first && v <= s.last) return s;
30683
+ }
30684
+ return void 0;
30685
+ }
30686
+ async function logToc(app, dir) {
30687
+ const tocs = tocsOf(app);
30688
+ const kept2 = tocs.get(dir);
30689
+ if (kept2 && kept2.gen === storeGeneration(app, dir)) {
30690
+ const stat = await storeStat(app, dir);
30691
+ if (stat !== void 0 ? (stat ? stat.mtime : null) === kept2.mtime : await stillNewest(app, kept2)) return kept2;
30692
+ }
30693
+ const toc = await buildToc(app, dir);
30694
+ tocs.set(dir, toc);
30695
+ return toc;
30696
+ }
30697
+ async function stillNewest(app, toc) {
30698
+ const latest = latestOf(toc);
30699
+ const where = latest < 0 ? null : inSegment(toc, latest)?.path ?? toc.files.get(latest);
30700
+ if (where !== null && !await storeExists(app, where)) return false;
30701
+ const next = commitPath(toc.dir, latest + 1);
30702
+ if (next !== toc.torn && await storeExists(app, next)) return false;
30703
+ return !await storeExists(app, segmentPath(toc.dir, latest + 1));
30704
+ }
30705
+ async function settled(app, toc) {
30706
+ toc.gen = storeGeneration(app, toc.dir);
30707
+ const stat = await storeStat(app, toc.dir);
30708
+ toc.mtime = stat ? stat.mtime : null;
30709
+ }
30710
+ async function versionText(app, toc, v) {
30711
+ const segments = segmentsWith(toc, v);
30712
+ for (const seg of segments) {
30713
+ const s = await segmentText(app, seg);
30714
+ const at = s.index.get(v);
30715
+ if (at) return s.text.slice(at[0], at[1]);
30716
+ }
30717
+ const file = toc.files.get(v);
30718
+ if (file !== void 0) return storeReadFile(app, file);
30719
+ if (segments.length === 0) throw new HistoryError(`${toc.dir} has no version ${v}`);
30720
+ throw new HistoryError(
30721
+ `${segments[0].path} does not hold version ${v} whole`,
30722
+ guide(`A crash lost version ${v}: no file of the log holds it whole. Restore the store from its backup (datanotes store restore --from <backup url> --database <folder> --force), then run the command again.`, "restoreStore")
30723
+ );
30724
+ }
30725
+ async function wholeVersions(app, seg) {
30726
+ return new Set((await segmentText(app, seg)).index.keys());
30727
+ }
30728
+ function segmentsWith(toc, v) {
30729
+ return toc.segments.filter((s) => v >= s.first && v <= s.last).reverse();
30730
+ }
30731
+ async function versionTexts(app, toc, versions) {
30732
+ const out2 = new Array(versions.length);
30733
+ const files = [];
30734
+ for (let i = 0; i < versions.length; i++) {
30735
+ const v = versions[i];
30736
+ if (inSegment(toc, v)) out2[i] = await versionText(app, toc, v);
30737
+ else files.push(i);
30738
+ }
30739
+ for (let i = 0; i < files.length; i += READ_AHEAD) {
30740
+ const chunk = files.slice(i, i + READ_AHEAD);
30741
+ const texts = await Promise.all(chunk.map((j) => versionText(app, toc, versions[j])));
30742
+ texts.forEach((t, k) => out2[chunk[k]] = t);
30743
+ }
30744
+ return out2;
30745
+ }
30746
+ async function setAside(app, path23) {
30747
+ const to = `${path23}.torn-${Date.now()}`;
30748
+ await storeRename(app, path23, to);
30749
+ return to;
30750
+ }
30751
+ async function appendVersion(app, dir, v, text5, format) {
30752
+ const toc = await logToc(app, dir);
30753
+ if (latestOf(toc) !== v - 1) {
30754
+ const file = toc.files.get(v);
30755
+ throw new HistoryError(file !== void 0 ? `log file exists: ${file}` : `log moved on: ${dir} is at version ${latestOf(toc)}, not ${v - 1}`);
30756
+ }
30757
+ const repaired = [];
30758
+ if (toc.torn) {
30759
+ repaired.push(`${toc.torn} set aside as ${await setAside(app, toc.torn)}`);
30760
+ toc.torn = null;
30761
+ }
30762
+ for (const seg of toc.segments) {
30763
+ if (seg.sealed || seg.tornAt === void 0 && seg.chars > 0) continue;
30764
+ const whole = (await storeReadFile(app, seg.path)).slice(0, seg.tornAt ?? 0);
30765
+ repaired.push(`${seg.path}: ${seg.chars - whole.length} characters of a torn version cut${whole === "" ? ", the file removed" : ""}`);
30766
+ if (whole === "") await storeRemove(app, seg.path);
30767
+ else await storeWrite(app, seg.path, whole);
30768
+ seg.chars = whole.length;
30769
+ delete seg.tornAt;
30770
+ const st = await storeStat(app, seg.path);
30771
+ if (st) seg.bytes = st.size;
30772
+ }
30773
+ toc.segments = toc.segments.filter((s) => s.sealed || s.chars > 0);
30774
+ if (format === "files") {
30775
+ const path23 = commitPath(dir, v);
30776
+ if (await storeExists(app, path23)) throw new HistoryError(`log file exists: ${path23}`);
30777
+ await storeWrite(app, path23, text5);
30778
+ toc.files.set(v, path23);
30779
+ } else await appendToSegment(app, toc, v, text5);
30780
+ toc.versions.push(v);
30781
+ await settled(app, toc);
30782
+ return repaired;
30783
+ }
30784
+ async function appendToSegment(app, toc, v, text5) {
30785
+ const chunk = framed(v, text5);
30786
+ const open2 = toc.segments.filter((s) => !s.sealed);
30787
+ const active = open2[open2.length - 1];
30788
+ const fits = active && active.last === v - 1 && active.chars < SEGMENT_POLICY.maxBytes && active.last - active.first + 1 < SEGMENT_POLICY.maxVersions;
30789
+ for (const seg of open2) if (!(fits && seg === active)) await seal(app, toc, seg);
30790
+ if (fits) {
30791
+ const st2 = await storeStat(app, active.path);
30792
+ if (st2 && active.bytes !== void 0 && st2.size !== active.bytes) throw new HistoryError(`log moved on: ${active.path} changed`);
30793
+ await storeAppend(app, active.path, chunk);
30794
+ active.last = v;
30795
+ active.chars += chunk.length;
30796
+ const after = await storeStat(app, active.path);
30797
+ if (after) active.bytes = after.size;
30798
+ return;
30799
+ }
30800
+ const path23 = segmentPath(toc.dir, v);
30801
+ if (await storeExists(app, path23)) throw new HistoryError(`log file exists: ${path23}`);
30802
+ await storeWrite(app, path23, chunk);
30803
+ const st = await storeStat(app, path23);
30804
+ toc.segments.push({ path: path23, first: v, last: v, sealed: false, chars: chunk.length, ...st ? { bytes: st.size } : {} });
30805
+ }
30806
+ async function seal(app, toc, seg) {
30807
+ const to = segmentPath(toc.dir, seg.first, seg.last);
30808
+ await storeRename(app, seg.path, to);
30809
+ seg.path = to;
30810
+ seg.sealed = true;
30811
+ delete seg.bytes;
30812
+ delete seg.chars;
30813
+ }
30814
+ async function writeSealedSegments(app, dir, versions) {
30815
+ const out2 = [];
30816
+ let run2 = [];
30817
+ let chars = 0;
30818
+ const flush2 = async () => {
30819
+ if (run2.length === 0) return;
30820
+ const path23 = segmentPath(dir, run2[0].version, run2[run2.length - 1].version);
30821
+ if (await holdsWhole(app, path23, run2.map((r) => r.version))) storeSyncNext(app, path23);
30822
+ else await storeWrite(app, path23, run2.map((r) => framed(r.version, r.text)).join(""));
30823
+ out2.push(path23);
30824
+ run2 = [];
30825
+ chars = 0;
30826
+ };
30827
+ for (const r of versions) {
30828
+ const last = run2[run2.length - 1];
30829
+ if (last && (r.version !== last.version + 1 || chars >= SEGMENT_POLICY.maxBytes || run2.length >= SEGMENT_POLICY.maxVersions)) await flush2();
30830
+ run2.push(r);
30831
+ chars += r.text.length;
30832
+ }
30833
+ await flush2();
30834
+ return out2;
30835
+ }
30836
+ async function holdsWhole(app, path23, versions) {
30837
+ const text5 = await storeRead(app, path23);
30838
+ if (text5 === null) return false;
30839
+ const { commits, validEnd } = parseSegment(text5);
30840
+ return validEnd === text5.length && commits.length === versions.length && commits.every((c, i) => c.version === versions[i]);
30841
+ }
30842
+ async function gzip(text5) {
30843
+ const stream = new Blob([text5]).stream().pipeThrough(new CompressionStream("gzip"));
30844
+ return new Response(stream).arrayBuffer();
30845
+ }
30846
+ async function gunzip(data) {
30847
+ const stream = new Blob([data]).stream().pipeThrough(new DecompressionStream("gzip"));
30848
+ return new Response(stream).text();
30849
+ }
30850
+ function chunked(lines) {
30851
+ const out2 = [];
30852
+ let cur = [];
30853
+ let chars = 0;
30854
+ for (const l of lines) {
30855
+ if (cur.length > 0 && chars + l.length + 1 > CHECKPOINT_PART.maxChars) {
30856
+ out2.push(cur);
30857
+ cur = [];
30858
+ chars = 0;
30859
+ }
30860
+ cur.push(l);
30861
+ chars += l.length + 1;
30862
+ }
30863
+ if (cur.length > 0 || out2.length === 0) out2.push(cur);
30864
+ return out2;
30865
+ }
30866
+ async function writeManifestCheckpoint(app, dir, head, rows) {
30867
+ const main6 = manifestPath(dir, head.version);
30868
+ if (await wholeManifest(app, dir, main6)) return [];
30869
+ const binary = storeHoldsBinary(app);
30870
+ const written2 = [];
30871
+ const textLines = rows.map((r) => JSON.stringify([r.path, r.content]));
30872
+ const partOf = [];
30873
+ const parts = chunked(textLines);
30874
+ let row = 0;
30875
+ const partNames = [];
30876
+ for (const [i, lines] of parts.entries()) {
30877
+ const name = `${pad2(head.version)}.part-${i}.jsonl${binary ? ".gz" : ""}`;
30878
+ const text5 = lines.length > 0 ? `${lines.join("\n")}
30879
+ ` : "";
30880
+ if (binary) await storeWriteBinary(app, `${dir}/${name}`, await gzip(text5));
30881
+ else await storeWrite(app, `${dir}/${name}`, text5);
30882
+ written2.push(`${dir}/${name}`);
30883
+ partNames.push(name);
30884
+ for (let k = 0; k < lines.length; k++) partOf[row++] = i;
30170
30885
  }
30171
30886
  const entries = rows.map((r, i) => JSON.stringify([r.path, r.hash, r.content.length, partOf[i]]));
30172
30887
  const pieces = chunked(entries);
@@ -30631,1128 +31346,499 @@ async function stateFromLogged(app, settings, state) {
30631
31346
  movesApplied: state.movesApplied
30632
31347
  };
30633
31348
  }
30634
- function infoOf(text5) {
30635
- return JSON.parse(text5.slice(0, text5.indexOf("\n"))).commitInfo;
30636
- }
30637
- async function commitInfos(app, settings, full) {
30638
- const toc = await tocOf(app, settings, full);
30639
- return (await versionTexts(app, toc, toc.versions)).map(infoOf);
30640
- }
30641
- async function newestCommitInfos(app, settings, full, limit, from) {
30642
- const toc = await tocOf(app, settings, full);
30643
- const out2 = [];
30644
- for (const v of [...toc.versions].reverse()) {
30645
- if (out2.length >= limit) break;
30646
- const info = infoOf(await versionText(app, toc, v));
30647
- if (from !== void 0 && Date.parse(info.timestamp) < from) break;
30648
- out2.push(info);
30649
- }
30650
- return out2;
30651
- }
30652
- async function history(app, settings, full, limit = 50) {
30653
- const versions = (await listVersions(app, settings, full)).reverse().slice(0, Math.max(limit, 0));
30654
- const out2 = [];
30655
- for (const v of versions) out2.push((await readCommit(app, settings, full, v)).commitInfo);
30656
- return out2;
30657
- }
30658
- function catalogEntryOptions(full, entry, settings) {
30659
- const [c, s, t] = full.split(".");
30660
- const ref2 = refOf(c, s, t);
30661
- const options = parseTableOptions(entryToOrdered(ref2, entry, settings).find(([k]) => k === TABLE_KEY)?.[1]);
30662
- const layout = catalogLayout(normalizeDatabaseFolder(settings.databaseFolder), ref2, options);
30663
- return [...options.exclude ?? [], layout.archive, transactionLogRoot(settings)];
30664
- }
30665
- async function readCurrentStates(app, settings, model, touched, only) {
30666
- const out2 = /* @__PURE__ */ new Map();
30667
- const excludes = /* @__PURE__ */ new Map();
30668
- for (const { ref: ref2, entry } of catalogTables(model)) {
30669
- if (only && !only.has(ref2.full)) continue;
30670
- out2.set(ref2.full, { entry, files: /* @__PURE__ */ new Map() });
30671
- excludes.set(ref2.full, catalogEntryOptions(ref2.full, entry, settings));
30672
- }
30673
- if (out2.size === 0) return out2;
30674
- const candidates = /* @__PURE__ */ new Map();
30675
- for (const p of touched) if (p.endsWith(".md")) candidates.set(p, null);
30676
- for (const f of app.vault.getMarkdownFiles()) {
30677
- const fm = app.metadataCache.getFileCache(f)?.frontmatter;
30678
- if (fm && fm[settings.tableKey] != null) candidates.set(f.path, f);
30679
- }
30680
- const reader = rowStateReader(app, settings, model, out2, excludes);
30681
- for (const path23 of [...candidates.keys()].sort()) {
30682
- const r = await reader(path23, candidates.get(path23) ?? null);
30683
- if (r) out2.get(r.table).files.set(path23, r.state);
30684
- }
30685
- return out2;
30686
- }
30687
- function rowStateReader(app, settings, model, tables, excludes) {
30688
- const resolved = /* @__PURE__ */ new Map();
30689
- return async (path23, given) => {
30690
- const file = given ?? app.vault.getAbstractFileByPath(path23);
30691
- if (file == null || !("extension" in file)) return null;
30692
- let raw;
30693
- try {
30694
- raw = await readFrontmatterValue(app, file, NO_KEYS, (ordered) => fmGet(ordered, settings.tableKey));
30695
- } catch {
30696
- return null;
30697
- }
30698
- const rawKey = typeof raw === "string" ? raw : JSON.stringify(raw ?? null);
30699
- if (!resolved.has(rawKey)) resolved.set(rawKey, rowTableFull(model, raw));
30700
- const full = resolved.get(rawKey);
30701
- if (full == null || !tables.has(full)) return null;
30702
- if (excludes.get(full).some((dir) => path23.startsWith(`${dir}/`))) return null;
30703
- return { table: full, state: await readHashed(app, file) };
30704
- };
30705
- }
30706
- function forgetCurrentStates(app) {
30707
- LIVE.delete(app);
30708
- }
30709
- async function currentStates(app, settings, model, touched, only) {
30710
- const idx = vaultIndex(app, settings);
30711
- if (!idx || idx.hasFailures) return readCurrentStates(app, settings, model, touched, only);
30712
- let live = LIVE.get(app);
30713
- const kept2 = JSON.stringify([...only].sort());
30714
- const changed2 = live && live.model === model && live.only === kept2 && live.epoch === idx.epoch && live.tableKey === settings.tableKey && live.databaseFolder === settings.databaseFolder ? idx.changedSince(live.cursor, idx.epoch) : null;
30715
- const cursor = idx.cursor;
30716
- if (!live || !changed2) {
30717
- const tables = await readCurrentStates(app, settings, model, touched, only);
30718
- const excludes = /* @__PURE__ */ new Map();
30719
- const tableOf = /* @__PURE__ */ new Map();
30720
- for (const [full, t] of tables) {
30721
- excludes.set(full, catalogEntryOptions(full, t.entry, settings));
30722
- for (const p of t.files.keys()) tableOf.set(p, full);
30723
- }
30724
- const touchedOnly = new Set([...tableOf.keys()].filter((p) => !(idx.note(p)?.tableValue != null)));
30725
- live = { epoch: idx.epoch, cursor, model, only: kept2, tableKey: settings.tableKey, databaseFolder: settings.databaseFolder, tables, excludes, tableOf, touchedOnly };
30726
- LIVE.set(app, live);
30727
- } else {
30728
- const named = new Set(touched.filter((p) => p.endsWith(".md")));
30729
- const reader = rowStateReader(app, settings, model, live.tables, live.excludes);
30730
- for (const path23 of /* @__PURE__ */ new Set([...changed2, ...named, ...live.touchedOnly])) {
30731
- const was = live.tableOf.get(path23);
30732
- if (was !== void 0) live.tables.get(was).files.delete(path23);
30733
- live.tableOf.delete(path23);
30734
- live.touchedOnly.delete(path23);
30735
- const n = idx.note(path23);
30736
- const listed = n !== void 0 && n.tableValue != null;
30737
- if (!listed && !named.has(path23)) continue;
30738
- const r = await reader(path23, n?.file ?? null);
30739
- if (!r) continue;
30740
- live.tables.get(r.table).files.set(path23, r.state);
30741
- live.tableOf.set(path23, r.table);
30742
- if (!listed) live.touchedOnly.add(path23);
30743
- }
30744
- live.cursor = cursor;
30745
- }
30746
- return live.tables;
30747
- }
30748
- async function verifyCurrentStates(app, settings, repair = false) {
30749
- const ms = await loadMetastore(app, settings);
30750
- const live = LIVE.get(app);
30751
- if (!ms || !live) return [];
30752
- const only = new Set(JSON.parse(live.only));
30753
- const kept2 = await currentStates(app, settings, ms.model, [], only);
30754
- const now = await readCurrentStates(app, settings, ms.model, [], only);
30755
- const out2 = [];
30756
- for (const [full, t] of now) {
30757
- const a = kept2.get(full).files;
30758
- const b = t.files;
30759
- for (const p of /* @__PURE__ */ new Set([...a.keys(), ...b.keys()])) if (a.get(p)?.hash !== b.get(p)?.hash) out2.push(`${full}: ${p}`);
30760
- }
30761
- if (out2.length > 0 && repair) forgetCurrentStates(app);
30762
- return out2.sort();
30763
- }
30764
- function checkpointDue(state, format) {
30765
- const since = state.sinceCheckpoint;
30766
- if (since.bytes >= Math.max(CHECKPOINT_POLICY.bytesFactor * state.bytes, CHECKPOINT_POLICY.minBytes)) return true;
30767
- if (format === "segments") return false;
30768
- return since.versions >= Math.max(CHECKPOINT_POLICY.maxVersions, Math.ceil(state.bytes / CHECKPOINT_POLICY.versionBytes));
30769
- }
30770
- async function checkpointRows(app, settings, state) {
30771
- const texts = await loggedTexts(app, settings, state, state.files.keys());
30772
- if (texts.size !== state.files.size) return null;
30773
- return [...state.files].sort(([a], [b]) => a < b ? -1 : 1).map(([path23, f]) => ({ path: path23, hash: f.hash, content: texts.get(path23) }));
30774
- }
30775
- async function writeDueCheckpoint(app, settings, full) {
30776
- const state = LAST.get(app)?.get(full);
30777
- if (!state || !checkpointDue(state, formatOf(settings))) return false;
30778
- if (!await stillLatest(app, settings, state)) return false;
30779
- const rows = await checkpointRows(app, settings, state);
30780
- if (!rows) return false;
30781
- await writeManifestCheckpoint(app, transactionLogDir(settings, full), { version: state.version, timestamp: state.timestamp, entry: state.entry, ...state.movesApplied ? { moves: state.movesApplied } : {} }, rows);
30782
- state.sinceCheckpoint = { versions: 0, bytes: 0 };
30783
- return true;
30784
- }
30785
- function formatOf(settings) {
30786
- return settings.transactionLogFormat === "files" ? "files" : "segments";
30787
- }
30788
- function setCheckpointRunner(app, run2) {
30789
- if (run2) CHECKPOINT_RUNNERS.set(app, run2);
30790
- else CHECKPOINT_RUNNERS.delete(app);
30791
- }
30792
- async function writeDueCheckpoints(app, stopped = () => false) {
30793
- let n = 0;
30794
- for (; ; ) {
30795
- const due = DUE.get(app);
30796
- const next = due ? [...due][0] : void 0;
30797
- if (!next || stopped()) break;
30798
- const [full, settings] = next;
30799
- due.delete(full);
30800
- if (await withLogLock(app, () => writeDueCheckpoint(app, settings, full))) n++;
30801
- }
30802
- return n;
30803
- }
30804
- async function wholeCheckpoint(app, dir, v) {
30805
- return parseCheckpoint(await storeRead(app, checkpointPath(dir, v)) ?? "")?.version === v;
30806
- }
30807
- async function writeCheckpoint(app, dir, state, format) {
30808
- const files = [...state.files.entries()].sort(([a], [b]) => a.localeCompare(b));
30809
- if (format === "files") {
30810
- const cp = {
30811
- version: state.version,
30812
- timestamp: state.timestamp,
30813
- entry: state.entry,
30814
- files: files.map(([path23, f]) => ({ path: path23, hash: f.hash, content: f.content })),
30815
- ...state.movesApplied ? { moves: state.movesApplied } : {}
30816
- };
30817
- const text5 = JSON.stringify(cp);
30818
- if (text5.length <= LEGACY_CHECKPOINT.maxChars) {
30819
- const path23 = checkpointPath(dir, state.version);
30820
- if (!await wholeCheckpoint(app, dir, state.version)) await storeWrite(app, path23, text5);
30821
- return;
30822
- }
30823
- }
30824
- await writeManifestCheckpoint(app, dir, { version: state.version, timestamp: state.timestamp, entry: state.entry, ...state.movesApplied ? { moves: state.movesApplied } : {} }, files.map(([path23, f]) => ({ path: path23, hash: f.hash, content: f.content })));
30825
- }
30826
- function forgetLoggedStates(app) {
30827
- LAST.delete(app);
30828
- }
30829
- async function knownState(app, settings, full) {
30830
- return LAST.get(app)?.get(full) ?? readLastState(app, settings, full);
30831
- }
30832
- async function readLastState(app, settings, full) {
30833
- let byTable = LAST.get(app);
30834
- if (!byTable) LAST.set(app, byTable = /* @__PURE__ */ new Map());
30835
- const state = await replayLogged(app, settings, full);
30836
- byTable.set(full, state);
30837
- return state;
30838
- }
30839
- async function stillLatest(app, settings, state) {
30840
- return latestOf(await tocOf(app, settings, state.full)) === state.version;
30841
- }
30842
- async function latestVersion(app, settings, full) {
30843
- const latest = latestOf(await tocOf(app, settings, full));
30844
- return latest >= 0 ? latest : null;
30845
- }
30846
- async function warmLoggedStates(app, settings, stopped = () => false) {
30847
- let n = 0;
30848
- for (const full of await loggedTables(app, settings)) {
30849
- if (stopped()) break;
30850
- await withLogLock(app, async () => {
30851
- if (LAST.get(app)?.has(full)) return;
30852
- await readLastState(app, settings, full);
30853
- n++;
30854
- });
30855
- }
30856
- return n;
30857
- }
30858
- async function changesSince(app, prev, cur) {
30859
- const entry = cur?.entry ?? null;
30860
- let files = cur?.files ?? /* @__PURE__ */ new Map();
30861
- const remove3 = [];
30862
- const add = [];
30863
- if (cur) {
30864
- for (const [path23, f] of prev.files) {
30865
- if (files.has(path23) || !await unreadableNow(app, path23)) continue;
30866
- if (files === cur.files) files = new Map(files);
30867
- files.set(path23, f);
30868
- }
30869
- }
30870
- for (const [path23, f] of prev.files) {
30871
- const now = files.get(path23);
30872
- if (!now || now.hash !== f.hash) remove3.push({ path: path23, hash: f.hash });
30873
- }
30874
- for (const [path23, f] of files) {
30875
- const was = prev.files.get(path23);
30876
- if (!was || was.hash !== f.hash) add.push({ path: path23, hash: f.hash, size: f.content.length, content: f.content });
30877
- }
30878
- add.sort((a, b) => a.path < b.path ? -1 : 1);
30879
- const metadataChanged = prev.version < 0 || entryHash(prev.entry) !== entryHash(entry);
30880
- return { entry, add, remove: remove3, metadataChanged };
30881
- }
30882
- function historyEnabled(model, entry) {
30883
- const own2 = entry.options?.history;
30884
- return typeof own2 === "boolean" ? own2 : model.config.history === true;
30885
- }
30886
- async function syncTransactionLogs(app, settings, options) {
30887
- const ms = await loadMetastore(app, settings);
30888
- if (!ms) return [];
30889
- const tables = catalogTables(ms.model);
30890
- const enabled = new Set(tables.filter(({ entry }) => historyEnabled(ms.model, entry)).map(({ ref: ref2 }) => ref2.full));
30891
- const defined = new Set(tables.map(({ ref: ref2 }) => ref2.full));
30892
- const orphans = await loggedTables(app, settings, (full) => !defined.has(full));
30893
- if (enabled.size === 0 && orphans.length === 0) return [];
30894
- const current2 = await currentStates(app, settings, ms.model, options.touched ?? [], enabled);
30895
- const names2 = /* @__PURE__ */ new Set([...enabled, ...orphans]);
30896
- const out2 = [];
30897
- const now = options.now ?? /* @__PURE__ */ new Date();
30898
- const format = formatOf(settings);
30899
- const planned = [];
30900
- for (const full of [...names2].sort()) {
30901
- if (options.tables && !options.tables.includes(full)) continue;
30902
- let prev = await knownState(app, settings, full);
30903
- const cur = current2.get(full);
30904
- if (prev.version < 0 && !cur) continue;
30905
- const due = (c) => c.metadataChanged || c.add.length > 0 || c.remove.length > 0 || options.always === true && cur !== void 0;
30906
- let changes = await changesSince(app, prev, cur);
30907
- if (!due(changes)) continue;
30908
- if (!await stillLatest(app, settings, prev)) {
30909
- prev = await readLastState(app, settings, full);
30910
- changes = await changesSince(app, prev, cur);
30911
- if (!due(changes)) continue;
30912
- }
30913
- const { entry, add, remove: remove3, metadataChanged } = changes;
30914
- const operation = prev.version < 0 && options.operation === EXTERNAL_EDIT ? "CREATE TABLE" : options.operation;
30915
- planned.push({ table: full, add, remove: remove3, metadataChanged, entry, prev, operation });
30916
- }
30917
- const external = planned.filter((p) => p.operation === EXTERNAL_EDIT);
30918
- let counts;
30919
- if (options.check && external.length > 0) {
30920
- const added = new Set(external.flatMap((p) => p.add.map((a) => a.path)));
30921
- const changes = [];
30922
- for (const p of external) {
30923
- const wanted = /* @__PURE__ */ new Set([...p.remove.map((r) => r.path), ...[...added].filter((x) => p.prev.files.has(x))]);
30924
- const texts = await loggedTexts(app, settings, p.prev, wanted);
30925
- const before = /* @__PURE__ */ new Map();
30926
- for (const x of wanted) if (texts.has(x)) before.set(x, { hash: p.prev.files.get(x).hash, content: texts.get(x) });
30927
- const { prev: _prev, operation: _op, ...change } = p;
30928
- changes.push({ ...change, before });
30929
- }
30930
- counts = await options.check(changes);
30931
- }
30932
- for (const { table: full, add, remove: remove3, metadataChanged, entry, prev, operation } of planned) {
30933
- const version2 = prev.version + 1;
30934
- const violations = operation === EXTERNAL_EDIT ? counts?.get(full) : void 0;
30935
- const commit = {
30936
- commitInfo: {
30937
- version: version2,
30938
- timestamp: now.toISOString(),
30939
- operation,
30940
- ...options.parameters && Object.keys(options.parameters).length > 0 ? { operationParameters: options.parameters } : {},
30941
- ...options.migrationId ? { migrationId: options.migrationId } : {},
30942
- operationMetrics: { numAddedFiles: add.length, numRemovedFiles: remove3.length, metadataChanged, ...violations !== void 0 ? { violations } : {} },
30943
- engineInfo: "datanotes",
30944
- ...options.actor ? { userName: actorName(options.actor), actor: options.actor } : {}
30945
- },
30946
- ...metadataChanged ? { metaData: { entry } } : {},
30947
- add: [...add].sort((a, b) => a.path.localeCompare(b.path)),
30948
- remove: [...remove3].sort((a, b) => a.path.localeCompare(b.path))
30949
- };
30950
- const text5 = serializeCommit(commit);
30951
- const repaired = await appendVersion(app, transactionLogDir(settings, full), version2, text5, format);
30952
- for (const r of repaired) telemetry().log("WARN", "datanotes transaction log repaired a version torn by a crash", { table: full, repair: r });
30953
- applyLogged(prev, commit);
30954
- prev.sinceCheckpoint = { versions: prev.sinceCheckpoint.versions + 1, bytes: prev.sinceCheckpoint.bytes + text5.length };
30955
- if (version2 > 0 && checkpointDue(prev, format)) {
30956
- let due = DUE.get(app);
30957
- if (!due) DUE.set(app, due = /* @__PURE__ */ new Map());
30958
- due.set(full, settings);
30959
- }
30960
- out2.push({ table: full, version: version2, operation: commit.commitInfo.operation });
30961
- }
30962
- if (DUE.get(app)?.size) {
30963
- const runner = CHECKPOINT_RUNNERS.get(app);
30964
- if (runner) runner();
30965
- else {
30966
- const due = DUE.get(app);
30967
- for (const [full, s] of [...due]) {
30968
- due.delete(full);
30969
- await writeDueCheckpoint(app, s, full);
30970
- }
30971
- }
30972
- }
30973
- return out2;
30974
- }
30975
- function withLogLock(app, fn) {
30976
- let activity = ACTIVITY.get(app);
30977
- if (!activity) ACTIVITY.set(app, activity = { pending: 0, lastEnd: 0 });
30978
- const a = activity;
30979
- a.pending++;
30980
- const prev = LOCKS.get(app) ?? Promise.resolve();
30981
- const wait = startSpan("engine.lock.acquire", { attributes: { "datanotes.lock.queued": a.pending - 1 } });
30982
- const held = () => {
30983
- wait.end();
30984
- return phase("engine.lock.hold", async () => {
30985
- try {
30986
- return await fn();
30987
- } finally {
30988
- await storeFlush(app).catch((e) => telemetry().log("WARN", "datanotes could not sync the store's files", { error: messageOf(e) }));
30989
- }
30990
- });
30991
- };
30992
- const run2 = prev.then(held, held);
30993
- const settled2 = run2.then(
30994
- () => void 0,
30995
- () => void 0
30996
- );
30997
- LOCKS.set(app, settled2);
30998
- void settled2.then(() => {
30999
- a.pending--;
31000
- a.lastEnd = Date.now();
31001
- });
31002
- return run2;
31003
- }
31004
- function logLockActivity(app) {
31005
- const a = ACTIVITY.get(app);
31006
- return { busy: (a?.pending ?? 0) > 0, lastEnd: a?.lastEnd ?? 0 };
31007
- }
31008
- async function checkpointFiles(app, dir, toc) {
31009
- const out2 = /* @__PURE__ */ new Map();
31010
- for (const cp of toc.checkpoints) {
31011
- const files = cp.kind === 1 ? [cp.path] : manifestFiles(dir, cp.path, (await readManifest(app, dir, cp.path))?.head ?? null);
31012
- out2.set(cp.version, [...out2.get(cp.version) ?? [], ...files]);
31013
- }
31014
- return out2;
31015
- }
31016
- async function vacuum(app, settings, full, retainFrom, dryRun) {
31017
- const dir = transactionLogDir(settings, full);
31018
- const toc = await logToc(app, dir);
31019
- const versions = toc.versions;
31020
- if (versions.length === 0) throw new HistoryError(`${full} has no history`);
31021
- const latest = versions[versions.length - 1];
31022
- const keep = retainFrom ?? latest;
31023
- if (!versions.includes(keep)) throw new HistoryError(`${full} has no version ${keep} (available: ${versions[0]}..${latest})`);
31024
- const doomed = [...toc.files].filter(([v]) => v < keep).map(([, p]) => p);
31025
- const straddling = toc.segments.filter((s) => s.first < keep && s.last >= keep);
31026
- for (const s of toc.segments) if (s.last < keep || straddling.includes(s)) doomed.push(s.path);
31027
- for (const [v, files] of await checkpointFiles(app, dir, toc)) if (v < keep) doomed.push(...files);
31028
- doomed.sort();
31029
- if (!dryRun) {
31030
- await writeCheckpoint(app, dir, await tableStateAt(app, settings, full, { version: keep }), formatOf(settings));
31031
- for (const s of straddling) {
31032
- const kept2 = [];
31033
- for (let v = keep; v <= s.last; v++) kept2.push({ version: v, text: await versionText(app, toc, v) });
31034
- await writeSealedSegments(app, dir, kept2);
31035
- }
31036
- await storeFlush(app);
31037
- for (const p of doomed) await storeRemove(app, p);
31038
- forgetLoggedStates(app);
31039
- }
31040
- return { table: full, retained_from: keep, deleted: doomed, dry_run: dryRun };
31041
- }
31042
- async function convertLog(app, settings, full, to) {
31043
- const dir = transactionLogDir(settings, full);
31044
- const toc = await logToc(app, dir);
31045
- const result = { table: full, versions: toc.versions.length, written: 0, removed: 0 };
31046
- if (to === "segments") {
31047
- for (const s of toc.segments) {
31048
- if (!s.sealed || (await wholeVersions(app, s)).size === s.last - s.first + 1) {
31049
- storeSyncNext(app, s.path);
31050
- continue;
31051
- }
31052
- const range = Array.from({ length: s.last - s.first + 1 }, (_, i) => s.first + i);
31053
- const texts2 = await versionTexts(app, toc, range);
31054
- await storeWrite(app, s.path, range.map((v, i) => framed(v, texts2[i])).join(""));
31055
- result.written++;
31056
- }
31057
- const loose = toc.versions.filter((v) => toc.files.has(v) && !toc.segments.some((s) => v >= s.first && v <= s.last));
31058
- const texts = await versionTexts(app, toc, loose);
31059
- const written2 = await writeSealedSegments(app, dir, loose.map((version2, i) => ({ version: version2, text: texts[i] })));
31060
- result.written += written2.length;
31061
- await storeFlush(app);
31062
- for (const v of toc.files.keys()) {
31063
- await storeRemove(app, toc.files.get(v));
31064
- result.removed++;
31065
- }
31066
- } else {
31067
- for (const seg of toc.segments) {
31068
- for (let v = seg.first; v <= seg.last; v++) {
31069
- const path23 = `${dir}/${pad2(v)}.json`;
31070
- const file = toc.files.get(v);
31071
- if (file === void 0) {
31072
- await storeWrite(app, path23, await versionText(app, toc, v));
31073
- result.written++;
31074
- } else storeSyncNext(app, file);
31075
- }
31076
- }
31077
- const oldest = toc.versions[0];
31078
- if (oldest !== void 0 && oldest > 0) {
31079
- if (await wholeCheckpoint(app, dir, oldest)) storeSyncNext(app, checkpointPath(dir, oldest));
31080
- else {
31081
- await writeCheckpoint(app, dir, await replayState(app, settings, full, { version: oldest }), "files");
31082
- result.written++;
31083
- }
31084
- }
31085
- await storeFlush(app);
31086
- for (const seg of toc.segments) {
31087
- await storeRemove(app, seg.path);
31088
- result.removed++;
31089
- }
31090
- }
31091
- forgetLoggedStates(app);
31092
- return result;
31093
- }
31094
- var TRANSACTION_LOG_FOLDER, CHECKPOINT_POLICY, LEGACY_CHECKPOINT, EXTERNAL_EDIT, AbsentTableError, ENTRY_HASHES, NO_KEYS, LIVE, DUE, CHECKPOINT_RUNNERS, LAST, LOCKS, ACTIVITY;
31095
- var init_transaction_log = __esm({
31096
- "src/core/transaction-log.ts"() {
31097
- "use strict";
31098
- init_file_cache();
31099
- init_frontmatter();
31100
- init_metastore();
31101
- init_actor();
31102
- init_store();
31103
- init_table_loader();
31104
- init_types_table();
31105
- init_database_move();
31106
- init_db_paths();
31107
- init_unreadable();
31108
- init_tracing();
31109
- init_vault_index();
31110
- init_telemetry();
31111
- init_message();
31112
- init_log_files();
31113
- TRANSACTION_LOG_FOLDER = "transaction_log";
31114
- CHECKPOINT_POLICY = { bytesFactor: 2, minBytes: 256 * 1024, maxVersions: 1e3, versionBytes: 16 * 1024 };
31115
- LEGACY_CHECKPOINT = { maxChars: 32 * 1024 * 1024 };
31116
- EXTERNAL_EDIT = "EXTERNAL_EDIT";
31117
- AbsentTableError = class extends HistoryError {
31118
- };
31119
- ENTRY_HASHES = /* @__PURE__ */ new WeakMap();
31120
- NO_KEYS = /* @__PURE__ */ new Set();
31121
- LIVE = /* @__PURE__ */ new WeakMap();
31122
- DUE = /* @__PURE__ */ new WeakMap();
31123
- CHECKPOINT_RUNNERS = /* @__PURE__ */ new WeakMap();
31124
- LAST = /* @__PURE__ */ new WeakMap();
31125
- LOCKS = /* @__PURE__ */ new WeakMap();
31126
- ACTIVITY = /* @__PURE__ */ new WeakMap();
31127
- }
31128
- });
31129
-
31130
- // src/core/changeset.ts
31131
- function createdHash(text5) {
31132
- try {
31133
- const { ordered, body } = readOrderedFromMarkdown(text5);
31134
- return contentHash(JSON.stringify([ordered, body]));
31135
- } catch {
31136
- return contentHash(text5);
31137
- }
31138
- }
31139
- function bodyChanges(e) {
31140
- return e.bodyAfter !== void 0 && e.bodyAfter !== e.bodyBefore;
31349
+ function infoOf(text5) {
31350
+ return JSON.parse(text5.slice(0, text5.indexOf("\n"))).commitInfo;
31141
31351
  }
31142
- function bodyOf(text5) {
31143
- const split2 = splitFrontmatter(text5);
31144
- return split2 ? split2.body : text5;
31352
+ async function commitInfos(app, settings, full) {
31353
+ const toc = await tocOf(app, settings, full);
31354
+ return (await versionTexts(app, toc, toc.versions)).map(infoOf);
31145
31355
  }
31146
- function replaceBody(text5, body) {
31147
- const split2 = splitFrontmatter(text5);
31148
- if (!split2) return body;
31149
- return text5.slice(0, text5.length - split2.body.length) + body;
31356
+ async function newestCommitInfos(app, settings, full, limit, from) {
31357
+ const toc = await tocOf(app, settings, full);
31358
+ const out2 = [];
31359
+ for (const v of [...toc.versions].reverse()) {
31360
+ if (out2.length >= limit) break;
31361
+ const info = infoOf(await versionText(app, toc, v));
31362
+ if (from !== void 0 && Date.parse(info.timestamp) < from) break;
31363
+ out2.push(info);
31364
+ }
31365
+ return out2;
31150
31366
  }
31151
- async function writeBody(app, file, expected, next) {
31152
- await app.vault.process(file, (text5) => {
31153
- if (bodyOf(text5) !== expected) throw new Error("note body changed since it was read");
31154
- return replaceBody(text5, next);
31155
- });
31367
+ async function history(app, settings, full, limit = 50) {
31368
+ const versions = (await listVersions(app, settings, full)).reverse().slice(0, Math.max(limit, 0));
31369
+ const out2 = [];
31370
+ for (const v of versions) out2.push((await readCommit(app, settings, full, v)).commitInfo);
31371
+ return out2;
31156
31372
  }
31157
- function emptyChangeSet(op2, summary, schema) {
31158
- return {
31159
- op: op2,
31160
- summary,
31161
- ...schema !== void 0 ? { schema } : {},
31162
- edits: [],
31163
- creates: [],
31164
- moves: [],
31165
- deletes: [],
31166
- texts: []
31167
- };
31373
+ function catalogEntryOptions(full, entry, settings) {
31374
+ const [c, s, t] = full.split(".");
31375
+ const ref2 = refOf(c, s, t);
31376
+ const options = parseTableOptions(entryToOrdered(ref2, entry, settings).find(([k]) => k === TABLE_KEY)?.[1]);
31377
+ const layout = catalogLayout(normalizeDatabaseFolder(settings.databaseFolder), ref2, options);
31378
+ return [...options.exclude ?? [], layout.archive, transactionLogRoot(settings)];
31168
31379
  }
31169
- function same(a, b) {
31170
- return JSON.stringify(a) === JSON.stringify(b);
31380
+ async function readCurrentStates(app, settings, model, touched, only) {
31381
+ const out2 = /* @__PURE__ */ new Map();
31382
+ const excludes = /* @__PURE__ */ new Map();
31383
+ for (const { ref: ref2, entry } of catalogTables(model)) {
31384
+ if (only && !only.has(ref2.full)) continue;
31385
+ out2.set(ref2.full, { entry, files: /* @__PURE__ */ new Map() });
31386
+ excludes.set(ref2.full, catalogEntryOptions(ref2.full, entry, settings));
31387
+ }
31388
+ if (out2.size === 0) return out2;
31389
+ const candidates = /* @__PURE__ */ new Map();
31390
+ for (const p of touched) if (p.endsWith(".md")) candidates.set(p, null);
31391
+ for (const f of app.vault.getMarkdownFiles()) {
31392
+ const fm = app.metadataCache.getFileCache(f)?.frontmatter;
31393
+ if (fm && fm[settings.tableKey] != null) candidates.set(f.path, f);
31394
+ }
31395
+ const reader = rowStateReader(app, settings, model, out2, excludes);
31396
+ for (const path23 of [...candidates.keys()].sort()) {
31397
+ const r = await reader(path23, candidates.get(path23) ?? null);
31398
+ if (r) out2.get(r.table).files.set(path23, r.state);
31399
+ }
31400
+ return out2;
31171
31401
  }
31172
- function diffOrdered(before, after) {
31173
- const b = new Map(before);
31174
- const a = new Map(after);
31175
- const d = {
31176
- set: [],
31177
- remove: [],
31178
- beforeValues: {},
31179
- beforeAbsent: [],
31180
- afterValues: {},
31181
- afterAbsent: []
31402
+ function rowStateReader(app, settings, model, tables, excludes) {
31403
+ const resolved = /* @__PURE__ */ new Map();
31404
+ return async (path23, given) => {
31405
+ const file = given ?? app.vault.getAbstractFileByPath(path23);
31406
+ if (file == null || !("extension" in file)) return null;
31407
+ let raw;
31408
+ try {
31409
+ raw = await readFrontmatterValue(app, file, NO_KEYS, (ordered) => fmGet(ordered, settings.tableKey));
31410
+ } catch {
31411
+ return null;
31412
+ }
31413
+ const rawKey = typeof raw === "string" ? raw : JSON.stringify(raw ?? null);
31414
+ if (!resolved.has(rawKey)) resolved.set(rawKey, rowTableFull(model, raw));
31415
+ const full = resolved.get(rawKey);
31416
+ if (full == null || !tables.has(full)) return null;
31417
+ if (excludes.get(full).some((dir) => path23.startsWith(`${dir}/`))) return null;
31418
+ return { table: full, state: await readHashed(app, file) };
31182
31419
  };
31183
- const keys = /* @__PURE__ */ new Set([...b.keys(), ...a.keys()]);
31184
- for (const k of keys) {
31185
- const inB = b.has(k);
31186
- const inA = a.has(k);
31187
- if (inB && inA && same(b.get(k), a.get(k))) continue;
31188
- if (inB) d.beforeValues[k] = b.get(k);
31189
- else d.beforeAbsent.push(k);
31190
- if (inA) {
31191
- d.afterValues[k] = a.get(k);
31192
- d.set.push([k, a.get(k)]);
31193
- } else {
31194
- d.afterAbsent.push(k);
31195
- d.remove.push(k);
31420
+ }
31421
+ function forgetCurrentStates(app) {
31422
+ LIVE.delete(app);
31423
+ }
31424
+ async function currentStates(app, settings, model, touched, only) {
31425
+ const idx = vaultIndex(app, settings);
31426
+ if (!idx || idx.hasFailures) return readCurrentStates(app, settings, model, touched, only);
31427
+ let live = LIVE.get(app);
31428
+ const kept2 = JSON.stringify([...only].sort());
31429
+ const changed2 = live && live.model === model && live.only === kept2 && live.epoch === idx.epoch && live.tableKey === settings.tableKey && live.databaseFolder === settings.databaseFolder ? idx.changedSince(live.cursor, idx.epoch) : null;
31430
+ const cursor = idx.cursor;
31431
+ if (!live || !changed2) {
31432
+ const tables = await readCurrentStates(app, settings, model, touched, only);
31433
+ const excludes = /* @__PURE__ */ new Map();
31434
+ const tableOf = /* @__PURE__ */ new Map();
31435
+ for (const [full, t] of tables) {
31436
+ excludes.set(full, catalogEntryOptions(full, t.entry, settings));
31437
+ for (const p of t.files.keys()) tableOf.set(p, full);
31438
+ }
31439
+ const touchedOnly = new Set([...tableOf.keys()].filter((p) => !(idx.note(p)?.tableValue != null)));
31440
+ live = { epoch: idx.epoch, cursor, model, only: kept2, tableKey: settings.tableKey, databaseFolder: settings.databaseFolder, tables, excludes, tableOf, touchedOnly };
31441
+ LIVE.set(app, live);
31442
+ } else {
31443
+ const named = new Set(touched.filter((p) => p.endsWith(".md")));
31444
+ const reader = rowStateReader(app, settings, model, live.tables, live.excludes);
31445
+ for (const path23 of /* @__PURE__ */ new Set([...changed2, ...named, ...live.touchedOnly])) {
31446
+ const was = live.tableOf.get(path23);
31447
+ if (was !== void 0) live.tables.get(was).files.delete(path23);
31448
+ live.tableOf.delete(path23);
31449
+ live.touchedOnly.delete(path23);
31450
+ const n = idx.note(path23);
31451
+ const listed = n !== void 0 && n.tableValue != null;
31452
+ if (!listed && !named.has(path23)) continue;
31453
+ const r = await reader(path23, n?.file ?? null);
31454
+ if (!r) continue;
31455
+ live.tables.get(r.table).files.set(path23, r.state);
31456
+ live.tableOf.set(path23, r.table);
31457
+ if (!listed) live.touchedOnly.add(path23);
31196
31458
  }
31459
+ live.cursor = cursor;
31197
31460
  }
31198
- return d;
31461
+ return live.tables;
31199
31462
  }
31200
- function editIsNoop(e) {
31201
- const d = diffOrdered(e.before, e.after);
31202
- const orderChanged = !same(
31203
- e.before.map(([k]) => k),
31204
- e.after.map(([k]) => k)
31205
- );
31206
- return d.set.length === 0 && d.remove.length === 0 && !orderChanged && !bodyChanges(e);
31463
+ async function verifyCurrentStates(app, settings, repair = false) {
31464
+ const ms = await loadMetastore(app, settings);
31465
+ const live = LIVE.get(app);
31466
+ if (!ms || !live) return [];
31467
+ const only = new Set(JSON.parse(live.only));
31468
+ const kept2 = await currentStates(app, settings, ms.model, [], only);
31469
+ const now = await readCurrentStates(app, settings, ms.model, [], only);
31470
+ const out2 = [];
31471
+ for (const [full, t] of now) {
31472
+ const a = kept2.get(full).files;
31473
+ const b = t.files;
31474
+ for (const p of /* @__PURE__ */ new Set([...a.keys(), ...b.keys()])) if (a.get(p)?.hash !== b.get(p)?.hash) out2.push(`${full}: ${p}`);
31475
+ }
31476
+ if (out2.length > 0 && repair) forgetCurrentStates(app);
31477
+ return out2.sort();
31207
31478
  }
31208
- function applyDiff(current2, set, remove3, order) {
31209
- let out2 = current2;
31210
- for (const k of remove3) out2 = fmDelete(out2, k);
31211
- for (const [k, v] of set) out2 = fmSet(out2, k, v);
31212
- return fmReorder(out2, order);
31479
+ function checkpointDue(state, format) {
31480
+ const since = state.sinceCheckpoint;
31481
+ if (since.bytes >= Math.max(CHECKPOINT_POLICY.bytesFactor * state.bytes, CHECKPOINT_POLICY.minBytes)) return true;
31482
+ if (format === "segments") return false;
31483
+ return since.versions >= Math.max(CHECKPOINT_POLICY.maxVersions, Math.ceil(state.bytes / CHECKPOINT_POLICY.versionBytes));
31213
31484
  }
31214
- function fileOrNull(app, path23) {
31215
- const f = app.vault.getAbstractFileByPath(path23);
31216
- return f != null && "extension" in f ? f : null;
31485
+ async function checkpointRows(app, settings, state) {
31486
+ const texts = await loggedTexts(app, settings, state, state.files.keys());
31487
+ if (texts.size !== state.files.size) return null;
31488
+ return [...state.files].sort(([a], [b]) => a < b ? -1 : 1).map(([path23, f]) => ({ path: path23, hash: f.hash, content: texts.get(path23) }));
31217
31489
  }
31218
- function parentFolder(path23) {
31219
- const i = path23.lastIndexOf("/");
31220
- return i < 0 ? "" : path23.slice(0, i);
31490
+ async function writeDueCheckpoint(app, settings, full) {
31491
+ const state = LAST.get(app)?.get(full);
31492
+ if (!state || !checkpointDue(state, formatOf(settings))) return false;
31493
+ if (!await stillLatest(app, settings, state)) return false;
31494
+ const rows = await checkpointRows(app, settings, state);
31495
+ if (!rows) return false;
31496
+ await writeManifestCheckpoint(app, transactionLogDir(settings, full), { version: state.version, timestamp: state.timestamp, entry: state.entry, ...state.movesApplied ? { moves: state.movesApplied } : {} }, rows);
31497
+ state.sinceCheckpoint = { versions: 0, bytes: 0 };
31498
+ return true;
31221
31499
  }
31222
- async function ensureFolder(app, folder) {
31223
- if (folder === "") return;
31224
- if (app.vault.getAbstractFileByPath(folder) != null) return;
31225
- await ensureFolder(app, parentFolder(folder));
31226
- await app.vault.createFolder(folder);
31500
+ function formatOf(settings) {
31501
+ return settings.transactionLogFormat === "files" ? "files" : "segments";
31227
31502
  }
31228
- async function readOrderedFor(app, file) {
31229
- return readOrderedFromMarkdown(await app.vault.read(file)).ordered;
31503
+ function setCheckpointRunner(app, run2) {
31504
+ if (run2) CHECKPOINT_RUNNERS.set(app, run2);
31505
+ else CHECKPOINT_RUNNERS.delete(app);
31230
31506
  }
31231
- async function fileExists(app, path23) {
31232
- return inStore(path23) ? storeExists(app, path23) : fileOrNull(app, path23) != null;
31507
+ async function writeDueCheckpoints(app, stopped = () => false) {
31508
+ let n = 0;
31509
+ for (; ; ) {
31510
+ const due = DUE.get(app);
31511
+ const next = due ? [...due][0] : void 0;
31512
+ if (!next || stopped()) break;
31513
+ const [full, settings] = next;
31514
+ due.delete(full);
31515
+ if (await withLogLock(app, () => writeDueCheckpoint(app, settings, full))) n++;
31516
+ }
31517
+ return n;
31233
31518
  }
31234
- async function occupied(app, path23) {
31235
- return inStore(path23) ? storeExists(app, path23) : app.vault.getAbstractFileByPath(path23) != null;
31519
+ async function wholeCheckpoint(app, dir, v) {
31520
+ return parseCheckpoint(await storeRead(app, checkpointPath(dir, v)) ?? "")?.version === v;
31236
31521
  }
31237
- async function storeFilesSince(app, moves) {
31238
- const folders = /* @__PURE__ */ new Map();
31239
- for (const m of moves) if (inStore(m.from) && inStore(m.to)) folders.set(parentFolder(m.to), parentFolder(m.from));
31240
- const moved = new Set(moves.map((m) => m.to));
31241
- const out2 = [];
31242
- for (const [to, from] of folders) {
31243
- for (const f of (await storeList(app, to)).files) {
31244
- if (!moved.has(f)) out2.push({ from: `${from}/${f.slice(to.length + 1)}`, to: f });
31522
+ async function writeCheckpoint(app, dir, state, format) {
31523
+ const files = [...state.files.entries()].sort(([a], [b]) => a.localeCompare(b));
31524
+ if (format === "files") {
31525
+ const cp = {
31526
+ version: state.version,
31527
+ timestamp: state.timestamp,
31528
+ entry: state.entry,
31529
+ files: files.map(([path23, f]) => ({ path: path23, hash: f.hash, content: f.content })),
31530
+ ...state.movesApplied ? { moves: state.movesApplied } : {}
31531
+ };
31532
+ const text5 = JSON.stringify(cp);
31533
+ if (text5.length <= LEGACY_CHECKPOINT.maxChars) {
31534
+ const path23 = checkpointPath(dir, state.version);
31535
+ if (!await wholeCheckpoint(app, dir, state.version)) await storeWrite(app, path23, text5);
31536
+ return;
31245
31537
  }
31246
31538
  }
31247
- return out2;
31539
+ await writeManifestCheckpoint(app, dir, { version: state.version, timestamp: state.timestamp, entry: state.entry, ...state.movesApplied ? { moves: state.movesApplied } : {} }, files.map(([path23, f]) => ({ path: path23, hash: f.hash, content: f.content })));
31248
31540
  }
31249
- function atOnceFor(count2) {
31250
- return count2 >= WRITE_CONCURRENCY.from ? WRITE_CONCURRENCY.atOnce : 1;
31541
+ function forgetLoggedStates(app) {
31542
+ LAST.delete(app);
31251
31543
  }
31252
- async function inOrder(items3, pathOf, fn, limit = atOnceFor(items3.length)) {
31253
- const results = new Array(items3.length);
31254
- let next = 0;
31255
- let stop = false;
31256
- const worker = async () => {
31257
- while (!stop && next < items3.length) {
31258
- const i = next++;
31259
- try {
31260
- results[i] = { ok: true, written: await fn(items3[i]) };
31261
- } catch (e) {
31262
- results[i] = { ok: false, error: messageOf(e) };
31263
- stop = true;
31264
- }
31265
- }
31266
- };
31267
- await Promise.all(Array.from({ length: Math.min(limit, items3.length) }, worker));
31268
- const first2 = results.findIndex((r) => r !== void 0 && !r.ok);
31269
- const kept2 = /* @__PURE__ */ new Set();
31270
- if (first2 >= 0) {
31271
- for (let i = results.length - 1; i > first2; i--) {
31272
- const r = results[i];
31273
- if (!r?.ok) continue;
31274
- try {
31275
- await r.written.undo();
31276
- } catch {
31277
- kept2.add(i);
31278
- }
31279
- }
31280
- }
31281
- const done = [];
31282
- results.forEach((r, i) => {
31283
- if (r?.ok && (first2 < 0 || i < first2 || kept2.has(i))) done.push(r.written.value);
31284
- });
31285
- const f = first2 >= 0 ? results[first2] : null;
31286
- return { done, failed: f ? { path: pathOf(items3[first2]), error: f.error } : null };
31544
+ async function knownState(app, settings, full) {
31545
+ return LAST.get(app)?.get(full) ?? readLastState(app, settings, full);
31287
31546
  }
31288
- async function moveFile(app, from, to) {
31289
- if (inStore(from)) return storeRename(app, from, to);
31290
- const f = fileOrNull(app, from);
31291
- if (!f) throw new Error(`file not found: ${from}`);
31292
- if (app.vault.getAbstractFileByPath(to) != null) throw new Error(`target exists: ${to}`);
31293
- await ensureFolder(app, parentFolder(to));
31294
- await app.fileManager.renameFile(f, to);
31547
+ async function readLastState(app, settings, full) {
31548
+ let byTable = LAST.get(app);
31549
+ if (!byTable) LAST.set(app, byTable = /* @__PURE__ */ new Map());
31550
+ const state = await replayLogged(app, settings, full);
31551
+ byTable.set(full, state);
31552
+ return state;
31295
31553
  }
31296
- async function applyChangeSetImpl(app, settings, cs, now = /* @__PURE__ */ new Date()) {
31297
- const rows = [];
31298
- const creates = [];
31299
- const moves = [];
31300
- const references = [];
31301
- const deleted = [];
31302
- const failed2 = [];
31303
- let referencesUpdated = 0;
31304
- const fail2 = (path23, e) => {
31305
- failed2.push({ path: path23, error: messageOf(e) });
31306
- };
31307
- run: {
31308
- const folders = /* @__PURE__ */ new Map();
31309
- const folderOnce = (folder) => {
31310
- if (folder === "") return Promise.resolve();
31311
- let p = folders.get(folder);
31312
- if (!p) {
31313
- p = folderOnce(parentFolder(folder)).then(async () => {
31314
- if (app.vault.getAbstractFileByPath(folder) == null) await app.vault.createFolder(folder);
31315
- });
31316
- folders.set(folder, p);
31317
- }
31318
- return p;
31319
- };
31320
- const created = await inOrder(cs.creates, (c) => c.path, async (c) => {
31321
- if (app.vault.getAbstractFileByPath(c.path) != null) throw new Error(`file exists: ${c.path}`);
31322
- await folderOnce(parentFolder(c.path));
31323
- const file = await app.vault.create(c.path, c.content);
31324
- return { value: c.path, undo: () => app.vault.delete(file, true) };
31325
- });
31326
- creates.push(...created.done);
31327
- if (created.failed) {
31328
- failed2.push(created.failed);
31329
- break run;
31330
- }
31331
- for (const t of cs.texts) {
31332
- try {
31333
- const existing = app.vault.getAbstractFileByPath(t.path);
31334
- if (existing == null) {
31335
- if (t.before !== "") throw new Error("file not found");
31336
- if (t.path === metastorePath(settings)) {
31337
- t.after = withNewDatabaseConfig(t.after);
31338
- if (settings.databaseId) t.after = withMetastoreId(t.after, settings.databaseId);
31339
- }
31340
- await ensureFolder(app, parentFolder(t.path));
31341
- await app.vault.create(t.path, t.after);
31342
- } else {
31343
- await app.vault.process(existing, (current2) => {
31344
- if (current2 !== t.before) throw new Error("file changed since it was read");
31345
- return t.after;
31346
- });
31347
- }
31348
- references.push({ path: t.path, before_text: t.before, after_text: t.after });
31349
- } catch (err) {
31350
- fail2(t.path, err);
31351
- break run;
31352
- }
31353
- }
31354
- const edits = cs.edits.filter((e) => !editIsNoop(e));
31355
- const distinct = new Set(edits.map((e) => e.path)).size === edits.length;
31356
- const atOnce = distinct ? atOnceFor(edits.length) : 1;
31357
- const edited = await inOrder(edits, (e) => e.path, async (e) => {
31358
- const file = fileOrNull(app, e.path);
31359
- if (!file) throw new Error("file not found");
31360
- const was = atOnce > 1 ? (await readHashed(app, file)).content : "";
31361
- const d = diffOrdered(e.before, e.after);
31362
- const order = e.after.map(([k]) => k);
31363
- const fmChanged = d.set.length > 0 || d.remove.length > 0 || !same(e.before.map(([k]) => k), order);
31364
- if (fmChanged) {
31365
- await writeFrontmatter(app, file, (fm) => applyDiff(fm, d.set, d.remove, order));
31366
- }
31367
- if (bodyChanges(e)) await writeBody(app, file, e.bodyBefore, e.bodyAfter);
31368
- const row = {
31369
- path: e.path,
31370
- before: d.beforeValues,
31371
- before_absent: d.beforeAbsent,
31372
- before_order: e.before.map(([k]) => k),
31373
- after: d.afterValues,
31374
- after_absent: d.afterAbsent,
31375
- ...bodyChanges(e) ? { body_before: e.bodyBefore, body_after: e.bodyAfter } : {}
31376
- };
31377
- return { value: row, undo: async () => void await app.vault.process(file, () => was) };
31378
- }, atOnce);
31379
- rows.push(...edited.done);
31380
- if (edited.failed) {
31381
- failed2.push(edited.failed);
31382
- break run;
31383
- }
31384
- for (const m of cs.moves) {
31385
- try {
31386
- await moveFile(app, m.from, m.to);
31387
- moves.push(m);
31388
- } catch (err) {
31389
- fail2(m.from, err);
31390
- break run;
31391
- }
31392
- }
31393
- for (const p of cs.deletes) {
31394
- const file = fileOrNull(app, p);
31395
- try {
31396
- if (!file) throw new Error("file not found");
31397
- const content = await app.vault.read(file);
31398
- await trash(app, file);
31399
- deleted.push({ path: p, content });
31400
- } catch (err) {
31401
- fail2(p, err);
31402
- break run;
31403
- }
31404
- }
31405
- }
31406
- const createdHashes = [];
31407
- const movedTo = new Map(moves.map((m) => [m.from, m.to]));
31408
- for (const p of creates) {
31409
- const f = fileOrNull(app, movedTo.get(p) ?? p);
31410
- if (!f) continue;
31411
- try {
31412
- createdHashes.push({ path: p, hash: createdHash(await app.vault.read(f)) });
31413
- } catch {
31414
- }
31415
- }
31416
- const journal = await writeJournal(
31417
- app,
31418
- settings,
31419
- {
31420
- op: cs.op,
31421
- ...cs.schema !== void 0 ? { schema: cs.schema } : {},
31422
- ...currentActor() ? { actor: currentActor() } : {},
31423
- summary: cs.summary,
31424
- executed_at: now.toISOString(),
31425
- rows,
31426
- creates,
31427
- ...createdHashes.length > 0 ? { created_hashes: createdHashes } : {},
31428
- moves,
31429
- references,
31430
- deleted,
31431
- failed: failed2
31432
- },
31433
- now
31434
- );
31435
- return {
31436
- ok: failed2.length === 0,
31437
- migrationId: journal.id,
31438
- journalPath: journal.path,
31439
- notesChanged: rows.length,
31440
- filesCreated: creates.length,
31441
- filesMoved: moves.length,
31442
- filesDeleted: deleted.length,
31443
- referencesUpdated,
31444
- failed: failed2
31445
- };
31554
+ async function stillLatest(app, settings, state) {
31555
+ return latestOf(await tocOf(app, settings, state.full)) === state.version;
31446
31556
  }
31447
- async function trash(app, file) {
31448
- const fm = app.fileManager;
31449
- if (typeof fm.trashFile === "function") {
31450
- await fm.trashFile(file);
31451
- } else {
31452
- await app.vault.trash(file, true);
31453
- }
31557
+ async function latestVersion(app, settings, full) {
31558
+ const latest = latestOf(await tocOf(app, settings, full));
31559
+ return latest >= 0 ? latest : null;
31454
31560
  }
31455
- async function undoChangeSet(app, entries, summary = `undo ${entries.map((e) => e.migration_id).join(", ")}`) {
31456
- const state = /* @__PURE__ */ new Map();
31457
- const texts = /* @__PURE__ */ new Map();
31458
- const origin = /* @__PURE__ */ new Map();
31459
- const vaultNote = async (path23) => {
31460
- const f = fileOrNull(app, path23);
31461
- if (!f) return null;
31462
- const text5 = await app.vault.read(f);
31463
- return { ordered: readOrderedFromMarkdown(text5).ordered, body: bodyOf(text5) };
31464
- };
31465
- const read3 = async (path23) => state.has(path23) ? state.get(path23) : vaultNote(path23);
31466
- for (const entry of entries) {
31467
- const movedTo = new Map(entry.moves.map((m) => [m.from, m.to]));
31468
- const currentPath = (p) => movedTo.get(p) ?? p;
31469
- for (const ref2 of entry.references) {
31470
- const path23 = currentPath(ref2.path);
31471
- if (/\.md$/i.test(path23)) {
31472
- const { ordered } = readOrderedFromMarkdown(ref2.before_text);
31473
- state.set(path23, { ordered, body: bodyOf(ref2.before_text) });
31474
- } else {
31475
- texts.set(path23, { before: texts.get(path23)?.before ?? ref2.after_text, after: ref2.before_text });
31476
- }
31477
- }
31478
- for (const r of entry.rows) {
31479
- const path23 = currentPath(r.path);
31480
- const cur = await read3(path23);
31481
- if (!cur) continue;
31482
- state.set(path23, { ...cur, ordered: applyDiff(cur.ordered, Object.entries(r.before), r.before_absent, r.before_order) });
31483
- }
31484
- for (const m of [...entry.moves].reverse()) {
31485
- state.set(m.from, await read3(m.to));
31486
- state.set(m.to, null);
31487
- origin.set(m.from, origin.get(m.to) ?? m.to);
31488
- origin.delete(m.to);
31561
+ async function warmLoggedStates(app, settings, stopped = () => false) {
31562
+ let n = 0;
31563
+ for (const full of await loggedTables(app, settings)) {
31564
+ if (stopped()) break;
31565
+ await withLogLock(app, async () => {
31566
+ if (LAST.get(app)?.has(full)) return;
31567
+ await readLastState(app, settings, full);
31568
+ n++;
31569
+ });
31570
+ }
31571
+ return n;
31572
+ }
31573
+ async function changesSince(app, prev, cur) {
31574
+ const entry = cur?.entry ?? null;
31575
+ let files = cur?.files ?? /* @__PURE__ */ new Map();
31576
+ const remove3 = [];
31577
+ const add = [];
31578
+ if (cur) {
31579
+ for (const [path23, f] of prev.files) {
31580
+ if (files.has(path23) || !await unreadableNow(app, path23)) continue;
31581
+ if (files === cur.files) files = new Map(files);
31582
+ files.set(path23, f);
31489
31583
  }
31490
- for (const c of entry.creates) state.set(c, null);
31491
- for (const d of entry.deleted) state.set(d.path, { ordered: readOrderedFromMarkdown(d.content).ordered, body: bodyOf(d.content) });
31492
31584
  }
31493
- const cs = emptyChangeSet("undo_migration", summary);
31494
- const moved = /* @__PURE__ */ new Set();
31495
- for (const [to, from] of origin) {
31496
- const final = state.get(to);
31497
- if (to === from || !final || !fileOrNull(app, from)) continue;
31498
- moved.add(to).add(from);
31499
- cs.moves.push({ from, to });
31500
- cs.edits.push({ path: from, before: (await vaultNote(from)).ordered, after: final.ordered });
31585
+ for (const [path23, f] of prev.files) {
31586
+ const now = files.get(path23);
31587
+ if (!now || now.hash !== f.hash) remove3.push({ path: path23, hash: f.hash });
31501
31588
  }
31502
- for (const [path23, final] of state) {
31503
- if (moved.has(path23)) continue;
31504
- const now = fileOrNull(app, path23) ? await vaultNote(path23) : null;
31505
- if (now && final) cs.edits.push({ path: path23, before: now.ordered, after: final.ordered });
31506
- else if (now) cs.deletes.push(path23);
31507
- else if (final) cs.creates.push({ path: path23, content: buildMarkdownWithFrontmatter(final.ordered, final.body) });
31589
+ for (const [path23, f] of files) {
31590
+ const was = prev.files.get(path23);
31591
+ if (!was || was.hash !== f.hash) add.push({ path: path23, hash: f.hash, size: f.content.length, content: f.content });
31508
31592
  }
31509
- for (const [path23, t] of texts) if (t.before !== t.after) cs.texts.push({ path: path23, ...t });
31510
- return cs;
31593
+ add.sort((a, b) => a.path < b.path ? -1 : 1);
31594
+ const metadataChanged = prev.version < 0 || entryHash(prev.entry) !== entryHash(entry);
31595
+ return { entry, add, remove: remove3, metadataChanged };
31511
31596
  }
31512
- async function undoMigration(app, settings, migrationId, now = /* @__PURE__ */ new Date(), check2, op2 = "undo_migration") {
31513
- const { entry, path: journalPath, undone } = await readJournal(app, settings, migrationId);
31514
- const conflicts = [];
31515
- const partial2 = entry.failed.length > 0;
31516
- const back = /* @__PURE__ */ new Set();
31517
- const why = RECORD_ONLY_OPS[entry.op];
31518
- if (why) return { ok: false, undoes: migrationId, conflicts: [{ path: journalPath, message: why }], failed: [] };
31519
- if (undone) {
31520
- return {
31521
- ok: false,
31522
- undoes: migrationId,
31523
- conflicts: [{ path: journalPath, message: "migration already undone" }],
31524
- failed: []
31525
- };
31526
- }
31527
- const movedTo = new Map(entry.moves.map((m) => [m.from, m.to]));
31528
- const currentPath = (p) => movedTo.get(p) ?? p;
31529
- for (const r of entry.rows) {
31530
- const f = fileOrNull(app, currentPath(r.path));
31531
- if (!f) {
31532
- conflicts.push({ path: r.path, message: "note no longer exists" });
31533
- continue;
31534
- }
31535
- const cur = new Map(await readOrderedFor(app, f));
31536
- if (partial2 && Object.entries(r.before).every(([k, v]) => cur.has(k) && same(cur.get(k), v)) && r.before_absent.every((k) => !cur.has(k)) && (r.body_before === void 0 || bodyOf(await app.vault.read(f)) === r.body_before)) {
31537
- back.add(r);
31538
- continue;
31539
- }
31540
- for (const [k, v] of Object.entries(r.after)) {
31541
- if (!cur.has(k) || !same(cur.get(k), v)) {
31542
- conflicts.push({ path: r.path, message: `"${k}" changed since the operation` });
31543
- }
31544
- }
31545
- for (const k of r.after_absent) {
31546
- if (cur.has(k)) conflicts.push({ path: r.path, message: `"${k}" was added since the operation` });
31547
- }
31548
- if (r.body_after !== void 0 && bodyOf(await app.vault.read(f)) !== r.body_after) {
31549
- conflicts.push({ path: r.path, message: "body changed since the operation" });
31597
+ function historyEnabled(model, entry) {
31598
+ const own2 = entry.options?.history;
31599
+ return typeof own2 === "boolean" ? own2 : model.config.history === true;
31600
+ }
31601
+ async function syncTransactionLogs(app, settings, options) {
31602
+ const ms = await loadMetastore(app, settings);
31603
+ if (!ms) return [];
31604
+ const tables = catalogTables(ms.model);
31605
+ const enabled = new Set(tables.filter(({ entry }) => historyEnabled(ms.model, entry)).map(({ ref: ref2 }) => ref2.full));
31606
+ const defined = new Set(tables.map(({ ref: ref2 }) => ref2.full));
31607
+ const orphans = await loggedTables(app, settings, (full) => !defined.has(full));
31608
+ if (enabled.size === 0 && orphans.length === 0) return [];
31609
+ const current2 = await currentStates(app, settings, ms.model, options.touched ?? [], enabled);
31610
+ const names2 = /* @__PURE__ */ new Set([...enabled, ...orphans]);
31611
+ const out2 = [];
31612
+ const now = options.now ?? /* @__PURE__ */ new Date();
31613
+ const format = formatOf(settings);
31614
+ const planned = [];
31615
+ for (const full of [...names2].sort()) {
31616
+ if (options.tables && !options.tables.includes(full)) continue;
31617
+ let prev = await knownState(app, settings, full);
31618
+ const cur = current2.get(full);
31619
+ if (prev.version < 0 && !cur) continue;
31620
+ const due = (c) => c.metadataChanged || c.add.length > 0 || c.remove.length > 0 || options.always === true && cur !== void 0;
31621
+ let changes = await changesSince(app, prev, cur);
31622
+ if (!due(changes)) continue;
31623
+ if (!await stillLatest(app, settings, prev)) {
31624
+ prev = await readLastState(app, settings, full);
31625
+ changes = await changesSince(app, prev, cur);
31626
+ if (!due(changes)) continue;
31550
31627
  }
31628
+ const { entry, add, remove: remove3, metadataChanged } = changes;
31629
+ const operation = prev.version < 0 && options.operation === EXTERNAL_EDIT ? "CREATE TABLE" : options.operation;
31630
+ planned.push({ table: full, add, remove: remove3, metadataChanged, entry, prev, operation });
31551
31631
  }
31552
- for (const ref2 of entry.references) {
31553
- const f = fileOrNull(app, currentPath(ref2.path));
31554
- const text5 = f ? await app.vault.read(f) : null;
31555
- if (partial2 && text5 === ref2.before_text) back.add(ref2);
31556
- else if (text5 !== ref2.after_text) {
31557
- conflicts.push({ path: ref2.path, message: "text changed since the operation" });
31632
+ const external = planned.filter((p) => p.operation === EXTERNAL_EDIT);
31633
+ let counts;
31634
+ if (options.check && external.length > 0) {
31635
+ const added = new Set(external.flatMap((p) => p.add.map((a) => a.path)));
31636
+ const changes = [];
31637
+ for (const p of external) {
31638
+ const wanted = /* @__PURE__ */ new Set([...p.remove.map((r) => r.path), ...[...added].filter((x) => p.prev.files.has(x))]);
31639
+ const texts = await loggedTexts(app, settings, p.prev, wanted);
31640
+ const before = /* @__PURE__ */ new Map();
31641
+ for (const x of wanted) if (texts.has(x)) before.set(x, { hash: p.prev.files.get(x).hash, content: texts.get(x) });
31642
+ const { prev: _prev, operation: _op, ...change } = p;
31643
+ changes.push({ ...change, before });
31558
31644
  }
31645
+ counts = await options.check(changes);
31559
31646
  }
31560
- const since = await storeFilesSince(app, entry.moves);
31561
- const vacuumed = /* @__PURE__ */ new Set();
31562
- for (const m of entry.moves) {
31563
- if (!await fileExists(app, m.to)) {
31564
- if (inStore(m.to)) vacuumed.add(m.to);
31565
- else if (partial2 && await fileExists(app, m.from)) back.add(m);
31566
- else conflicts.push({ path: m.to, message: "moved file no longer exists" });
31567
- } else if (await occupied(app, m.from)) {
31568
- conflicts.push({ path: m.from, message: "original path is occupied" });
31647
+ for (const { table: full, add, remove: remove3, metadataChanged, entry, prev, operation } of planned) {
31648
+ const version2 = prev.version + 1;
31649
+ const violations = operation === EXTERNAL_EDIT ? counts?.get(full) : void 0;
31650
+ const commit = {
31651
+ commitInfo: {
31652
+ version: version2,
31653
+ timestamp: now.toISOString(),
31654
+ operation,
31655
+ ...options.parameters && Object.keys(options.parameters).length > 0 ? { operationParameters: options.parameters } : {},
31656
+ ...options.migrationId ? { migrationId: options.migrationId } : {},
31657
+ operationMetrics: { numAddedFiles: add.length, numRemovedFiles: remove3.length, metadataChanged, ...violations !== void 0 ? { violations } : {} },
31658
+ engineInfo: "datanotes",
31659
+ ...options.actor ? { userName: actorName(options.actor), actor: options.actor } : {}
31660
+ },
31661
+ ...metadataChanged ? { metaData: { entry } } : {},
31662
+ add: [...add].sort((a, b) => a.path.localeCompare(b.path)),
31663
+ remove: [...remove3].sort((a, b) => a.path.localeCompare(b.path))
31664
+ };
31665
+ const text5 = serializeCommit(commit);
31666
+ const repaired = await appendVersion(app, transactionLogDir(settings, full), version2, text5, format);
31667
+ for (const r of repaired) telemetry().log("WARN", "datanotes transaction log repaired a version torn by a crash", { table: full, repair: r });
31668
+ applyLogged(prev, commit);
31669
+ prev.sinceCheckpoint = { versions: prev.sinceCheckpoint.versions + 1, bytes: prev.sinceCheckpoint.bytes + text5.length };
31670
+ if (version2 > 0 && checkpointDue(prev, format)) {
31671
+ let due = DUE.get(app);
31672
+ if (!due) DUE.set(app, due = /* @__PURE__ */ new Map());
31673
+ due.set(full, settings);
31569
31674
  }
31675
+ out2.push({ table: full, version: version2, operation: commit.commitInfo.operation });
31570
31676
  }
31571
- for (const m of since) {
31572
- if (await occupied(app, m.from)) conflicts.push({ path: m.from, message: "original path is occupied" });
31573
- }
31574
- const hashOfCreated = new Map((entry.created_hashes ?? []).map((h) => [h.path, h.hash]));
31575
- for (const c of entry.creates) {
31576
- const f = fileOrNull(app, currentPath(c));
31577
- if (f) {
31578
- const was = hashOfCreated.get(c);
31579
- if (!partial2 && was !== void 0 && createdHash(await app.vault.read(f)) !== was) {
31580
- conflicts.push({ path: currentPath(c), message: "created note changed since the operation" });
31677
+ if (DUE.get(app)?.size) {
31678
+ const runner = CHECKPOINT_RUNNERS.get(app);
31679
+ if (runner) runner();
31680
+ else {
31681
+ const due = DUE.get(app);
31682
+ for (const [full, s] of [...due]) {
31683
+ due.delete(full);
31684
+ await writeDueCheckpoint(app, s, full);
31581
31685
  }
31582
- continue;
31583
- }
31584
- if (partial2) back.add(c);
31585
- else conflicts.push({ path: c, message: "created file no longer exists" });
31586
- }
31587
- for (const d of entry.deleted) {
31588
- const f = fileOrNull(app, d.path);
31589
- if (partial2 && f && await app.vault.read(f) === d.content) back.add(d);
31590
- else if (app.vault.getAbstractFileByPath(d.path) != null) {
31591
- conflicts.push({ path: d.path, message: "deleted path is occupied" });
31592
31686
  }
31593
31687
  }
31594
- if (conflicts.length > 0) return { ok: false, undoes: migrationId, conflicts, failed: [] };
31595
- if (check2) {
31596
- const refused2 = await check2(await undoChangeSet(app, [entry], `undo ${migrationId}`));
31597
- if (refused2.length > 0) return { ok: false, undoes: migrationId, conflicts: refused2, failed: [] };
31598
- }
31599
- const failed2 = [];
31600
- const undoRows = [];
31601
- const undoMoves = [];
31602
- const undoReferences = [];
31603
- const undoCreates = [];
31604
- const undoCreatedHashes = [];
31605
- const undoDeleted = [];
31606
- const steps = [];
31607
- for (const ref2 of entry.references) {
31608
- if (back.has(ref2)) continue;
31609
- steps.push({
31610
- path: currentPath(ref2.path),
31611
- run: async () => {
31612
- const f = fileOrNull(app, currentPath(ref2.path));
31613
- await app.vault.process(f, () => ref2.before_text);
31614
- undoReferences.push({ path: currentPath(ref2.path), before_text: ref2.after_text, after_text: ref2.before_text });
31688
+ return out2;
31689
+ }
31690
+ function withLogLock(app, fn) {
31691
+ let activity = ACTIVITY.get(app);
31692
+ if (!activity) ACTIVITY.set(app, activity = { pending: 0, lastEnd: 0 });
31693
+ const a = activity;
31694
+ a.pending++;
31695
+ const prev = LOCKS.get(app) ?? Promise.resolve();
31696
+ const wait = startSpan("engine.lock.acquire", { attributes: { "datanotes.lock.queued": a.pending - 1 } });
31697
+ const held = () => {
31698
+ wait.end();
31699
+ return phase("engine.lock.hold", async () => {
31700
+ try {
31701
+ return await fn();
31702
+ } finally {
31703
+ await storeFlush(app).catch((e) => telemetry().log("WARN", "datanotes could not sync the store's files", { error: messageOf(e) }));
31615
31704
  }
31616
31705
  });
31706
+ };
31707
+ const run2 = prev.then(held, held);
31708
+ const settled2 = run2.then(
31709
+ () => void 0,
31710
+ () => void 0
31711
+ );
31712
+ LOCKS.set(app, settled2);
31713
+ void settled2.then(() => {
31714
+ a.pending--;
31715
+ a.lastEnd = Date.now();
31716
+ });
31717
+ return run2;
31718
+ }
31719
+ function logLockActivity(app) {
31720
+ const a = ACTIVITY.get(app);
31721
+ return { busy: (a?.pending ?? 0) > 0, lastEnd: a?.lastEnd ?? 0 };
31722
+ }
31723
+ async function checkpointFiles(app, dir, toc) {
31724
+ const out2 = /* @__PURE__ */ new Map();
31725
+ for (const cp of toc.checkpoints) {
31726
+ const files = cp.kind === 1 ? [cp.path] : manifestFiles(dir, cp.path, (await readManifest(app, dir, cp.path))?.head ?? null);
31727
+ out2.set(cp.version, [...out2.get(cp.version) ?? [], ...files]);
31617
31728
  }
31618
- for (const r of entry.rows) {
31619
- if (back.has(r)) continue;
31620
- steps.push({
31621
- path: currentPath(r.path),
31622
- run: async () => {
31623
- const f = fileOrNull(app, currentPath(r.path));
31624
- const order = (await readOrderedFor(app, f)).map(([k]) => k);
31625
- if (Object.keys(r.after).length > 0 || r.after_absent.length > 0 || Object.keys(r.before).length > 0) {
31626
- await writeFrontmatter(
31627
- app,
31628
- f,
31629
- (fm) => applyDiff(fm, Object.entries(r.before), r.before_absent, r.before_order)
31630
- );
31631
- }
31632
- if (r.body_after !== void 0) await writeBody(app, f, r.body_after, r.body_before);
31633
- undoRows.push({
31634
- path: currentPath(r.path),
31635
- before: r.after,
31636
- before_absent: r.after_absent,
31637
- before_order: order,
31638
- after: r.before,
31639
- after_absent: r.before_absent,
31640
- ...r.body_after !== void 0 ? { body_before: r.body_after, body_after: r.body_before } : {}
31641
- });
31642
- }
31643
- });
31729
+ return out2;
31730
+ }
31731
+ async function vacuum(app, settings, full, retainFrom, dryRun) {
31732
+ const dir = transactionLogDir(settings, full);
31733
+ const toc = await logToc(app, dir);
31734
+ const versions = toc.versions;
31735
+ if (versions.length === 0) throw new HistoryError(`${full} has no history`);
31736
+ const latest = versions[versions.length - 1];
31737
+ const keep = retainFrom ?? latest;
31738
+ if (!versions.includes(keep)) throw new HistoryError(`${full} has no version ${keep} (available: ${versions[0]}..${latest})`);
31739
+ const doomed = [...toc.files].filter(([v]) => v < keep).map(([, p]) => p);
31740
+ const straddling = toc.segments.filter((s) => s.first < keep && s.last >= keep);
31741
+ for (const s of toc.segments) if (s.last < keep || straddling.includes(s)) doomed.push(s.path);
31742
+ for (const [v, files] of await checkpointFiles(app, dir, toc)) if (v < keep) doomed.push(...files);
31743
+ doomed.sort();
31744
+ if (!dryRun) {
31745
+ await writeCheckpoint(app, dir, await tableStateAt(app, settings, full, { version: keep }), formatOf(settings));
31746
+ for (const s of straddling) {
31747
+ const kept2 = [];
31748
+ for (let v = keep; v <= s.last; v++) kept2.push({ version: v, text: await versionText(app, toc, v) });
31749
+ await writeSealedSegments(app, dir, kept2);
31750
+ }
31751
+ await storeFlush(app);
31752
+ for (const p of doomed) await storeRemove(app, p);
31753
+ forgetLoggedStates(app);
31644
31754
  }
31645
- for (const m of [...[...entry.moves].reverse().filter((x) => !vacuumed.has(x.to) && !back.has(x)), ...since]) {
31646
- steps.push({
31647
- path: m.to,
31648
- run: async () => {
31649
- await moveFile(app, m.to, m.from);
31650
- undoMoves.push({ from: m.to, to: m.from });
31755
+ return { table: full, retained_from: keep, deleted: doomed, dry_run: dryRun };
31756
+ }
31757
+ async function convertLog(app, settings, full, to) {
31758
+ const dir = transactionLogDir(settings, full);
31759
+ const toc = await logToc(app, dir);
31760
+ const result = { table: full, versions: toc.versions.length, written: 0, removed: 0 };
31761
+ if (to === "segments") {
31762
+ for (const s of toc.segments) {
31763
+ if (!s.sealed || (await wholeVersions(app, s)).size === s.last - s.first + 1) {
31764
+ storeSyncNext(app, s.path);
31765
+ continue;
31651
31766
  }
31652
- });
31653
- }
31654
- for (const c of entry.creates) {
31655
- if (back.has(c)) continue;
31656
- steps.push({
31657
- path: c,
31658
- run: async () => {
31659
- const f = fileOrNull(app, c);
31660
- const content = await app.vault.read(f);
31661
- await trash(app, f);
31662
- undoDeleted.push({ path: c, content });
31767
+ const range = Array.from({ length: s.last - s.first + 1 }, (_, i) => s.first + i);
31768
+ const texts2 = await versionTexts(app, toc, range);
31769
+ await storeWrite(app, s.path, range.map((v, i) => framed(v, texts2[i])).join(""));
31770
+ result.written++;
31771
+ }
31772
+ const loose = toc.versions.filter((v) => toc.files.has(v) && !toc.segments.some((s) => v >= s.first && v <= s.last));
31773
+ const texts = await versionTexts(app, toc, loose);
31774
+ const written2 = await writeSealedSegments(app, dir, loose.map((version2, i) => ({ version: version2, text: texts[i] })));
31775
+ result.written += written2.length;
31776
+ await storeFlush(app);
31777
+ for (const v of toc.files.keys()) {
31778
+ await storeRemove(app, toc.files.get(v));
31779
+ result.removed++;
31780
+ }
31781
+ } else {
31782
+ for (const seg of toc.segments) {
31783
+ for (let v = seg.first; v <= seg.last; v++) {
31784
+ const path23 = `${dir}/${pad2(v)}.json`;
31785
+ const file = toc.files.get(v);
31786
+ if (file === void 0) {
31787
+ await storeWrite(app, path23, await versionText(app, toc, v));
31788
+ result.written++;
31789
+ } else storeSyncNext(app, file);
31663
31790
  }
31664
- });
31665
- }
31666
- for (const d of entry.deleted) {
31667
- if (back.has(d)) continue;
31668
- steps.push({
31669
- path: d.path,
31670
- run: async () => {
31671
- await ensureFolder(app, parentFolder(d.path));
31672
- await app.vault.create(d.path, d.content);
31673
- undoCreates.push(d.path);
31674
- undoCreatedHashes.push({ path: d.path, hash: createdHash(d.content) });
31791
+ }
31792
+ const oldest = toc.versions[0];
31793
+ if (oldest !== void 0 && oldest > 0) {
31794
+ if (await wholeCheckpoint(app, dir, oldest)) storeSyncNext(app, checkpointPath(dir, oldest));
31795
+ else {
31796
+ await writeCheckpoint(app, dir, await replayState(app, settings, full, { version: oldest }), "files");
31797
+ result.written++;
31675
31798
  }
31676
- });
31677
- }
31678
- let done = 0;
31679
- try {
31680
- for (const s of steps) {
31681
- await s.run();
31682
- done++;
31683
31799
  }
31684
- } catch (e) {
31685
- failed2.push({ path: "undo", error: messageOf(e) });
31686
- }
31687
- const journal = await writeJournal(
31688
- app,
31689
- settings,
31690
- {
31691
- op: op2,
31692
- ...entry.schema !== void 0 ? { schema: entry.schema } : {},
31693
- ...currentActor() ? { actor: currentActor() } : {},
31694
- summary: `undo ${migrationId}`,
31695
- executed_at: now.toISOString(),
31696
- rows: undoRows,
31697
- creates: undoCreates,
31698
- ...undoCreatedHashes.length > 0 ? { created_hashes: undoCreatedHashes } : {},
31699
- moves: undoMoves,
31700
- references: undoReferences,
31701
- deleted: undoDeleted,
31702
- failed: failed2,
31703
- undoes: migrationId
31704
- },
31705
- now
31706
- );
31707
- if (failed2.length === 0) {
31708
- await markUndone(app, settings, migrationId, journal.id, now);
31800
+ await storeFlush(app);
31801
+ for (const seg of toc.segments) {
31802
+ await storeRemove(app, seg.path);
31803
+ result.removed++;
31804
+ }
31709
31805
  }
31710
- return {
31711
- ok: failed2.length === 0,
31712
- undoes: migrationId,
31713
- migration_id: journal.id,
31714
- conflicts: [],
31715
- failed: failed2,
31716
- ...failed2.length > 0 ? { left: [...new Set(steps.slice(done).map((s) => s.path))] } : {},
31717
- touched: [
31718
- ...entry.rows.map((r) => r.path),
31719
- ...entry.creates,
31720
- ...[...entry.moves, ...since].flatMap((m) => [m.from, m.to]),
31721
- ...entry.references.map((r) => r.path),
31722
- ...entry.deleted.map((d) => d.path)
31723
- ]
31724
- };
31725
- }
31726
- function applyChangeSet(...args) {
31727
- const cs = args[2];
31728
- return phase("engine.apply", async (span) => {
31729
- const r = await applyChangeSetImpl(...args);
31730
- span.setAttributes({ "datanotes.notes_changed": r.notesChanged, "datanotes.files_created": r.filesCreated, "datanotes.files_moved": r.filesMoved, "datanotes.files_deleted": r.filesDeleted, "datanotes.failed": r.failed.length });
31731
- if (!r.ok) span.setAttribute("error.type", "write_failed").setStatus("error", "the write failed partway");
31732
- return r;
31733
- }, { "datanotes.edits": cs.edits.length, "datanotes.creates": cs.creates.length, "datanotes.moves": cs.moves.length, "datanotes.deletes": cs.deletes.length });
31806
+ forgetLoggedStates(app);
31807
+ return result;
31734
31808
  }
31735
- var inStore, WRITE_CONCURRENCY, RECORD_ONLY_OPS;
31736
- var init_changeset = __esm({
31737
- "src/core/changeset.ts"() {
31809
+ var TRANSACTION_LOG_FOLDER, CHECKPOINT_POLICY, LEGACY_CHECKPOINT, EXTERNAL_EDIT, AbsentTableError, ENTRY_HASHES, NO_KEYS, LIVE, DUE, CHECKPOINT_RUNNERS, LAST, LOCKS, ACTIVITY;
31810
+ var init_transaction_log = __esm({
31811
+ "src/core/transaction-log.ts"() {
31738
31812
  "use strict";
31813
+ init_file_cache();
31739
31814
  init_frontmatter();
31815
+ init_metastore();
31740
31816
  init_actor();
31741
- init_journal();
31742
31817
  init_store();
31743
- init_metastore();
31744
- init_database_id();
31745
- init_message();
31746
- init_file_cache();
31818
+ init_table_loader();
31819
+ init_types_table();
31820
+ init_database_move();
31821
+ init_db_paths();
31822
+ init_unreadable();
31747
31823
  init_tracing();
31748
- init_journal();
31749
- inStore = isStorePath;
31750
- WRITE_CONCURRENCY = { atOnce: 16, from: 32 };
31751
- RECORD_ONLY_OPS = {
31752
- move_database: "a move of the database folder cannot be undone: move it back with configure_database (root)",
31753
- set_database_id: "the database's id cannot be taken back: the engine finds its store by it",
31754
- rollback: "the rollback of a failed write cannot be undone: it put the database back as it was; run the write again instead"
31824
+ init_vault_index();
31825
+ init_telemetry();
31826
+ init_message();
31827
+ init_log_files();
31828
+ TRANSACTION_LOG_FOLDER = "transaction_log";
31829
+ CHECKPOINT_POLICY = { bytesFactor: 2, minBytes: 256 * 1024, maxVersions: 1e3, versionBytes: 16 * 1024 };
31830
+ LEGACY_CHECKPOINT = { maxChars: 32 * 1024 * 1024 };
31831
+ EXTERNAL_EDIT = "EXTERNAL_EDIT";
31832
+ AbsentTableError = class extends HistoryError {
31755
31833
  };
31834
+ ENTRY_HASHES = /* @__PURE__ */ new WeakMap();
31835
+ NO_KEYS = /* @__PURE__ */ new Set();
31836
+ LIVE = /* @__PURE__ */ new WeakMap();
31837
+ DUE = /* @__PURE__ */ new WeakMap();
31838
+ CHECKPOINT_RUNNERS = /* @__PURE__ */ new WeakMap();
31839
+ LAST = /* @__PURE__ */ new WeakMap();
31840
+ LOCKS = /* @__PURE__ */ new WeakMap();
31841
+ ACTIVITY = /* @__PURE__ */ new WeakMap();
31756
31842
  }
31757
31843
  });
31758
31844
 
@@ -32904,7 +32990,7 @@ var init_database = __esm({
32904
32990
 
32905
32991
  // src/core/write-check.ts
32906
32992
  function staleWrite(app) {
32907
- const ctx = storage3?.getStore();
32993
+ const ctx = storage4?.getStore();
32908
32994
  if (ctx === void 0 || ctx.gen === generation(app)) return false;
32909
32995
  ctx.stale = true;
32910
32996
  return true;
@@ -32912,12 +32998,12 @@ function staleWrite(app) {
32912
32998
  function wrote(app) {
32913
32999
  const next = generation(app) + 1;
32914
33000
  GENERATION.set(app, next);
32915
- const ctx = storage3?.getStore();
33001
+ const ctx = storage4?.getStore();
32916
33002
  if (ctx) ctx.gen = next;
32917
33003
  }
32918
33004
  async function runWrite(app, fn, isStale) {
32919
- if (!storage3) return fn();
32920
- const s = storage3;
33005
+ if (!storage4) return fn();
33006
+ const s = storage4;
32921
33007
  let ctx = { gen: 0, stale: false };
32922
33008
  const attempt = () => s.run(ctx = { gen: generation(app), stale: false }, fn);
32923
33009
  let result = await attempt();
@@ -32927,19 +33013,19 @@ async function runWrite(app, fn, isStale) {
32927
33013
  function holdsStaleWrite(value) {
32928
33014
  return JSON.stringify(value ?? null).includes(STALE_WRITE);
32929
33015
  }
32930
- var STALE_WRITE, WRITE_ATTEMPTS, GENERATION, storage3, generation;
33016
+ var STALE_WRITE, WRITE_ATTEMPTS, GENERATION, storage4, generation;
32931
33017
  var init_write_check = __esm({
32932
33018
  "src/core/write-check.ts"() {
32933
33019
  "use strict";
32934
33020
  STALE_WRITE = "the database changed while this write was being checked";
32935
33021
  WRITE_ATTEMPTS = 5;
32936
33022
  GENERATION = /* @__PURE__ */ new WeakMap();
32937
- storage3 = null;
33023
+ storage4 = null;
32938
33024
  try {
32939
33025
  const { AsyncLocalStorage: AsyncLocalStorage2 } = require("node:async_hooks");
32940
- storage3 = new AsyncLocalStorage2();
33026
+ storage4 = new AsyncLocalStorage2();
32941
33027
  } catch {
32942
- storage3 = null;
33028
+ storage4 = null;
32943
33029
  }
32944
33030
  generation = (app) => GENERATION.get(app) ?? 0;
32945
33031
  }
@@ -33251,14 +33337,23 @@ function readableKeys(t, settings) {
33251
33337
  function rowGetter(app, t, settings, tz = "") {
33252
33338
  const real = knownKeys(t, settings);
33253
33339
  let inlinks = null;
33340
+ const scope = currentReadScope();
33341
+ const seen = /* @__PURE__ */ new Map();
33342
+ const counts = (src) => {
33343
+ if (!scope) return true;
33344
+ let ok2 = seen.get(src);
33345
+ if (ok2 === void 0) seen.set(src, ok2 = pathReadable(app, settings, src, scope));
33346
+ return ok2;
33347
+ };
33254
33348
  const backlinks = (path23) => {
33255
33349
  if (!app) return null;
33256
33350
  const kept2 = app.metadataCache.backlinksOf;
33257
- if (kept2) return Object.keys(kept2.call(app.metadataCache, path23)).filter((src) => src !== path23).length;
33351
+ if (kept2) return Object.keys(kept2.call(app.metadataCache, path23)).filter((src) => src !== path23 && counts(src)).length;
33258
33352
  if (!inlinks) {
33259
33353
  inlinks = /* @__PURE__ */ new Map();
33260
33354
  const resolved = app.metadataCache.resolvedLinks;
33261
33355
  for (const [src, dests] of Object.entries(resolved)) {
33356
+ if (!counts(src)) continue;
33262
33357
  for (const d of Object.keys(dests)) if (d !== src) inlinks.set(d, (inlinks.get(d) ?? 0) + 1);
33263
33358
  }
33264
33359
  }
@@ -35090,6 +35185,7 @@ async function finish(app, settings, cs, conflicts, violations, opts) {
35090
35185
  base.conflicts = conflicts;
35091
35186
  base.violations = violations.filter((v) => v.violations.length > 0);
35092
35187
  if (base.conflicts.length > 0 || base.violations.length > 0) return base;
35188
+ await assertChangeSetWritable(app, settings, cs);
35093
35189
  if (dryRun) return { ...base, ok: true };
35094
35190
  const done = await whileWriting(app, () => withLogLock(app, async () => {
35095
35191
  if (staleWrite(app)) return null;
@@ -36037,6 +36133,7 @@ var init_engine = __esm({
36037
36133
  init_actor();
36038
36134
  init_journal();
36039
36135
  init_inverse();
36136
+ init_write_scope();
36040
36137
  init_file_cache();
36041
36138
  init_transaction_log();
36042
36139
  init_changeset();
@@ -66871,17 +66968,28 @@ var SemanticIndex = class {
66871
66968
  if (this.queries.size > QUERY_VECTORS_MAX) this.queries.delete(this.queries.keys().next().value);
66872
66969
  return v;
66873
66970
  }
66971
+ /** The index's counts; under a read scope (read-scope.ts), of the notes the token may read only. */
66874
66972
  status() {
66875
66973
  const s = this.getSettings();
66974
+ const scope = currentReadScope();
66975
+ const readable = (path23) => !scope || pathReadable(this.app, s, path23, scope);
66976
+ let files = 0;
66876
66977
  let passages = 0;
66877
- for (const f of this.files.values()) passages += f.passages.length;
66978
+ for (const [path23, f] of this.files) {
66979
+ if (!readable(path23)) continue;
66980
+ files++;
66981
+ passages += f.passages.length;
66982
+ }
66983
+ let pending = 0;
66984
+ if (scope) for (const path23 of this.pending) pending += readable(path23) ? 1 : 0;
66985
+ else pending = this.pending.size;
66878
66986
  return {
66879
66987
  enabled: s.semanticEnabled,
66880
66988
  provider: s.semanticProvider,
66881
66989
  model: s.semanticModel,
66882
- files: this.files.size,
66990
+ files,
66883
66991
  passages,
66884
- pending: this.pending.size,
66992
+ pending,
66885
66993
  running: this.running !== null,
66886
66994
  ...this.lastSync ? { last_sync: this.lastSync } : {},
66887
66995
  ...this.lastError ? { last_error: this.lastError } : {}
@@ -69078,6 +69186,7 @@ init_guidance();
69078
69186
  init_requests();
69079
69187
  init_actor();
69080
69188
  init_read_scope();
69189
+ init_write_scope();
69081
69190
  init_changeset_checks();
69082
69191
 
69083
69192
  // src/server/token-routes.ts
@@ -69110,6 +69219,8 @@ var READ_ROUTES = /* @__PURE__ */ new Set(["/manual/search/", "/tokens/me/", "/t
69110
69219
  var OWNER_ADMIN_ROUTES = /* @__PURE__ */ new Set(["/tokens/", "/tokens/create/", "/tokens/update/", "/tokens/delete/", "/tokens/rotate/", "/extensions/record/", "/extensions/delete/", "/status/", "/stop/"]);
69111
69220
  var READ_SCOPE_UNSUPPORTED = "read permissions need Node's AsyncLocalStorage";
69112
69221
  var READ_SCOPE_UNSUPPORTED_HINT = guide(`This token has "read" patterns, which this runtime cannot enforce: run the engine on Node (datanotes serve), or have the owner pass "read": null to tokens/update/ for this token.`, "tokenRead");
69222
+ var WRITE_SCOPE_UNSUPPORTED = "write permissions need Node's AsyncLocalStorage";
69223
+ var WRITE_SCOPE_UNSUPPORTED_HINT = guide(`This token's "write" patterns limit the tables its writes may change, which this runtime cannot enforce: run the engine on Node (datanotes serve), or write with a token whose "write" is ["*"].`, "tokenWrite");
69113
69224
 
69114
69225
  // src/server/scheduler.ts
69115
69226
  init_telemetry();
@@ -69358,7 +69469,7 @@ function errorResponse(e, op2, route) {
69358
69469
  if (e instanceof MigrationNotFoundError) return { status: 404, json: body(guide("list_migrations lists the journal ids (migration_id) of the writes, newest first.", "undo")) };
69359
69470
  if (e instanceof VaultPathNotFoundError) return { status: 404, json: body(guide("list_folder lists a folder, search_notes finds notes by their words.", "readNote")) };
69360
69471
  if (e instanceof ReadDeniedError) return { status: 403, json: body(READ_DENIED_GUIDANCE) };
69361
- if (e instanceof Forbidden) return { status: 403, json: body(FORBIDDEN_HINT) };
69472
+ if (e instanceof Forbidden || e instanceof WriteDeniedError) return { status: 403, json: body(FORBIDDEN_HINT) };
69362
69473
  if (e instanceof AmbiguousCatalogRefError) {
69363
69474
  return { status: 409, json: { ...body(guide(`Name the table in full (catalog.schema.table): ${e.candidates.join(" or ")}.`, "catalogs")), tables: e.candidates } };
69364
69475
  }
@@ -69766,6 +69877,7 @@ function createHandlers(app, getSettings, saveSettings = async () => {
69766
69877
  batch: async (b) => {
69767
69878
  const operations = list(b, "operations");
69768
69879
  if (operations.length === 0) throw new BadRequest(`"operations" must be a non-empty list`);
69880
+ const denied = /* @__PURE__ */ new Set();
69769
69881
  const steps = operations.map((raw, i) => {
69770
69882
  const { op: op2, ...args } = raw;
69771
69883
  if (typeof op2 !== "string") throw new BadRequest(`operation ${i} needs "op"`);
@@ -69780,7 +69892,10 @@ function createHandlers(app, getSettings, saveSettings = async () => {
69780
69892
  return {
69781
69893
  op: op2,
69782
69894
  run: async (dryRun) => {
69783
- const res = await ops[op2]({ ...args, dry_run: dryRun }).catch((e) => errorResponse(e, op2));
69895
+ const res = await ops[op2]({ ...args, dry_run: dryRun }).catch((e) => {
69896
+ if (e instanceof WriteDeniedError) denied.add(i);
69897
+ return errorResponse(e, op2);
69898
+ });
69784
69899
  if ("op" in res.json) return res.json;
69785
69900
  throw stepFailure(res.json);
69786
69901
  }
@@ -69791,8 +69906,13 @@ function createHandlers(app, getSettings, saveSettings = async () => {
69791
69906
  ...str(b, "summary") !== void 0 ? { summary: str(b, "summary") } : {}
69792
69907
  });
69793
69908
  const failedStep = r.failed_at !== void 0 && (r.steps[r.failed_at]?.result?.failed.length ?? 0) > 0;
69794
- const status = r.ok ? 200 : r.rolled_back === false ? 500 : writeStatus(failedStep, true);
69909
+ const forbidden = r.failed_at !== void 0 ? denied.has(r.failed_at) ? r.failed_at : void 0 : [...denied].sort((a, b2) => a - b2)[0];
69910
+ const status = r.ok ? 200 : r.rolled_back === false ? 500 : forbidden !== void 0 ? 403 : writeStatus(failedStep, true);
69795
69911
  const answer = redactBatchRollback(r, app, s());
69912
+ if (status === 403) {
69913
+ const step = answer.steps[forbidden];
69914
+ return { status, json: { ...answer, error: `operation ${forbidden}: ${step.error}`, hint: step.hint, docs: step.docs } };
69915
+ }
69796
69916
  return { status, json: answer.ok ? answer : { ...answer, ...batchGuidance(answer) } };
69797
69917
  },
69798
69918
  // --- queries ---
@@ -70256,6 +70376,7 @@ var STATUS_TEXT = {
70256
70376
  409: "Ambiguous table name: it matches several tables (`paths` or `tables` lists them)."
70257
70377
  };
70258
70378
  var READ_DENIED_TEXT = "A named token whose `read` patterns do not cover the table, view or note the request names (such as `token cook may not read personal.health.weight`). Sets it did not name (listings, bases, search, the graph, the change feed) are filtered instead.";
70379
+ var SESSION_TAKEN_TEXT = "The `session` belongs to another named token: the first token to report a session owns it (compared on its first 16 characters), and only that token and the owner may report it again. Nothing is stored.";
70259
70380
  var ERRORS = {
70260
70381
  400: { description: STATUS_TEXT[400], content: json(ref("Error")) },
70261
70382
  401: { $ref: "#/components/responses/Unauthorized" },
@@ -70271,6 +70392,7 @@ function postRoute(d) {
70271
70392
  const read3 = op2 !== void 0 ? READ_OPERATIONS.has(op2) : READ_ROUTES.has(d.route) && d.route !== "/tokens/update/";
70272
70393
  if (!read3) responses["403"] = ERRORS[403];
70273
70394
  else if (op2 !== void 0) responses["403"] = ERRORS.readDenied;
70395
+ if (d.forbidden) responses["403"] = { description: d.forbidden, content: json(ref("Error")) };
70274
70396
  if (d.namesTable) responses["404"] = ERRORS[404];
70275
70397
  const refused2 = op2 !== void 0 ? refusedStatus(op2) : void 0;
70276
70398
  if (refused2 === 409) {
@@ -70609,6 +70731,7 @@ function openApiDocument(version2) {
70609
70731
  route: "/sessions/identify/",
70610
70732
  operationId: "identify_session",
70611
70733
  summary: "Tell the engine the model and harness behind an agent session (what an MCP server reports): later requests carrying that session in `X-Datanotes-Actor` are journaled with it. Any token may call it.",
70734
+ forbidden: SESSION_TAKEN_TEXT,
70612
70735
  body: { type: "object", required: ["session"], properties: { session: { type: "string" }, model: { type: "string" }, harness: { type: "string" } } },
70613
70736
  bodyRequired: true,
70614
70737
  result: ok
@@ -70617,6 +70740,7 @@ function openApiDocument(version2) {
70617
70740
  route: "/tool-calls/record/",
70618
70741
  operationId: "record_tool_call",
70619
70742
  summary: "Record an agent's tool call in the change feed (`tool_call` events): what an MCP server reports after each call, its own tools included (`served_by`). Any token may call it.",
70743
+ forbidden: SESSION_TAKEN_TEXT,
70620
70744
  body: {
70621
70745
  type: "object",
70622
70746
  required: ["tool"],
@@ -71352,6 +71476,8 @@ var MAX_IDENTITIES = 2e3;
71352
71476
  var SessionIdentities = class {
71353
71477
  constructor() {
71354
71478
  this.map = /* @__PURE__ */ new Map();
71479
+ /** Who reported each session first: "owner" or a token's name, by the session's first 16 characters. */
71480
+ this.owners = /* @__PURE__ */ new Map();
71355
71481
  }
71356
71482
  set(key2, info) {
71357
71483
  const clean2 = Object.fromEntries(Object.entries(info).filter(([, v]) => typeof v === "string" && v.trim() !== "").map(([k, v]) => [k, String(v).trim().slice(0, 200)]));
@@ -71363,6 +71489,21 @@ var SessionIdentities = class {
71363
71489
  get(key2) {
71364
71490
  return this.map.get(key2) ?? this.map.get(key2.slice(0, 16));
71365
71491
  }
71492
+ /**
71493
+ * Claim a session for a credential ("owner" or a token's name): the first one to report it keeps
71494
+ * it. Answers the token that holds it when that is another named token, else null (claimed, or
71495
+ * the owner, who may report any session). Keyed by the first 16 characters, as lookups fall back
71496
+ * to them: a token cannot take over another's session with a longer or shorter id.
71497
+ */
71498
+ claim(key2, who) {
71499
+ const k = key2.slice(0, 16);
71500
+ const held = this.owners.get(k);
71501
+ if (held !== void 0 && held !== who && who !== "owner") return held;
71502
+ this.owners.delete(k);
71503
+ if (this.owners.size >= MAX_IDENTITIES) this.owners.delete(this.owners.keys().next().value);
71504
+ this.owners.set(k, held ?? who);
71505
+ return null;
71506
+ }
71366
71507
  };
71367
71508
 
71368
71509
  // src/server/runtime.ts
@@ -71749,6 +71890,7 @@ async function engineStatus(i) {
71749
71890
  // src/server/tokens.ts
71750
71891
  init_actor();
71751
71892
  init_read_scope();
71893
+ init_write_scope();
71752
71894
  init_metastore();
71753
71895
  init_store();
71754
71896
  init_telemetry();
@@ -71768,6 +71910,13 @@ function tableAllowed(full, patterns) {
71768
71910
  function scopeOf(t) {
71769
71911
  return t.read === void 0 || t.read.includes("*") ? null : { token: t.name, patterns: t.read };
71770
71912
  }
71913
+ function writeScopeOf(t) {
71914
+ return t.write.includes("*") ? null : { token: t.name, patterns: t.write };
71915
+ }
71916
+ function writesRows(route) {
71917
+ const op2 = operationOf(route);
71918
+ return op2 === "batch" || op2 !== void 0 && Object.prototype.hasOwnProperty.call(ROW_WRITES, op2);
71919
+ }
71771
71920
  function readCovers(r, w) {
71772
71921
  return r === "*" || r === w || r.endsWith(".*") && w.startsWith(r.slice(0, -1));
71773
71922
  }
@@ -72010,7 +72159,12 @@ var TokenRegistry = class {
72010
72159
  telemetry().log("WARN", "datanotes token refused", { token_name: name, "http.route": route, reason: READ_SCOPE_UNSUPPORTED });
72011
72160
  return { status: 503, json: { error: READ_SCOPE_UNSUPPORTED, ...READ_SCOPE_UNSUPPORTED_HINT } };
72012
72161
  }
72013
- return runWithReadScope(scope, () => dispatch2(route, handler, body));
72162
+ const writeScope = writeScopeOf(token);
72163
+ if (writeScope && writesRows(route) && !(this.deps.readScopeSupported ?? READ_SCOPE_SUPPORTED)) {
72164
+ telemetry().log("WARN", "datanotes token refused", { token_name: name, "http.route": route, reason: WRITE_SCOPE_UNSUPPORTED });
72165
+ return { status: 503, json: { error: WRITE_SCOPE_UNSUPPORTED, ...WRITE_SCOPE_UNSUPPORTED_HINT } };
72166
+ }
72167
+ return runWithReadScope(scope, () => runWithWriteScope(writeScope, () => dispatch2(route, handler, body)));
72014
72168
  };
72015
72169
  }
72016
72170
  // --- routes --------------------------------------------------------------------------------
@@ -72254,6 +72408,7 @@ var ExtensionRegistry = class {
72254
72408
  init_metastore();
72255
72409
  init_query_tables();
72256
72410
  init_file_cache();
72411
+ init_read_scope();
72257
72412
  init_vault_index();
72258
72413
  var CACHED_READS = /* @__PURE__ */ new Set([
72259
72414
  "select_rows",
@@ -72389,7 +72544,7 @@ var ReadCache = class {
72389
72544
  if (this.maxBytes === 0 || !CACHED_READS.has(op2)) return null;
72390
72545
  const settings = this.getSettings();
72391
72546
  const model = (await loadMetastore(this.app, settings).catch(() => null))?.model ?? null;
72392
- const deps = readDeps(op2, body, model);
72547
+ const deps = readDeps(op2, body, model, currentReadScope() !== null);
72393
72548
  if (!deps) return null;
72394
72549
  if (!this.looseKnown) this.indexLoose(settings);
72395
72550
  if (deps.links && !this.linkPrints) this.printLinks();
@@ -72535,7 +72690,7 @@ var ReadCache = class {
72535
72690
  function settingsKey(s) {
72536
72691
  return JSON.stringify(s, (k, v) => SECRET_SETTINGS.has(k) ? void 0 : v);
72537
72692
  }
72538
- function readDeps(op2, body, model) {
72693
+ function readDeps(op2, body, model, restricted = false) {
72539
72694
  if (!CACHED_READS.has(op2)) return null;
72540
72695
  const text5 = JSON.stringify(body);
72541
72696
  if (text5.includes("{now")) return null;
@@ -72546,7 +72701,10 @@ function readDeps(op2, body, model) {
72546
72701
  const found = /* @__PURE__ */ new Set();
72547
72702
  const r = queryTables(body, model, found, 0);
72548
72703
  if (r.now || r.store) return null;
72549
- if (!r.vault) return { tables: [...found].sort(), today: today || r.today, ...r.links ? { links: true } : {} };
72704
+ if (!r.vault) {
72705
+ const tables = r.links && restricted ? catalogTables(model).map(({ ref: ref2 }) => ref2.full) : [...found];
72706
+ return { tables: tables.sort(), today: today || r.today, ...r.links ? { links: true } : {} };
72707
+ }
72550
72708
  const views = viewTimes(model);
72551
72709
  if (views.now) return null;
72552
72710
  return { tables: "vault", today: today || r.today || views.today };
@@ -72575,6 +72733,7 @@ function viewTimes(model) {
72575
72733
  // src/server/runtime.ts
72576
72734
  init_message();
72577
72735
  init_hints();
72736
+ var SESSION_TAKEN_HINT = guide("Use your own session id, such as a new UUID for each agent session. This session belongs to another token: only that token and the owner may report it.", "sessionOwner");
72578
72737
  var REPORT_HINT = guide(`Send {"session", "model", "harness"} to sessions/identify/ and {"tool", "args", "ok", ...} to tool-calls/record/; DatanotesClient.recordToolCall does it.`, "reportAgents");
72579
72738
  var EXTERNAL_EDIT_DELAY_MS = 3e3;
72580
72739
  var LOG_CHECK_EVERY_MS = 10 * 6e4;
@@ -72709,6 +72868,8 @@ var EngineRuntime = class {
72709
72868
  "/sessions/identify/": async (body) => {
72710
72869
  const session = typeof body.session === "string" ? body.session.trim() : "";
72711
72870
  if (!session) return { status: 400, json: { error: `"session" is required`, ...REPORT_HINT } };
72871
+ const taken = this.sessionTaken(session);
72872
+ if (taken) return taken;
72712
72873
  this.identities.set(session, { model: body.model, harness: body.harness });
72713
72874
  return { status: 200, json: { ok: true } };
72714
72875
  },
@@ -72724,6 +72885,8 @@ var EngineRuntime = class {
72724
72885
  // an MCP server reports the tool calls of its agents (the change feed's tool_call events)
72725
72886
  "/tool-calls/record/": async (body) => {
72726
72887
  if (typeof body.tool !== "string" || !body.tool) return { status: 400, json: { error: `"tool" is required`, ...REPORT_HINT } };
72888
+ const taken = typeof body.session === "string" && body.session.trim() ? this.sessionTaken(body.session.trim()) : null;
72889
+ if (taken) return taken;
72727
72890
  const actor = currentActor();
72728
72891
  await this.recordToolCall({
72729
72892
  tool: body.tool,
@@ -72772,6 +72935,14 @@ var EngineRuntime = class {
72772
72935
  } : {}
72773
72936
  };
72774
72937
  }
72938
+ /**
72939
+ * The 403 when the session belongs to another named token; else null, and the session is
72940
+ * claimed for the caller when nobody reported it before (the first reporter keeps it).
72941
+ */
72942
+ sessionTaken(session) {
72943
+ if (this.identities.claim(session, tokenNameOf(currentActor()) ?? "owner") === null) return null;
72944
+ return { status: 403, json: { error: `session "${session}" belongs to another token`, ...SESSION_TAKEN_HINT } };
72945
+ }
72775
72946
  /**
72776
72947
  * An agent's tool call, as a change-feed event (secrets removed, long values cut); settled once
72777
72948
  * it is recorded. The feed is kept in the store, which the store backup commits: no secret.
@@ -77216,7 +77387,7 @@ function setUp(o, vault, home, explicit, self) {
77216
77387
  // src/node/service.ts
77217
77388
  init_local();
77218
77389
  init_hints();
77219
- var VERSION = true ? "3.4.0" : "dev";
77390
+ var VERSION = true ? "3.4.1" : "dev";
77220
77391
  function parsePort(raw, from = "--port") {
77221
77392
  const n = /^\s*\d+\s*$/.test(raw) ? Number(raw) : NaN;
77222
77393
  if (n === 0 || Number.isInteger(n) && n >= 1024 && n <= 65535) return n;
@@ -78281,7 +78452,7 @@ Docs: https://datanotes.dev`;
78281
78452
 
78282
78453
  // src/node/cli.ts
78283
78454
  init_message();
78284
- var VERSION2 = true ? "3.4.0" : "dev";
78455
+ var VERSION2 = true ? "3.4.1" : "dev";
78285
78456
  var consoleError = console.error;
78286
78457
  async function main5(argv = process.argv.slice(2)) {
78287
78458
  const [command, ...rest] = argv;