foottrace 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +73 -2
- package/dist/esm/index.d.ts +27 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/lib/capture/freeze.d.ts +91 -0
- package/dist/esm/lib/capture/freeze.js +313 -0
- package/dist/esm/lib/capture/ownData.d.ts +18 -0
- package/dist/esm/lib/capture/ownData.js +35 -0
- package/dist/esm/lib/capture/valueKinds.d.ts +57 -0
- package/dist/esm/lib/capture/valueKinds.js +141 -0
- package/dist/esm/lib/ids/runtimeStageId.d.ts +173 -0
- package/dist/esm/lib/ids/runtimeStageId.js +193 -0
- package/dist/esm/lib/memory/EventLog.d.ts +105 -0
- package/dist/esm/lib/memory/EventLog.js +147 -0
- package/dist/esm/lib/memory/RecordFrame.d.ts +232 -0
- package/dist/esm/lib/memory/RecordFrame.js +295 -0
- package/dist/esm/lib/memory/SharedMemory.d.ts +46 -0
- package/dist/esm/lib/memory/SharedMemory.js +87 -0
- package/dist/esm/lib/memory/TransactionBuffer.d.ts +521 -0
- package/dist/esm/lib/memory/TransactionBuffer.js +946 -0
- package/dist/esm/lib/memory/admission.d.ts +81 -0
- package/dist/esm/lib/memory/admission.js +152 -0
- package/dist/esm/lib/memory/backtrack.d.ts +286 -0
- package/dist/esm/lib/memory/backtrack.js +456 -0
- package/dist/esm/lib/memory/commitLogUtils.d.ts +217 -0
- package/dist/esm/lib/memory/commitLogUtils.js +383 -0
- package/dist/esm/lib/memory/deltaEncoding.d.ts +66 -0
- package/dist/esm/lib/memory/deltaEncoding.js +121 -0
- package/dist/esm/lib/memory/equality.d.ts +93 -0
- package/dist/esm/lib/memory/equality.js +261 -0
- package/dist/esm/lib/memory/eventPosition.d.ts +18 -0
- package/dist/esm/lib/memory/eventPosition.js +1 -0
- package/dist/esm/lib/memory/honesty.d.ts +93 -0
- package/dist/esm/lib/memory/honesty.js +91 -0
- package/dist/esm/lib/memory/keyPaths.d.ts +125 -0
- package/dist/esm/lib/memory/keyPaths.js +226 -0
- package/dist/esm/lib/memory/logModel.d.ts +76 -0
- package/dist/esm/lib/memory/logModel.js +363 -0
- package/dist/esm/lib/memory/merge.d.ts +26 -0
- package/dist/esm/lib/memory/merge.js +51 -0
- package/dist/esm/lib/memory/pathOps.d.ts +118 -0
- package/dist/esm/lib/memory/pathOps.js +292 -0
- package/dist/esm/lib/memory/paths.d.ts +56 -0
- package/dist/esm/lib/memory/paths.js +60 -0
- package/dist/esm/lib/memory/placeholders.d.ts +39 -0
- package/dist/esm/lib/memory/placeholders.js +39 -0
- package/dist/esm/lib/memory/recordCommit.d.ts +45 -0
- package/dist/esm/lib/memory/recordCommit.js +60 -0
- package/dist/esm/lib/memory/scrub.d.ts +52 -0
- package/dist/esm/lib/memory/scrub.js +72 -0
- package/dist/esm/lib/memory/types.d.ts +209 -0
- package/dist/esm/lib/memory/types.js +9 -0
- package/dist/esm/lib/memory/utils.d.ts +73 -0
- package/dist/esm/lib/memory/utils.js +150 -0
- package/dist/esm/lib/memory/verbs.d.ts +333 -0
- package/dist/esm/lib/memory/verbs.js +426 -0
- package/dist/esm/lib/recorder/CommitRangeIndex.d.ts +147 -0
- package/dist/esm/lib/recorder/CommitRangeIndex.js +206 -0
- package/dist/esm/lib/slice/elementProvenance.d.ts +81 -0
- package/dist/esm/lib/slice/elementProvenance.js +189 -0
- package/dist/esm/lib/slice/forwardSliceForKey.d.ts +88 -0
- package/dist/esm/lib/slice/forwardSliceForKey.js +242 -0
- package/dist/esm/lib/slice/index.d.ts +26 -0
- package/dist/esm/lib/slice/index.js +25 -0
- package/dist/esm/lib/slice/keyIndex.d.ts +137 -0
- package/dist/esm/lib/slice/keyIndex.js +292 -0
- package/dist/esm/lib/slice/keyTimeline.d.ts +49 -0
- package/dist/esm/lib/slice/keyTimeline.js +112 -0
- package/dist/esm/lib/slice/keysReadSources.d.ts +51 -0
- package/dist/esm/lib/slice/keysReadSources.js +94 -0
- package/dist/esm/lib/slice/serialize.d.ts +78 -0
- package/dist/esm/lib/slice/serialize.js +277 -0
- package/dist/esm/lib/slice/sliceForKey.d.ts +40 -0
- package/dist/esm/lib/slice/sliceForKey.js +78 -0
- package/dist/esm/lib/slice/types.d.ts +567 -0
- package/dist/esm/lib/slice/types.js +24 -0
- package/dist/esm/lib/time-travel/axis.d.ts +119 -0
- package/dist/esm/lib/time-travel/axis.js +178 -0
- package/dist/esm/lib/time-travel/bundles.d.ts +77 -0
- package/dist/esm/lib/time-travel/bundles.js +135 -0
- package/dist/esm/lib/time-travel/chain.d.ts +93 -0
- package/dist/esm/lib/time-travel/chain.js +187 -0
- package/dist/esm/lib/time-travel/commitStops.d.ts +55 -0
- package/dist/esm/lib/time-travel/commitStops.js +201 -0
- package/dist/esm/lib/time-travel/index.d.ts +22 -0
- package/dist/esm/lib/time-travel/index.js +20 -0
- package/dist/esm/lib/time-travel/stateAt.d.ts +102 -0
- package/dist/esm/lib/time-travel/stateAt.js +152 -0
- package/dist/esm/lib/time-travel/tagStops.d.ts +72 -0
- package/dist/esm/lib/time-travel/tagStops.js +101 -0
- package/dist/esm/lib/time-travel/timeTravel.d.ts +70 -0
- package/dist/esm/lib/time-travel/timeTravel.js +359 -0
- package/dist/esm/lib/time-travel/types.d.ts +470 -0
- package/dist/esm/lib/time-travel/types.js +22 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/paths.d.ts +4 -0
- package/dist/esm/paths.js +4 -0
- package/dist/esm/write.d.ts +49 -0
- package/dist/esm/write.js +47 -0
- package/dist/index.js +67 -0
- package/dist/lib/capture/freeze.js +319 -0
- package/dist/lib/capture/ownData.js +40 -0
- package/dist/lib/capture/valueKinds.js +145 -0
- package/dist/lib/ids/runtimeStageId.js +209 -0
- package/dist/lib/memory/EventLog.js +151 -0
- package/dist/lib/memory/RecordFrame.js +299 -0
- package/dist/lib/memory/SharedMemory.js +91 -0
- package/dist/lib/memory/TransactionBuffer.js +950 -0
- package/dist/lib/memory/admission.js +158 -0
- package/dist/lib/memory/backtrack.js +462 -0
- package/dist/lib/memory/commitLogUtils.js +398 -0
- package/dist/lib/memory/deltaEncoding.js +127 -0
- package/dist/lib/memory/equality.js +265 -0
- package/dist/lib/memory/eventPosition.js +2 -0
- package/dist/lib/memory/honesty.js +94 -0
- package/dist/lib/memory/keyPaths.js +242 -0
- package/dist/lib/memory/logModel.js +371 -0
- package/dist/lib/memory/merge.js +55 -0
- package/dist/lib/memory/pathOps.js +308 -0
- package/dist/lib/memory/paths.js +66 -0
- package/dist/lib/memory/placeholders.js +42 -0
- package/dist/lib/memory/recordCommit.js +64 -0
- package/dist/lib/memory/scrub.js +77 -0
- package/dist/lib/memory/types.js +10 -0
- package/dist/lib/memory/utils.js +169 -0
- package/dist/lib/memory/verbs.js +443 -0
- package/dist/lib/recorder/CommitRangeIndex.js +210 -0
- package/dist/lib/slice/elementProvenance.js +194 -0
- package/dist/lib/slice/forwardSliceForKey.js +246 -0
- package/dist/lib/slice/index.js +42 -0
- package/dist/lib/slice/keyIndex.js +312 -0
- package/dist/lib/slice/keyTimeline.js +116 -0
- package/dist/lib/slice/keysReadSources.js +100 -0
- package/dist/lib/slice/serialize.js +285 -0
- package/dist/lib/slice/sliceForKey.js +82 -0
- package/dist/lib/slice/types.js +25 -0
- package/dist/lib/time-travel/axis.js +183 -0
- package/dist/lib/time-travel/bundles.js +143 -0
- package/dist/lib/time-travel/chain.js +192 -0
- package/dist/lib/time-travel/commitStops.js +205 -0
- package/dist/lib/time-travel/index.js +31 -0
- package/dist/lib/time-travel/stateAt.js +159 -0
- package/dist/lib/time-travel/tagStops.js +105 -0
- package/dist/lib/time-travel/timeTravel.js +363 -0
- package/dist/lib/time-travel/types.js +23 -0
- package/dist/paths.js +14 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/lib/capture/freeze.d.ts +91 -0
- package/dist/types/lib/capture/ownData.d.ts +18 -0
- package/dist/types/lib/capture/valueKinds.d.ts +57 -0
- package/dist/types/lib/ids/runtimeStageId.d.ts +173 -0
- package/dist/types/lib/memory/EventLog.d.ts +105 -0
- package/dist/types/lib/memory/RecordFrame.d.ts +232 -0
- package/dist/types/lib/memory/SharedMemory.d.ts +46 -0
- package/dist/types/lib/memory/TransactionBuffer.d.ts +521 -0
- package/dist/types/lib/memory/admission.d.ts +81 -0
- package/dist/types/lib/memory/backtrack.d.ts +286 -0
- package/dist/types/lib/memory/commitLogUtils.d.ts +217 -0
- package/dist/types/lib/memory/deltaEncoding.d.ts +66 -0
- package/dist/types/lib/memory/equality.d.ts +93 -0
- package/dist/types/lib/memory/eventPosition.d.ts +18 -0
- package/dist/types/lib/memory/honesty.d.ts +93 -0
- package/dist/types/lib/memory/keyPaths.d.ts +125 -0
- package/dist/types/lib/memory/logModel.d.ts +76 -0
- package/dist/types/lib/memory/merge.d.ts +26 -0
- package/dist/types/lib/memory/pathOps.d.ts +118 -0
- package/dist/types/lib/memory/paths.d.ts +56 -0
- package/dist/types/lib/memory/placeholders.d.ts +39 -0
- package/dist/types/lib/memory/recordCommit.d.ts +45 -0
- package/dist/types/lib/memory/scrub.d.ts +52 -0
- package/dist/types/lib/memory/types.d.ts +209 -0
- package/dist/types/lib/memory/utils.d.ts +73 -0
- package/dist/types/lib/memory/verbs.d.ts +333 -0
- package/dist/types/lib/recorder/CommitRangeIndex.d.ts +147 -0
- package/dist/types/lib/slice/elementProvenance.d.ts +81 -0
- package/dist/types/lib/slice/forwardSliceForKey.d.ts +88 -0
- package/dist/types/lib/slice/index.d.ts +26 -0
- package/dist/types/lib/slice/keyIndex.d.ts +137 -0
- package/dist/types/lib/slice/keyTimeline.d.ts +49 -0
- package/dist/types/lib/slice/keysReadSources.d.ts +51 -0
- package/dist/types/lib/slice/serialize.d.ts +78 -0
- package/dist/types/lib/slice/sliceForKey.d.ts +40 -0
- package/dist/types/lib/slice/types.d.ts +567 -0
- package/dist/types/lib/time-travel/axis.d.ts +119 -0
- package/dist/types/lib/time-travel/bundles.d.ts +77 -0
- package/dist/types/lib/time-travel/chain.d.ts +93 -0
- package/dist/types/lib/time-travel/commitStops.d.ts +55 -0
- package/dist/types/lib/time-travel/index.d.ts +22 -0
- package/dist/types/lib/time-travel/stateAt.d.ts +102 -0
- package/dist/types/lib/time-travel/tagStops.d.ts +72 -0
- package/dist/types/lib/time-travel/timeTravel.d.ts +70 -0
- package/dist/types/lib/time-travel/types.d.ts +470 -0
- package/dist/types/paths.d.ts +4 -0
- package/dist/types/write.d.ts +49 -0
- package/dist/write.js +56 -0
- package/package.json +48 -4
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* logModel.ts — the read model of ONE commit log: the writer rule and the value rule of `keyPaths.ts`,
|
|
4
|
+
* answered at a cost proportional to the answer, not to the log (F3, 9.33.0).
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS FILE EXISTS. The rules are simple to state and expensive to apply naively: a key's writers
|
|
7
|
+
* include the commits AROUND it whose change reached it, and deciding that needs the value at the key
|
|
8
|
+
* before and after such a commit — a fold of its top-level key. Folding once per KEY made a causal walk
|
|
9
|
+
* over nested keys cubic in the log (every key re-scanned the log and re-folded its root). Here:
|
|
10
|
+
*
|
|
11
|
+
* - the log is indexed ONCE (`keyPaths · buildWriterIndex`, a path trie): a key's candidates cost
|
|
12
|
+
* O(depth + matches) — its node, its ancestors, its subtree — and the rows under a top-level key are
|
|
13
|
+
* reached through the index, never a scan;
|
|
14
|
+
* - each top-level key is folded ONCE (`verbs · foldKey`, copy-on-write), keeping its value before and
|
|
15
|
+
* after every commit a verdict can ask about: a commit with a row on a path that has written paths
|
|
16
|
+
* below it (it can be AROUND a written key), and a commit that writes THROUGH a string-valued
|
|
17
|
+
* container (`nativeSet` replaces the string, and the characters a key read from it vanish — the one
|
|
18
|
+
* way a row BESIDE a key moves it). A key's verdict is then two `nativeGet`s and a `deepEqual`;
|
|
19
|
+
* - the last writer before a point is found LAZILY: the last write on or inside the key, then only the
|
|
20
|
+
* around-candidates after it — most questions need no verdict at all.
|
|
21
|
+
*
|
|
22
|
+
* One shape keeps a per-key fold (memoised per key): a key that was never written, under a container that
|
|
23
|
+
* was only ever written WHOLE (its node has no written children, so no generation is kept there).
|
|
24
|
+
*
|
|
25
|
+
* A model of a FROZEN log (every engine log since F3: `EventLog · record` freezes each bundle and
|
|
26
|
+
* `getSnapshot` freezes the array) is memoised on the log — the record cannot change under it. A log that
|
|
27
|
+
* is not frozen (hand-built, parsed from JSON) gets a fresh model per question.
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.LogModel = exports.leavesStringAbove = exports.writesAtOrInside = exports.memoisedModel = exports.logModel = void 0;
|
|
31
|
+
const equality_js_1 = require("./equality.js");
|
|
32
|
+
const keyPaths_js_1 = require("./keyPaths.js");
|
|
33
|
+
const pathOps_js_1 = require("./pathOps.js");
|
|
34
|
+
const paths_js_1 = require("./paths.js");
|
|
35
|
+
const verbs_js_1 = require("./verbs.js");
|
|
36
|
+
const MODELS = new WeakMap();
|
|
37
|
+
/** Is the log immutable — the array and every bundle and trace frozen (an engine snapshot's log)? */
|
|
38
|
+
function isFrozenLog(log) {
|
|
39
|
+
if (!Object.isFrozen(log))
|
|
40
|
+
return false;
|
|
41
|
+
// The shape `EventLog · record` leaves: the bundle, its payloads and its rows. A hand-built log
|
|
42
|
+
// frozen less deeply than that could change after a query, so it is never memoised.
|
|
43
|
+
for (const bundle of log) {
|
|
44
|
+
if (!Object.isFrozen(bundle) || !Object.isFrozen(bundle.trace))
|
|
45
|
+
return false;
|
|
46
|
+
if (!Object.isFrozen(bundle.overwrite) || !Object.isFrozen(bundle.updates))
|
|
47
|
+
return false;
|
|
48
|
+
if (!bundle.trace.every((row) => Object.isFrozen(row)))
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
/** The read model of `log` — memoised when the log is frozen, fresh otherwise. */
|
|
54
|
+
function logModel(log) {
|
|
55
|
+
const memo = MODELS.get(log);
|
|
56
|
+
if (memo !== undefined)
|
|
57
|
+
return memo;
|
|
58
|
+
const model = new LogModel(log);
|
|
59
|
+
if (isFrozenLog(log))
|
|
60
|
+
MODELS.set(log, model);
|
|
61
|
+
return model;
|
|
62
|
+
}
|
|
63
|
+
exports.logModel = logModel;
|
|
64
|
+
/** The memoised model of a frozen log (built on first ask), or undefined for a log that is not frozen. */
|
|
65
|
+
function memoisedModel(log) {
|
|
66
|
+
return MODELS.get(log) ?? (isFrozenLog(log) ? logModel(log) : undefined);
|
|
67
|
+
}
|
|
68
|
+
exports.memoisedModel = memoisedModel;
|
|
69
|
+
/** Does the commit hold a non-`delete` row on `key` or inside it? (A `delete` cannot replace a string container.) */
|
|
70
|
+
function writesAtOrInside(bundle, key) {
|
|
71
|
+
for (const t of bundle.trace) {
|
|
72
|
+
if (t.verb === 'delete')
|
|
73
|
+
continue;
|
|
74
|
+
const r = (0, keyPaths_js_1.relation)(t.path, key);
|
|
75
|
+
if (r === 'exact' || r === 'inside')
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
exports.writesAtOrInside = writesAtOrInside;
|
|
81
|
+
/**
|
|
82
|
+
* Can this commit leave a STRING-valued container above `key` — a row around the key whose recorded value
|
|
83
|
+
* holds a string at the row's path or anywhere between it and the key? Only then can a later write beside the
|
|
84
|
+
* key move it (`nativeSet` replaces the string). A conservative, payload-only check: no fold.
|
|
85
|
+
*/
|
|
86
|
+
function leavesStringAbove(bundle, key) {
|
|
87
|
+
const keySegs = key.split(paths_js_1.DELIM);
|
|
88
|
+
for (const t of bundle.trace) {
|
|
89
|
+
if (t.verb === 'delete' || (0, keyPaths_js_1.relation)(t.path, key) !== 'around')
|
|
90
|
+
continue;
|
|
91
|
+
const segs = t.path.split(paths_js_1.DELIM);
|
|
92
|
+
let value = (0, pathOps_js_1.nativeGet)(t.verb === 'merge' ? bundle.updates : bundle.overwrite, segs);
|
|
93
|
+
for (let depth = segs.length; depth < keySegs.length; depth++) {
|
|
94
|
+
if (typeof value === 'string')
|
|
95
|
+
return true;
|
|
96
|
+
if (value === null || typeof value !== 'object')
|
|
97
|
+
break;
|
|
98
|
+
value = (0, pathOps_js_1.nativeGet)(value, [keySegs[depth]]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
exports.leavesStringAbove = leavesStringAbove;
|
|
104
|
+
class LogModel {
|
|
105
|
+
log;
|
|
106
|
+
index;
|
|
107
|
+
roots = new Map();
|
|
108
|
+
histories = new Map();
|
|
109
|
+
changesByKey = new Map();
|
|
110
|
+
writersByKey = new Map();
|
|
111
|
+
constructor(log) {
|
|
112
|
+
this.log = log;
|
|
113
|
+
this.index = (0, keyPaths_js_1.buildWriterIndex)(log);
|
|
114
|
+
for (const bundle of log)
|
|
115
|
+
keyPaths_js_1.queryWork.units += bundle.trace.length;
|
|
116
|
+
}
|
|
117
|
+
// ── rows ─────────────────────────────────────────────────────────────────
|
|
118
|
+
rootRows(root) {
|
|
119
|
+
let entry = this.roots.get(root);
|
|
120
|
+
if (entry !== undefined)
|
|
121
|
+
return entry;
|
|
122
|
+
const node = this.index.roots.get(root);
|
|
123
|
+
const rows = [];
|
|
124
|
+
let refused;
|
|
125
|
+
let refusedAt = Number.POSITIVE_INFINITY;
|
|
126
|
+
if (node !== undefined) {
|
|
127
|
+
for (const c of (0, keyPaths_js_1.subtreePositions)(node)) {
|
|
128
|
+
const trace = this.log[c].trace;
|
|
129
|
+
keyPaths_js_1.queryWork.units += trace.length;
|
|
130
|
+
for (let row = 0; row < trace.length && refused === undefined; row++) {
|
|
131
|
+
const { path, verb } = trace[row];
|
|
132
|
+
if (path !== root && (0, keyPaths_js_1.relation)(path, root) !== 'inside')
|
|
133
|
+
continue;
|
|
134
|
+
if (!(0, verbs_js_1.isVerb)(verb)) {
|
|
135
|
+
refused = new verbs_js_1.UnknownVerbError(verb, { path, row, commit: c });
|
|
136
|
+
refusedAt = c;
|
|
137
|
+
// the refused commit and everything after it are left out: an answer that reaches them is refused
|
|
138
|
+
while (rows.length > 0 && rows[rows.length - 1].commitIdx === c)
|
|
139
|
+
rows.pop();
|
|
140
|
+
}
|
|
141
|
+
else
|
|
142
|
+
rows.push({ verb, bundle: this.log[c], commitIdx: c, path });
|
|
143
|
+
}
|
|
144
|
+
if (refused !== undefined)
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
entry = { rows, ...(refused !== undefined && { refused }), refusedAt };
|
|
149
|
+
this.roots.set(root, entry);
|
|
150
|
+
return entry;
|
|
151
|
+
}
|
|
152
|
+
/** Refuse an answer that reaches a row whose verb the law does not know (one in commits `after` < c ≤ `end`). */
|
|
153
|
+
refuseThrough(root, end, after = -1) {
|
|
154
|
+
const rr = this.rootRows(root);
|
|
155
|
+
if (rr.refused !== undefined && rr.refusedAt <= end && rr.refusedAt > after)
|
|
156
|
+
throw rr.refused;
|
|
157
|
+
return rr;
|
|
158
|
+
}
|
|
159
|
+
/** Every row under `key`'s top-level key up to `end`, with its relation to `key` — through the index. */
|
|
160
|
+
rowsUnderRoot(key, end) {
|
|
161
|
+
const rr = this.refuseThrough((0, keyPaths_js_1.rootOf)(key), end);
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const t of rr.rows) {
|
|
164
|
+
if (t.commitIdx > end)
|
|
165
|
+
break;
|
|
166
|
+
const r = (0, keyPaths_js_1.relation)(t.path, key);
|
|
167
|
+
out.push(r === undefined ? t : { ...t, relation: r });
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
// ── the root fold, once per top-level key ───────────────────────────────
|
|
172
|
+
history(root) {
|
|
173
|
+
let h = this.histories.get(root);
|
|
174
|
+
if (h !== undefined)
|
|
175
|
+
return h;
|
|
176
|
+
const kept = new Map();
|
|
177
|
+
const replaced = new Map();
|
|
178
|
+
const rr = this.rootRows(root);
|
|
179
|
+
keyPaths_js_1.queryWork.units += rr.rows.length;
|
|
180
|
+
(0, verbs_js_1.foldKey)(rr.rows, [root], {
|
|
181
|
+
generations: {
|
|
182
|
+
at: (c, state) => {
|
|
183
|
+
let keep = false;
|
|
184
|
+
keyPaths_js_1.queryWork.units += this.log[c].trace.length;
|
|
185
|
+
for (const t of this.log[c].trace) {
|
|
186
|
+
if (t.path !== root && (0, keyPaths_js_1.relation)(t.path, root) !== 'inside')
|
|
187
|
+
continue;
|
|
188
|
+
if (((0, keyPaths_js_1.nodeAt)(this.index, t.path)?.children.size ?? 0) > 0)
|
|
189
|
+
keep = true;
|
|
190
|
+
const segs = t.path.split(paths_js_1.DELIM);
|
|
191
|
+
for (let j = 1; j < segs.length; j++) {
|
|
192
|
+
if (typeof (0, pathOps_js_1.nativeGet)(state, segs.slice(0, j)) !== 'string')
|
|
193
|
+
continue;
|
|
194
|
+
const container = (0, keyPaths_js_1.nodeAt)(this.index, segs.slice(0, j).join(paths_js_1.DELIM));
|
|
195
|
+
if (container !== undefined) {
|
|
196
|
+
const list = replaced.get(container) ?? [];
|
|
197
|
+
if (list[list.length - 1] !== c)
|
|
198
|
+
list.push(c);
|
|
199
|
+
replaced.set(container, list);
|
|
200
|
+
}
|
|
201
|
+
keep = true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return keep;
|
|
205
|
+
},
|
|
206
|
+
keep: (c, before, after) => {
|
|
207
|
+
kept.set(c, { before, after });
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
h = { kept, keptAt: [...kept.keys()].sort((a, b) => a - b), replaced };
|
|
212
|
+
this.histories.set(root, h);
|
|
213
|
+
return h;
|
|
214
|
+
}
|
|
215
|
+
/** A key never written, under a container only ever written whole: no generation is kept for it. */
|
|
216
|
+
needsOwnFold(key, ancestors) {
|
|
217
|
+
return (0, keyPaths_js_1.nodeAt)(this.index, key) === undefined && ancestors[ancestors.length - 1].children.size === 0;
|
|
218
|
+
}
|
|
219
|
+
/** Every commit across which the value at `key` changed — one fold for this key (the fallback shape). */
|
|
220
|
+
ownChanges(key) {
|
|
221
|
+
let changes = this.changesByKey.get(key);
|
|
222
|
+
if (changes !== undefined)
|
|
223
|
+
return changes;
|
|
224
|
+
const segs = key.split(paths_js_1.DELIM);
|
|
225
|
+
const verdict = new Map();
|
|
226
|
+
let commit = -1;
|
|
227
|
+
let start;
|
|
228
|
+
keyPaths_js_1.queryWork.units += this.rootRows(segs[0]).rows.length;
|
|
229
|
+
(0, verbs_js_1.foldKey)(this.rootRows(segs[0]).rows, segs, {
|
|
230
|
+
everyRow: true,
|
|
231
|
+
observe: (touch, before, after) => {
|
|
232
|
+
if (touch.commitIdx !== commit) {
|
|
233
|
+
commit = touch.commitIdx;
|
|
234
|
+
start = before;
|
|
235
|
+
}
|
|
236
|
+
verdict.set(commit, !(0, equality_js_1.deepEqual)(start, after, 'copies'));
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
changes = [...verdict].filter(([, changed]) => changed).map(([c]) => c);
|
|
240
|
+
this.changesByKey.set(key, changes);
|
|
241
|
+
return changes;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The commits in the OPEN range (`after`, `before`) with no row on or inside `key` across which the value
|
|
245
|
+
* at `key` changed — the AROUND half of the writer rule (and the string-container case beside it).
|
|
246
|
+
*/
|
|
247
|
+
changedAround(key, after, before) {
|
|
248
|
+
const ancestors = (0, keyPaths_js_1.ancestorNodes)(this.index, key);
|
|
249
|
+
if (ancestors.length === 0 || ancestors.every((a) => a.positions.length === 0))
|
|
250
|
+
return [];
|
|
251
|
+
const root = ancestors[0].path;
|
|
252
|
+
// The answer reads the rows of the window: refuse it when one of them is a verb the law does not know.
|
|
253
|
+
this.refuseThrough(root, before - 1, after);
|
|
254
|
+
if (this.needsOwnFold(key, ancestors))
|
|
255
|
+
return (0, keyPaths_js_1.between)(this.ownChanges(key), after, before);
|
|
256
|
+
const h = this.history(root);
|
|
257
|
+
const candidates = (0, keyPaths_js_1.ascendingUnion)([
|
|
258
|
+
...ancestors.map((a) => (0, keyPaths_js_1.between)(a.positions, after, before)),
|
|
259
|
+
...ancestors.map((a) => (0, keyPaths_js_1.between)(h.replaced.get(a) ?? [], after, before)),
|
|
260
|
+
]);
|
|
261
|
+
const rest = key.split(paths_js_1.DELIM).slice(1);
|
|
262
|
+
keyPaths_js_1.queryWork.units += candidates.length;
|
|
263
|
+
return candidates.filter((c) => {
|
|
264
|
+
const g = h.kept.get(c);
|
|
265
|
+
return g !== undefined && !(0, equality_js_1.deepEqual)((0, pathOps_js_1.nativeGet)(g.before, rest), (0, pathOps_js_1.nativeGet)(g.after, rest), 'copies');
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
// ── the writer rule ──────────────────────────────────────────────────────
|
|
269
|
+
/** Every commit that WROTE `key`, ascending (memoised per key). */
|
|
270
|
+
writersOf(key) {
|
|
271
|
+
let writers = this.writersByKey.get(key);
|
|
272
|
+
if (writers !== undefined)
|
|
273
|
+
return writers;
|
|
274
|
+
const node = (0, keyPaths_js_1.nodeAt)(this.index, key);
|
|
275
|
+
const atOrInside = node === undefined ? [] : (0, keyPaths_js_1.subtreePositions)(node);
|
|
276
|
+
const around = this.changedAround(key, -1, Number.POSITIVE_INFINITY);
|
|
277
|
+
writers = around.length === 0 ? atOrInside.slice() : (0, keyPaths_js_1.ascendingUnion)([atOrInside, around]);
|
|
278
|
+
this.writersByKey.set(key, writers);
|
|
279
|
+
return writers;
|
|
280
|
+
}
|
|
281
|
+
/** The ARRAY position of the last commit before `before` that wrote `key`, or -1. */
|
|
282
|
+
lastWriterBefore(key, before) {
|
|
283
|
+
keyPaths_js_1.queryWork.units += 1;
|
|
284
|
+
const node = (0, keyPaths_js_1.nodeAt)(this.index, key);
|
|
285
|
+
const lastAI = node === undefined ? -1 : (0, keyPaths_js_1.lastBefore)((0, keyPaths_js_1.subtreePositions)(node), before);
|
|
286
|
+
const ancestors = (0, keyPaths_js_1.ancestorNodes)(this.index, key);
|
|
287
|
+
keyPaths_js_1.queryWork.units += ancestors.length; // the scans over them below
|
|
288
|
+
if (ancestors.length === 0)
|
|
289
|
+
return lastAI;
|
|
290
|
+
// A row around the key in the last write's OWN commit counts when it can leave a string container above
|
|
291
|
+
// the key (a later write beside the key then replaces it).
|
|
292
|
+
const aroundInWindow = ancestors.some((a) => (0, keyPaths_js_1.between)(a.positions, lastAI, before).length > 0) ||
|
|
293
|
+
(lastAI !== -1 && leavesStringAbove(this.log[lastAI], key));
|
|
294
|
+
// A `delete` cannot replace a string container, so a character read through one can still move after it.
|
|
295
|
+
const deleteOnly = lastAI !== -1 && !writesAtOrInside(this.log[lastAI], key);
|
|
296
|
+
if (!aroundInWindow && !(deleteOnly && ancestors.some((a) => a.positions.length > 0)))
|
|
297
|
+
return lastAI;
|
|
298
|
+
const changed = this.changedAround(key, lastAI, before);
|
|
299
|
+
return changed.length > 0 ? changed[changed.length - 1] : lastAI;
|
|
300
|
+
}
|
|
301
|
+
// ── the value rule ───────────────────────────────────────────────────────
|
|
302
|
+
/**
|
|
303
|
+
* The value of `key` after commit `idx` (inclusive). The fold of the rows under its top-level key — but
|
|
304
|
+
* when its last total row up to `idx` is a `set` ON the key and nothing around it was written since, only
|
|
305
|
+
* the rows on and inside the key from that `set` on can move it, and only they are folded.
|
|
306
|
+
*/
|
|
307
|
+
valueAt(key, idx) {
|
|
308
|
+
const segs = key.split(paths_js_1.DELIM);
|
|
309
|
+
this.refuseThrough(segs[0], idx);
|
|
310
|
+
const node = (0, keyPaths_js_1.nodeAt)(this.index, key);
|
|
311
|
+
const ancestors = (0, keyPaths_js_1.ancestorNodes)(this.index, key);
|
|
312
|
+
if (node === undefined && ancestors.length === 0)
|
|
313
|
+
return undefined;
|
|
314
|
+
if (node !== undefined) {
|
|
315
|
+
const anchor = (0, keyPaths_js_1.lastBefore)(node.totals, idx + 1);
|
|
316
|
+
if (anchor !== -1 &&
|
|
317
|
+
lastTotalIsSet(this.log[anchor], key) &&
|
|
318
|
+
ancestors.every((a) => (0, keyPaths_js_1.between)(a.positions, anchor - 1, idx + 1).length === 0)) {
|
|
319
|
+
const rows = [];
|
|
320
|
+
for (const c of (0, keyPaths_js_1.between)((0, keyPaths_js_1.subtreePositions)(node), anchor - 1, idx + 1)) {
|
|
321
|
+
const trace = this.log[c].trace;
|
|
322
|
+
for (let row = 0; row < trace.length; row++) {
|
|
323
|
+
const r = (0, keyPaths_js_1.relation)(trace[row].path, key);
|
|
324
|
+
if (r === 'exact' || r === 'inside') {
|
|
325
|
+
rows.push({
|
|
326
|
+
verb: trace[row].verb,
|
|
327
|
+
bundle: this.log[c],
|
|
328
|
+
commitIdx: c,
|
|
329
|
+
path: trace[row].path,
|
|
330
|
+
relation: r,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
keyPaths_js_1.queryWork.units += rows.length;
|
|
336
|
+
return (0, verbs_js_1.foldKey)(rows, segs);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const rows = this.rowsUnderRoot(key, idx);
|
|
340
|
+
keyPaths_js_1.queryWork.units += rows.length;
|
|
341
|
+
if (!rows.some((row) => row.relation !== undefined))
|
|
342
|
+
return undefined;
|
|
343
|
+
// A memoised model folds from the last generation it keeps at or before `idx` — the rows after it only.
|
|
344
|
+
if (MODELS.get(this.log) === this) {
|
|
345
|
+
const h = this.history(segs[0]);
|
|
346
|
+
const kept = (0, keyPaths_js_1.lastBefore)(h.keptAt, idx + 1);
|
|
347
|
+
if (kept !== -1) {
|
|
348
|
+
const after = rows.filter((row) => row.commitIdx > kept);
|
|
349
|
+
// Folded FROM the kept generation, the answer can BE it (no row after it) or share its parts, and
|
|
350
|
+
// the memo answers every later question: detach it, as the value rule promises (until 9.44.2 an
|
|
351
|
+
// edit of one answer changed the next).
|
|
352
|
+
return detached((0, verbs_js_1.foldKey)(after, segs, { anchored: true, start: h.kept.get(kept)?.after }));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return (0, verbs_js_1.foldKey)(rows, segs, { anchored: true });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
exports.LogModel = LogModel;
|
|
359
|
+
/** A value the caller owns: a container is cloned, a primitive is itself. */
|
|
360
|
+
function detached(value) {
|
|
361
|
+
return value !== null && typeof value === 'object' ? structuredClone(value) : value;
|
|
362
|
+
}
|
|
363
|
+
/** Is the last `set` / `delete` row ON `key` in this commit a `set`? */
|
|
364
|
+
function lastTotalIsSet(bundle, key) {
|
|
365
|
+
for (let i = bundle.trace.length - 1; i >= 0; i--) {
|
|
366
|
+
const t = bundle.trace[i];
|
|
367
|
+
if (t.path === key && (t.verb === 'set' || t.verb === 'delete'))
|
|
368
|
+
return t.verb === 'set';
|
|
369
|
+
}
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* merge.ts — the union merge the `merge` verb applies. A leaf (L0, imports
|
|
4
|
+
* only the own-slot writer): `deepSmartMerge`, the law every replay, fold and reader
|
|
5
|
+
* of a recorded `merge` row runs. Re-exported from `utils.ts`, so no importer
|
|
6
|
+
* moved.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.deepSmartMerge = void 0;
|
|
10
|
+
const ownData_js_1 = require("../capture/ownData.js");
|
|
11
|
+
/**
|
|
12
|
+
* Deep union merge helper.
|
|
13
|
+
* - Arrays (non-empty): union without duplicates (encounter order preserved)
|
|
14
|
+
* - Arrays (empty): replace — src `[]` clears the destination array.
|
|
15
|
+
* Rationale: writing `scope.tags = []` means "clear tags", not "append nothing".
|
|
16
|
+
* Without this rule, an empty-array write silently becomes a no-op which is
|
|
17
|
+
* impossible to distinguish from a bug.
|
|
18
|
+
* - Objects: recursive merge of own enumerable string keys, including special keys;
|
|
19
|
+
* inherited destination values and setters never participate.
|
|
20
|
+
* - Primitives: source wins
|
|
21
|
+
*
|
|
22
|
+
* Terminates on a CYCLIC `src` (9.18.1, same law as {@link deepEqual}: a
|
|
23
|
+
* state value survives `structuredClone`, which preserves cycles). A `src`
|
|
24
|
+
* object re-entered while it is still being merged higher up the stack hands
|
|
25
|
+
* back the output being built for it, so the merged value mirrors the cycle
|
|
26
|
+
* instead of unrolling it forever. Only the ancestors on the stack are
|
|
27
|
+
* guarded — a shared (acyclic) `src` reference met again at a different
|
|
28
|
+
* `dst` merges against THAT `dst`, exactly as before.
|
|
29
|
+
*/
|
|
30
|
+
function deepSmartMerge(dst, src) {
|
|
31
|
+
return mergeGuarded(dst, src, undefined);
|
|
32
|
+
}
|
|
33
|
+
exports.deepSmartMerge = deepSmartMerge;
|
|
34
|
+
function mergeGuarded(dst, src, inFlight) {
|
|
35
|
+
if (src === null || typeof src !== 'object')
|
|
36
|
+
return src;
|
|
37
|
+
if (Array.isArray(src)) {
|
|
38
|
+
if (src.length === 0)
|
|
39
|
+
return []; // empty src = clear, not no-op
|
|
40
|
+
if (Array.isArray(dst))
|
|
41
|
+
return [...new Set([...dst, ...src])];
|
|
42
|
+
return [...src];
|
|
43
|
+
}
|
|
44
|
+
if (inFlight?.has(src))
|
|
45
|
+
return inFlight.get(src); // cycle — re-enter the value being built
|
|
46
|
+
const out = { ...(dst && typeof dst === 'object' ? dst : {}) };
|
|
47
|
+
(inFlight ??= new WeakMap()).set(src, out);
|
|
48
|
+
for (const k of Object.keys(src)) {
|
|
49
|
+
const owns = Object.prototype.hasOwnProperty.call(out, k);
|
|
50
|
+
const value = mergeGuarded(owns ? out[k] : undefined, src[k], inFlight);
|
|
51
|
+
(0, ownData_js_1.putOwn)(out, k, value);
|
|
52
|
+
}
|
|
53
|
+
inFlight.delete(src);
|
|
54
|
+
return out;
|
|
55
|
+
}
|