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,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* utils.ts — nested-object helpers for the heap, and the memory leaves' one
|
|
4
|
+
* re-export surface.
|
|
5
|
+
*
|
|
6
|
+
* The helpers here (`setNestedValue`, `updateNestedValue`, `updateValue`,
|
|
7
|
+
* `getNestedValue`) walk a nested object by path with
|
|
8
|
+
* prototype-pollution protection. Everything else this file used to hold has an
|
|
9
|
+
* owner of its own and is re-exported, so no importer moved: the path codec
|
|
10
|
+
* (`paths.ts`), structural equality (`equality.ts`) and the union merge
|
|
11
|
+
* (`merge.ts`) at L0, and the verb law (`verbs.ts`) at L1 — `applySmartMerge`,
|
|
12
|
+
* `nextGeneration`, `applySmartMergeInto`, `dryFold`, `supersededByNextSet`.
|
|
13
|
+
* The log's scrub, `redactPatch`, moved out in 9.33.0; it is the record's own, `scrub.ts` (C4).
|
|
14
|
+
* The writers take an ADDRESS, a path prefix their caller computed, and never
|
|
15
|
+
* name one (C2: the engine decides where a stage writes, `runAddress.ts · runAddress`).
|
|
16
|
+
* Zero external dependencies.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.getNestedValue = exports.updateValue = exports.updateNestedValue = exports.setNestedValue = exports.isDeniedWritePath = exports.getRunAndGlobalPaths = exports.supersededByNextSet = exports.nextGeneration = exports.dryFold = exports.applySmartMergeInto = exports.applySmartMerge = exports.pathSegments = exports.normalisePath = exports.DELIM = exports.deepSmartMerge = exports.deepEqual = void 0;
|
|
20
|
+
const ownData_js_1 = require("../capture/ownData.js");
|
|
21
|
+
const pathOps_js_1 = require("./pathOps.js");
|
|
22
|
+
var equality_js_1 = require("./equality.js");
|
|
23
|
+
Object.defineProperty(exports, "deepEqual", { enumerable: true, get: function () { return equality_js_1.deepEqual; } });
|
|
24
|
+
var merge_js_1 = require("./merge.js");
|
|
25
|
+
Object.defineProperty(exports, "deepSmartMerge", { enumerable: true, get: function () { return merge_js_1.deepSmartMerge; } });
|
|
26
|
+
var paths_js_1 = require("./paths.js");
|
|
27
|
+
Object.defineProperty(exports, "DELIM", { enumerable: true, get: function () { return paths_js_1.DELIM; } });
|
|
28
|
+
Object.defineProperty(exports, "normalisePath", { enumerable: true, get: function () { return paths_js_1.normalisePath; } });
|
|
29
|
+
Object.defineProperty(exports, "pathSegments", { enumerable: true, get: function () { return paths_js_1.pathSegments; } });
|
|
30
|
+
var verbs_js_1 = require("./verbs.js");
|
|
31
|
+
Object.defineProperty(exports, "applySmartMerge", { enumerable: true, get: function () { return verbs_js_1.applySmartMerge; } });
|
|
32
|
+
Object.defineProperty(exports, "applySmartMergeInto", { enumerable: true, get: function () { return verbs_js_1.applySmartMergeInto; } });
|
|
33
|
+
Object.defineProperty(exports, "dryFold", { enumerable: true, get: function () { return verbs_js_1.dryFold; } });
|
|
34
|
+
Object.defineProperty(exports, "nextGeneration", { enumerable: true, get: function () { return verbs_js_1.nextGeneration; } });
|
|
35
|
+
Object.defineProperty(exports, "supersededByNextSet", { enumerable: true, get: function () { return verbs_js_1.supersededByNextSet; } });
|
|
36
|
+
/**
|
|
37
|
+
* The two places a value at `path` can sit: under `address` (`runPath`, absent
|
|
38
|
+
* for the root address `[]`) and at the root (`globalPath`). `address` is a
|
|
39
|
+
* path prefix the caller computed — for the engine, a frame's run namespace
|
|
40
|
+
* (`runAddress.ts · runAddress`, C2), held by its record frame (`RecordFrame · address`, C3).
|
|
41
|
+
*/
|
|
42
|
+
function getRunAndGlobalPaths(address = [], path = []) {
|
|
43
|
+
return {
|
|
44
|
+
runPath: addressOf(address).length > 0 ? pathUnder(address, path) : undefined,
|
|
45
|
+
globalPath: [...path],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
exports.getRunAndGlobalPaths = getRunAndGlobalPaths;
|
|
49
|
+
/** An address is an array. A run id still passed in its place (C2) is refused, never spread into its characters. */
|
|
50
|
+
function addressOf(address) {
|
|
51
|
+
if (Array.isArray(address))
|
|
52
|
+
return address;
|
|
53
|
+
throw new TypeError(`footprintjs: an address is an array path prefix, got ${address === null ? 'null' : typeof address}`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `address` then `path`, as one new array. An index loop, because the double
|
|
57
|
+
* spread `[...address, ...path]` costs twice as much and a frame builds one on
|
|
58
|
+
* every read it serves from live state (a fork child reading a root key).
|
|
59
|
+
*/
|
|
60
|
+
function pathUnder(address, path) {
|
|
61
|
+
const out = new Array(address.length + path.length);
|
|
62
|
+
for (let i = 0; i < address.length; i++)
|
|
63
|
+
out[i] = address[i];
|
|
64
|
+
for (let i = 0; i < path.length; i++)
|
|
65
|
+
out[address.length + i] = path[i];
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Refuse the WHOLE selector before creating a container or copying a generation.
|
|
70
|
+
* The address's array contract is checked even when another selector is denied.
|
|
71
|
+
* Payload keys are data, not selectors, and are deliberately not inspected.
|
|
72
|
+
* @internal Shared by the nested writers and SharedMemory's pre-copy boundary.
|
|
73
|
+
*/
|
|
74
|
+
function isDeniedWritePath(address = [], path = [], field) {
|
|
75
|
+
const prefix = addressOf(address);
|
|
76
|
+
return prefix.some(pathOps_js_1.isDeniedSegment) || path.some(pathOps_js_1.isDeniedSegment) || (0, pathOps_js_1.isDeniedSegment)(field);
|
|
77
|
+
}
|
|
78
|
+
exports.isDeniedWritePath = isDeniedWritePath;
|
|
79
|
+
/**
|
|
80
|
+
* The container at `address` + `path` inside `obj`, each missing one created
|
|
81
|
+
* on the way: the one AT the address from `defaultValues` when given, every
|
|
82
|
+
* other one empty. The one walk both nested writers share.
|
|
83
|
+
*/
|
|
84
|
+
function containerAt(obj, address = [], path = [], defaultValues) {
|
|
85
|
+
const segments = pathUnder(addressOf(address), path);
|
|
86
|
+
let current = obj;
|
|
87
|
+
for (let i = 0; i < segments.length; i++) {
|
|
88
|
+
const key = segments[i];
|
|
89
|
+
if (!Object.prototype.hasOwnProperty.call(current, key)) {
|
|
90
|
+
(0, ownData_js_1.setOwnValue)(current, key, i === address.length - 1 && defaultValues ? defaultValues : {});
|
|
91
|
+
}
|
|
92
|
+
current = current[key];
|
|
93
|
+
}
|
|
94
|
+
return current;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Sets a value at a nested path under `address`, creating intermediate objects as needed.
|
|
98
|
+
* A denied address/path/field leaves `obj` unchanged, including its prototype.
|
|
99
|
+
*/
|
|
100
|
+
function setNestedValue(obj, address, path, field, value, defaultValues) {
|
|
101
|
+
if (isDeniedWritePath(address, path, field))
|
|
102
|
+
return obj;
|
|
103
|
+
(0, ownData_js_1.setOwnValue)(containerAt(obj, address, path, defaultValues), field, value);
|
|
104
|
+
return obj;
|
|
105
|
+
}
|
|
106
|
+
exports.setNestedValue = setNestedValue;
|
|
107
|
+
/**
|
|
108
|
+
* Updates a value in the object at a nested path under `address`.
|
|
109
|
+
* A denied address/path/field leaves `obj` unchanged, including its prototype.
|
|
110
|
+
* - Arrays: concatenate
|
|
111
|
+
* - Objects: shallow merge at each level
|
|
112
|
+
* - Primitives: replace
|
|
113
|
+
*/
|
|
114
|
+
function updateNestedValue(obj, address, path, field, value, defaultValues) {
|
|
115
|
+
if (isDeniedWritePath(address, path, field))
|
|
116
|
+
return obj;
|
|
117
|
+
updateValue(containerAt(obj, address, path, defaultValues), field, value);
|
|
118
|
+
return obj;
|
|
119
|
+
}
|
|
120
|
+
exports.updateNestedValue = updateNestedValue;
|
|
121
|
+
/**
|
|
122
|
+
* In-place value update with merge semantics.
|
|
123
|
+
* - Arrays (non-empty): concatenate onto existing
|
|
124
|
+
* - Arrays (empty): direct replace — writing `[]` clears the field
|
|
125
|
+
* - Objects (non-empty): shallow merge (spread)
|
|
126
|
+
* - Objects (empty): direct replace — writing `{}` clears the field
|
|
127
|
+
* - Primitives: direct assignment
|
|
128
|
+
*
|
|
129
|
+
* Note on empty arrays: both `value && Array.isArray(value)` and
|
|
130
|
+
* `Array.isArray(value)` evaluate the same for arrays — `[]` is truthy in
|
|
131
|
+
* JavaScript, so the `&&` guard was never the issue. The actual bug was the
|
|
132
|
+
* concat path: `[...cur, ...[]]` silently returned `cur` unchanged when `value`
|
|
133
|
+
* was `[]`, making `updateValue(obj, 'tags', [])` a no-op instead of a clear.
|
|
134
|
+
* The fix is the explicit `value.length === 0` early-return branch.
|
|
135
|
+
*/
|
|
136
|
+
function updateValue(object, key, value) {
|
|
137
|
+
if ((0, pathOps_js_1.isDeniedSegment)(key))
|
|
138
|
+
return;
|
|
139
|
+
if (Array.isArray(value)) {
|
|
140
|
+
if (value.length === 0) {
|
|
141
|
+
(0, ownData_js_1.setOwnValue)(object, key, value); // clear: [] replaces whatever was there
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const cur = Object.prototype.hasOwnProperty.call(object, key) ? object[key] : undefined;
|
|
145
|
+
(0, ownData_js_1.setOwnValue)(object, key, cur === undefined ? value : [...cur, ...value]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else if (value && typeof value === 'object' && Object.keys(value).length) {
|
|
149
|
+
const cur = Object.prototype.hasOwnProperty.call(object, key) ? object[key] : undefined;
|
|
150
|
+
(0, ownData_js_1.setOwnValue)(object, key, cur === undefined ? value : { ...cur, ...value });
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
(0, ownData_js_1.setOwnValue)(object, key, value);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
exports.updateValue = updateValue;
|
|
157
|
+
/**
|
|
158
|
+
* Gets a value at a nested path with prototype-pollution protection.
|
|
159
|
+
*/
|
|
160
|
+
function getNestedValue(root, path, field) {
|
|
161
|
+
const node = path && path.length > 0 ? (0, pathOps_js_1.nativeGet)(root, path) : root;
|
|
162
|
+
if (field === undefined || node === undefined)
|
|
163
|
+
return node;
|
|
164
|
+
if (node !== null && typeof node === 'object' && Object.prototype.hasOwnProperty.call(node, field)) {
|
|
165
|
+
return node[field];
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
exports.getNestedValue = getNestedValue;
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* verbs.ts — THE VERB LAW (L1): the one place a commit row becomes a value.
|
|
4
|
+
*
|
|
5
|
+
* A row of a commit bundle's `trace` carries one of four verbs —
|
|
6
|
+
* `set | merge | append | delete` — and ONE step turns it into a value:
|
|
7
|
+
* {@link applyVerb}. State is a left fold of that step (the event-sourcing
|
|
8
|
+
* reducer): {@link foldRows} folds one bundle's rows into a state, and
|
|
9
|
+
* {@link foldKey} folds ONE KEY across a log — every row under the key's
|
|
10
|
+
* top-level key, read at the key (the value rule, `keyPaths.ts`). Every reader
|
|
11
|
+
* of the log runs one of the two —
|
|
12
|
+
*
|
|
13
|
+
* - the live commit and the redacted mirror (`nextGeneration`),
|
|
14
|
+
* - the public replay (`applySmartMerge`), the read-side folds
|
|
15
|
+
* (`applySmartMergeInto` — `EventLog.materialise`, `stateAt`) and the
|
|
16
|
+
* admitted record's comparison (`dryFold`): all {@link foldRows},
|
|
17
|
+
* - `commitValueAt`, and `arrayProvenance` as an OBSERVER of the same fold:
|
|
18
|
+
* {@link foldKey}.
|
|
19
|
+
*
|
|
20
|
+
* Three questions that used to be answered once per reader are answered here
|
|
21
|
+
* once: what a verb does to the value before it ({@link applyVerb}), how the
|
|
22
|
+
* result is placed ({@link placeVerb}), and how the log's own values are
|
|
23
|
+
* treated on the way in (the clone discipline — a PARAMETER of the fold, see
|
|
24
|
+
* {@link Discipline} and {@link RecordedPayload}). A new verb is one new arm of
|
|
25
|
+
* `applyVerb` and one row of the traits table; the compiler refuses either
|
|
26
|
+
* missing ({@link isTotal}, {@link recordsTail}, {@link VERBS}).
|
|
27
|
+
*
|
|
28
|
+
* An UNKNOWN verb is refused with {@link UnknownVerbError}, never folded as a
|
|
29
|
+
* `merge` (the old silent default). Engine-written logs carry the four, so
|
|
30
|
+
* every engine run folds byte-for-byte as before.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.foldKey = exports.dryFold = exports.applySmartMergeInto = exports.nextGeneration = exports.applySmartMerge = exports.foldRows = exports.supersededByNextSet = exports.placeVerb = exports.applyVerb = exports.RecordedPayload = exports.ABSENT = exports.UnknownVerbError = exports.recordsTail = exports.isTotal = exports.isVerb = exports.VERBS = void 0;
|
|
34
|
+
const merge_js_1 = require("./merge.js");
|
|
35
|
+
const pathOps_js_1 = require("./pathOps.js");
|
|
36
|
+
const paths_js_1 = require("./paths.js");
|
|
37
|
+
/**
|
|
38
|
+
* The vocabulary: each verb with the two traits readers branch on, in the
|
|
39
|
+
* order the contract names them. A `Record` over the union — the compiler
|
|
40
|
+
* refuses a table that misses (or invents) a verb, so a verb cannot be added
|
|
41
|
+
* without saying what it is.
|
|
42
|
+
*
|
|
43
|
+
* `total` — the row decides the value ALONE; it reads nothing from what came
|
|
44
|
+
* before it ({@link isTotal}).
|
|
45
|
+
* `tail` — the row records ONLY what it adds, not the whole value
|
|
46
|
+
* ({@link recordsTail}).
|
|
47
|
+
*/
|
|
48
|
+
const TRAITS = {
|
|
49
|
+
set: { total: true, tail: false },
|
|
50
|
+
merge: { total: false, tail: false },
|
|
51
|
+
append: { total: false, tail: true },
|
|
52
|
+
delete: { total: true, tail: false },
|
|
53
|
+
};
|
|
54
|
+
/** The four verbs, in the order the contract names them. */
|
|
55
|
+
exports.VERBS = Object.freeze(Object.keys(TRAITS));
|
|
56
|
+
/** Is `value` one of the four? The question every reader of a foreign log asks first. */
|
|
57
|
+
function isVerb(value) {
|
|
58
|
+
return typeof value === 'string' && Object.prototype.hasOwnProperty.call(TRAITS, value);
|
|
59
|
+
}
|
|
60
|
+
exports.isVerb = isVerb;
|
|
61
|
+
/**
|
|
62
|
+
* Does the verb decide the value ALONE — read nothing from what came before
|
|
63
|
+
* it? `set` and `delete`. A reader that only needs the final value may start
|
|
64
|
+
* at the last such row ({@link foldKey} `anchored`); one that tracks how the
|
|
65
|
+
* value grew (provenance) starts at the first. Pinned against
|
|
66
|
+
* {@link applyVerb} itself, so a new verb cannot misreport it.
|
|
67
|
+
*/
|
|
68
|
+
function isTotal(verb) {
|
|
69
|
+
return TRAITS[verb].total;
|
|
70
|
+
}
|
|
71
|
+
exports.isTotal = isTotal;
|
|
72
|
+
/**
|
|
73
|
+
* Does the row record ONLY what it adds — a tail — rather than the whole
|
|
74
|
+
* value? `append`. What the row added is then known exactly: a reader that
|
|
75
|
+
* tracks where array elements came from attributes them to this row without
|
|
76
|
+
* comparing arrays, where every other verb has to be inferred from.
|
|
77
|
+
*/
|
|
78
|
+
function recordsTail(verb) {
|
|
79
|
+
return TRAITS[verb].tail;
|
|
80
|
+
}
|
|
81
|
+
exports.recordsTail = recordsTail;
|
|
82
|
+
/**
|
|
83
|
+
* Thrown by the readers of a commit log — {@link foldRows} (behind
|
|
84
|
+
* `applySmartMerge`), `commitValueAt`, `arrayProvenance` — when a row carries a
|
|
85
|
+
* verb outside {@link VERBS}: a foreign or corrupted log fails loudly instead
|
|
86
|
+
* of being replayed as a `merge`. `verb`, `path`, `row` and `commit` name the
|
|
87
|
+
* row (`path` / `row` / `commit` are absent when the caller handed over a
|
|
88
|
+
* bare verb); `path` is the trace's own DELIM-joined form — take it apart with
|
|
89
|
+
* `pathSegments`. The message quotes `verb` and `path`, so a hostile log cannot
|
|
90
|
+
* reshape it.
|
|
91
|
+
*/
|
|
92
|
+
class UnknownVerbError extends Error {
|
|
93
|
+
verb;
|
|
94
|
+
path;
|
|
95
|
+
row;
|
|
96
|
+
commit;
|
|
97
|
+
constructor(verb, site) {
|
|
98
|
+
const named = typeof verb === 'string' ? JSON.stringify(verb) : verb === undefined ? 'undefined' : `of type ${typeof verb}`;
|
|
99
|
+
// The verb and the path come from a log nobody vetted: quoted (JSON), so a
|
|
100
|
+
// control character in either cannot reshape the message.
|
|
101
|
+
const where = site === undefined
|
|
102
|
+
? ''
|
|
103
|
+
: ` on trace row ${site.row} (path ${JSON.stringify((0, paths_js_1.pathSegments)(site.path).join(' › '))}${site.commit === undefined ? '' : `, commit ${site.commit}`})`;
|
|
104
|
+
const one = exports.VERBS.join(' | ');
|
|
105
|
+
super(`unknown verb ${named}${where}: a commit row is one of ${one} — the log is refused, not replayed as a merge`);
|
|
106
|
+
this.name = 'UnknownVerbError';
|
|
107
|
+
this.verb = verb;
|
|
108
|
+
if (site !== undefined) {
|
|
109
|
+
this.path = site.path;
|
|
110
|
+
this.row = site.row;
|
|
111
|
+
if (site.commit !== undefined)
|
|
112
|
+
this.commit = site.commit;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.UnknownVerbError = UnknownVerbError;
|
|
117
|
+
function refuseVerb(verb) {
|
|
118
|
+
throw new UnknownVerbError(verb);
|
|
119
|
+
}
|
|
120
|
+
// ─── The step ────────────────────────────────────────────────────────────────
|
|
121
|
+
/**
|
|
122
|
+
* What a `delete` row leaves behind: the KEY IS GONE — which is not the value
|
|
123
|
+
* `undefined` (a root replay removes the key; a per-path fold reads it as
|
|
124
|
+
* `undefined`). {@link applyVerb} returns it, {@link placeVerb} acts on it.
|
|
125
|
+
*/
|
|
126
|
+
exports.ABSENT = Symbol('footprintjs.verbs.absent');
|
|
127
|
+
/**
|
|
128
|
+
* One bundle's recorded payload, read by path under a clone discipline — the
|
|
129
|
+
* values {@link applyVerb} reads out of the log.
|
|
130
|
+
*
|
|
131
|
+
* `detach` — `true` for every fold that keeps its result: a `set` value or an
|
|
132
|
+
* `append` tail is a clone of what the log holds and the merge delta comes from
|
|
133
|
+
* ONE detached copy of `updates`, so the result never aliases the log. `false`
|
|
134
|
+
* only for the comparison fold ({@link dryFold}), which places recorded values
|
|
135
|
+
* as recorded and drops its result.
|
|
136
|
+
*
|
|
137
|
+
* The delta is detached ONCE per bundle, lazily — a bundle with no `merge` row
|
|
138
|
+
* pays nothing. `deepSmartMerge` dedups an array union BY REFERENCE and two
|
|
139
|
+
* `merge` rows of one bundle replay the same accumulated delta, so the rows
|
|
140
|
+
* must keep seeing the same objects; a clone per row duplicated their elements
|
|
141
|
+
* (the differential caught it in the replay; `commitValueAt` carried the bug
|
|
142
|
+
* until it folded through here). `only` narrows the copy to ONE path — a
|
|
143
|
+
* per-path fold reads nothing else, and must not pay for the rest of the
|
|
144
|
+
* bundle.
|
|
145
|
+
*/
|
|
146
|
+
class RecordedPayload {
|
|
147
|
+
updates;
|
|
148
|
+
overwrite;
|
|
149
|
+
detach;
|
|
150
|
+
only;
|
|
151
|
+
deltas;
|
|
152
|
+
constructor(updates, overwrite, detach, only) {
|
|
153
|
+
this.updates = updates;
|
|
154
|
+
this.overwrite = overwrite;
|
|
155
|
+
this.detach = detach;
|
|
156
|
+
this.only = only;
|
|
157
|
+
}
|
|
158
|
+
/** What a `set` places and an `append` adds: `overwrite[path]`, cloned when detaching. */
|
|
159
|
+
value(segs) {
|
|
160
|
+
const recorded = (0, pathOps_js_1.nativeGet)(this.overwrite, segs);
|
|
161
|
+
return this.detach ? structuredClone(recorded) : recorded;
|
|
162
|
+
}
|
|
163
|
+
/** The accumulated merge delta at `path`: `updates[path]`, from the bundle's one copy. */
|
|
164
|
+
delta(segs) {
|
|
165
|
+
if (this.deltas === undefined) {
|
|
166
|
+
const source = this.only === undefined ? this.updates : atPath(this.updates, this.only);
|
|
167
|
+
this.deltas = this.detach ? structuredClone(source) : source;
|
|
168
|
+
}
|
|
169
|
+
return (0, pathOps_js_1.nativeGet)(this.deltas, segs);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
exports.RecordedPayload = RecordedPayload;
|
|
173
|
+
/** `tree` reduced to ONE path: that sub-tree, by reference, under fresh containers. */
|
|
174
|
+
function atPath(tree, segs) {
|
|
175
|
+
const leaf = (0, pathOps_js_1.nativeGet)(tree, segs);
|
|
176
|
+
return leaf === undefined ? {} : (0, pathOps_js_1.nativeSet)({}, segs, leaf);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* ONE row, ONE step: the value at the row's path AFTER the row, given the
|
|
180
|
+
* value BEFORE it and the bundle's recorded payload. THE verb law — no other
|
|
181
|
+
* function in the library has an arm per verb.
|
|
182
|
+
*
|
|
183
|
+
* - `'set'` — the recorded value (`overwrite[path]`, the full final value).
|
|
184
|
+
* - `'merge'` — `deepSmartMerge` of the accumulated `updates[path]` delta onto
|
|
185
|
+
* the value before.
|
|
186
|
+
* - `'append'` — (delta mode) `overwrite[path]` holds ONLY the tail: concatenate
|
|
187
|
+
* it onto the value before. When either is not an array (an out-of-order
|
|
188
|
+
* replay base, or a REDACTED tail — `scrubPatch` replaces matched payloads
|
|
189
|
+
* with the `'REDACTED'` string) the tail BECOMES the value — the same
|
|
190
|
+
* terminal value a redacted or corrupt `set` produces.
|
|
191
|
+
* - `'delete'` — {@link ABSENT}: the key is removed. The path stays enumerated
|
|
192
|
+
* in `overwrite` (value `undefined`) for key-set consumers; the step ignores
|
|
193
|
+
* that value.
|
|
194
|
+
*
|
|
195
|
+
* Pure: it reads the payload and never writes. `verb` must be a {@link Verb}
|
|
196
|
+
* (the folds check with {@link isVerb} first and name the row); anything else
|
|
197
|
+
* that reaches it is refused with {@link UnknownVerbError}.
|
|
198
|
+
*/
|
|
199
|
+
function applyVerb(verb, before, at, segs) {
|
|
200
|
+
if (verb === 'set')
|
|
201
|
+
return at.value(segs);
|
|
202
|
+
if (verb === 'append') {
|
|
203
|
+
const tail = at.value(segs);
|
|
204
|
+
return Array.isArray(before) && Array.isArray(tail) ? [...before, ...tail] : tail;
|
|
205
|
+
}
|
|
206
|
+
if (verb === 'delete')
|
|
207
|
+
return exports.ABSENT;
|
|
208
|
+
if (verb === 'merge')
|
|
209
|
+
return (0, merge_js_1.deepSmartMerge)(before, at.delta(segs));
|
|
210
|
+
return refuseVerb(verb); // `verb` is `never` here — a fifth verb in the union stops compiling
|
|
211
|
+
}
|
|
212
|
+
exports.applyVerb = applyVerb;
|
|
213
|
+
/** Put a step's value at its path in `out` — or remove the key, for {@link ABSENT}. */
|
|
214
|
+
function placeVerb(out, segs, next) {
|
|
215
|
+
if (next === exports.ABSENT)
|
|
216
|
+
(0, pathOps_js_1.nativeDelete)(out, segs);
|
|
217
|
+
else
|
|
218
|
+
(0, pathOps_js_1.nativeSet)(out, segs, next);
|
|
219
|
+
}
|
|
220
|
+
exports.placeVerb = placeVerb;
|
|
221
|
+
/**
|
|
222
|
+
* Is row `i` a `set` that the NEXT row sets again — same path, same verb — so
|
|
223
|
+
* that applying it is work the next row redoes?
|
|
224
|
+
*
|
|
225
|
+
* The one skip every replay iterator shares (9.22.1). A `set` row writes a
|
|
226
|
+
* clone of the recorded value at its path; the next row, a `set` of the same
|
|
227
|
+
* path, writes a clone of the SAME recorded value over it (the bundle's patch
|
|
228
|
+
* tree is fixed), and nothing runs in between — so the first write is
|
|
229
|
+
* unobservable, container creation and key order included. Only CONSECUTIVE
|
|
230
|
+
* rows qualify: a row in between (an ancestor `merge`, a sibling `set` that
|
|
231
|
+
* creates a container) can see the intermediate value, and a `merge`,
|
|
232
|
+
* `append` or `delete` next row depends on what is there. Rows are never
|
|
233
|
+
* dropped from the log — the 9.22.0 element-write funnel records N
|
|
234
|
+
* whole-array `set` rows on one path, and this is what makes replaying them
|
|
235
|
+
* O(N) instead of O(N × rows).
|
|
236
|
+
*
|
|
237
|
+
* Asked by {@link foldRows} — live state and the redacted mirror through
|
|
238
|
+
* {@link nextGeneration}, `EventLog.materialise` and `stateAt` through
|
|
239
|
+
* {@link applySmartMergeInto}, the public {@link applySmartMerge}, and the
|
|
240
|
+
* admitted record's {@link dryFold}. {@link foldKey} needs no skip: it
|
|
241
|
+
* anchors at the top-level key's LAST `set`/`delete`, or (provenance) a
|
|
242
|
+
* repeated `set` of the one recorded value changes no birth.
|
|
243
|
+
*/
|
|
244
|
+
function supersededByNextSet(rows, i) {
|
|
245
|
+
const row = rows[i];
|
|
246
|
+
if (row.verb !== 'set')
|
|
247
|
+
return false;
|
|
248
|
+
const next = rows[i + 1];
|
|
249
|
+
return next !== undefined && next.verb === 'set' && next.path === row.path;
|
|
250
|
+
}
|
|
251
|
+
exports.supersededByNextSet = supersededByNextSet;
|
|
252
|
+
/** Does any row write THROUGH a container (a delimited, nested path)? */
|
|
253
|
+
function hasNestedRow(rows) {
|
|
254
|
+
for (let i = 0; i < rows.length; i++)
|
|
255
|
+
if (rows[i].path.indexOf(paths_js_1.DELIM) !== -1)
|
|
256
|
+
return true;
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Fold one bundle's rows into `out`, in place: for each row (a `set` the next
|
|
261
|
+
* row sets again is skipped — {@link supersededByNextSet}) take the step
|
|
262
|
+
* ({@link applyVerb}) and put its value where it belongs ({@link placeVerb}).
|
|
263
|
+
* `out` is the caller's — see {@link Discipline} for what it must be.
|
|
264
|
+
*
|
|
265
|
+
* A row whose verb is not one of {@link VERBS} stops the fold with
|
|
266
|
+
* {@link UnknownVerbError} naming the row; the rows before it have been
|
|
267
|
+
* applied, to a state the caller discards (every in-tree caller does).
|
|
268
|
+
*/
|
|
269
|
+
function foldRows(out, updates, overwrite, rows, discipline) {
|
|
270
|
+
const detach = discipline !== 'byReference';
|
|
271
|
+
const owned = discipline !== 'private' && hasNestedRow(rows) ? new WeakSet() : undefined;
|
|
272
|
+
(0, pathOps_js_1.own)(out, owned);
|
|
273
|
+
const at = new RecordedPayload(updates, overwrite, detach);
|
|
274
|
+
for (let i = 0; i < rows.length; i++) {
|
|
275
|
+
if (supersededByNextSet(rows, i))
|
|
276
|
+
continue;
|
|
277
|
+
const { path, verb } = rows[i];
|
|
278
|
+
if (!isVerb(verb))
|
|
279
|
+
throw new UnknownVerbError(verb, { path, row: i });
|
|
280
|
+
const segs = path.split(paths_js_1.DELIM);
|
|
281
|
+
if (owned !== undefined && segs.length > 1)
|
|
282
|
+
(0, pathOps_js_1.ownSpine)(out, segs, owned);
|
|
283
|
+
// A total verb reads nothing from what came before it (isTotal) — so the
|
|
284
|
+
// fold does not look.
|
|
285
|
+
const next = applyVerb(verb, isTotal(verb) ? undefined : (0, pathOps_js_1.nativeGet)(out, segs), at, segs);
|
|
286
|
+
// A value this fold CREATED is its own to edit in place; a recorded value
|
|
287
|
+
// placed by reference is the log's.
|
|
288
|
+
if (detach || verb === 'merge')
|
|
289
|
+
(0, pathOps_js_1.own)(next, owned);
|
|
290
|
+
placeVerb(out, segs, next);
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
exports.foldRows = foldRows;
|
|
295
|
+
/**
|
|
296
|
+
* Applies a commit bundle to a base state by replaying operations in order.
|
|
297
|
+
* Guarantees "last writer wins" semantics.
|
|
298
|
+
*
|
|
299
|
+
* Returns a FULLY DETACHED result: the base is deep-cloned first and every
|
|
300
|
+
* value the replay writes is its own (`set` / `append` clone the recorded
|
|
301
|
+
* value; the `merge` arm reads a detached copy of `updates`), so the result
|
|
302
|
+
* shares no container with `base`, `updates` or `overwrite`. This is the
|
|
303
|
+
* public (`foottrace`) contract, unchanged by copy-on-write
|
|
304
|
+
* (9.29.0) — byte-identical to 9.28.0 for every caller. The engine's own
|
|
305
|
+
* commit does NOT come here: `SharedMemory.applyPatch` builds the next
|
|
306
|
+
* generation with `nextGeneration`, which copies only the written
|
|
307
|
+
* paths. Both run the ONE fold, {@link foldRows}; the verbs are
|
|
308
|
+
* {@link applyVerb}'s.
|
|
309
|
+
*
|
|
310
|
+
* Verb arms — see {@link applyVerb}. A row with any other verb throws
|
|
311
|
+
* {@link UnknownVerbError}.
|
|
312
|
+
*
|
|
313
|
+
* Work, not bytes (9.22.1): a `set` row the NEXT row re-sets is skipped
|
|
314
|
+
* ({@link supersededByNextSet}) — the rows stay in the log, only the clone
|
|
315
|
+
* each would have paid is not. Every consumer inherits the skip from
|
|
316
|
+
* `foldRows`; there is no second replay loop to keep in step.
|
|
317
|
+
*/
|
|
318
|
+
function applySmartMerge(base, updates, overwrite, trace) {
|
|
319
|
+
return foldRows(structuredClone(base), updates, overwrite, trace, 'private');
|
|
320
|
+
}
|
|
321
|
+
exports.applySmartMerge = applySmartMerge;
|
|
322
|
+
/**
|
|
323
|
+
* The engine's commit (copy-on-write, 9.29.0 — docs/design/2026-10-copy-on-
|
|
324
|
+
* write-commit.md): the NEXT committed generation. A copy of `base`'s ROOT
|
|
325
|
+
* plus a copy of every container on each written path; every other subtree
|
|
326
|
+
* is SHARED with `base`, which is never edited — committed state is
|
|
327
|
+
* immutable-after-swap, so sharing is safe, and a commit costs O(what it
|
|
328
|
+
* wrote), not O(state). `SharedMemory.applyPatch` (live state and the
|
|
329
|
+
* redacted mirror) is its caller. Internal: the public replay is
|
|
330
|
+
* {@link applySmartMerge}.
|
|
331
|
+
*/
|
|
332
|
+
function nextGeneration(base, updates, overwrite, trace) {
|
|
333
|
+
return foldRows((0, pathOps_js_1.ownedRootOf)(base), updates, overwrite, trace, 'pathCopy');
|
|
334
|
+
}
|
|
335
|
+
exports.nextGeneration = nextGeneration;
|
|
336
|
+
/**
|
|
337
|
+
* The same replay, INTO `target` — no clone of its own. For a caller that
|
|
338
|
+
* already holds a private working copy (the read-side folds: `stateAt` in
|
|
339
|
+
* `time-travel/stateAt.ts` and `EventLog.materialise` clone their base ONCE
|
|
340
|
+
* and then apply every bundle here), so a fold over N bundles costs N row
|
|
341
|
+
* applications, not N clones of the whole state. Never hand it committed
|
|
342
|
+
* state: it mutates the ROOT in place. Below the root it follows the live
|
|
343
|
+
* commit's law ({@link nextGeneration}) — a container this call did not
|
|
344
|
+
* create is copied before a write passes through it — so a fold and the live
|
|
345
|
+
* state give the same value at every path, aliased subtrees included.
|
|
346
|
+
* The values it writes are its own, so `target` never aliases the log.
|
|
347
|
+
*/
|
|
348
|
+
function applySmartMergeInto(target, updates, overwrite, trace) {
|
|
349
|
+
return foldRows(target, updates, overwrite, trace, 'pathCopy');
|
|
350
|
+
}
|
|
351
|
+
exports.applySmartMergeInto = applySmartMergeInto;
|
|
352
|
+
/**
|
|
353
|
+
* The fold, for COMPARISON only (9.30.0 — the admitted record): `base` with
|
|
354
|
+
* the bundle replayed by the one verb law, copy-on-write below the root,
|
|
355
|
+
* every recorded value placed BY REFERENCE — no clone, so it costs what the
|
|
356
|
+
* bundle wrote and nothing more. It aliases the payload and every subtree of
|
|
357
|
+
* `base` the bundle did not write, and edits neither. Never hand it out,
|
|
358
|
+
* store it or write through it: `TransactionBuffer` builds one to ask whether
|
|
359
|
+
* a bundle folds back to what the stage read, then drops it.
|
|
360
|
+
*/
|
|
361
|
+
function dryFold(base, updates, overwrite, trace) {
|
|
362
|
+
return foldRows((0, pathOps_js_1.ownedRootOf)(base), updates, overwrite, trace, 'byReference');
|
|
363
|
+
}
|
|
364
|
+
exports.dryFold = dryFold;
|
|
365
|
+
/**
|
|
366
|
+
* The value of ONE key after `touches` — EVERY row under its top-level key, in
|
|
367
|
+
* commit order, each already checked with {@link isVerb} — folded from an absent
|
|
368
|
+
* key, read at the key (`segs`, its DELIM segments): the value rule of
|
|
369
|
+
* `keyPaths.ts`, the fold `stateAt` does restricted to one top-level key. Top-level
|
|
370
|
+
* keys never interact under the step, so the restriction is exact.
|
|
371
|
+
*
|
|
372
|
+
* The same step ({@link applyVerb}) and the same discipline as {@link foldRows}'s
|
|
373
|
+
* `'pathCopy'`: `set` / `append` values are clones, the merge delta is detached
|
|
374
|
+
* once per BUNDLE (narrowed to the top-level key), so two `merge` rows of one
|
|
375
|
+
* bundle see the same objects, and a row that writes through a container this
|
|
376
|
+
* fold did not create copies it first. Returns `undefined` for an absent key.
|
|
377
|
+
* O(the rows under the key's top-level key), not O(the log).
|
|
378
|
+
*/
|
|
379
|
+
function foldKey(touches, segs, options = {}) {
|
|
380
|
+
const root = segs[0];
|
|
381
|
+
let from = 0;
|
|
382
|
+
if (options.anchored === true) {
|
|
383
|
+
for (let i = touches.length - 1; i >= 0; i--) {
|
|
384
|
+
if (isTotal(touches[i].verb) && touches[i].path === root) {
|
|
385
|
+
from = i;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const state = {};
|
|
391
|
+
if (options.start !== undefined)
|
|
392
|
+
state[root] = options.start;
|
|
393
|
+
let owned = new WeakSet();
|
|
394
|
+
owned.add(state);
|
|
395
|
+
const observe = options.observe;
|
|
396
|
+
const generations = options.generations;
|
|
397
|
+
let kept = -1;
|
|
398
|
+
let keptBefore;
|
|
399
|
+
/** Drop ownership of every container: the next write through one copies it, so a kept value stays as it is. */
|
|
400
|
+
const release = () => {
|
|
401
|
+
owned = new WeakSet();
|
|
402
|
+
owned.add(state);
|
|
403
|
+
};
|
|
404
|
+
let at;
|
|
405
|
+
let atCommit = -1;
|
|
406
|
+
for (let i = from; i < touches.length; i++) {
|
|
407
|
+
const touch = touches[i];
|
|
408
|
+
if (at === undefined || touch.commitIdx !== atCommit) {
|
|
409
|
+
if (kept !== -1) {
|
|
410
|
+
generations.keep(kept, keptBefore, (0, pathOps_js_1.nativeGet)(state, segs));
|
|
411
|
+
kept = -1;
|
|
412
|
+
release();
|
|
413
|
+
}
|
|
414
|
+
if (generations !== undefined && generations.at(touch.commitIdx, state)) {
|
|
415
|
+
release();
|
|
416
|
+
kept = touch.commitIdx;
|
|
417
|
+
keptBefore = (0, pathOps_js_1.nativeGet)(state, segs);
|
|
418
|
+
}
|
|
419
|
+
at = new RecordedPayload(touch.bundle.updates, touch.bundle.overwrite, true, [root]);
|
|
420
|
+
atCommit = touch.commitIdx;
|
|
421
|
+
}
|
|
422
|
+
const rowSegs = touch.path === root ? [root] : touch.path.split(paths_js_1.DELIM);
|
|
423
|
+
const watched = observe !== undefined && (touch.relation !== undefined || options.everyRow === true);
|
|
424
|
+
let before;
|
|
425
|
+
if (watched) {
|
|
426
|
+
before = (0, pathOps_js_1.nativeGet)(state, segs);
|
|
427
|
+
if (touch.relation === 'inside' && Array.isArray(before))
|
|
428
|
+
before = before.slice();
|
|
429
|
+
}
|
|
430
|
+
if (rowSegs.length > 1)
|
|
431
|
+
(0, pathOps_js_1.ownSpine)(state, rowSegs, owned);
|
|
432
|
+
// A total verb reads nothing from what came before it (isTotal).
|
|
433
|
+
const next = applyVerb(touch.verb, isTotal(touch.verb) ? undefined : (0, pathOps_js_1.nativeGet)(state, rowSegs), at, rowSegs);
|
|
434
|
+
(0, pathOps_js_1.own)(next, owned);
|
|
435
|
+
placeVerb(state, rowSegs, next);
|
|
436
|
+
if (watched)
|
|
437
|
+
observe(touch, before, (0, pathOps_js_1.nativeGet)(state, segs));
|
|
438
|
+
}
|
|
439
|
+
if (kept !== -1)
|
|
440
|
+
generations.keep(kept, keptBefore, (0, pathOps_js_1.nativeGet)(state, segs));
|
|
441
|
+
return (0, pathOps_js_1.nativeGet)(state, segs);
|
|
442
|
+
}
|
|
443
|
+
exports.foldKey = foldKey;
|