datanotes 3.4.0 → 3.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/api/extension-columns.d.cts +64 -0
  2. package/dist/api/extension-columns.d.ts +64 -0
  3. package/dist/api/extension-plan.d.cts +1 -1
  4. package/dist/api/extension-plan.d.ts +1 -1
  5. package/dist/datanotes.cjs +2251 -1834
  6. package/dist/docs/administration/extensions.md +2 -1
  7. package/dist/docs/api/operations/identify_session.md +1 -0
  8. package/dist/docs/api/operations/record_tool_call.md +1 -0
  9. package/dist/docs/api.md +1 -1
  10. package/dist/docs/develop/write-an-extension.md +8 -2
  11. package/dist/docs/guides/data-quality/check-constraints.md +10 -10
  12. package/dist/docs/guides/data-quality/hand-edits.md +8 -8
  13. package/dist/docs/guides/database-objects/bases.md +11 -11
  14. package/dist/docs/guides/database-objects/catalogs-and-schemas.md +10 -10
  15. package/dist/docs/guides/database-objects/columns.md +39 -39
  16. package/dist/docs/guides/database-objects/relations.md +28 -28
  17. package/dist/docs/guides/database-objects/tables.md +26 -26
  18. package/dist/docs/guides/database-objects/views.md +19 -19
  19. package/dist/docs/guides/history/change-data-feed.md +34 -34
  20. package/dist/docs/guides/history/journal-and-undo.md +12 -12
  21. package/dist/docs/guides/history/restore.md +20 -20
  22. package/dist/docs/guides/history/table-history.md +31 -31
  23. package/dist/docs/guides/read-and-write/automatic-links.md +15 -15
  24. package/dist/docs/guides/read-and-write/import-notes.md +20 -20
  25. package/dist/docs/guides/read-and-write/write-rows.md +10 -10
  26. package/dist/docs/guides/search/text.md +1 -1
  27. package/dist/docs/guides/security/tokens.md +17 -4
  28. package/dist/docs/guides/security/who-did-what.md +8 -0
  29. package/dist/docs/reference/cli.md +1 -1
  30. package/dist/docs/release-notes.md +28 -0
  31. package/package.json +1 -1
@@ -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,2004 +29757,2088 @@ 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;
30170
- }
30171
- const entries = rows.map((r, i) => JSON.stringify([r.path, r.hash, r.content.length, partOf[i]]));
30172
- const pieces = chunked(entries);
30173
- const more = pieces.slice(1).map((_, i) => `${pad2(head.version)}.manifest-${i + 1}.jsonl`);
30174
- for (const [i, lines] of pieces.slice(1).entries()) {
30175
- await storeWrite(app, `${dir}/${more[i]}`, `${lines.join("\n")}
30176
- `);
30177
- written2.push(`${dir}/${more[i]}`);
30178
- }
30179
- const header2 = { format: 2, version: head.version, timestamp: head.timestamp, entry: head.entry, ...head.moves ? { moves: head.moves } : {}, rows: rows.length, parts: partNames, ...more.length > 0 ? { more } : {} };
30180
- await storeWrite(app, main6, `${JSON.stringify({ checkpoint: header2 })}
30181
- ${pieces[0].length > 0 ? `${pieces[0].join("\n")}
30182
- ` : ""}`);
30183
- written2.push(main6);
30184
- return written2;
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);
30185
29925
  }
30186
- async function readManifest(app, dir, path23) {
30187
- try {
30188
- const text5 = await storeRead(app, path23);
30189
- if (text5 == null) return null;
30190
- const lines = text5.split("\n").filter((l) => l !== "");
30191
- const head = JSON.parse(lines[0] ?? "null")?.checkpoint;
30192
- if (!head || head.format !== 2) return null;
30193
- const rows = lines.slice(1).map((l) => JSON.parse(l));
30194
- for (const name of head.more ?? []) {
30195
- const more = await storeRead(app, `${dir}/${name}`);
30196
- if (more == null) return null;
30197
- for (const l of more.split("\n")) if (l !== "") rows.push(JSON.parse(l));
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
+ }
30198
30034
  }
30199
- if (rows.length !== head.rows) return null;
30200
- return { head, rows };
30201
- } catch {
30202
- return null;
30203
30035
  }
30204
- }
30205
- async function wholeManifest(app, dir, path23) {
30206
- const m = await readManifest(app, dir, path23);
30207
- if (!m) return false;
30208
- for (const name of m.head.parts) if (!await readPart(app, dir, name)) return false;
30209
- return true;
30210
- }
30211
- async function readPart(app, dir, name) {
30212
- try {
30213
- let text5;
30214
- if (name.endsWith(".gz")) {
30215
- const data = await storeReadBinary(app, `${dir}/${name}`);
30216
- text5 = data ? await gunzip(data) : null;
30217
- } else text5 = await storeRead(app, `${dir}/${name}`);
30218
- if (text5 == null) return null;
30219
- const out2 = /* @__PURE__ */ new Map();
30220
- for (const l of text5.split("\n")) {
30221
- if (l === "") continue;
30222
- const [path23, content] = JSON.parse(l);
30223
- out2.set(path23, content);
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 {
30224
30044
  }
30225
- return out2;
30226
- } catch {
30227
- return null;
30228
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
+ };
30229
30076
  }
30230
- function manifestFiles(dir, path23, head) {
30231
- return [path23, ...(head?.more ?? []).map((n) => `${dir}/${n}`), ...(head?.parts ?? []).map((n) => `${dir}/${n}`)];
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
+ }
30232
30084
  }
30233
- var HistoryError, SEGMENT_POLICY, CHECKPOINT_PART, pad2, commitPath, checkpointPath, manifestPath, segmentPath, COMMIT_RE, CHECKPOINT_RE, MANIFEST_RE, SEGMENT_RE, TOCS, SEGMENT_TEXTS, SEGMENT_TEXTS_KEPT, READ_AHEAD;
30234
- var init_log_files = __esm({
30235
- "src/core/log-files.ts"() {
30236
- "use strict";
30237
- init_hints();
30238
- init_file_cache();
30239
- init_store();
30240
- HistoryError = class extends GuidedError {
30241
- constructor() {
30242
- super(...arguments);
30243
- this.name = "HistoryError";
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 });
30244
30106
  }
30245
- };
30246
- SEGMENT_POLICY = { maxBytes: 8 * 1024 * 1024, maxVersions: 1e3 };
30247
- CHECKPOINT_PART = { maxChars: 4 * 1024 * 1024 };
30248
- pad2 = (v) => String(v).padStart(20, "0");
30249
- commitPath = (dir, v) => `${dir}/${pad2(v)}.json`;
30250
- checkpointPath = (dir, v) => `${dir}/${pad2(v)}.checkpoint.json`;
30251
- manifestPath = (dir, v) => `${dir}/${pad2(v)}.manifest.jsonl`;
30252
- segmentPath = (dir, first2, last) => `${dir}/${pad2(first2)}${last === void 0 ? "" : `-${pad2(last)}`}.segment.jsonl`;
30253
- COMMIT_RE = /\/(\d{20})\.json$/;
30254
- CHECKPOINT_RE = /\/(\d{20})\.checkpoint\.json$/;
30255
- MANIFEST_RE = /\/(\d{20})\.manifest\.jsonl$/;
30256
- SEGMENT_RE = /\/(\d{20})(?:-(\d{20}))?\.segment\.jsonl$/;
30257
- TOCS = /* @__PURE__ */ new WeakMap();
30258
- SEGMENT_TEXTS = /* @__PURE__ */ new WeakMap();
30259
- SEGMENT_TEXTS_KEPT = 3;
30260
- READ_AHEAD = 16;
30261
- }
30262
- });
30263
-
30264
- // src/core/transaction-log.ts
30265
- async function unreadableNow(app, path23) {
30266
- const f = app.vault.getAbstractFileByPath(path23);
30267
- if (f == null || !("extension" in f)) return false;
30268
- try {
30269
- return frontmatterError((await readHashed(app, f)).content) !== null;
30270
- } catch {
30271
- return false;
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) });
30272
30122
  }
30273
- }
30274
- function transactionLogRoot(_settings) {
30275
- return storePath(TRANSACTION_LOG_FOLDER);
30276
- }
30277
- function transactionLogDir(settings, full) {
30278
- return `${transactionLogRoot(settings)}/${full}`;
30279
- }
30280
- function entryHash(entry) {
30281
- if (entry == null) return "";
30282
- let h = ENTRY_HASHES.get(entry);
30283
- if (h === void 0) ENTRY_HASHES.set(entry, h = contentHash(JSON.stringify(entry)));
30284
- return h;
30285
- }
30286
- async function loggedTables(app, settings, among) {
30287
- const base = `${transactionLogRoot(settings)}/`;
30288
- const out2 = [];
30289
- for (const dir of (await storeList(app, transactionLogRoot(settings))).folders) {
30290
- const full = dir.slice(base.length);
30291
- if (among && !among(full)) continue;
30292
- if ((await storeList(app, dir)).files.some(holdsVersions)) out2.push(full);
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 });
30293
30131
  }
30294
- return out2.sort();
30295
- }
30296
- function tocOf(app, settings, full) {
30297
- return logToc(app, transactionLogDir(settings, full));
30298
- }
30299
- async function listVersions(app, settings, full) {
30300
- return [...(await tocOf(app, settings, full)).versions];
30301
- }
30302
- function parseCommit(text5) {
30303
- const commit = { commitInfo: void 0, add: [], remove: [] };
30304
- for (const line of text5.split("\n")) {
30305
- if (line.trim() === "") continue;
30306
- const action = JSON.parse(line);
30307
- if (action.commitInfo) commit.commitInfo = action.commitInfo;
30308
- else if ("metaData" in action) commit.metaData = action.metaData;
30309
- else if (action.add) commit.add.push(action.add);
30310
- else if (action.remove) commit.remove.push(action.remove);
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) });
30311
30138
  }
30312
- if (!commit.commitInfo) throw new HistoryError("log commit without commitInfo");
30313
- return commit;
30314
- }
30315
- function serializeCommit(c) {
30316
- const lines = [JSON.stringify({ commitInfo: c.commitInfo })];
30317
- if (c.metaData !== void 0) lines.push(JSON.stringify({ metaData: c.metaData }));
30318
- for (const r of c.remove) lines.push(JSON.stringify({ remove: r }));
30319
- for (const a of c.add) lines.push(JSON.stringify({ add: a }));
30320
- return `${lines.join("\n")}
30321
- `;
30322
- }
30323
- async function readCommit(app, settings, full, version2) {
30324
- const toc = await tocOf(app, settings, full);
30325
- if (!toc.versions.includes(version2)) throw new HistoryError(`${full} has no version ${version2}`);
30326
- return movedCommit(parseCommit(await versionText(app, toc, version2)), await databaseMoves(app));
30139
+ for (const [path23, t] of texts) if (t.before !== t.after) cs.texts.push({ path: path23, ...t });
30140
+ return cs;
30327
30141
  }
30328
- function movedCommit(c, moves) {
30329
- let out2 = c;
30330
- for (const m of moves) {
30331
- if (m.at <= c.commitInfo.timestamp) continue;
30332
- out2 = {
30333
- ...out2,
30334
- ...out2.metaData !== void 0 ? { metaData: { entry: moveEntryPaths(out2.metaData.entry, m.from, m.to, m.entries) } } : {},
30335
- add: out2.add.map((a) => ({ ...a, path: movePath(a.path, m.from, m.to, m.entries), content: moveLinks(a.content, m.from, m.to, m.entries) })),
30336
- remove: out2.remove.map((r) => ({ ...r, path: movePath(r.path, m.from, m.to, m.entries) }))
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: []
30337
30155
  };
30338
30156
  }
30339
- return out2;
30340
- }
30341
- function moveState(state, m) {
30342
- const files = /* @__PURE__ */ new Map();
30343
- for (const [path23, f] of state.files) {
30344
- const content = moveLinks(f.content, m.from, m.to, m.entries);
30345
- files.set(movePath(path23, m.from, m.to, m.entries), content === f.content ? f : { ...f, hash: contentHash(content), content });
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
+ }
30346
30181
  }
30347
- state.files = files;
30348
- state.entry = moveEntryPaths(state.entry, m.from, m.to, m.entries);
30349
- }
30350
- function emptyState(full) {
30351
- return { full, version: -1, timestamp: "", entry: null, files: /* @__PURE__ */ new Map() };
30352
- }
30353
- function applyCommit(state, c) {
30354
- const files = state.files;
30355
- const at = c.commitInfo.version;
30356
- for (const r of c.remove) files.delete(r.path);
30357
- for (const a of c.add) files.set(a.path, { hash: a.hash, content: a.content, at });
30358
- return {
30359
- full: state.full,
30360
- version: c.commitInfo.version,
30361
- timestamp: c.commitInfo.timestamp,
30362
- entry: c.metaData !== void 0 ? c.metaData.entry : state.entry,
30363
- files
30364
- };
30365
- }
30366
- function applyLogged(state, c) {
30367
- const at = c.commitInfo.version;
30368
- for (const r of c.remove) {
30369
- const old = state.files.get(r.path);
30370
- if (old) state.bytes -= old.size;
30371
- state.files.delete(r.path);
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
+ }
30372
30189
  }
30373
- for (const a of c.add) {
30374
- const old = state.files.get(a.path);
30375
- if (old) state.bytes -= old.size;
30376
- const size = a.content.length;
30377
- state.files.set(a.path, { hash: a.hash, size, at });
30378
- state.bytes += size;
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
+ }
30379
30200
  }
30380
- state.version = at;
30381
- state.timestamp = c.commitInfo.timestamp;
30382
- if (c.metaData !== void 0) state.entry = c.metaData.entry;
30383
- }
30384
- function parseCheckpoint(text5) {
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;
30385
30309
  try {
30386
- const c = JSON.parse(text5);
30387
- return c !== null && typeof c === "object" && Number.isInteger(c.version) && Array.isArray(c.files) ? c : null;
30388
- } catch {
30389
- return null;
30310
+ for (const s of steps) {
30311
+ await s.run();
30312
+ done++;
30313
+ }
30314
+ } catch (e) {
30315
+ failed2.push({ path: "undo", error: messageOf(e) });
30390
30316
  }
30391
- }
30392
- async function readCheckpoint(app, dir, cp) {
30393
- if (cp.kind === 1) {
30394
- const text5 = await storeRead(app, cp.path);
30395
- if (text5 == null) return null;
30396
- const c = parseCheckpoint(text5);
30397
- if (!c) return null;
30398
- return { version: c.version, timestamp: c.timestamp, entry: c.entry, ...c.moves !== void 0 ? { moves: c.moves } : {}, files: new Map(c.files.map((f) => [f.path, { hash: f.hash, content: f.content, at: c.version, cp: true }])) };
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);
30399
30339
  }
30400
- const m = await readManifest(app, dir, cp.path);
30401
- if (!m) return null;
30402
- const texts = /* @__PURE__ */ new Map();
30403
- for (const name of m.head.parts) {
30404
- const part = await readPart(app, dir, name);
30405
- if (!part) return null;
30406
- for (const [p, t] of part) texts.set(p, t);
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
+ };
30407
30386
  }
30408
- const files = /* @__PURE__ */ new Map();
30409
- for (const [path23, hash] of m.rows) {
30410
- const content = texts.get(path23);
30411
- if (content === void 0) return null;
30412
- files.set(path23, { hash, content, at: m.head.version, cp: true });
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();
30413
30394
  }
30414
- return { version: m.head.version, timestamp: m.head.timestamp, entry: m.head.entry, ...m.head.moves !== void 0 ? { moves: m.head.moves } : {}, files };
30395
+ if (!scope && !storage3.getStore()) return fn();
30396
+ return storage3.run({ scope }, fn);
30415
30397
  }
30416
- async function tableStateAt(app, settings, full, asOf2 = {}) {
30417
- const known = LAST.get(app)?.get(full);
30418
- if (known && known.version >= 0 && isLatest(known, asOf2) && await stillLatest(app, settings, known)) {
30419
- const fromNotes = await stateFromLogged(app, settings, known);
30420
- if (fromNotes) return fromNotes;
30421
- }
30422
- return replayState(app, settings, full, asOf2);
30398
+ function currentWriteScope() {
30399
+ return storage3?.getStore()?.scope ?? null;
30423
30400
  }
30424
- function isLatest(known, asOf2) {
30425
- if (asOf2.version !== void 0) return asOf2.version === known.version && asOf2.timestamp === void 0;
30426
- if (asOf2.timestamp === void 0) return true;
30427
- const at = Date.parse(asOf2.timestamp);
30428
- return !Number.isNaN(at) && at >= Date.parse(known.timestamp);
30401
+ function tableOfFrontmatter(model, settings, fm) {
30402
+ return rowTableFull(model, fmGet(fm, settings.tableKey));
30429
30403
  }
30430
- async function replayState(app, settings, full, asOf2 = {}) {
30431
- const dir = transactionLogDir(settings, full);
30432
- const toc = await logToc(app, dir);
30433
- const versions = toc.versions;
30434
- if (versions.length === 0) {
30435
- if (asOf2.version !== void 0 || asOf2.timestamp !== void 0) {
30436
- throw new HistoryError(
30437
- `${full} has no history: time travel is off for it (turn it on with configure_database(history=true) or alter_table(history=true))`
30438
- );
30439
- }
30440
- return emptyState(full);
30441
- }
30442
- let target = versions[versions.length - 1];
30443
- if (asOf2.version !== void 0) {
30444
- if (!versions.includes(asOf2.version)) {
30445
- throw new HistoryError(`${full} has no version ${asOf2.version} (available: ${versions[0]}..${target})`);
30446
- }
30447
- target = asOf2.version;
30448
- }
30449
- const limitMs = asOf2.timestamp !== void 0 ? Date.parse(asOf2.timestamp) : void 0;
30450
- if (limitMs !== void 0 && Number.isNaN(limitMs)) throw new HistoryError(`"timestamp" is not a date-time: ${asOf2.timestamp}`);
30451
- const moves = await databaseMoves(app);
30452
- let applied = 0;
30453
- const moveUpTo = (timestamp) => {
30454
- while (applied < moves.length && (timestamp === null || moves[applied].at <= timestamp)) moveState(state, moves[applied++]);
30404
+ function changeSetTables(app, settings, model, cs) {
30405
+ const out2 = /* @__PURE__ */ new Set();
30406
+ const add = (full) => {
30407
+ if (full !== null) out2.add(full);
30455
30408
  };
30456
- let state = emptyState(full);
30457
- for (const cp of [...toc.checkpoints].reverse()) {
30458
- if (cp.version > target) continue;
30459
- const c = await readCheckpoint(app, dir, cp);
30460
- if (!c) continue;
30461
- if (limitMs !== void 0 && Date.parse(c.timestamp) > limitMs) continue;
30462
- state = { full, version: c.version, timestamp: c.timestamp, entry: c.entry, files: c.files };
30463
- applied = Math.min(c.moves ?? moves.filter((m) => m.at <= c.timestamp).length, moves.length);
30464
- break;
30465
- }
30466
- const since = { versions: 0, bytes: 0 };
30467
- const replay = versions.filter((v) => v > state.version && v <= target);
30468
- const texts = await versionTexts(app, toc, replay);
30469
- for (const text5 of texts) {
30470
- const c = parseCommit(text5);
30471
- if (limitMs !== void 0 && Date.parse(c.commitInfo.timestamp) > limitMs) break;
30472
- moveUpTo(c.commitInfo.timestamp);
30473
- state = applyCommit(state, c);
30474
- since.versions++;
30475
- since.bytes += text5.length;
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));
30476
30414
  }
30477
- moveUpTo(null);
30478
- state.sinceCheckpoint = since;
30479
- state.movesApplied = applied;
30480
- if (state.version < 0) throw new AbsentTableError(`${full} did not exist at ${asOf2.timestamp}`);
30481
- return state;
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;
30482
30419
  }
30483
- async function replayLogged(app, settings, full) {
30484
- const dir = transactionLogDir(settings, full);
30485
- const toc = await logToc(app, dir);
30486
- const empty = { full, version: -1, timestamp: "", entry: null, files: /* @__PURE__ */ new Map(), sinceCheckpoint: { versions: 0, bytes: 0 }, bytes: 0 };
30487
- if (toc.versions.length === 0) return empty;
30488
- const moves = await databaseMoves(app);
30489
- let state = empty;
30490
- let applied = 0;
30491
- for (const cp of [...toc.checkpoints].reverse()) {
30492
- if (cp.kind === 2) {
30493
- const m = await readManifest(app, dir, cp.path);
30494
- if (!m || !await partsThere(app, dir, m.head.parts)) continue;
30495
- const files = /* @__PURE__ */ new Map();
30496
- let bytes = 0;
30497
- for (const [path23, hash, size] of m.rows) {
30498
- files.set(path23, { hash, size, at: m.head.version, cp: true });
30499
- bytes += size;
30500
- }
30501
- state = { full, version: m.head.version, timestamp: m.head.timestamp, entry: m.head.entry, files, sinceCheckpoint: { versions: 0, bytes: 0 }, bytes };
30502
- applied = Math.min(m.head.moves ?? moves.filter((x) => x.at <= m.head.timestamp).length, moves.length);
30503
- } else {
30504
- const c = await readCheckpoint(app, dir, cp);
30505
- if (!c) continue;
30506
- const files = /* @__PURE__ */ new Map();
30507
- let bytes = 0;
30508
- for (const [path23, f] of c.files) {
30509
- files.set(path23, { hash: f.hash, size: f.content.length, at: c.version, cp: true });
30510
- bytes += f.content.length;
30511
- }
30512
- state = { full, version: c.version, timestamp: c.timestamp, entry: c.entry, files, sinceCheckpoint: { versions: 0, bytes: 0 }, bytes };
30513
- applied = Math.min(c.moves ?? moves.filter((x) => x.at <= c.timestamp).length, moves.length);
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;
30514
30445
  }
30515
- break;
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
+ };
30516
30470
  }
30517
- if (applied < moves.length) return loggedOf(await replayState(app, settings, full));
30518
- const replay = toc.versions.filter((v) => v > state.version);
30519
- for (const text5 of await versionTexts(app, toc, replay)) {
30520
- applyLogged(state, parseCommit(text5));
30521
- state.sinceCheckpoint.versions++;
30522
- state.sinceCheckpoint.bytes += text5.length;
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);
30523
30480
  }
30524
- if (moves.length > 0) state.movesApplied = applied;
30525
- return state;
30526
- }
30527
- async function partsThere(app, dir, parts) {
30528
- const { files } = await storeList(app, dir);
30529
- const there = new Set(files);
30530
- return parts.every((n) => there.has(`${dir}/${n}`));
30531
30481
  }
30532
- function loggedOf(state) {
30533
- const files = /* @__PURE__ */ new Map();
30534
- let bytes = 0;
30535
- for (const [path23, f] of state.files) {
30536
- files.set(path23, { hash: f.hash, size: f.content.length, at: f.at, cp: f.cp });
30537
- bytes += f.content.length;
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
+ }
30538
30494
  }
30539
- return {
30540
- full: state.full,
30541
- version: state.version,
30542
- timestamp: state.timestamp,
30543
- entry: state.entry,
30544
- files,
30545
- sinceCheckpoint: state.sinceCheckpoint,
30546
- movesApplied: state.movesApplied,
30547
- bytes
30548
- };
30495
+ return void 0;
30549
30496
  }
30550
- async function loggedTexts(app, settings, state, paths) {
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) {
30551
30501
  const out2 = /* @__PURE__ */ new Map();
30552
- const fromCommit = /* @__PURE__ */ new Map();
30553
- const fromCheckpoint = /* @__PURE__ */ new Map();
30554
- for (const path23 of paths) {
30555
- const f = state.files.get(path23);
30556
- const note = app.vault.getAbstractFileByPath(path23);
30557
- if (note && "extension" in note) {
30558
- try {
30559
- const h = await readHashed(app, note);
30560
- if (h.hash === f.hash) {
30561
- out2.set(path23, h.content);
30562
- continue;
30563
- }
30564
- } catch {
30565
- }
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 {
30566
30507
  }
30567
- const by = f.cp ? fromCheckpoint : fromCommit;
30568
- const list4 = by.get(f.at);
30569
- if (list4) list4.push(path23);
30570
- else by.set(f.at, [path23]);
30571
30508
  }
30572
- for (const [v, list4] of fromCommit) {
30573
- let c;
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;
30574
30528
  try {
30575
- c = await readCommit(app, settings, state.full, v);
30529
+ cached2 = idx ? void 0 : app.metadataCache.getFileCache(f)?.frontmatter;
30576
30530
  } catch {
30577
- continue;
30531
+ cached2 = void 0;
30578
30532
  }
30579
- const want = new Set(list4);
30580
- for (const a of c.add) if (want.has(a.path) && contentHash(a.content) === state.files.get(a.path).hash) out2.set(a.path, a.content);
30581
- }
30582
- for (const [v, list4] of fromCheckpoint) await textsFromCheckpoint(app, settings, state, v, list4, out2);
30583
- const missing = [...fromCommit.values(), ...fromCheckpoint.values()].flat().filter((p) => !out2.has(p));
30584
- if (missing.length > 0) {
30533
+ if (cached2) continue;
30534
+ let text5;
30585
30535
  try {
30586
- const whole = await replayState(app, settings, state.full, { version: state.version });
30587
- for (const p of missing) {
30588
- const x = whole.files.get(p);
30589
- if (x && x.hash === state.files.get(p).hash) out2.set(p, x.content);
30590
- }
30536
+ text5 = (await readHashed(app, f)).content;
30591
30537
  } catch {
30538
+ continue;
30592
30539
  }
30540
+ const u = unreadableRowOf(model, settings, f.path, text5, excluded);
30541
+ if (u && (table === void 0 || u.table === table)) out2.push(u);
30593
30542
  }
30594
- return out2;
30543
+ return out2.sort((a, b) => a.path.localeCompare(b.path));
30595
30544
  }
30596
- async function textsFromCheckpoint(app, settings, state, version2, paths, out2) {
30597
- const dir = transactionLogDir(settings, state.full);
30598
- const toc = await logToc(app, dir);
30599
- const cp = [...toc.checkpoints].reverse().find((c) => c.version === version2);
30600
- if (!cp) return;
30601
- const want = new Set(paths);
30602
- const keep = (path23, content) => {
30603
- if (want.has(path23) && contentHash(content) === state.files.get(path23).hash) out2.set(path23, content);
30604
- };
30605
- if (cp.kind === 1) {
30606
- const c = await readCheckpoint(app, dir, cp).catch(() => null);
30607
- for (const [path23, f] of c?.files ?? []) keep(path23, f.content);
30608
- return;
30609
- }
30610
- const m = await readManifest(app, dir, cp.path);
30611
- if (!m) return;
30612
- const partOf = new Map(m.rows.map(([path23, , , part]) => [path23, part]));
30613
- const parts = new Set(paths.map((p) => partOf.get(p)).filter((x) => x !== void 0));
30614
- for (const i of parts) {
30615
- const part = await readPart(app, dir, m.head.parts[i]);
30616
- for (const [path23, content] of part ?? []) keep(path23, content);
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();
30617
30558
  }
30559
+ });
30560
+
30561
+ // src/core/log-files.ts
30562
+ function holdsVersions(path23) {
30563
+ return COMMIT_RE.test(path23) || SEGMENT_RE.test(path23);
30618
30564
  }
30619
- async function stateFromLogged(app, settings, state) {
30620
- const texts = await loggedTexts(app, settings, state, state.files.keys());
30621
- if (texts.size !== state.files.size) return null;
30622
- const files = /* @__PURE__ */ new Map();
30623
- for (const [path23, f] of state.files) files.set(path23, { hash: f.hash, content: texts.get(path23), at: f.at, cp: f.cp });
30624
- return {
30625
- full: state.full,
30626
- version: state.version,
30627
- timestamp: state.timestamp,
30628
- entry: state.entry,
30629
- files,
30630
- sinceCheckpoint: { ...state.sinceCheckpoint },
30631
- movesApplied: state.movesApplied
30632
- };
30633
- }
30634
- function infoOf(text5) {
30635
- return JSON.parse(text5.slice(0, text5.indexOf("\n"))).commitInfo;
30565
+ function tocsOf(app) {
30566
+ let m = TOCS.get(app);
30567
+ if (!m) TOCS.set(app, m = /* @__PURE__ */ new Map());
30568
+ return m;
30636
30569
  }
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);
30570
+ function latestOf(toc) {
30571
+ return toc.versions.length > 0 ? toc.versions[toc.versions.length - 1] : -1;
30640
30572
  }
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);
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;
30649
30602
  }
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;
30603
+ return { commits, validEnd: pos };
30657
30604
  }
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)];
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}`;
30664
30609
  }
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);
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;
30679
30619
  }
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);
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);
30684
30628
  }
30685
30629
  return out2;
30686
30630
  }
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);
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();
30743
30665
  }
30744
- live.cursor = cursor;
30745
30666
  }
30746
- return live.tables;
30667
+ return toc;
30747
30668
  }
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}`);
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;
30760
30677
  }
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
30678
  }
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++;
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;
30801
30683
  }
30802
- return n;
30803
- }
30804
- async function wholeCheckpoint(app, dir, v) {
30805
- return parseCheckpoint(await storeRead(app, checkpointPath(dir, v)) ?? "")?.version === v;
30684
+ return void 0;
30806
30685
  }
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
- }
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;
30823
30692
  }
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 })));
30693
+ const toc = await buildToc(app, dir);
30694
+ tocs.set(dir, toc);
30695
+ return toc;
30825
30696
  }
30826
- function forgetLoggedStates(app) {
30827
- LAST.delete(app);
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));
30828
30704
  }
30829
- async function knownState(app, settings, full) {
30830
- return LAST.get(app)?.get(full) ?? readLastState(app, settings, full);
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;
30831
30709
  }
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;
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
+ );
30838
30724
  }
30839
- async function stillLatest(app, settings, state) {
30840
- return latestOf(await tocOf(app, settings, state.full)) === state.version;
30725
+ async function wholeVersions(app, seg) {
30726
+ return new Set((await segmentText(app, seg)).index.keys());
30841
30727
  }
30842
- async function latestVersion(app, settings, full) {
30843
- const latest = latestOf(await tocOf(app, settings, full));
30844
- return latest >= 0 ? latest : null;
30728
+ function segmentsWith(toc, v) {
30729
+ return toc.segments.filter((s) => v >= s.first && v <= s.last).reverse();
30845
30730
  }
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
- });
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);
30855
30738
  }
30856
- return n;
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;
30857
30745
  }
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
- }
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}`);
30869
30756
  }
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 });
30757
+ const repaired = [];
30758
+ if (toc.torn) {
30759
+ repaired.push(`${toc.torn} set aside as ${await setAside(app, toc.torn)}`);
30760
+ toc.torn = null;
30873
30761
  }
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 });
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;
30877
30772
  }
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 };
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;
30881
30783
  }
30882
- function historyEnabled(model, entry) {
30883
- const own2 = entry.options?.history;
30884
- return typeof own2 === "boolean" ? own2 : model.config.history === true;
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 } : {} });
30885
30805
  }
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]);
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) {
30896
30815
  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
- }
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;
30972
30832
  }
30833
+ await flush2();
30973
30834
  return out2;
30974
30835
  }
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;
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]);
31003
30841
  }
31004
- function logLockActivity(app) {
31005
- const a = ACTIVITY.get(app);
31006
- return { busy: (a?.pending ?? 0) > 0, lastEnd: a?.lastEnd ?? 0 };
30842
+ async function gzip(text5) {
30843
+ const stream = new Blob([text5]).stream().pipeThrough(new CompressionStream("gzip"));
30844
+ return new Response(stream).arrayBuffer();
31007
30845
  }
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;
30846
+ async function gunzip(data) {
30847
+ const stream = new Blob([data]).stream().pipeThrough(new DecompressionStream("gzip"));
30848
+ return new Response(stream).text();
31015
30849
  }
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);
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;
31035
30859
  }
31036
- await storeFlush(app);
31037
- for (const p of doomed) await storeRemove(app, p);
31038
- forgetLoggedStates(app);
30860
+ cur.push(l);
30861
+ chars += l.length + 1;
31039
30862
  }
31040
- return { table: full, retained_from: keep, deleted: doomed, dry_run: dryRun };
30863
+ if (cur.length > 0 || out2.length === 0) out2.push(cur);
30864
+ return out2;
31041
30865
  }
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
- }
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;
30885
+ }
30886
+ const entries = rows.map((r, i) => JSON.stringify([r.path, r.hash, r.content.length, partOf[i]]));
30887
+ const pieces = chunked(entries);
30888
+ const more = pieces.slice(1).map((_, i) => `${pad2(head.version)}.manifest-${i + 1}.jsonl`);
30889
+ for (const [i, lines] of pieces.slice(1).entries()) {
30890
+ await storeWrite(app, `${dir}/${more[i]}`, `${lines.join("\n")}
30891
+ `);
30892
+ written2.push(`${dir}/${more[i]}`);
30893
+ }
30894
+ const header2 = { format: 2, version: head.version, timestamp: head.timestamp, entry: head.entry, ...head.moves ? { moves: head.moves } : {}, rows: rows.length, parts: partNames, ...more.length > 0 ? { more } : {} };
30895
+ await storeWrite(app, main6, `${JSON.stringify({ checkpoint: header2 })}
30896
+ ${pieces[0].length > 0 ? `${pieces[0].join("\n")}
30897
+ ` : ""}`);
30898
+ written2.push(main6);
30899
+ return written2;
30900
+ }
30901
+ async function readManifest(app, dir, path23) {
30902
+ try {
30903
+ const text5 = await storeRead(app, path23);
30904
+ if (text5 == null) return null;
30905
+ const lines = text5.split("\n").filter((l) => l !== "");
30906
+ const head = JSON.parse(lines[0] ?? "null")?.checkpoint;
30907
+ if (!head || head.format !== 2) return null;
30908
+ const rows = lines.slice(1).map((l) => JSON.parse(l));
30909
+ for (const name of head.more ?? []) {
30910
+ const more = await storeRead(app, `${dir}/${name}`);
30911
+ if (more == null) return null;
30912
+ for (const l of more.split("\n")) if (l !== "") rows.push(JSON.parse(l));
31084
30913
  }
31085
- await storeFlush(app);
31086
- for (const seg of toc.segments) {
31087
- await storeRemove(app, seg.path);
31088
- result.removed++;
30914
+ if (rows.length !== head.rows) return null;
30915
+ return { head, rows };
30916
+ } catch {
30917
+ return null;
30918
+ }
30919
+ }
30920
+ async function wholeManifest(app, dir, path23) {
30921
+ const m = await readManifest(app, dir, path23);
30922
+ if (!m) return false;
30923
+ for (const name of m.head.parts) if (!await readPart(app, dir, name)) return false;
30924
+ return true;
30925
+ }
30926
+ async function readPart(app, dir, name) {
30927
+ try {
30928
+ let text5;
30929
+ if (name.endsWith(".gz")) {
30930
+ const data = await storeReadBinary(app, `${dir}/${name}`);
30931
+ text5 = data ? await gunzip(data) : null;
30932
+ } else text5 = await storeRead(app, `${dir}/${name}`);
30933
+ if (text5 == null) return null;
30934
+ const out2 = /* @__PURE__ */ new Map();
30935
+ for (const l of text5.split("\n")) {
30936
+ if (l === "") continue;
30937
+ const [path23, content] = JSON.parse(l);
30938
+ out2.set(path23, content);
31089
30939
  }
30940
+ return out2;
30941
+ } catch {
30942
+ return null;
31090
30943
  }
31091
- forgetLoggedStates(app);
31092
- return result;
31093
30944
  }
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"() {
30945
+ function manifestFiles(dir, path23, head) {
30946
+ return [path23, ...(head?.more ?? []).map((n) => `${dir}/${n}`), ...(head?.parts ?? []).map((n) => `${dir}/${n}`)];
30947
+ }
30948
+ var HistoryError, SEGMENT_POLICY, CHECKPOINT_PART, pad2, commitPath, checkpointPath, manifestPath, segmentPath, COMMIT_RE, CHECKPOINT_RE, MANIFEST_RE, SEGMENT_RE, TOCS, SEGMENT_TEXTS, SEGMENT_TEXTS_KEPT, READ_AHEAD;
30949
+ var init_log_files = __esm({
30950
+ "src/core/log-files.ts"() {
31097
30951
  "use strict";
30952
+ init_hints();
31098
30953
  init_file_cache();
31099
- init_frontmatter();
31100
- init_metastore();
31101
- init_actor();
31102
30954
  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 {
30955
+ HistoryError = class extends GuidedError {
30956
+ constructor() {
30957
+ super(...arguments);
30958
+ this.name = "HistoryError";
30959
+ }
31118
30960
  };
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();
30961
+ SEGMENT_POLICY = { maxBytes: 8 * 1024 * 1024, maxVersions: 1e3 };
30962
+ CHECKPOINT_PART = { maxChars: 4 * 1024 * 1024 };
30963
+ pad2 = (v) => String(v).padStart(20, "0");
30964
+ commitPath = (dir, v) => `${dir}/${pad2(v)}.json`;
30965
+ checkpointPath = (dir, v) => `${dir}/${pad2(v)}.checkpoint.json`;
30966
+ manifestPath = (dir, v) => `${dir}/${pad2(v)}.manifest.jsonl`;
30967
+ segmentPath = (dir, first2, last) => `${dir}/${pad2(first2)}${last === void 0 ? "" : `-${pad2(last)}`}.segment.jsonl`;
30968
+ COMMIT_RE = /\/(\d{20})\.json$/;
30969
+ CHECKPOINT_RE = /\/(\d{20})\.checkpoint\.json$/;
30970
+ MANIFEST_RE = /\/(\d{20})\.manifest\.jsonl$/;
30971
+ SEGMENT_RE = /\/(\d{20})(?:-(\d{20}))?\.segment\.jsonl$/;
30972
+ TOCS = /* @__PURE__ */ new WeakMap();
30973
+ SEGMENT_TEXTS = /* @__PURE__ */ new WeakMap();
30974
+ SEGMENT_TEXTS_KEPT = 3;
30975
+ READ_AHEAD = 16;
31127
30976
  }
31128
30977
  });
31129
30978
 
31130
- // src/core/changeset.ts
31131
- function createdHash(text5) {
30979
+ // src/core/transaction-log.ts
30980
+ async function unreadableNow(app, path23) {
30981
+ const f = app.vault.getAbstractFileByPath(path23);
30982
+ if (f == null || !("extension" in f)) return false;
31132
30983
  try {
31133
- const { ordered, body } = readOrderedFromMarkdown(text5);
31134
- return contentHash(JSON.stringify([ordered, body]));
30984
+ return frontmatterError((await readHashed(app, f)).content) !== null;
31135
30985
  } catch {
31136
- return contentHash(text5);
30986
+ return false;
31137
30987
  }
31138
30988
  }
31139
- function bodyChanges(e) {
31140
- return e.bodyAfter !== void 0 && e.bodyAfter !== e.bodyBefore;
30989
+ function transactionLogRoot(_settings) {
30990
+ return storePath(TRANSACTION_LOG_FOLDER);
31141
30991
  }
31142
- function bodyOf(text5) {
31143
- const split2 = splitFrontmatter(text5);
31144
- return split2 ? split2.body : text5;
30992
+ function transactionLogDir(settings, full) {
30993
+ return `${transactionLogRoot(settings)}/${full}`;
31145
30994
  }
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;
30995
+ function entryHash(entry) {
30996
+ if (entry == null) return "";
30997
+ let h = ENTRY_HASHES.get(entry);
30998
+ if (h === void 0) ENTRY_HASHES.set(entry, h = contentHash(JSON.stringify(entry)));
30999
+ return h;
31150
31000
  }
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
- });
31001
+ async function loggedTables(app, settings, among) {
31002
+ const base = `${transactionLogRoot(settings)}/`;
31003
+ const out2 = [];
31004
+ for (const dir of (await storeList(app, transactionLogRoot(settings))).folders) {
31005
+ const full = dir.slice(base.length);
31006
+ if (among && !among(full)) continue;
31007
+ if ((await storeList(app, dir)).files.some(holdsVersions)) out2.push(full);
31008
+ }
31009
+ return out2.sort();
31156
31010
  }
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
- };
31011
+ function tocOf(app, settings, full) {
31012
+ return logToc(app, transactionLogDir(settings, full));
31168
31013
  }
31169
- function same(a, b) {
31170
- return JSON.stringify(a) === JSON.stringify(b);
31014
+ async function listVersions(app, settings, full) {
31015
+ return [...(await tocOf(app, settings, full)).versions];
31171
31016
  }
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: []
31182
- };
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);
31196
- }
31017
+ function parseCommit(text5) {
31018
+ const commit = { commitInfo: void 0, add: [], remove: [] };
31019
+ for (const line of text5.split("\n")) {
31020
+ if (line.trim() === "") continue;
31021
+ const action = JSON.parse(line);
31022
+ if (action.commitInfo) commit.commitInfo = action.commitInfo;
31023
+ else if ("metaData" in action) commit.metaData = action.metaData;
31024
+ else if (action.add) commit.add.push(action.add);
31025
+ else if (action.remove) commit.remove.push(action.remove);
31197
31026
  }
31198
- return d;
31027
+ if (!commit.commitInfo) throw new HistoryError("log commit without commitInfo");
31028
+ return commit;
31199
31029
  }
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);
31030
+ function serializeCommit(c) {
31031
+ const lines = [JSON.stringify({ commitInfo: c.commitInfo })];
31032
+ if (c.metaData !== void 0) lines.push(JSON.stringify({ metaData: c.metaData }));
31033
+ for (const r of c.remove) lines.push(JSON.stringify({ remove: r }));
31034
+ for (const a of c.add) lines.push(JSON.stringify({ add: a }));
31035
+ return `${lines.join("\n")}
31036
+ `;
31207
31037
  }
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);
31038
+ async function readCommit(app, settings, full, version2) {
31039
+ const toc = await tocOf(app, settings, full);
31040
+ if (!toc.versions.includes(version2)) throw new HistoryError(`${full} has no version ${version2}`);
31041
+ return movedCommit(parseCommit(await versionText(app, toc, version2)), await databaseMoves(app));
31213
31042
  }
31214
- function fileOrNull(app, path23) {
31215
- const f = app.vault.getAbstractFileByPath(path23);
31216
- return f != null && "extension" in f ? f : null;
31043
+ function movedCommit(c, moves) {
31044
+ let out2 = c;
31045
+ for (const m of moves) {
31046
+ if (m.at <= c.commitInfo.timestamp) continue;
31047
+ out2 = {
31048
+ ...out2,
31049
+ ...out2.metaData !== void 0 ? { metaData: { entry: moveEntryPaths(out2.metaData.entry, m.from, m.to, m.entries) } } : {},
31050
+ add: out2.add.map((a) => ({ ...a, path: movePath(a.path, m.from, m.to, m.entries), content: moveLinks(a.content, m.from, m.to, m.entries) })),
31051
+ remove: out2.remove.map((r) => ({ ...r, path: movePath(r.path, m.from, m.to, m.entries) }))
31052
+ };
31053
+ }
31054
+ return out2;
31217
31055
  }
31218
- function parentFolder(path23) {
31219
- const i = path23.lastIndexOf("/");
31220
- return i < 0 ? "" : path23.slice(0, i);
31056
+ function moveState(state, m) {
31057
+ const files = /* @__PURE__ */ new Map();
31058
+ for (const [path23, f] of state.files) {
31059
+ const content = moveLinks(f.content, m.from, m.to, m.entries);
31060
+ files.set(movePath(path23, m.from, m.to, m.entries), content === f.content ? f : { ...f, hash: contentHash(content), content });
31061
+ }
31062
+ state.files = files;
31063
+ state.entry = moveEntryPaths(state.entry, m.from, m.to, m.entries);
31221
31064
  }
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);
31065
+ function emptyState(full) {
31066
+ return { full, version: -1, timestamp: "", entry: null, files: /* @__PURE__ */ new Map() };
31227
31067
  }
31228
- async function readOrderedFor(app, file) {
31229
- return readOrderedFromMarkdown(await app.vault.read(file)).ordered;
31068
+ function applyCommit(state, c) {
31069
+ const files = state.files;
31070
+ const at = c.commitInfo.version;
31071
+ for (const r of c.remove) files.delete(r.path);
31072
+ for (const a of c.add) files.set(a.path, { hash: a.hash, content: a.content, at });
31073
+ return {
31074
+ full: state.full,
31075
+ version: c.commitInfo.version,
31076
+ timestamp: c.commitInfo.timestamp,
31077
+ entry: c.metaData !== void 0 ? c.metaData.entry : state.entry,
31078
+ files
31079
+ };
31230
31080
  }
31231
- async function fileExists(app, path23) {
31232
- return inStore(path23) ? storeExists(app, path23) : fileOrNull(app, path23) != null;
31081
+ function applyLogged(state, c) {
31082
+ const at = c.commitInfo.version;
31083
+ for (const r of c.remove) {
31084
+ const old = state.files.get(r.path);
31085
+ if (old) state.bytes -= old.size;
31086
+ state.files.delete(r.path);
31087
+ }
31088
+ for (const a of c.add) {
31089
+ const old = state.files.get(a.path);
31090
+ if (old) state.bytes -= old.size;
31091
+ const size = a.content.length;
31092
+ state.files.set(a.path, { hash: a.hash, size, at });
31093
+ state.bytes += size;
31094
+ }
31095
+ state.version = at;
31096
+ state.timestamp = c.commitInfo.timestamp;
31097
+ if (c.metaData !== void 0) state.entry = c.metaData.entry;
31233
31098
  }
31234
- async function occupied(app, path23) {
31235
- return inStore(path23) ? storeExists(app, path23) : app.vault.getAbstractFileByPath(path23) != null;
31099
+ function parseCheckpoint(text5) {
31100
+ try {
31101
+ const c = JSON.parse(text5);
31102
+ return c !== null && typeof c === "object" && Number.isInteger(c.version) && Array.isArray(c.files) ? c : null;
31103
+ } catch {
31104
+ return null;
31105
+ }
31236
31106
  }
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 });
31245
- }
31107
+ async function readCheckpoint(app, dir, cp) {
31108
+ if (cp.kind === 1) {
31109
+ const text5 = await storeRead(app, cp.path);
31110
+ if (text5 == null) return null;
31111
+ const c = parseCheckpoint(text5);
31112
+ if (!c) return null;
31113
+ return { version: c.version, timestamp: c.timestamp, entry: c.entry, ...c.moves !== void 0 ? { moves: c.moves } : {}, files: new Map(c.files.map((f) => [f.path, { hash: f.hash, content: f.content, at: c.version, cp: true }])) };
31246
31114
  }
31247
- return out2;
31115
+ const m = await readManifest(app, dir, cp.path);
31116
+ if (!m) return null;
31117
+ const texts = /* @__PURE__ */ new Map();
31118
+ for (const name of m.head.parts) {
31119
+ const part = await readPart(app, dir, name);
31120
+ if (!part) return null;
31121
+ for (const [p, t] of part) texts.set(p, t);
31122
+ }
31123
+ const files = /* @__PURE__ */ new Map();
31124
+ for (const [path23, hash] of m.rows) {
31125
+ const content = texts.get(path23);
31126
+ if (content === void 0) return null;
31127
+ files.set(path23, { hash, content, at: m.head.version, cp: true });
31128
+ }
31129
+ return { version: m.head.version, timestamp: m.head.timestamp, entry: m.head.entry, ...m.head.moves !== void 0 ? { moves: m.head.moves } : {}, files };
31248
31130
  }
31249
- function atOnceFor(count2) {
31250
- return count2 >= WRITE_CONCURRENCY.from ? WRITE_CONCURRENCY.atOnce : 1;
31131
+ async function tableStateAt(app, settings, full, asOf2 = {}) {
31132
+ const known = LAST.get(app)?.get(full);
31133
+ if (known && known.version >= 0 && isLatest(known, asOf2) && await stillLatest(app, settings, known)) {
31134
+ const fromNotes = await stateFromLogged(app, settings, known);
31135
+ if (fromNotes) return fromNotes;
31136
+ }
31137
+ return replayState(app, settings, full, asOf2);
31251
31138
  }
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
- }
31139
+ function isLatest(known, asOf2) {
31140
+ if (asOf2.version !== void 0) return asOf2.version === known.version && asOf2.timestamp === void 0;
31141
+ if (asOf2.timestamp === void 0) return true;
31142
+ const at = Date.parse(asOf2.timestamp);
31143
+ return !Number.isNaN(at) && at >= Date.parse(known.timestamp);
31144
+ }
31145
+ async function replayState(app, settings, full, asOf2 = {}) {
31146
+ const dir = transactionLogDir(settings, full);
31147
+ const toc = await logToc(app, dir);
31148
+ const versions = toc.versions;
31149
+ if (versions.length === 0) {
31150
+ if (asOf2.version !== void 0 || asOf2.timestamp !== void 0) {
31151
+ throw new HistoryError(
31152
+ `${full} has no history: time travel is off for it (turn it on with configure_database(history=true) or alter_table(history=true))`
31153
+ );
31265
31154
  }
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
- }
31155
+ return emptyState(full);
31156
+ }
31157
+ let target = versions[versions.length - 1];
31158
+ if (asOf2.version !== void 0) {
31159
+ if (!versions.includes(asOf2.version)) {
31160
+ throw new HistoryError(`${full} has no version ${asOf2.version} (available: ${versions[0]}..${target})`);
31279
31161
  }
31162
+ target = asOf2.version;
31280
31163
  }
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 };
31287
- }
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);
31295
- }
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) });
31164
+ const limitMs = asOf2.timestamp !== void 0 ? Date.parse(asOf2.timestamp) : void 0;
31165
+ if (limitMs !== void 0 && Number.isNaN(limitMs)) throw new HistoryError(`"timestamp" is not a date-time: ${asOf2.timestamp}`);
31166
+ const moves = await databaseMoves(app);
31167
+ let applied = 0;
31168
+ const moveUpTo = (timestamp) => {
31169
+ while (applied < moves.length && (timestamp === null || moves[applied].at <= timestamp)) moveState(state, moves[applied++]);
31306
31170
  };
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;
31171
+ let state = emptyState(full);
31172
+ for (const cp of [...toc.checkpoints].reverse()) {
31173
+ if (cp.version > target) continue;
31174
+ const c = await readCheckpoint(app, dir, cp);
31175
+ if (!c) continue;
31176
+ if (limitMs !== void 0 && Date.parse(c.timestamp) > limitMs) continue;
31177
+ state = { full, version: c.version, timestamp: c.timestamp, entry: c.entry, files: c.files };
31178
+ applied = Math.min(c.moves ?? moves.filter((m) => m.at <= c.timestamp).length, moves.length);
31179
+ break;
31180
+ }
31181
+ const since = { versions: 0, bytes: 0 };
31182
+ const replay = versions.filter((v) => v > state.version && v <= target);
31183
+ const texts = await versionTexts(app, toc, replay);
31184
+ for (const text5 of texts) {
31185
+ const c = parseCommit(text5);
31186
+ if (limitMs !== void 0 && Date.parse(c.commitInfo.timestamp) > limitMs) break;
31187
+ moveUpTo(c.commitInfo.timestamp);
31188
+ state = applyCommit(state, c);
31189
+ since.versions++;
31190
+ since.bytes += text5.length;
31191
+ }
31192
+ moveUpTo(null);
31193
+ state.sinceCheckpoint = since;
31194
+ state.movesApplied = applied;
31195
+ if (state.version < 0) throw new AbsentTableError(`${full} did not exist at ${asOf2.timestamp}`);
31196
+ return state;
31197
+ }
31198
+ async function replayLogged(app, settings, full) {
31199
+ const dir = transactionLogDir(settings, full);
31200
+ const toc = await logToc(app, dir);
31201
+ const empty = { full, version: -1, timestamp: "", entry: null, files: /* @__PURE__ */ new Map(), sinceCheckpoint: { versions: 0, bytes: 0 }, bytes: 0 };
31202
+ if (toc.versions.length === 0) return empty;
31203
+ const moves = await databaseMoves(app);
31204
+ let state = empty;
31205
+ let applied = 0;
31206
+ for (const cp of [...toc.checkpoints].reverse()) {
31207
+ if (cp.kind === 2) {
31208
+ const m = await readManifest(app, dir, cp.path);
31209
+ if (!m || !await partsThere(app, dir, m.head.parts)) continue;
31210
+ const files = /* @__PURE__ */ new Map();
31211
+ let bytes = 0;
31212
+ for (const [path23, hash, size] of m.rows) {
31213
+ files.set(path23, { hash, size, at: m.head.version, cp: true });
31214
+ bytes += size;
31352
31215
  }
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));
31216
+ state = { full, version: m.head.version, timestamp: m.head.timestamp, entry: m.head.entry, files, sinceCheckpoint: { versions: 0, bytes: 0 }, bytes };
31217
+ applied = Math.min(m.head.moves ?? moves.filter((x) => x.at <= m.head.timestamp).length, moves.length);
31218
+ } else {
31219
+ const c = await readCheckpoint(app, dir, cp);
31220
+ if (!c) continue;
31221
+ const files = /* @__PURE__ */ new Map();
31222
+ let bytes = 0;
31223
+ for (const [path23, f] of c.files) {
31224
+ files.set(path23, { hash: f.hash, size: f.content.length, at: c.version, cp: true });
31225
+ bytes += f.content.length;
31366
31226
  }
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;
31227
+ state = { full, version: c.version, timestamp: c.timestamp, entry: c.entry, files, sinceCheckpoint: { versions: 0, bytes: 0 }, bytes };
31228
+ applied = Math.min(c.moves ?? moves.filter((x) => x.at <= c.timestamp).length, moves.length);
31383
31229
  }
31384
- for (const m of cs.moves) {
31230
+ break;
31231
+ }
31232
+ if (applied < moves.length) return loggedOf(await replayState(app, settings, full));
31233
+ const replay = toc.versions.filter((v) => v > state.version);
31234
+ for (const text5 of await versionTexts(app, toc, replay)) {
31235
+ applyLogged(state, parseCommit(text5));
31236
+ state.sinceCheckpoint.versions++;
31237
+ state.sinceCheckpoint.bytes += text5.length;
31238
+ }
31239
+ if (moves.length > 0) state.movesApplied = applied;
31240
+ return state;
31241
+ }
31242
+ async function partsThere(app, dir, parts) {
31243
+ const { files } = await storeList(app, dir);
31244
+ const there = new Set(files);
31245
+ return parts.every((n) => there.has(`${dir}/${n}`));
31246
+ }
31247
+ function loggedOf(state) {
31248
+ const files = /* @__PURE__ */ new Map();
31249
+ let bytes = 0;
31250
+ for (const [path23, f] of state.files) {
31251
+ files.set(path23, { hash: f.hash, size: f.content.length, at: f.at, cp: f.cp });
31252
+ bytes += f.content.length;
31253
+ }
31254
+ return {
31255
+ full: state.full,
31256
+ version: state.version,
31257
+ timestamp: state.timestamp,
31258
+ entry: state.entry,
31259
+ files,
31260
+ sinceCheckpoint: state.sinceCheckpoint,
31261
+ movesApplied: state.movesApplied,
31262
+ bytes
31263
+ };
31264
+ }
31265
+ async function loggedTexts(app, settings, state, paths) {
31266
+ const out2 = /* @__PURE__ */ new Map();
31267
+ const fromCommit = /* @__PURE__ */ new Map();
31268
+ const fromCheckpoint = /* @__PURE__ */ new Map();
31269
+ for (const path23 of paths) {
31270
+ const f = state.files.get(path23);
31271
+ const note = app.vault.getAbstractFileByPath(path23);
31272
+ if (note && "extension" in note) {
31385
31273
  try {
31386
- await moveFile(app, m.from, m.to);
31387
- moves.push(m);
31388
- } catch (err) {
31389
- fail2(m.from, err);
31390
- break run;
31274
+ const h = await readHashed(app, note);
31275
+ if (h.hash === f.hash) {
31276
+ out2.set(path23, h.content);
31277
+ continue;
31278
+ }
31279
+ } catch {
31391
31280
  }
31392
31281
  }
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
- }
31282
+ const by = f.cp ? fromCheckpoint : fromCommit;
31283
+ const list4 = by.get(f.at);
31284
+ if (list4) list4.push(path23);
31285
+ else by.set(f.at, [path23]);
31286
+ }
31287
+ for (const [v, list4] of fromCommit) {
31288
+ let c;
31289
+ try {
31290
+ c = await readCommit(app, settings, state.full, v);
31291
+ } catch {
31292
+ continue;
31404
31293
  }
31294
+ const want = new Set(list4);
31295
+ for (const a of c.add) if (want.has(a.path) && contentHash(a.content) === state.files.get(a.path).hash) out2.set(a.path, a.content);
31405
31296
  }
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;
31297
+ for (const [v, list4] of fromCheckpoint) await textsFromCheckpoint(app, settings, state, v, list4, out2);
31298
+ const missing = [...fromCommit.values(), ...fromCheckpoint.values()].flat().filter((p) => !out2.has(p));
31299
+ if (missing.length > 0) {
31411
31300
  try {
31412
- createdHashes.push({ path: p, hash: createdHash(await app.vault.read(f)) });
31301
+ const whole = await replayState(app, settings, state.full, { version: state.version });
31302
+ for (const p of missing) {
31303
+ const x = whole.files.get(p);
31304
+ if (x && x.hash === state.files.get(p).hash) out2.set(p, x.content);
31305
+ }
31413
31306
  } catch {
31414
31307
  }
31415
31308
  }
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
- );
31309
+ return out2;
31310
+ }
31311
+ async function textsFromCheckpoint(app, settings, state, version2, paths, out2) {
31312
+ const dir = transactionLogDir(settings, state.full);
31313
+ const toc = await logToc(app, dir);
31314
+ const cp = [...toc.checkpoints].reverse().find((c) => c.version === version2);
31315
+ if (!cp) return;
31316
+ const want = new Set(paths);
31317
+ const keep = (path23, content) => {
31318
+ if (want.has(path23) && contentHash(content) === state.files.get(path23).hash) out2.set(path23, content);
31319
+ };
31320
+ if (cp.kind === 1) {
31321
+ const c = await readCheckpoint(app, dir, cp).catch(() => null);
31322
+ for (const [path23, f] of c?.files ?? []) keep(path23, f.content);
31323
+ return;
31324
+ }
31325
+ const m = await readManifest(app, dir, cp.path);
31326
+ if (!m) return;
31327
+ const partOf = new Map(m.rows.map(([path23, , , part]) => [path23, part]));
31328
+ const parts = new Set(paths.map((p) => partOf.get(p)).filter((x) => x !== void 0));
31329
+ for (const i of parts) {
31330
+ const part = await readPart(app, dir, m.head.parts[i]);
31331
+ for (const [path23, content] of part ?? []) keep(path23, content);
31332
+ }
31333
+ }
31334
+ async function stateFromLogged(app, settings, state) {
31335
+ const texts = await loggedTexts(app, settings, state, state.files.keys());
31336
+ if (texts.size !== state.files.size) return null;
31337
+ const files = /* @__PURE__ */ new Map();
31338
+ for (const [path23, f] of state.files) files.set(path23, { hash: f.hash, content: texts.get(path23), at: f.at, cp: f.cp });
31435
31339
  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
31340
+ full: state.full,
31341
+ version: state.version,
31342
+ timestamp: state.timestamp,
31343
+ entry: state.entry,
31344
+ files,
31345
+ sinceCheckpoint: { ...state.sinceCheckpoint },
31346
+ movesApplied: state.movesApplied
31445
31347
  };
31446
31348
  }
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);
31349
+ function infoOf(text5) {
31350
+ return JSON.parse(text5.slice(0, text5.indexOf("\n"))).commitInfo;
31351
+ }
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);
31355
+ }
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);
31453
31364
  }
31365
+ return out2;
31454
31366
  }
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
- }
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;
31372
+ }
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)];
31379
+ }
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;
31401
+ }
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;
31477
31412
  }
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) });
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) };
31419
+ };
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);
31483
31438
  }
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);
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);
31489
31458
  }
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) });
31459
+ live.cursor = cursor;
31492
31460
  }
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 });
31461
+ return live.tables;
31462
+ }
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}`);
31501
31475
  }
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) });
31476
+ if (out2.length > 0 && repair) forgetCurrentStates(app);
31477
+ return out2.sort();
31478
+ }
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));
31484
+ }
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) }));
31489
+ }
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;
31499
+ }
31500
+ function formatOf(settings) {
31501
+ return settings.transactionLogFormat === "files" ? "files" : "segments";
31502
+ }
31503
+ function setCheckpointRunner(app, run2) {
31504
+ if (run2) CHECKPOINT_RUNNERS.set(app, run2);
31505
+ else CHECKPOINT_RUNNERS.delete(app);
31506
+ }
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++;
31508
31516
  }
31509
- for (const [path23, t] of texts) if (t.before !== t.after) cs.texts.push({ path: path23, ...t });
31510
- return cs;
31517
+ return n;
31511
31518
  }
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: []
31519
+ async function wholeCheckpoint(app, dir, v) {
31520
+ return parseCheckpoint(await storeRead(app, checkpointPath(dir, v)) ?? "")?.version === v;
31521
+ }
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 } : {}
31525
31531
  };
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" });
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;
31550
31537
  }
31551
31538
  }
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" });
31558
- }
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 })));
31540
+ }
31541
+ function forgetLoggedStates(app) {
31542
+ LAST.delete(app);
31543
+ }
31544
+ async function knownState(app, settings, full) {
31545
+ return LAST.get(app)?.get(full) ?? readLastState(app, settings, full);
31546
+ }
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;
31553
+ }
31554
+ async function stillLatest(app, settings, state) {
31555
+ return latestOf(await tocOf(app, settings, state.full)) === state.version;
31556
+ }
31557
+ async function latestVersion(app, settings, full) {
31558
+ const latest = latestOf(await tocOf(app, settings, full));
31559
+ return latest >= 0 ? latest : null;
31560
+ }
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
+ });
31559
31570
  }
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" });
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);
31569
31583
  }
31570
31584
  }
31571
- for (const m of since) {
31572
- if (await occupied(app, m.from)) conflicts.push({ path: m.from, message: "original path is occupied" });
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 });
31573
31588
  }
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" });
31581
- }
31582
- continue;
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 });
31592
+ }
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 };
31596
+ }
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;
31583
31627
  }
31584
- if (partial2) back.add(c);
31585
- else conflicts.push({ path: c, message: "created file no longer exists" });
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 });
31586
31631
  }
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" });
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 });
31592
31644
  }
31645
+ counts = await options.check(changes);
31593
31646
  }
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: [] };
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);
31674
+ }
31675
+ out2.push({ table: full, version: version2, operation: commit.commitInfo.operation });
31598
31676
  }
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 });
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);
31615
31685
  }
31616
- });
31686
+ }
31617
31687
  }
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
- });
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) }));
31642
31704
  }
31643
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]);
31644
31728
  }
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 });
31651
- }
31652
- });
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);
31653
31754
  }
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 });
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;
31663
31766
  }
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) });
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);
31675
31790
  }
31676
- });
31677
- }
31678
- let done = 0;
31679
- try {
31680
- for (const s of steps) {
31681
- await s.run();
31682
- done++;
31683
31791
  }
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);
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++;
31798
+ }
31799
+ }
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();
@@ -38840,11 +38937,223 @@ var init_extension_host = __esm({
38840
38937
  }
38841
38938
  });
38842
38939
 
38940
+ // src/api/extension-columns.ts
38941
+ function roleOf(v) {
38942
+ if (typeof v !== "string") return null;
38943
+ const m = ROLE_REF.exec(v);
38944
+ return m ? m[1] : null;
38945
+ }
38946
+ var isObject5, LOGICAL2, DATE_EXPR3, QUERY_KEYS, JOIN_KEYS, UNION_PART_KEYS, ColumnBinder;
38947
+ var init_extension_columns = __esm({
38948
+ "src/api/extension-columns.ts"() {
38949
+ "use strict";
38950
+ init_extension_manifest();
38951
+ isObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
38952
+ LOGICAL2 = /* @__PURE__ */ new Set(["or", "and", "not"]);
38953
+ DATE_EXPR3 = /^date\((.+)\)([+-]\d+)?$/;
38954
+ QUERY_KEYS = /* @__PURE__ */ new Set(["table", "columns", "where", "order_by", "limit", "offset", "expand", "join", "group_by", "aggregates", "include_body", "union"]);
38955
+ JOIN_KEYS = /* @__PURE__ */ new Set(["table", "on", "as", "type", "columns", "where"]);
38956
+ UNION_PART_KEYS = /* @__PURE__ */ new Set(["table", "columns", "where"]);
38957
+ ColumnBinder = class {
38958
+ constructor(manifest, map) {
38959
+ this.manifest = manifest;
38960
+ this.issues = [];
38961
+ this.map = {};
38962
+ for (const [role, cols] of Object.entries(map)) {
38963
+ const differing = Object.entries(cols).filter(([from, to]) => from !== to);
38964
+ if (differing.length > 0) this.map[role] = Object.fromEntries(differing);
38965
+ }
38966
+ }
38967
+ /** True when no role has a column bound to another name: every rewrite is the identity. */
38968
+ get empty() {
38969
+ return Object.keys(this.map).length === 0;
38970
+ }
38971
+ /** The bound name of a role's manifest column. */
38972
+ name(role, column) {
38973
+ return role === null ? column : this.map[role]?.[column] ?? column;
38974
+ }
38975
+ /** The manifest columns of a role bound to another name. */
38976
+ mapped(role) {
38977
+ return Object.keys(this.map[role] ?? {});
38978
+ }
38979
+ /** The role a link column of `role` points to (`ref: "@x"`, or `self`), or null. */
38980
+ linkRole(role, column) {
38981
+ const c = typeof column === "string" ? this.manifest.tables[role]?.columns.find((x) => x.name === column) : column;
38982
+ if (!c || c.ref === void 0) return null;
38983
+ if (!(c.type === "link" || c.type === "list" && c.items === "link")) return null;
38984
+ return c.ref === "self" ? role : roleOf(c.ref);
38985
+ }
38986
+ /** A dotted column path read on `role`: each part on the role the previous link points to. */
38987
+ path(role, dotted) {
38988
+ if (role === null || this.empty) return dotted;
38989
+ const parts = dotted.split(".");
38990
+ const out2 = [];
38991
+ let cur = role;
38992
+ for (const part of parts) {
38993
+ out2.push(this.name(cur, part));
38994
+ cur = cur === null ? null : this.linkRole(cur, part);
38995
+ }
38996
+ return out2.join(".");
38997
+ }
38998
+ /** A group_by or join expression: `col`, `path`, `date(col)`, `date(col)+1`. */
38999
+ expr(role, e) {
39000
+ const d = DATE_EXPR3.exec(e);
39001
+ return d ? `date(${this.path(role, d[1])})${d[2] ?? ""}` : this.path(role, e);
39002
+ }
39003
+ /** A `where` filter (or a CHECK) on `role`: keys are column paths; `or` / `and` / `not` nest. */
39004
+ where(role, where) {
39005
+ if (!isObject5(where) || role === null || this.empty) return where;
39006
+ const out2 = {};
39007
+ for (const [k, v] of Object.entries(where)) {
39008
+ if (LOGICAL2.has(k)) out2[k] = Array.isArray(v) ? v.map((w) => this.where(role, w)) : this.where(role, v);
39009
+ else out2[this.path(role, k)] = v;
39010
+ }
39011
+ return out2;
39012
+ }
39013
+ /** A column spec of `role`: its link filter (`ref_where`), `ref_by`, `inverse`. */
39014
+ column(role, c) {
39015
+ if (this.empty) return c;
39016
+ let out2 = c;
39017
+ const target = this.linkRole(role, c);
39018
+ if (c.ref_where !== void 0) out2 = { ...out2, ref_where: this.where(target, c.ref_where) };
39019
+ if (isObject5(c.ref_by)) {
39020
+ const by = c.ref_by;
39021
+ const col = this.name(role, String(by.column));
39022
+ const tgt = this.name(target, String(by.target_column ?? by.column));
39023
+ const { target_column: _t, ...rest } = by;
39024
+ out2 = { ...out2, ref_by: { ...rest, column: col, ...tgt !== col ? { target_column: tgt } : {} } };
39025
+ }
39026
+ if (typeof c.inverse === "string") out2 = { ...out2, inverse: this.name(target, c.inverse) };
39027
+ return out2;
39028
+ }
39029
+ /** A view query: the tables it reads are roles (`@meal`); every column it names follows them. */
39030
+ query(view, q) {
39031
+ if (this.empty) return q;
39032
+ if (Array.isArray(q.union)) return this.union(view, q);
39033
+ const role = roleOf(q.table);
39034
+ const out2 = {};
39035
+ const aliases = new Set(isObject5(q.aggregates) ? Object.keys(q.aggregates) : []);
39036
+ const col = (v) => aliases.has(v) ? v : this.path(role, v);
39037
+ for (const [k, v] of Object.entries(q)) {
39038
+ switch (k) {
39039
+ case "columns":
39040
+ case "expand":
39041
+ out2[k] = Array.isArray(v) ? v.map((x) => typeof x === "string" ? col(x) : x) : v;
39042
+ break;
39043
+ case "order_by":
39044
+ out2[k] = Array.isArray(v) ? v.map((x) => this.order(x, col)) : this.order(v, col);
39045
+ break;
39046
+ case "where":
39047
+ out2[k] = this.where(role, v);
39048
+ break;
39049
+ case "group_by":
39050
+ out2[k] = Array.isArray(v) ? v.map((x) => typeof x === "string" ? this.expr(role, x) : x) : typeof v === "string" ? this.expr(role, v) : v;
39051
+ break;
39052
+ case "aggregates":
39053
+ out2[k] = isObject5(v) ? Object.fromEntries(Object.entries(v).map(([a, spec2]) => [a, this.aggregate(role, spec2)])) : v;
39054
+ break;
39055
+ case "join":
39056
+ out2[k] = isObject5(v) ? this.join(view, role, v) : v;
39057
+ break;
39058
+ default:
39059
+ if (!QUERY_KEYS.has(k)) this.unknown(view, [role], k, v);
39060
+ out2[k] = v;
39061
+ }
39062
+ }
39063
+ return out2;
39064
+ }
39065
+ order(v, col) {
39066
+ if (typeof v !== "string") return v;
39067
+ return v.startsWith("-") ? `-${col(v.slice(1))}` : col(v);
39068
+ }
39069
+ /** `count`, `count:col`, `sum:meal.kcal`. */
39070
+ aggregate(role, spec2) {
39071
+ if (typeof spec2 !== "string") return spec2;
39072
+ const i = spec2.indexOf(":");
39073
+ return i < 0 ? spec2 : `${spec2.slice(0, i)}:${this.path(role, spec2.slice(i + 1))}`;
39074
+ }
39075
+ /** `{table, on: {left expr: right expr}, as, type, columns, where}`: `columns` and `where` read the joined table. */
39076
+ join(view, left, j) {
39077
+ const right = roleOf(j.table);
39078
+ const out2 = {};
39079
+ for (const [k, v] of Object.entries(j)) {
39080
+ if (k === "on" && isObject5(v)) {
39081
+ out2[k] = Object.fromEntries(Object.entries(v).map(([a, b]) => [this.expr(left, a), typeof b === "string" ? this.expr(right, b) : b]));
39082
+ } else if (k === "columns" && Array.isArray(v)) out2[k] = v.map((x) => typeof x === "string" ? this.path(right, x) : x);
39083
+ else if (k === "where") out2[k] = this.where(right, v);
39084
+ else {
39085
+ if (!JOIN_KEYS.has(k)) this.unknown(view, [left, right], `join.${k}`, v);
39086
+ out2[k] = v;
39087
+ }
39088
+ }
39089
+ return out2;
39090
+ }
39091
+ /** `{union: [{table, columns, where}], …}`: each part on its role; the outer clauses read every part, so a mapped column there is an issue. */
39092
+ union(view, q) {
39093
+ const roles = [];
39094
+ const parts = q.union.map((p) => {
39095
+ if (!isObject5(p)) return p;
39096
+ const role = roleOf(p.table);
39097
+ roles.push(role);
39098
+ const out2 = {};
39099
+ for (const [k, v] of Object.entries(p)) {
39100
+ if (k === "columns" && Array.isArray(v)) out2[k] = v.map((x) => typeof x === "string" ? this.path(role, x) : x);
39101
+ else if (k === "where") out2[k] = this.where(role, v);
39102
+ else {
39103
+ if (!UNION_PART_KEYS.has(k)) this.unknown(view, [role], `union.${k}`, v);
39104
+ out2[k] = v;
39105
+ }
39106
+ }
39107
+ return out2;
39108
+ });
39109
+ for (const [k, v] of Object.entries(q)) if (k !== "union") this.unknown(view, roles, `union ${k}`, v);
39110
+ return { ...q, union: parts };
39111
+ }
39112
+ /** A clause the binder does not rewrite: an issue for each mapped column it names. */
39113
+ unknown(view, roles, key2, v) {
39114
+ for (const n of this.named(v, roles.filter((r) => r !== null), () => true)) this.issues.push({ ...n, where: `view ${view} (${key2})` });
39115
+ }
39116
+ /**
39117
+ * The mapped columns a value names, where nothing rewrites it: a word that is a mapped column of
39118
+ * one of `roles`, or a later part of a dotted path (`author.name`) that is a mapped column of any
39119
+ * role. `keys(key, inKeys)` tells whether the keys under `key` are names too.
39120
+ */
39121
+ named(v, roles, keys) {
39122
+ const words = /* @__PURE__ */ new Set();
39123
+ const tails = /* @__PURE__ */ new Set();
39124
+ const text5 = (x) => {
39125
+ for (const w of x.split(/[^A-Za-z0-9_.]+/)) {
39126
+ const parts = w.split(".");
39127
+ if (parts.length === 1) words.add(w);
39128
+ else for (const p of parts.slice(1)) tails.add(p);
39129
+ }
39130
+ };
39131
+ const walk = (x, inKeys) => {
39132
+ if (typeof x === "string") text5(x);
39133
+ else if (Array.isArray(x)) x.forEach((y) => walk(y, inKeys));
39134
+ else if (isObject5(x)) {
39135
+ for (const [k, y] of Object.entries(x)) {
39136
+ if (inKeys) text5(k);
39137
+ walk(y, keys(k, inKeys));
39138
+ }
39139
+ }
39140
+ };
39141
+ walk(v, keys("", false));
39142
+ const out2 = [];
39143
+ for (const role of Object.keys(this.map)) {
39144
+ for (const column of this.mapped(role)) if (tails.has(column) || roles.includes(role) && words.has(column)) out2.push({ role, column });
39145
+ }
39146
+ return out2;
39147
+ }
39148
+ };
39149
+ }
39150
+ });
39151
+
38843
39152
  // src/api/extension-plan.ts
38844
39153
  function stable(v) {
38845
39154
  const walk = (x) => {
38846
39155
  if (Array.isArray(x)) return x.map(walk);
38847
- if (isObject5(x)) {
39156
+ if (isObject6(x)) {
38848
39157
  const out2 = {};
38849
39158
  for (const k of Object.keys(x).sort()) out2[k] = walk(x[k]);
38850
39159
  return out2;
@@ -39018,6 +39327,10 @@ function planChanges(input) {
39018
39327
  for (const role of m.roles) tables[role] = choiceOf.get(role)?.table ?? `@${role}`;
39019
39328
  const bind = (v) => bindRoles(v, tables);
39020
39329
  const existing = (c) => c.decision === "adopt" || c.decision === "match";
39330
+ const columnMap = {};
39331
+ for (const c of input.chosen) if (!c.owned && c.decision === "match") columnMap[c.role] = { ...recorded[c.role]?.columns ?? {}, ...choices.columns[c.role] ?? {} };
39332
+ const binder = new ColumnBinder(m, columnMap);
39333
+ const bindColumn = (role, c) => bind(spec(binder.column(role, c)));
39021
39334
  const model = /* @__PURE__ */ new Map();
39022
39335
  for (const c of input.chosen) if (existing(c)) model.set(c.table, modelTable(input.described[c.table] ?? { qualified: c.table, columns: [] }));
39023
39336
  const views = new Set(input.listing.views.map((v) => v.qualified));
@@ -39045,6 +39358,19 @@ function planChanges(input) {
39045
39358
  );
39046
39359
  continue;
39047
39360
  }
39361
+ const touched = mappedNamed(raw);
39362
+ if (touched !== null) {
39363
+ migrationProblems.push(
39364
+ problem(
39365
+ "migration_conflict",
39366
+ `migration ${g.version}: ${raw.op} names column ${touched.column} of ${touched.role}, which reads your column ${binder.name(touched.role, touched.column)}`,
39367
+ `Rename your column to ${touched.column} with rename_column and run the command again without --map ${touched.role}.${touched.column}, or ask the extension's author.`,
39368
+ EXT_DOCS.update,
39369
+ { role: touched.role, column: touched.column }
39370
+ )
39371
+ );
39372
+ continue;
39373
+ }
39048
39374
  const op2 = bind(raw);
39049
39375
  const r = simulate(op2, g.version);
39050
39376
  if (typeof r === "string") {
@@ -39056,6 +39382,9 @@ function planChanges(input) {
39056
39382
  }
39057
39383
  }
39058
39384
  }
39385
+ function mappedNamed(raw) {
39386
+ return binder.named(raw, rolesNamed(raw), (k, inKeys) => inKeys || FILTER_KEYS.has(k))[0] ?? null;
39387
+ }
39059
39388
  function qualify(op2) {
39060
39389
  return `${String(op2.catalog ?? catalog)}.${String(op2.schema ?? viewSchema)}.${String(op2.name)}`;
39061
39390
  }
@@ -39188,7 +39517,7 @@ function planChanges(input) {
39188
39517
  schema,
39189
39518
  name: tname,
39190
39519
  description: def.description,
39191
- columns: bind(def.columns.map(spec)),
39520
+ columns: def.columns.map((c) => bindColumn(role, c)),
39192
39521
  ...def.filename !== void 0 ? { filename: def.filename } : {},
39193
39522
  ...def.aliases !== void 0 ? { aliases: def.aliases } : {}
39194
39523
  });
@@ -39197,8 +39526,8 @@ function planChanges(input) {
39197
39526
  }
39198
39527
  const actualCols = model.get(q).columns;
39199
39528
  for (const c of def.columns) {
39200
- const bound = bind(spec(c));
39201
- const actualName = choices.columns[role]?.[c.name] ?? recorded[role]?.columns?.[c.name] ?? c.name;
39529
+ const bound = bindColumn(role, c);
39530
+ const actualName = binder.name(role, c.name);
39202
39531
  const actual = actualCols.get(actualName);
39203
39532
  if (actual) {
39204
39533
  const diff = columnDifference(bound, actual, q, false);
@@ -39239,7 +39568,7 @@ function planChanges(input) {
39239
39568
  const q = t.table;
39240
39569
  const tags = { extension: name, extension_role: role };
39241
39570
  const instructions = def.instructions === void 0 ? void 0 : bind(def.instructions);
39242
- const constraint = (c) => ({ op: "add_constraint", table: q, name: c.name, check: bind(c.check), hint: c.hint });
39571
+ const constraint = (c) => ({ op: "add_constraint", table: q, name: c.name, check: bind(binder.where(role, c.check)), hint: c.hint });
39243
39572
  if (t.decision === "create") {
39244
39573
  const [cat, schema, tname] = q.split(".");
39245
39574
  operations.push({
@@ -39248,7 +39577,7 @@ function planChanges(input) {
39248
39577
  schema,
39249
39578
  name: tname,
39250
39579
  description: def.description,
39251
- columns: bind(def.columns.map(spec)),
39580
+ columns: def.columns.map((c) => bindColumn(role, c)),
39252
39581
  ...def.filename !== void 0 ? { filename: def.filename } : {},
39253
39582
  ...def.aliases !== void 0 ? { aliases: def.aliases } : {},
39254
39583
  ...instructions !== void 0 ? { instructions } : {},
@@ -39261,7 +39590,7 @@ function planChanges(input) {
39261
39590
  }
39262
39591
  const mt = model.get(q);
39263
39592
  for (const c of def.columns) {
39264
- const bound = bind(spec(c));
39593
+ const bound = bindColumn(role, c);
39265
39594
  const actual = mt.columns.get(c.name);
39266
39595
  if (!actual) {
39267
39596
  operations.push({ op: "add_column", table: q, column: bound });
@@ -39285,7 +39614,7 @@ function planChanges(input) {
39285
39614
  if (!actual) {
39286
39615
  operations.push(constraint(c));
39287
39616
  t.changes.push(`add CHECK ${c.name}`);
39288
- } else if (stable(actual.check) !== stable(bind(c.check)) || actual.hint !== c.hint) {
39617
+ } else if (stable(actual.check) !== stable(bind(binder.where(role, c.check))) || actual.hint !== c.hint) {
39289
39618
  operations.push({ op: "drop_constraint", table: q, name: c.name }, constraint(c));
39290
39619
  t.changes.push(`replace CHECK ${c.name}`);
39291
39620
  }
@@ -39313,13 +39642,14 @@ function planChanges(input) {
39313
39642
  for (const v of Object.keys(m.views)) {
39314
39643
  const def = m.views[v];
39315
39644
  const q = `${catalog}.${viewSchema}.${v}`;
39645
+ const query = binder.query(v, def.query);
39316
39646
  const create = {
39317
39647
  op: "create_view",
39318
39648
  catalog,
39319
39649
  schema: viewSchema,
39320
39650
  name: v,
39321
39651
  description: def.description,
39322
- query: bind(def.query),
39652
+ query: bind(query),
39323
39653
  ...def.aliases !== void 0 ? { aliases: def.aliases } : {},
39324
39654
  ...def.labels !== void 0 ? { labels: def.labels } : {}
39325
39655
  };
@@ -39328,7 +39658,7 @@ function planChanges(input) {
39328
39658
  viewPlan.changes.push(`create view ${v}`);
39329
39659
  } else if (action === "install") {
39330
39660
  reconcileProblems.push(problem("view_exists", `${q} exists already`, "Drop or rename that view (drop_view), or install with --schema <another schema>.", EXT_DOCS.install, { table: q }));
39331
- } else if (stable(oldManifest.views[v]) !== stable(def)) {
39661
+ } else if (stable(oldManifest.views[v]) !== stable(def) || stable(query) !== stable(def.query)) {
39332
39662
  operations.push({ ...create, replace: true });
39333
39663
  viewPlan.changes.push(`replace view ${v}`);
39334
39664
  }
@@ -39344,6 +39674,17 @@ function planChanges(input) {
39344
39674
  if (m.tables[role] === void 0) notes.push(`${role} (${rt.table}) is no longer used by the extension: it stays in the database`);
39345
39675
  }
39346
39676
  }
39677
+ for (const i of binder.issues) {
39678
+ reconcileProblems.push(
39679
+ problem(
39680
+ "column_map_unsupported",
39681
+ `${i.role}: ${i.where} names column ${i.column}, which reads your column ${binder.name(i.role, i.column)}: the installer cannot rewrite it there`,
39682
+ `Rename your column to ${i.column} with rename_column and leave --map ${i.role}.${i.column} out, or ask the extension's author.`,
39683
+ EXT_DOCS.tables,
39684
+ { role: i.role, column: i.column }
39685
+ )
39686
+ );
39687
+ }
39347
39688
  const toolProblems = [];
39348
39689
  const others = input.others.filter((o) => o.name !== name);
39349
39690
  const boundNames = (roles) => roles.map((r) => tables[r]);
@@ -39464,16 +39805,18 @@ function renderPlan(plan) {
39464
39805
  lines.push("", plan.agent_message, plan.summary);
39465
39806
  return lines.join("\n");
39466
39807
  }
39467
- var AGENT_MESSAGE, MAX_OPERATIONS, isObject5, obj3, list2, tableName, isLink;
39808
+ var AGENT_MESSAGE, FILTER_KEYS, MAX_OPERATIONS, isObject6, obj3, list2, tableName, isLink;
39468
39809
  var init_extension_plan = __esm({
39469
39810
  "src/api/extension-plan.ts"() {
39470
39811
  "use strict";
39812
+ init_extension_columns();
39471
39813
  init_extension_manifest();
39472
39814
  init_guidance();
39473
39815
  AGENT_MESSAGE = `For the agent installing this extension: show this plan to the user before anything is written. Where a table says "proposed", the installer chose it: ask the user to confirm it, to pick another table (--map <role>=<catalog.schema.table>, --map <role>.<column>=<column>), or to create it (--create <role>). Add a missing column only after the user agrees (--add <role>.<column>). Solve each problem the same way. Then run the same command again with the user's choices and --yes, only once the user approved the tables it creates or changes, the tools and the token's permissions.`;
39816
+ FILTER_KEYS = /* @__PURE__ */ new Set(["check", "where", "ref_where", "query", "on"]);
39474
39817
  MAX_OPERATIONS = 200;
39475
- isObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
39476
- obj3 = (v) => isObject5(v) ? v : {};
39818
+ isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
39819
+ obj3 = (v) => isObject6(v) ? v : {};
39477
39820
  list2 = (v) => Array.isArray(v) ? v : [];
39478
39821
  tableName = (t, role) => t.name ?? role;
39479
39822
  isLink = (c) => c.type === "link" || c.type === "list" && c.items === "link";
@@ -40600,7 +40943,7 @@ __export(util_exports, {
40600
40943
  hexToUint8Array: () => hexToUint8Array,
40601
40944
  hide: () => hide,
40602
40945
  installLazyProp: () => installLazyProp,
40603
- isObject: () => isObject6,
40946
+ isObject: () => isObject7,
40604
40947
  isPlainObject: () => isPlainObject2,
40605
40948
  issue: () => issue,
40606
40949
  joinValues: () => joinValues,
@@ -40807,11 +41150,11 @@ function esc(str3) {
40807
41150
  function slugify(input) {
40808
41151
  return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
40809
41152
  }
40810
- function isObject6(data) {
41153
+ function isObject7(data) {
40811
41154
  return typeof data === "object" && data !== null && !Array.isArray(data);
40812
41155
  }
40813
41156
  function isPlainObject2(o) {
40814
- if (isObject6(o) === false)
41157
+ if (isObject7(o) === false)
40815
41158
  return false;
40816
41159
  const ctor = o.constructor;
40817
41160
  if (ctor === void 0)
@@ -40819,7 +41162,7 @@ function isPlainObject2(o) {
40819
41162
  if (typeof ctor !== "function")
40820
41163
  return true;
40821
41164
  const prot = ctor.prototype;
40822
- if (isObject6(prot) === false)
41165
+ if (isObject7(prot) === false)
40823
41166
  return false;
40824
41167
  if (Object.prototype.hasOwnProperty.call(prot, "isPrototypeOf") === false) {
40825
41168
  return false;
@@ -43218,7 +43561,7 @@ var init_schemas = __esm({
43218
43561
  }
43219
43562
  return propValues;
43220
43563
  });
43221
- const isObject7 = isObject6;
43564
+ const isObject8 = isObject7;
43222
43565
  const catchall = def.catchall;
43223
43566
  let value;
43224
43567
  const memo2 = globalConfig.memoizer;
@@ -43226,7 +43569,7 @@ var init_schemas = __esm({
43226
43569
  inst._zod.parse = (payload, ctx) => {
43227
43570
  value ?? (value = _normalized.value);
43228
43571
  const input = payload.value;
43229
- if (!isObject7(input)) {
43572
+ if (!isObject8(input)) {
43230
43573
  payload.issues.push({
43231
43574
  expected: "object",
43232
43575
  code: "invalid_type",
@@ -43362,7 +43705,7 @@ var init_schemas = __esm({
43362
43705
  return doc.compile();
43363
43706
  };
43364
43707
  let fastpass;
43365
- const isObject7 = isObject6;
43708
+ const isObject8 = isObject7;
43366
43709
  const jit = !globalConfig.jitless;
43367
43710
  const allowsEval2 = allowsEval;
43368
43711
  const fastEnabled = jit && allowsEval2.value;
@@ -43371,7 +43714,7 @@ var init_schemas = __esm({
43371
43714
  inst._zod.parse = (payload, ctx) => {
43372
43715
  value ?? (value = _normalized.value);
43373
43716
  const input = payload.value;
43374
- if (!isObject7(input)) {
43717
+ if (!isObject8(input)) {
43375
43718
  payload.issues.push({
43376
43719
  expected: "object",
43377
43720
  code: "invalid_type",
@@ -43471,7 +43814,7 @@ var init_schemas = __esm({
43471
43814
  const disc = cached(() => discriminatorMap(def));
43472
43815
  inst._zod.parse = (payload, ctx) => {
43473
43816
  const input = payload.value;
43474
- if (!isObject6(input)) {
43817
+ if (!isObject7(input)) {
43475
43818
  payload.issues.push({
43476
43819
  code: "invalid_type",
43477
43820
  expected: "object",
@@ -66871,17 +67214,28 @@ var SemanticIndex = class {
66871
67214
  if (this.queries.size > QUERY_VECTORS_MAX) this.queries.delete(this.queries.keys().next().value);
66872
67215
  return v;
66873
67216
  }
67217
+ /** The index's counts; under a read scope (read-scope.ts), of the notes the token may read only. */
66874
67218
  status() {
66875
67219
  const s = this.getSettings();
67220
+ const scope = currentReadScope();
67221
+ const readable = (path23) => !scope || pathReadable(this.app, s, path23, scope);
67222
+ let files = 0;
66876
67223
  let passages = 0;
66877
- for (const f of this.files.values()) passages += f.passages.length;
67224
+ for (const [path23, f] of this.files) {
67225
+ if (!readable(path23)) continue;
67226
+ files++;
67227
+ passages += f.passages.length;
67228
+ }
67229
+ let pending = 0;
67230
+ if (scope) for (const path23 of this.pending) pending += readable(path23) ? 1 : 0;
67231
+ else pending = this.pending.size;
66878
67232
  return {
66879
67233
  enabled: s.semanticEnabled,
66880
67234
  provider: s.semanticProvider,
66881
67235
  model: s.semanticModel,
66882
- files: this.files.size,
67236
+ files,
66883
67237
  passages,
66884
- pending: this.pending.size,
67238
+ pending,
66885
67239
  running: this.running !== null,
66886
67240
  ...this.lastSync ? { last_sync: this.lastSync } : {},
66887
67241
  ...this.lastError ? { last_error: this.lastError } : {}
@@ -69078,6 +69432,7 @@ init_guidance();
69078
69432
  init_requests();
69079
69433
  init_actor();
69080
69434
  init_read_scope();
69435
+ init_write_scope();
69081
69436
  init_changeset_checks();
69082
69437
 
69083
69438
  // src/server/token-routes.ts
@@ -69110,6 +69465,8 @@ var READ_ROUTES = /* @__PURE__ */ new Set(["/manual/search/", "/tokens/me/", "/t
69110
69465
  var OWNER_ADMIN_ROUTES = /* @__PURE__ */ new Set(["/tokens/", "/tokens/create/", "/tokens/update/", "/tokens/delete/", "/tokens/rotate/", "/extensions/record/", "/extensions/delete/", "/status/", "/stop/"]);
69111
69466
  var READ_SCOPE_UNSUPPORTED = "read permissions need Node's AsyncLocalStorage";
69112
69467
  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");
69468
+ var WRITE_SCOPE_UNSUPPORTED = "write permissions need Node's AsyncLocalStorage";
69469
+ 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
69470
 
69114
69471
  // src/server/scheduler.ts
69115
69472
  init_telemetry();
@@ -69358,7 +69715,7 @@ function errorResponse(e, op2, route) {
69358
69715
  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
69716
  if (e instanceof VaultPathNotFoundError) return { status: 404, json: body(guide("list_folder lists a folder, search_notes finds notes by their words.", "readNote")) };
69360
69717
  if (e instanceof ReadDeniedError) return { status: 403, json: body(READ_DENIED_GUIDANCE) };
69361
- if (e instanceof Forbidden) return { status: 403, json: body(FORBIDDEN_HINT) };
69718
+ if (e instanceof Forbidden || e instanceof WriteDeniedError) return { status: 403, json: body(FORBIDDEN_HINT) };
69362
69719
  if (e instanceof AmbiguousCatalogRefError) {
69363
69720
  return { status: 409, json: { ...body(guide(`Name the table in full (catalog.schema.table): ${e.candidates.join(" or ")}.`, "catalogs")), tables: e.candidates } };
69364
69721
  }
@@ -69766,6 +70123,7 @@ function createHandlers(app, getSettings, saveSettings = async () => {
69766
70123
  batch: async (b) => {
69767
70124
  const operations = list(b, "operations");
69768
70125
  if (operations.length === 0) throw new BadRequest(`"operations" must be a non-empty list`);
70126
+ const denied = /* @__PURE__ */ new Set();
69769
70127
  const steps = operations.map((raw, i) => {
69770
70128
  const { op: op2, ...args } = raw;
69771
70129
  if (typeof op2 !== "string") throw new BadRequest(`operation ${i} needs "op"`);
@@ -69780,7 +70138,10 @@ function createHandlers(app, getSettings, saveSettings = async () => {
69780
70138
  return {
69781
70139
  op: op2,
69782
70140
  run: async (dryRun) => {
69783
- const res = await ops[op2]({ ...args, dry_run: dryRun }).catch((e) => errorResponse(e, op2));
70141
+ const res = await ops[op2]({ ...args, dry_run: dryRun }).catch((e) => {
70142
+ if (e instanceof WriteDeniedError) denied.add(i);
70143
+ return errorResponse(e, op2);
70144
+ });
69784
70145
  if ("op" in res.json) return res.json;
69785
70146
  throw stepFailure(res.json);
69786
70147
  }
@@ -69791,8 +70152,13 @@ function createHandlers(app, getSettings, saveSettings = async () => {
69791
70152
  ...str(b, "summary") !== void 0 ? { summary: str(b, "summary") } : {}
69792
70153
  });
69793
70154
  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);
70155
+ const forbidden = r.failed_at !== void 0 ? denied.has(r.failed_at) ? r.failed_at : void 0 : [...denied].sort((a, b2) => a - b2)[0];
70156
+ const status = r.ok ? 200 : r.rolled_back === false ? 500 : forbidden !== void 0 ? 403 : writeStatus(failedStep, true);
69795
70157
  const answer = redactBatchRollback(r, app, s());
70158
+ if (status === 403) {
70159
+ const step = answer.steps[forbidden];
70160
+ return { status, json: { ...answer, error: `operation ${forbidden}: ${step.error}`, hint: step.hint, docs: step.docs } };
70161
+ }
69796
70162
  return { status, json: answer.ok ? answer : { ...answer, ...batchGuidance(answer) } };
69797
70163
  },
69798
70164
  // --- queries ---
@@ -70256,6 +70622,7 @@ var STATUS_TEXT = {
70256
70622
  409: "Ambiguous table name: it matches several tables (`paths` or `tables` lists them)."
70257
70623
  };
70258
70624
  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.";
70625
+ 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
70626
  var ERRORS = {
70260
70627
  400: { description: STATUS_TEXT[400], content: json(ref("Error")) },
70261
70628
  401: { $ref: "#/components/responses/Unauthorized" },
@@ -70271,6 +70638,7 @@ function postRoute(d) {
70271
70638
  const read3 = op2 !== void 0 ? READ_OPERATIONS.has(op2) : READ_ROUTES.has(d.route) && d.route !== "/tokens/update/";
70272
70639
  if (!read3) responses["403"] = ERRORS[403];
70273
70640
  else if (op2 !== void 0) responses["403"] = ERRORS.readDenied;
70641
+ if (d.forbidden) responses["403"] = { description: d.forbidden, content: json(ref("Error")) };
70274
70642
  if (d.namesTable) responses["404"] = ERRORS[404];
70275
70643
  const refused2 = op2 !== void 0 ? refusedStatus(op2) : void 0;
70276
70644
  if (refused2 === 409) {
@@ -70609,6 +70977,7 @@ function openApiDocument(version2) {
70609
70977
  route: "/sessions/identify/",
70610
70978
  operationId: "identify_session",
70611
70979
  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.",
70980
+ forbidden: SESSION_TAKEN_TEXT,
70612
70981
  body: { type: "object", required: ["session"], properties: { session: { type: "string" }, model: { type: "string" }, harness: { type: "string" } } },
70613
70982
  bodyRequired: true,
70614
70983
  result: ok
@@ -70617,6 +70986,7 @@ function openApiDocument(version2) {
70617
70986
  route: "/tool-calls/record/",
70618
70987
  operationId: "record_tool_call",
70619
70988
  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.",
70989
+ forbidden: SESSION_TAKEN_TEXT,
70620
70990
  body: {
70621
70991
  type: "object",
70622
70992
  required: ["tool"],
@@ -71352,6 +71722,8 @@ var MAX_IDENTITIES = 2e3;
71352
71722
  var SessionIdentities = class {
71353
71723
  constructor() {
71354
71724
  this.map = /* @__PURE__ */ new Map();
71725
+ /** Who reported each session first: "owner" or a token's name, by the session's first 16 characters. */
71726
+ this.owners = /* @__PURE__ */ new Map();
71355
71727
  }
71356
71728
  set(key2, info) {
71357
71729
  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 +71735,21 @@ var SessionIdentities = class {
71363
71735
  get(key2) {
71364
71736
  return this.map.get(key2) ?? this.map.get(key2.slice(0, 16));
71365
71737
  }
71738
+ /**
71739
+ * Claim a session for a credential ("owner" or a token's name): the first one to report it keeps
71740
+ * it. Answers the token that holds it when that is another named token, else null (claimed, or
71741
+ * the owner, who may report any session). Keyed by the first 16 characters, as lookups fall back
71742
+ * to them: a token cannot take over another's session with a longer or shorter id.
71743
+ */
71744
+ claim(key2, who) {
71745
+ const k = key2.slice(0, 16);
71746
+ const held = this.owners.get(k);
71747
+ if (held !== void 0 && held !== who && who !== "owner") return held;
71748
+ this.owners.delete(k);
71749
+ if (this.owners.size >= MAX_IDENTITIES) this.owners.delete(this.owners.keys().next().value);
71750
+ this.owners.set(k, held ?? who);
71751
+ return null;
71752
+ }
71366
71753
  };
71367
71754
 
71368
71755
  // src/server/runtime.ts
@@ -71749,6 +72136,7 @@ async function engineStatus(i) {
71749
72136
  // src/server/tokens.ts
71750
72137
  init_actor();
71751
72138
  init_read_scope();
72139
+ init_write_scope();
71752
72140
  init_metastore();
71753
72141
  init_store();
71754
72142
  init_telemetry();
@@ -71768,6 +72156,13 @@ function tableAllowed(full, patterns) {
71768
72156
  function scopeOf(t) {
71769
72157
  return t.read === void 0 || t.read.includes("*") ? null : { token: t.name, patterns: t.read };
71770
72158
  }
72159
+ function writeScopeOf(t) {
72160
+ return t.write.includes("*") ? null : { token: t.name, patterns: t.write };
72161
+ }
72162
+ function writesRows(route) {
72163
+ const op2 = operationOf(route);
72164
+ return op2 === "batch" || op2 !== void 0 && Object.prototype.hasOwnProperty.call(ROW_WRITES, op2);
72165
+ }
71771
72166
  function readCovers(r, w) {
71772
72167
  return r === "*" || r === w || r.endsWith(".*") && w.startsWith(r.slice(0, -1));
71773
72168
  }
@@ -72010,7 +72405,12 @@ var TokenRegistry = class {
72010
72405
  telemetry().log("WARN", "datanotes token refused", { token_name: name, "http.route": route, reason: READ_SCOPE_UNSUPPORTED });
72011
72406
  return { status: 503, json: { error: READ_SCOPE_UNSUPPORTED, ...READ_SCOPE_UNSUPPORTED_HINT } };
72012
72407
  }
72013
- return runWithReadScope(scope, () => dispatch2(route, handler, body));
72408
+ const writeScope = writeScopeOf(token);
72409
+ if (writeScope && writesRows(route) && !(this.deps.readScopeSupported ?? READ_SCOPE_SUPPORTED)) {
72410
+ telemetry().log("WARN", "datanotes token refused", { token_name: name, "http.route": route, reason: WRITE_SCOPE_UNSUPPORTED });
72411
+ return { status: 503, json: { error: WRITE_SCOPE_UNSUPPORTED, ...WRITE_SCOPE_UNSUPPORTED_HINT } };
72412
+ }
72413
+ return runWithReadScope(scope, () => runWithWriteScope(writeScope, () => dispatch2(route, handler, body)));
72014
72414
  };
72015
72415
  }
72016
72416
  // --- routes --------------------------------------------------------------------------------
@@ -72254,6 +72654,7 @@ var ExtensionRegistry = class {
72254
72654
  init_metastore();
72255
72655
  init_query_tables();
72256
72656
  init_file_cache();
72657
+ init_read_scope();
72257
72658
  init_vault_index();
72258
72659
  var CACHED_READS = /* @__PURE__ */ new Set([
72259
72660
  "select_rows",
@@ -72389,7 +72790,7 @@ var ReadCache = class {
72389
72790
  if (this.maxBytes === 0 || !CACHED_READS.has(op2)) return null;
72390
72791
  const settings = this.getSettings();
72391
72792
  const model = (await loadMetastore(this.app, settings).catch(() => null))?.model ?? null;
72392
- const deps = readDeps(op2, body, model);
72793
+ const deps = readDeps(op2, body, model, currentReadScope() !== null);
72393
72794
  if (!deps) return null;
72394
72795
  if (!this.looseKnown) this.indexLoose(settings);
72395
72796
  if (deps.links && !this.linkPrints) this.printLinks();
@@ -72535,7 +72936,7 @@ var ReadCache = class {
72535
72936
  function settingsKey(s) {
72536
72937
  return JSON.stringify(s, (k, v) => SECRET_SETTINGS.has(k) ? void 0 : v);
72537
72938
  }
72538
- function readDeps(op2, body, model) {
72939
+ function readDeps(op2, body, model, restricted = false) {
72539
72940
  if (!CACHED_READS.has(op2)) return null;
72540
72941
  const text5 = JSON.stringify(body);
72541
72942
  if (text5.includes("{now")) return null;
@@ -72546,7 +72947,10 @@ function readDeps(op2, body, model) {
72546
72947
  const found = /* @__PURE__ */ new Set();
72547
72948
  const r = queryTables(body, model, found, 0);
72548
72949
  if (r.now || r.store) return null;
72549
- if (!r.vault) return { tables: [...found].sort(), today: today || r.today, ...r.links ? { links: true } : {} };
72950
+ if (!r.vault) {
72951
+ const tables = r.links && restricted ? catalogTables(model).map(({ ref: ref2 }) => ref2.full) : [...found];
72952
+ return { tables: tables.sort(), today: today || r.today, ...r.links ? { links: true } : {} };
72953
+ }
72550
72954
  const views = viewTimes(model);
72551
72955
  if (views.now) return null;
72552
72956
  return { tables: "vault", today: today || r.today || views.today };
@@ -72575,6 +72979,7 @@ function viewTimes(model) {
72575
72979
  // src/server/runtime.ts
72576
72980
  init_message();
72577
72981
  init_hints();
72982
+ 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
72983
  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
72984
  var EXTERNAL_EDIT_DELAY_MS = 3e3;
72580
72985
  var LOG_CHECK_EVERY_MS = 10 * 6e4;
@@ -72709,6 +73114,8 @@ var EngineRuntime = class {
72709
73114
  "/sessions/identify/": async (body) => {
72710
73115
  const session = typeof body.session === "string" ? body.session.trim() : "";
72711
73116
  if (!session) return { status: 400, json: { error: `"session" is required`, ...REPORT_HINT } };
73117
+ const taken = this.sessionTaken(session);
73118
+ if (taken) return taken;
72712
73119
  this.identities.set(session, { model: body.model, harness: body.harness });
72713
73120
  return { status: 200, json: { ok: true } };
72714
73121
  },
@@ -72724,6 +73131,8 @@ var EngineRuntime = class {
72724
73131
  // an MCP server reports the tool calls of its agents (the change feed's tool_call events)
72725
73132
  "/tool-calls/record/": async (body) => {
72726
73133
  if (typeof body.tool !== "string" || !body.tool) return { status: 400, json: { error: `"tool" is required`, ...REPORT_HINT } };
73134
+ const taken = typeof body.session === "string" && body.session.trim() ? this.sessionTaken(body.session.trim()) : null;
73135
+ if (taken) return taken;
72727
73136
  const actor = currentActor();
72728
73137
  await this.recordToolCall({
72729
73138
  tool: body.tool,
@@ -72772,6 +73181,14 @@ var EngineRuntime = class {
72772
73181
  } : {}
72773
73182
  };
72774
73183
  }
73184
+ /**
73185
+ * The 403 when the session belongs to another named token; else null, and the session is
73186
+ * claimed for the caller when nobody reported it before (the first reporter keeps it).
73187
+ */
73188
+ sessionTaken(session) {
73189
+ if (this.identities.claim(session, tokenNameOf(currentActor()) ?? "owner") === null) return null;
73190
+ return { status: 403, json: { error: `session "${session}" belongs to another token`, ...SESSION_TAKEN_HINT } };
73191
+ }
72775
73192
  /**
72776
73193
  * An agent's tool call, as a change-feed event (secrets removed, long values cut); settled once
72777
73194
  * it is recorded. The feed is kept in the store, which the store backup commits: no secret.
@@ -77216,7 +77633,7 @@ function setUp(o, vault, home, explicit, self) {
77216
77633
  // src/node/service.ts
77217
77634
  init_local();
77218
77635
  init_hints();
77219
- var VERSION = true ? "3.4.0" : "dev";
77636
+ var VERSION = true ? "3.4.2" : "dev";
77220
77637
  function parsePort(raw, from = "--port") {
77221
77638
  const n = /^\s*\d+\s*$/.test(raw) ? Number(raw) : NaN;
77222
77639
  if (n === 0 || Number.isInteger(n) && n >= 1024 && n <= 65535) return n;
@@ -78281,7 +78698,7 @@ Docs: https://datanotes.dev`;
78281
78698
 
78282
78699
  // src/node/cli.ts
78283
78700
  init_message();
78284
- var VERSION2 = true ? "3.4.0" : "dev";
78701
+ var VERSION2 = true ? "3.4.2" : "dev";
78285
78702
  var consoleError = console.error;
78286
78703
  async function main5(argv = process.argv.slice(2)) {
78287
78704
  const [command, ...rest] = argv;