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,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventLog — Time-travel snapshot storage for flowchart execution
|
|
3
|
+
*
|
|
4
|
+
* Like git history: stores commit bundles (diffs), not full snapshots.
|
|
5
|
+
*
|
|
6
|
+
* THE RECORD IS IMMUTABLE FROM `record` (F3, ruling R3). Every bundle is deep-frozen as it is
|
|
7
|
+
* recorded — the bundle, `overwrite` and `updates` at every depth, the trace and each row, the
|
|
8
|
+
* row's `readKeys`, `redactedPaths`, `tags`, `untrackedSources` — so a holder of a snapshot can no
|
|
9
|
+
* longer rewrite the history every later answer is folded from: an assignment into a bundle throws
|
|
10
|
+
* a `TypeError` in strict code. Safe because nothing outside the log holds a bundle's containers:
|
|
11
|
+
* the log's payload is the commit's own copy (`recordCommit` hands it the output of
|
|
12
|
+
* `scrubPatch`), and live state, the redacted mirror and write retention each take their own copy
|
|
13
|
+
* (`verbs.ts · foldRows` detaches, `StageContext · retainedForm` clones) — pinned by
|
|
14
|
+
* test/lib/memory/property/record-reachability.property.test.ts.
|
|
15
|
+
*
|
|
16
|
+
* What `Object.freeze` cannot seal — a Date's time, a Map's or Set's entries, a buffer's bytes, a
|
|
17
|
+
* RegExp, an Error's stack — is never handed out to a reader (9.44.2): `record` remembers that a
|
|
18
|
+
* bundle holds such a value (`capture/freeze.ts · freezeRecord`), and `list()` — like the public
|
|
19
|
+
* snapshot, `runner/snapshot.ts · servedSnapshot` — serves that bundle as a copy of its open paths
|
|
20
|
+
* (`serveRecord`, mapped at its first serve): fresh frozen containers down to each such value, a
|
|
21
|
+
* fresh copy of it, every other part the log's own. A reader edits only its own copy; every later
|
|
22
|
+
* serve reads the record as recorded. `recorded()` is the engine's own, unserved read. Named holes: an object hung on an array EXPANDO (arrays are walked
|
|
23
|
+
* by index — the `'indices'` walk, which keeps the freeze inside its budget; an expando is out of
|
|
24
|
+
* contract for state) and a `SharedArrayBuffer` (every copy shares its memory). A frozen RegExp's
|
|
25
|
+
* `lastIndex` is read-only, so a `/g` or `/y` regex read from a bundle throws when `exec` or
|
|
26
|
+
* `replace` advance it.
|
|
27
|
+
*/
|
|
28
|
+
import { freezeRecord, serveRecord } from '../capture/freeze.js';
|
|
29
|
+
import { applySmartMergeInto } from './utils.js';
|
|
30
|
+
export class EventLog {
|
|
31
|
+
/** Base snapshot BEFORE the first stage mutates anything. */
|
|
32
|
+
base;
|
|
33
|
+
/** Ordered list of commit bundles. */
|
|
34
|
+
steps = [];
|
|
35
|
+
sourceAddress;
|
|
36
|
+
sourceAddressRetired = false;
|
|
37
|
+
constructor(initialMemory) {
|
|
38
|
+
// Normalised to an object: the base is a STATE, and every fold over it
|
|
39
|
+
// (`materialise`, `stateAt`) applies path writes to it. `undefined` —
|
|
40
|
+
// what a run with no `initialContext` used to store here — made every
|
|
41
|
+
// one of those folds throw on the first `set`.
|
|
42
|
+
this.base = initialMemory === undefined ? {} : structuredClone(initialMemory);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The fold BASE — the state as it stood before the first commit.
|
|
46
|
+
*
|
|
47
|
+
* This is the other half of the commit log: bundles are DIFFS, so a log
|
|
48
|
+
* alone cannot reconstruct state that was seeded before the run (an
|
|
49
|
+
* executor `initialContext`, `defaultValuesForContext`, a resume's
|
|
50
|
+
* `checkpoint.sharedState`, a subflow's `inputMapper` seed). Travelling
|
|
51
|
+
* with the log — `RuntimeSnapshot.initialState` — is what lets an OFFLINE
|
|
52
|
+
* consumer fold; without it the honest answer is a partial one
|
|
53
|
+
* (`stateAt`'s `basis: 'log-only'`).
|
|
54
|
+
*
|
|
55
|
+
* Returns a fresh detached clone on every call — the caller owns it.
|
|
56
|
+
*/
|
|
57
|
+
getInitialState() {
|
|
58
|
+
return structuredClone(this.base);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reconstructs the full state at any given step.
|
|
62
|
+
* Replays commits from the beginning — O(n) but low memory footprint.
|
|
63
|
+
*
|
|
64
|
+
* INDEX CONVENTION — `stepIdx` is EXCLUSIVE: `materialise(3)` folds commits
|
|
65
|
+
* 0, 1 and 2. `stateAt(source, commitIdx)` (foottrace) folds the
|
|
66
|
+
* same log but its `commitIdx` is INCLUSIVE, because a reader's cursor asks
|
|
67
|
+
* "the state AT this stop", which must include that stop's own commit. Two
|
|
68
|
+
* folds over one log with opposite conventions: check which one you hold.
|
|
69
|
+
*
|
|
70
|
+
* One clone of the base per call, then every bundle replayed INTO that
|
|
71
|
+
* private copy (`applySmartMergeInto` — the live commit's copy-on-write law
|
|
72
|
+
* below the root, 9.29.0, so the fold and the live state agree at every
|
|
73
|
+
* path). The result is the caller's: it shares nothing with the base or
|
|
74
|
+
* the log.
|
|
75
|
+
*
|
|
76
|
+
* @deprecated Since 9.33.0 — use `stateAt` from `foottrace`, which folds the same log
|
|
77
|
+
* from the same base (`RuntimeSnapshot.initialState`) and says how it was derived (`basis`).
|
|
78
|
+
* Nothing in the library calls this; it stays for one minor and is then removed. Mind the
|
|
79
|
+
* index: `materialise(n)` folds commits `0..n-1`, `stateAt(source, n - 1)` the same commits.
|
|
80
|
+
*/
|
|
81
|
+
materialise(stepIdx = this.steps.length) {
|
|
82
|
+
const out = structuredClone(this.base);
|
|
83
|
+
for (let i = 0; i < stepIdx; i++) {
|
|
84
|
+
const { overwrite, updates, trace } = this.steps[i];
|
|
85
|
+
applySmartMergeInto(out, updates, overwrite, trace);
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Persists a commit bundle for a finished stage, and FREEZES it (see the module doc): the bundle
|
|
91
|
+
* is stamped with its position, then deep-frozen, then appended. The engine hands it a bundle
|
|
92
|
+
* built for the log alone; a caller of this class (`foottrace/write`) hands over a bundle it
|
|
93
|
+
* will no longer edit — its own object is the one frozen.
|
|
94
|
+
*/
|
|
95
|
+
record(bundle) {
|
|
96
|
+
bundle.idx = this.steps.length;
|
|
97
|
+
freezeRecord(bundle, 'indices');
|
|
98
|
+
this.steps.push(bundle);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Every recorded commit bundle, AS SERVED (9.44.2): a new array each call; a bundle freezing sealed
|
|
102
|
+
* whole is the log's own (frozen) object, one holding a Date, Map, buffer … is a copy of its open
|
|
103
|
+
* paths (`capture/freeze.ts · serveRecord`) — the reader's own, so no edit reaches the log.
|
|
104
|
+
*/
|
|
105
|
+
list() {
|
|
106
|
+
return this.steps.map(serveRecord);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* @internal The bundles as the log holds them — each frozen, NOT served — in a new array, so a holder's
|
|
110
|
+
* `splice` reaches only its own. For the engine's own snapshot (`ExecutionRuntime · getSnapshot`, which
|
|
111
|
+
* a subflow mount takes once per mount), so the run serves nothing; a reader is served by `list()` and
|
|
112
|
+
* `getSnapshot().commitLog`. A known open door (CHANGELOG 9.44.2): a holder of these bundles can still
|
|
113
|
+
* edit a Date, Map or buffer inside them.
|
|
114
|
+
*/
|
|
115
|
+
recorded() {
|
|
116
|
+
return this.steps.slice();
|
|
117
|
+
}
|
|
118
|
+
/** Number of recorded commits. */
|
|
119
|
+
get length() {
|
|
120
|
+
return this.steps.length;
|
|
121
|
+
}
|
|
122
|
+
/** Immutable identity of this log, absent until the engine binds it. */
|
|
123
|
+
get address() {
|
|
124
|
+
return this.sourceAddress;
|
|
125
|
+
}
|
|
126
|
+
/** @internal Bind once; resuming the same log must not rename its earlier positions. */
|
|
127
|
+
bindAddress(logRunId, drillPath) {
|
|
128
|
+
if (this.sourceAddressRetired)
|
|
129
|
+
return;
|
|
130
|
+
this.sourceAddress ??= Object.freeze({ logRunId, drillPath: Object.freeze([...drillPath]) });
|
|
131
|
+
}
|
|
132
|
+
/** O(1) source-time capture. No counter, observer bookkeeping, or future commit prediction. */
|
|
133
|
+
capturePosition(runId) {
|
|
134
|
+
if (!this.sourceAddress)
|
|
135
|
+
return undefined;
|
|
136
|
+
return Object.freeze({ runId, ...this.sourceAddress, committedThroughIdx: this.steps.length - 1 });
|
|
137
|
+
}
|
|
138
|
+
/** Wipes history (useful for test resets). */
|
|
139
|
+
clear() {
|
|
140
|
+
this.steps = [];
|
|
141
|
+
// A reset prefix cannot reuse old coordinates, even if another stage of
|
|
142
|
+
// the SAME leg binds it again. Keep clear's data behavior, but retire
|
|
143
|
+
// addressing for this log; known coordinates require a fresh EventLog.
|
|
144
|
+
this.sourceAddress = undefined;
|
|
145
|
+
this.sourceAddressRetired = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecordFrame — the record half of one stage's frame (C3, L3).
|
|
3
|
+
*
|
|
4
|
+
* What a stage's frame needs to READ and WRITE the record, and nothing about the stage inside a run:
|
|
5
|
+
* where it writes (its address), what it read first (the first-touch base), what it staged (the lazy
|
|
6
|
+
* transaction buffer) and which of it the log must not show (the scrub of each write), which keys it
|
|
7
|
+
* read (the readKeys list), and where a commit lands (live state, the redacted mirror, the log —
|
|
8
|
+
* through `recordCommit`). The engine's frame (`StageContext`, L4) composes one and keeps the rest:
|
|
9
|
+
* retention, diagnostics, the dev-mode warnings, the commit observer and the redaction DECISION —
|
|
10
|
+
* which value is secret (`redaction.ts · decideWrite`); the frame writes the bytes it is handed (C4).
|
|
11
|
+
*
|
|
12
|
+
* THE LAW:
|
|
13
|
+
* 1. ADDRESS. The frame reads and writes at its address, a path prefix the engine computed and hands
|
|
14
|
+
* over as data (C2: `['runs', <id>]` or `[]`, the root). It moves only before the first write:
|
|
15
|
+
* the buffer keeps the address it was built at (the engine refuses a later move).
|
|
16
|
+
* 2. FIRST TOUCH. The committed generation the frame first touched — its first read OR first write —
|
|
17
|
+
* is held by reference, never cloned: the read view before the first write and the buffer's
|
|
18
|
+
* net-change diff base after it.
|
|
19
|
+
* 3. TWO TIERS. A read is served from the stage's own view (the buffer's working copy once the stage
|
|
20
|
+
* wrote, else the first-touch base); a key absent there is served from LIVE state (at the
|
|
21
|
+
* address, then the root). After the first write the buffer's diff base is first given a private
|
|
22
|
+
* copy at that path (`TransactionBuffer · detachBase`).
|
|
23
|
+
* 4. LAZY BUFFER. The buffer is built at the first write — on the first-touch base, at the address,
|
|
24
|
+
* under the encoding. A frame that never writes builds none, and its commit is the empty bundle.
|
|
25
|
+
* 5. READ KEYS. Under `writeProvenance: 'reads-prefix'` every noted read puts its user-level key
|
|
26
|
+
* (dotted for a nested path) on the frame's list, once, in order; each staged row carries a copy
|
|
27
|
+
* of the list as it stands when the row is staged.
|
|
28
|
+
* 6. COMMIT, RELEASE, DISCARD. `commit` takes the buffer's payload (none = the empty commit), then
|
|
29
|
+
* the names, and hands both to `recordCommit`. `release` drops the buffer and the base; a frame
|
|
30
|
+
* touched again re-anchors on the state as it stands then. The readKeys list survives a release,
|
|
31
|
+
* and goes with `discard` (a failed attempt never happened).
|
|
32
|
+
* 7. WRITE. `write(path, value, verb, scrub?)` is the one way to stage: `set`, `merge` or `delete`
|
|
33
|
+
* at an absolute path (the address already joined — `at`), with the bytes of a redaction
|
|
34
|
+
* verdict. `whole` → the log and the mirror carry `LOG_PLACEHOLDER` at the path; `fields` → at
|
|
35
|
+
* each field inside the value (a literal key and, when dotted, the nested path) — wherever it
|
|
36
|
+
* holds a value at commit (`scrub.ts`). No scrub → the value as it is. The frame never decides
|
|
37
|
+
* a scrub, and nothing outside it reaches its buffer.
|
|
38
|
+
*/
|
|
39
|
+
import type { OpVerb } from './deltaEncoding.js';
|
|
40
|
+
import type { EventLog } from './EventLog.js';
|
|
41
|
+
import { type CommitStamp } from './recordCommit.js';
|
|
42
|
+
import type { SharedMemory } from './SharedMemory.js';
|
|
43
|
+
import type { CommitValuesMode, WriteProvenanceMode } from './types.js';
|
|
44
|
+
/**
|
|
45
|
+
* The two dials that shape a frame's bytes: the log's value encoding and the per-row read prefix.
|
|
46
|
+
* Held by reference and swapped whole (`useEncoding`), never edited — the engine hands the run's
|
|
47
|
+
* frozen policy itself. Absent: `'full'` values and no read prefix.
|
|
48
|
+
*/
|
|
49
|
+
export interface RecordEncoding {
|
|
50
|
+
readonly commitValues: CommitValuesMode;
|
|
51
|
+
readonly writeProvenance: WriteProvenanceMode;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* What a stage can stage (law 7) — the transaction buffer's op verbs (`deltaEncoding.ts · OpVerb`), one
|
|
55
|
+
* union. `append` is not one: it is how the log encodes a grown array (`'delta'`).
|
|
56
|
+
*/
|
|
57
|
+
export type WriteVerb = OpVerb;
|
|
58
|
+
/**
|
|
59
|
+
* The bytes of a redaction verdict for one write (law 7): which of it the commit log and the redacted
|
|
60
|
+
* mirror must not show. The engine decides it (`redaction.ts · decideWrite`); the frame only writes it.
|
|
61
|
+
*/
|
|
62
|
+
export interface WriteScrub {
|
|
63
|
+
/** The whole value: `LOG_PLACEHOLDER` at the write's path. */
|
|
64
|
+
readonly whole?: boolean;
|
|
65
|
+
/** Dot-paths INSIDE the value, relative to it: `LOG_PLACEHOLDER` at each one that holds a value. */
|
|
66
|
+
readonly fields?: readonly string[];
|
|
67
|
+
}
|
|
68
|
+
export declare class RecordFrame {
|
|
69
|
+
/** Live committed state — the heap every frame of the run shares. */
|
|
70
|
+
readonly state: SharedMemory;
|
|
71
|
+
/** The log the frame's commits land on, when the run keeps one. */
|
|
72
|
+
readonly log?: EventLog;
|
|
73
|
+
/** The redacted mirror the frame's commits land on, when the run keeps one ({@link useMirror}). */
|
|
74
|
+
private _mirror?;
|
|
75
|
+
/** Where the frame reads and writes (law 1). */
|
|
76
|
+
private _address;
|
|
77
|
+
/** The two dials (law 4, 5); absent → the record's defaults. */
|
|
78
|
+
private encoding?;
|
|
79
|
+
/**
|
|
80
|
+
* The committed generation at the frame's FIRST touch (law 2) — held by REFERENCE, never cloned.
|
|
81
|
+
* See {@link firstTouch} for the algorithm and the immutability invariant that makes a bare
|
|
82
|
+
* reference safe.
|
|
83
|
+
*/
|
|
84
|
+
private base?;
|
|
85
|
+
/** The stage's staged writes — built at its first write (law 4), released at commit. */
|
|
86
|
+
private buffer?;
|
|
87
|
+
/**
|
|
88
|
+
* Keys tracked-read so far, in order (law 5) — the source of each staged row's `readKeys`. Only
|
|
89
|
+
* allocated under `'reads-prefix'`; insertion-ordered (a Set) and monotonic, which is what makes
|
|
90
|
+
* "last write's prefix == union" hold in delta mode.
|
|
91
|
+
*/
|
|
92
|
+
private readKeys?;
|
|
93
|
+
constructor(state: SharedMemory, log?: EventLog, address?: readonly string[]);
|
|
94
|
+
/** Where the frame reads and writes (law 1). */
|
|
95
|
+
get address(): readonly string[];
|
|
96
|
+
/**
|
|
97
|
+
* Read and write at `address` from now on — before the frame's first write (law 1): the buffer keeps
|
|
98
|
+
* the address it was built at. The engine's one caller, `StageContext · useAddressOf`, refuses a move
|
|
99
|
+
* once the frame has staged.
|
|
100
|
+
*/
|
|
101
|
+
useAddress(address: readonly string[]): void;
|
|
102
|
+
/** The two dials the frame encodes under from its next buffer and its next read on (law 4, 5). */
|
|
103
|
+
useEncoding(encoding: RecordEncoding): void;
|
|
104
|
+
/** The redacted mirror, if the run keeps one. */
|
|
105
|
+
get mirror(): SharedMemory | undefined;
|
|
106
|
+
/** Land every commit from now on on `mirror` too, with the scrubbed rows the log takes (`recordCommit`). */
|
|
107
|
+
useMirror(mirror: SharedMemory | undefined): void;
|
|
108
|
+
/** The absolute path of `key` under `path`: the frame's address, then the path, then the key. */
|
|
109
|
+
at(path: readonly string[], key: string): string[];
|
|
110
|
+
/**
|
|
111
|
+
* ── The first-touch base (#13) ───────────────────────────────────────────
|
|
112
|
+
*
|
|
113
|
+
* WHAT: returns the committed shared state as it was at this frame's FIRST touch (first read or
|
|
114
|
+
* first write), capturing the reference on first call. Serves two consumers: reads before the first
|
|
115
|
+
* write ({@link read}) and the transaction buffer's diff base ({@link getTransactionBuffer}).
|
|
116
|
+
*
|
|
117
|
+
* WHY A BARE REFERENCE IS SAFE — the invariant this rests on: committed state is
|
|
118
|
+
* immutable-after-swap. Every write to `SharedMemory` builds the NEXT generation and swaps it in
|
|
119
|
+
* (copy-on-write, 9.29.0: `applyPatch` via `nextGeneration`, and `setValue`/`updateValue` too) — it
|
|
120
|
+
* copies the root and the containers on each written path, shares the rest, and never edits a
|
|
121
|
+
* container of the generation a stage captured here. Holding the reference therefore gives this
|
|
122
|
+
* stage a stable snapshot at zero cost — no clone, which is the entire point of #13. The transaction
|
|
123
|
+
* buffer's net-change diff base rests on the same guarantee: it IS this view.
|
|
124
|
+
*
|
|
125
|
+
* WHY FIRST TOUCH, not first write: the pre-#13 eager engine cloned the state into the buffer at
|
|
126
|
+
* the stage's first ACCESS, anchoring both its snapshot reads and its commit baseline (the
|
|
127
|
+
* net-change diff base) there. #13's first cut anchored the lazy buffer at first WRITE — observably
|
|
128
|
+
* different when something else commits in the gap between this stage's first read and its first
|
|
129
|
+
* write. That gap is REACHABLE: fork siblings are namespace-isolated for run-scoped keys (each child
|
|
130
|
+
* writes under `runs/<childId>/`), but ROOT-level keys are shared — written via `setGlobal` from
|
|
131
|
+
* consumer scope code and, critically, by `SubflowInputMapper`'s output mapping
|
|
132
|
+
* (`parentContext.setGlobal`), which is exactly what runs when a subflow is a fork branch. A
|
|
133
|
+
* sibling's root-key commit landing in the gap would shift this stage's diff base, making its
|
|
134
|
+
* CommitBundle record a phantom change (or swallow a real one) relative to the eager engine.
|
|
135
|
+
* Anchoring the view at first touch restores the EXACT eager semantics — sequential AND parallel —
|
|
136
|
+
* at zero clone cost.
|
|
137
|
+
*
|
|
138
|
+
* Read visibility is two-tier, matching eager byte-for-byte: keys present in the view at first
|
|
139
|
+
* touch read repeatably from it; keys ABSENT from it fall back to LIVE state (the eager engine's
|
|
140
|
+
* exact fallback — a mid-flight sibling root-key write was always visible to reads, and stays
|
|
141
|
+
* visible; only the DIFF BASE is pinned).
|
|
142
|
+
*/
|
|
143
|
+
private firstTouch;
|
|
144
|
+
/**
|
|
145
|
+
* The transaction buffer, created lazily on the frame's FIRST WRITE (#13).
|
|
146
|
+
*
|
|
147
|
+
* Reads NEVER construct it: read-your-writes only matters once a staged write exists, so before that
|
|
148
|
+
* {@link read} serves from the first-touch base and {@link commit} records an empty bundle — all with
|
|
149
|
+
* ZERO `structuredClone`s of the shared state.
|
|
150
|
+
*
|
|
151
|
+
* The buffer's base is the FIRST-TOUCH view, NOT the live state at write time: under parallel forks
|
|
152
|
+
* a sibling may have committed between this frame's first read and this write, and the net-change
|
|
153
|
+
* diff base must stay anchored at first touch to match the eager engine — see {@link firstTouch}.
|
|
154
|
+
*
|
|
155
|
+
* Private since C4: every write goes through {@link write}, so the scrub travels with the op.
|
|
156
|
+
*/
|
|
157
|
+
private getTransactionBuffer;
|
|
158
|
+
/**
|
|
159
|
+
* Stage one write (law 7): `verb` at `path` — an absolute path, the address already joined
|
|
160
|
+
* ({@link at}) — and the bytes of its redaction verdict. The first write builds the buffer (law 4).
|
|
161
|
+
*
|
|
162
|
+
* The scrub registers paths, never values: `whole` marks the op's own path, `fields` each field
|
|
163
|
+
* inside the value (a literal key and, when dotted, the nested path); `recordCommit` puts the
|
|
164
|
+
* log's placeholder wherever one of them holds a value at commit, for the mirror and the log only
|
|
165
|
+
* (`scrub.ts`). Live state takes the value as it is. A path marked by a write that the net-change
|
|
166
|
+
* filter drops goes with it.
|
|
167
|
+
*/
|
|
168
|
+
write(path: (string | number)[], value: unknown, verb: WriteVerb, scrub?: WriteScrub): void;
|
|
169
|
+
/** Has the frame staged a write since its last release — does its buffer exist? */
|
|
170
|
+
get hasStaged(): boolean;
|
|
171
|
+
/**
|
|
172
|
+
* The frame's read, mirroring the eager engine's read order byte-for-byte (law 3):
|
|
173
|
+
*
|
|
174
|
+
* 1. staged writes + first-touch snapshot — `buffer.get` over its workingCopy when the buffer
|
|
175
|
+
* exists, else `nativeGet` over the zero-clone first-touch base (the buffer's base IS that
|
|
176
|
+
* view, so the two tiers agree on content);
|
|
177
|
+
* 2. LIVE state via `SharedMemory · getValue` for keys absent from the snapshot — including its
|
|
178
|
+
* address→root fallback. The eager engine had this exact live fallback for snapshot-missing
|
|
179
|
+
* keys; byte-identity over purity. After the frame's first write such a value can be the very
|
|
180
|
+
* container the buffer's diff base holds at the path (the stage deleted or unset it, or
|
|
181
|
+
* replaced a container above it), so the base is detached there first
|
|
182
|
+
* (`TransactionBuffer · detachBase`): an in-place edit of the value (out of contract) written
|
|
183
|
+
* back is recorded, as on 9.28.0, whose base was a clone taken at the first write.
|
|
184
|
+
*
|
|
185
|
+
* Reads never construct the buffer (#13): a frame that never writes performs zero clones of the
|
|
186
|
+
* shared state.
|
|
187
|
+
*/
|
|
188
|
+
read(path: string[], key?: string): unknown;
|
|
189
|
+
/** Put a tracked read's user-level key on the readKeys list (law 5) — a no-op unless the frame keeps one. */
|
|
190
|
+
noteRead(path: readonly string[], key: string): void;
|
|
191
|
+
/**
|
|
192
|
+
* Live committed state at `key`: at the frame's address, else at the root — never the stage's own
|
|
193
|
+
* view. Typed as `SharedMemory · getValue` types it, which `StageContext · getRoot` hands on.
|
|
194
|
+
*/
|
|
195
|
+
readLive(key: string): any;
|
|
196
|
+
/** Live committed state at `key` at the root — typed as `SharedMemory · getValue` (`StageContext · getGlobal`). */
|
|
197
|
+
readGlobal(key: string): any;
|
|
198
|
+
/** Did a staged op touch `path` — on it, above it or below it? (A report's question; never the hot path.) */
|
|
199
|
+
wasStaged(path: (string | number)[]): boolean;
|
|
200
|
+
/**
|
|
201
|
+
* The value at `path` in what the stage has seen — its working copy once it wrote, else its
|
|
202
|
+
* first-touch base — WITHOUT taking a private copy: for a report that only compares.
|
|
203
|
+
*/
|
|
204
|
+
peek(path: (string | number)[]): unknown;
|
|
205
|
+
/** The value at `path` in the first-touch base — committed state as the frame first touched it. */
|
|
206
|
+
baseAt(path: (string | number)[]): unknown;
|
|
207
|
+
/**
|
|
208
|
+
* Record what the frame staged (law 6): the buffer's payload first — none, the empty commit, when
|
|
209
|
+
* it staged nothing (#13: zero clones) — then the names (`stampOf()`), read once the payload is
|
|
210
|
+
* built, and both to `recordCommit`. User code can run while the payload is built (a getter on a
|
|
211
|
+
* staged value, which the net-change compare and the clone invoke); what it adds to the names —
|
|
212
|
+
* an untracked read, say — is on the bundle.
|
|
213
|
+
*/
|
|
214
|
+
commit(stampOf: () => CommitStamp): void;
|
|
215
|
+
/**
|
|
216
|
+
* End the frame's hold on state (#13b, law 6): drop the buffer (its working copy and whatever private
|
|
217
|
+
* copies its reads took) and the first-touch base (a reference that pins one committed-state
|
|
218
|
+
* GENERATION). Both re-create lazily: a later read re-anchors on the CURRENT committed state, and a
|
|
219
|
+
* later write builds a fresh buffer on it — so a second commit diffs against the state after the
|
|
220
|
+
* first. Without the release, the execution tree — which retains every frame for the lifetime of
|
|
221
|
+
* the run — retains one state generation per executed stage (measured O(N²) before copy-on-write;
|
|
222
|
+
* a 500-iteration agent OOMed a default Node heap, backlog #18).
|
|
223
|
+
*/
|
|
224
|
+
release(): void;
|
|
225
|
+
/**
|
|
226
|
+
* Throw away everything the frame staged and read (law 6): the buffer, the first-touch base (the next
|
|
227
|
+
* attempt re-anchors on committed state as it stands NOW) and the readKeys list — a discarded
|
|
228
|
+
* attempt's reads must never reach the next attempt's `TraceEntry.readKeys`, or a backward slice
|
|
229
|
+
* would follow an edge that no committed write ever had. Nothing is recorded.
|
|
230
|
+
*/
|
|
231
|
+
discard(): void;
|
|
232
|
+
}
|