foottrace 1.0.3 → 1.2.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/README.md +137 -4
- package/dist/cause.js +14 -0
- package/dist/esm/cause.d.ts +8 -0
- package/dist/esm/cause.js +5 -0
- package/dist/esm/index.d.ts +7 -2
- package/dist/esm/index.js +2 -1
- package/dist/esm/lib/cause/admission.d.ts +27 -0
- package/dist/esm/lib/cause/admission.js +59 -0
- package/dist/esm/lib/cause/explain.d.ts +5 -0
- package/dist/esm/lib/cause/explain.js +114 -0
- package/dist/esm/lib/cause/ranking.d.ts +6 -0
- package/dist/esm/lib/cause/ranking.js +10 -0
- package/dist/esm/lib/cause/recordAdapter.d.ts +9 -0
- package/dist/esm/lib/cause/recordAdapter.js +49 -0
- package/dist/esm/lib/cause/recordChildren.d.ts +4 -0
- package/dist/esm/lib/cause/recordChildren.js +79 -0
- package/dist/esm/lib/cause/recordControl.d.ts +13 -0
- package/dist/esm/lib/cause/recordControl.js +66 -0
- package/dist/esm/lib/cause/recordCoverage.d.ts +24 -0
- package/dist/esm/lib/cause/recordCoverage.js +87 -0
- package/dist/esm/lib/cause/recordIdentity.d.ts +9 -0
- package/dist/esm/lib/cause/recordIdentity.js +67 -0
- package/dist/esm/lib/cause/recordLoading.d.ts +7 -0
- package/dist/esm/lib/cause/recordLoading.js +116 -0
- package/dist/esm/lib/cause/recordParents.d.ts +4 -0
- package/dist/esm/lib/cause/recordParents.js +86 -0
- package/dist/esm/lib/cause/recordScope.d.ts +8 -0
- package/dist/esm/lib/cause/recordScope.js +60 -0
- package/dist/esm/lib/cause/recordTypes.d.ts +67 -0
- package/dist/esm/lib/cause/recordTypes.js +1 -0
- package/dist/esm/lib/cause/recordValidation.d.ts +33 -0
- package/dist/esm/lib/cause/recordValidation.js +198 -0
- package/dist/esm/lib/cause/session.d.ts +30 -0
- package/dist/esm/lib/cause/session.js +161 -0
- package/dist/esm/lib/cause/storedRecordAdapter.d.ts +11 -0
- package/dist/esm/lib/cause/storedRecordAdapter.js +57 -0
- package/dist/esm/lib/cause/strategies.d.ts +3 -0
- package/dist/esm/lib/cause/strategies.js +21 -0
- package/dist/esm/lib/cause/types.d.ts +90 -0
- package/dist/esm/lib/cause/types.js +1 -0
- package/dist/esm/lib/cause/validation.d.ts +24 -0
- package/dist/esm/lib/cause/validation.js +134 -0
- package/dist/esm/lib/cause/walk.d.ts +12 -0
- package/dist/esm/lib/cause/walk.js +174 -0
- package/dist/esm/lib/ids/attemptId.d.ts +63 -0
- package/dist/esm/lib/ids/attemptId.js +91 -0
- package/dist/esm/lib/memory/EventLog.d.ts +89 -3
- package/dist/esm/lib/memory/EventLog.js +242 -3
- package/dist/esm/lib/memory/honesty.d.ts +6 -4
- package/dist/esm/lib/memory/honesty.js +7 -4
- package/dist/esm/lib/memory/sink.d.ts +148 -0
- package/dist/esm/lib/memory/sink.js +92 -0
- package/dist/esm/lib/memory/types.d.ts +4 -1
- package/dist/esm/lib/memory/types.js +1 -1
- package/dist/esm/lib/time-travel/bundles.d.ts +2 -0
- package/dist/esm/lib/time-travel/bundles.js +16 -0
- package/dist/esm/lib/time-travel/commitStops.js +21 -2
- package/dist/esm/lib/time-travel/index.d.ts +2 -0
- package/dist/esm/lib/time-travel/index.js +1 -0
- package/dist/esm/lib/time-travel/storedRecord.d.ts +195 -0
- package/dist/esm/lib/time-travel/storedRecord.js +494 -0
- package/dist/esm/lib/time-travel/timeTravel.js +11 -0
- package/dist/esm/lib/time-travel/types.d.ts +4 -1
- package/dist/esm/write.d.ts +7 -0
- package/dist/esm/write.js +6 -0
- package/dist/index.js +6 -2
- package/dist/lib/cause/admission.js +67 -0
- package/dist/lib/cause/explain.js +119 -0
- package/dist/lib/cause/ranking.js +14 -0
- package/dist/lib/cause/recordAdapter.js +55 -0
- package/dist/lib/cause/recordChildren.js +83 -0
- package/dist/lib/cause/recordControl.js +74 -0
- package/dist/lib/cause/recordCoverage.js +99 -0
- package/dist/lib/cause/recordIdentity.js +73 -0
- package/dist/lib/cause/recordLoading.js +121 -0
- package/dist/lib/cause/recordParents.js +90 -0
- package/dist/lib/cause/recordScope.js +66 -0
- package/dist/lib/cause/recordTypes.js +2 -0
- package/dist/lib/cause/recordValidation.js +213 -0
- package/dist/lib/cause/session.js +165 -0
- package/dist/lib/cause/storedRecordAdapter.js +61 -0
- package/dist/lib/cause/strategies.js +26 -0
- package/dist/lib/cause/types.js +2 -0
- package/dist/lib/cause/validation.js +150 -0
- package/dist/lib/cause/walk.js +180 -0
- package/dist/lib/ids/attemptId.js +96 -0
- package/dist/lib/memory/EventLog.js +242 -3
- package/dist/lib/memory/honesty.js +7 -4
- package/dist/lib/memory/sink.js +98 -0
- package/dist/lib/memory/types.js +1 -1
- package/dist/lib/time-travel/bundles.js +18 -1
- package/dist/lib/time-travel/commitStops.js +21 -2
- package/dist/lib/time-travel/index.js +3 -1
- package/dist/lib/time-travel/storedRecord.js +498 -0
- package/dist/lib/time-travel/timeTravel.js +11 -0
- package/dist/types/cause.d.ts +8 -0
- package/dist/types/index.d.ts +7 -2
- package/dist/types/lib/cause/admission.d.ts +27 -0
- package/dist/types/lib/cause/explain.d.ts +5 -0
- package/dist/types/lib/cause/ranking.d.ts +6 -0
- package/dist/types/lib/cause/recordAdapter.d.ts +9 -0
- package/dist/types/lib/cause/recordChildren.d.ts +4 -0
- package/dist/types/lib/cause/recordControl.d.ts +13 -0
- package/dist/types/lib/cause/recordCoverage.d.ts +24 -0
- package/dist/types/lib/cause/recordIdentity.d.ts +9 -0
- package/dist/types/lib/cause/recordLoading.d.ts +7 -0
- package/dist/types/lib/cause/recordParents.d.ts +4 -0
- package/dist/types/lib/cause/recordScope.d.ts +8 -0
- package/dist/types/lib/cause/recordTypes.d.ts +67 -0
- package/dist/types/lib/cause/recordValidation.d.ts +33 -0
- package/dist/types/lib/cause/session.d.ts +30 -0
- package/dist/types/lib/cause/storedRecordAdapter.d.ts +11 -0
- package/dist/types/lib/cause/strategies.d.ts +3 -0
- package/dist/types/lib/cause/types.d.ts +90 -0
- package/dist/types/lib/cause/validation.d.ts +24 -0
- package/dist/types/lib/cause/walk.d.ts +12 -0
- package/dist/types/lib/ids/attemptId.d.ts +63 -0
- package/dist/types/lib/memory/EventLog.d.ts +89 -3
- package/dist/types/lib/memory/honesty.d.ts +6 -4
- package/dist/types/lib/memory/sink.d.ts +148 -0
- package/dist/types/lib/memory/types.d.ts +4 -1
- package/dist/types/lib/time-travel/bundles.d.ts +2 -0
- package/dist/types/lib/time-travel/index.d.ts +2 -0
- package/dist/types/lib/time-travel/storedRecord.d.ts +195 -0
- package/dist/types/lib/time-travel/types.d.ts +4 -1
- package/dist/types/write.d.ts +7 -0
- package/dist/write.js +8 -1
- package/package.json +67 -13
|
@@ -24,9 +24,92 @@
|
|
|
24
24
|
* contract for state) and a `SharedArrayBuffer` (every copy shares its memory). A frozen RegExp's
|
|
25
25
|
* `lastIndex` is read-only, so a `/g` or `/y` regex read from a bundle throws when `exec` or
|
|
26
26
|
* `replace` advance it.
|
|
27
|
+
*
|
|
28
|
+
* A SINK (1.1.0, optional). `new EventLog(base, { sink, address, meta? })` also hands every entry to a
|
|
29
|
+
* `RecordSink` AS IT LANDS (`sink.ts`): `opened` from the constructor (the base, as the sink's own
|
|
30
|
+
* copy), `commit` from `record` after the bundle is frozen and appended (the bundle as served),
|
|
31
|
+
* `note` from `note`, `closed` from `close`. The log's own record never depends on the sink: the
|
|
32
|
+
* bundle is in `steps` before the sink sees it, and nothing the sink does reaches the writer — a
|
|
33
|
+
* throw, a rejected promise or an entry the log refuses to send (a `note` whose data is not a plain
|
|
34
|
+
* object that survives `structuredClone`) is counted (`sinkFailures`) and reported to
|
|
35
|
+
* `sink.onFailure`, and the writer goes on; a `meta` that will not copy is lost on its own and its
|
|
36
|
+
* entry still goes. Without a sink, nothing here runs: the one-argument log writes the same bytes,
|
|
37
|
+
* and has the same own fields, it always did.
|
|
27
38
|
*/
|
|
28
39
|
import { freezeRecord, serveRecord } from '../capture/freeze.js';
|
|
40
|
+
import { isEntryObject, LOG_OUTCOMES, sendEntry, } from './sink.js';
|
|
29
41
|
import { applySmartMergeInto } from './utils.js';
|
|
42
|
+
/** True when `value` has a callable `then` — an adapter that answered with a promise. */
|
|
43
|
+
function isThenable(value) {
|
|
44
|
+
return ((typeof value === 'object' || typeof value === 'function') &&
|
|
45
|
+
value !== null &&
|
|
46
|
+
typeof value.then === 'function');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The address, copied segment by segment and frozen — the log's identity cannot be edited through the
|
|
50
|
+
* caller's object, and a path with a hole or a non-string segment is refused rather than copied as one.
|
|
51
|
+
*/
|
|
52
|
+
function frozenAddress(address) {
|
|
53
|
+
const { logRunId, drillPath } = (address ?? {});
|
|
54
|
+
const refused = () => new TypeError('EventLog: a sink needs an address — { logRunId: string, drillPath: string[] }');
|
|
55
|
+
if (typeof logRunId !== 'string' || !Array.isArray(drillPath))
|
|
56
|
+
throw refused();
|
|
57
|
+
const segments = [];
|
|
58
|
+
for (let i = 0; i < drillPath.length; i++) {
|
|
59
|
+
const segment = drillPath[i];
|
|
60
|
+
if (typeof segment !== 'string')
|
|
61
|
+
throw refused();
|
|
62
|
+
segments.push(segment);
|
|
63
|
+
}
|
|
64
|
+
return Object.freeze({ logRunId, drillPath: Object.freeze(segments) });
|
|
65
|
+
}
|
|
66
|
+
/** A writer's payload as the sink keeps it: a plain object, copied — or a `TypeError` naming `field`. */
|
|
67
|
+
function copiedPayload(value, field) {
|
|
68
|
+
if (!isEntryObject(value))
|
|
69
|
+
throw new TypeError(`EventLog: ${field} must be a plain object`);
|
|
70
|
+
return structuredClone(value);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The entry with its `meta` copied — or, when `meta` cannot be copied, the entry WITHOUT it and the
|
|
74
|
+
* error: the structural facts (the base, how the log ended, its length) still go; only `meta` is lost.
|
|
75
|
+
*/
|
|
76
|
+
function withCopiedMeta(entry) {
|
|
77
|
+
if (entry.meta === undefined)
|
|
78
|
+
return { ready: entry };
|
|
79
|
+
try {
|
|
80
|
+
return { ready: { ...entry, meta: copiedPayload(entry.meta, 'meta') } };
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const { meta: _dropped, ...rest } = entry;
|
|
84
|
+
return { ready: rest, dropped: error };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The entry as the sink may keep it: a payload checked and copied (the writer keeps its own object),
|
|
89
|
+
* the bundle served. `dropped` is a `meta` that could not be copied (the entry goes without it). Throws
|
|
90
|
+
* when the entry itself cannot be sent; the caller counts it as a lost entry.
|
|
91
|
+
*/
|
|
92
|
+
function detached(entry) {
|
|
93
|
+
switch (entry.kind) {
|
|
94
|
+
case 'opened':
|
|
95
|
+
return withCopiedMeta(entry);
|
|
96
|
+
case 'commit':
|
|
97
|
+
return { ready: { ...entry, bundle: serveRecord(entry.bundle) } };
|
|
98
|
+
case 'note':
|
|
99
|
+
if (typeof entry.note !== 'string')
|
|
100
|
+
throw new TypeError('EventLog.note: kind must be a string');
|
|
101
|
+
return { ready: { ...entry, data: copiedPayload(entry.data, 'note data') } };
|
|
102
|
+
case 'closed':
|
|
103
|
+
if (!LOG_OUTCOMES.includes(entry.outcome)) {
|
|
104
|
+
throw new TypeError(`EventLog.close: outcome must be ${LOG_OUTCOMES.join(', ')}`);
|
|
105
|
+
}
|
|
106
|
+
return withCopiedMeta(entry);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Swallows a rejection nobody waits for. */
|
|
110
|
+
function ignore() {
|
|
111
|
+
// A failure report that fails changes nothing: the loss is already counted.
|
|
112
|
+
}
|
|
30
113
|
export class EventLog {
|
|
31
114
|
/** Base snapshot BEFORE the first stage mutates anything. */
|
|
32
115
|
base;
|
|
@@ -34,12 +117,39 @@ export class EventLog {
|
|
|
34
117
|
steps = [];
|
|
35
118
|
sourceAddress;
|
|
36
119
|
sourceAddressRetired = false;
|
|
37
|
-
|
|
120
|
+
/**
|
|
121
|
+
* @param initialMemory The fold base — the state before the first commit (copied).
|
|
122
|
+
* @param options With a `sink`: every entry goes to it as it lands, under `address` — which also
|
|
123
|
+
* becomes this log's {@link address} — and `meta` rides the `opened` entry. The sink is told
|
|
124
|
+
* `opened` before the constructor returns.
|
|
125
|
+
* @throws TypeError when `options` names no sink, a malformed address, or a base that is not a plain
|
|
126
|
+
* object (the `opened` entry carries it) — a writer's mistake, refused at the door. Nothing the sink
|
|
127
|
+
* itself does is ever thrown.
|
|
128
|
+
*/
|
|
129
|
+
constructor(initialMemory, options) {
|
|
38
130
|
// Normalised to an object: the base is a STATE, and every fold over it
|
|
39
131
|
// (`materialise`, `stateAt`) applies path writes to it. `undefined` —
|
|
40
132
|
// what a run with no `initialContext` used to store here — made every
|
|
41
133
|
// one of those folds throw on the first `set`.
|
|
42
134
|
this.base = initialMemory === undefined ? {} : structuredClone(initialMemory);
|
|
135
|
+
if (options === undefined)
|
|
136
|
+
return;
|
|
137
|
+
if (!isEntryObject(options.sink))
|
|
138
|
+
throw new TypeError('EventLog: options.sink must be a RecordSink');
|
|
139
|
+
if (!isEntryObject(this.base)) {
|
|
140
|
+
throw new TypeError('EventLog: a log that writes to a sink needs a plain-object base — its opened entry carries it');
|
|
141
|
+
}
|
|
142
|
+
const address = frozenAddress(options.address);
|
|
143
|
+
this.sourceAddress = address;
|
|
144
|
+
this.sink = options.sink;
|
|
145
|
+
this.lostEntries = 0;
|
|
146
|
+
this.send({
|
|
147
|
+
kind: 'opened',
|
|
148
|
+
address,
|
|
149
|
+
at: Date.now(),
|
|
150
|
+
initialState: this.getInitialState(),
|
|
151
|
+
...(options.meta === undefined ? {} : { meta: options.meta }),
|
|
152
|
+
});
|
|
43
153
|
}
|
|
44
154
|
/**
|
|
45
155
|
* The fold BASE — the state as it stood before the first commit.
|
|
@@ -96,6 +206,124 @@ export class EventLog {
|
|
|
96
206
|
bundle.idx = this.steps.length;
|
|
97
207
|
freezeRecord(bundle, 'indices');
|
|
98
208
|
this.steps.push(bundle);
|
|
209
|
+
if (this.sink !== undefined)
|
|
210
|
+
this.send({ kind: 'commit', address: this.sourceAddress, at: Date.now(), bundle });
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Note something beside the log, in the sink, as it happens — for evidence that must survive a run
|
|
214
|
+
* that never closes (an attempt that ended, before the next one starts). `kind` and `data` are the
|
|
215
|
+
* writer's: opaque to the record. Notes never enter the log's bundles, so the log's bytes are the
|
|
216
|
+
* same with or without them. Without a sink, nothing happens.
|
|
217
|
+
*
|
|
218
|
+
* Never throws: a `kind` that is not a string, `data` that is not a plain object surviving
|
|
219
|
+
* `structuredClone`, or a sink that fails is counted in {@link sinkFailures} and reported to
|
|
220
|
+
* `sink.onFailure`. The sink keeps its own copy of `data`.
|
|
221
|
+
*/
|
|
222
|
+
note(kind, data) {
|
|
223
|
+
if (this.sink === undefined)
|
|
224
|
+
return;
|
|
225
|
+
this.send({ kind: 'note', address: this.sourceAddress, at: Date.now(), note: kind, data });
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Tell the sink this log ended — `outcome` and, for the reader's check that the stored log is
|
|
229
|
+
* whole, how many commits it holds now. `meta` rides the entry, copied. Without a sink, nothing
|
|
230
|
+
* happens. The log itself stays as it is: a later `record` still lands, and still reaches the sink
|
|
231
|
+
* (a reader then reads the log as not complete until it is closed again).
|
|
232
|
+
*
|
|
233
|
+
* Never throws: an unknown outcome, a `meta` that is not a plain object surviving `structuredClone`,
|
|
234
|
+
* or a sink that fails is counted in {@link sinkFailures} and reported to `sink.onFailure`.
|
|
235
|
+
*/
|
|
236
|
+
close(outcome, meta) {
|
|
237
|
+
if (this.sink === undefined)
|
|
238
|
+
return;
|
|
239
|
+
this.send({
|
|
240
|
+
kind: 'closed',
|
|
241
|
+
address: this.sourceAddress,
|
|
242
|
+
at: Date.now(),
|
|
243
|
+
outcome,
|
|
244
|
+
commits: this.steps.length,
|
|
245
|
+
...(meta === undefined ? {} : { meta }),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* How many entries the sink did not take: a sink method that threw or whose promise rejected, and an
|
|
250
|
+
* entry the log refused to send — plus each `meta` the log could not copy (that entry went without
|
|
251
|
+
* it). Counted as each happens (a rejection, when it settles); 0 without a sink. The stored record
|
|
252
|
+
* says the same from its side: a missing commit is a gap.
|
|
253
|
+
*/
|
|
254
|
+
get sinkFailures() {
|
|
255
|
+
return this.lostEntries ?? 0;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* One entry to the sink. Nothing here reaches the writer: a refusal, a throw or a rejection is a lost
|
|
259
|
+
* entry; a `meta` that cannot be copied is lost on its own — counted and reported — and the entry goes
|
|
260
|
+
* without it.
|
|
261
|
+
*/
|
|
262
|
+
send(entry) {
|
|
263
|
+
const sink = this.sink;
|
|
264
|
+
if (sink === undefined)
|
|
265
|
+
return;
|
|
266
|
+
let sent = entry;
|
|
267
|
+
try {
|
|
268
|
+
const { ready, dropped } = detached(entry);
|
|
269
|
+
sent = Object.freeze(ready);
|
|
270
|
+
if (dropped !== undefined)
|
|
271
|
+
this.lost(sink, entry, dropped);
|
|
272
|
+
const returned = sendEntry(sink, sent);
|
|
273
|
+
if (isThenable(returned))
|
|
274
|
+
this.watch(sink, sent, returned);
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
this.lost(sink, sent, error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Count the rejection of an adapter's promise once. A native promise is watched through the real
|
|
282
|
+
* `Promise.prototype.then` (an own `then` on it is not trusted); any other thenable is adopted into a
|
|
283
|
+
* fresh promise first, whose settling functions run once. Either way the rejection is handled.
|
|
284
|
+
*/
|
|
285
|
+
watch(sink, entry, returned) {
|
|
286
|
+
let counted = false;
|
|
287
|
+
const onRejected = (error) => {
|
|
288
|
+
if (counted)
|
|
289
|
+
return;
|
|
290
|
+
counted = true;
|
|
291
|
+
this.lost(sink, entry, error);
|
|
292
|
+
};
|
|
293
|
+
if (returned instanceof Promise)
|
|
294
|
+
Promise.prototype.then.call(returned, undefined, onRejected);
|
|
295
|
+
else
|
|
296
|
+
new Promise((resolve) => resolve(returned)).then(undefined, onRejected);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Count a lost entry and tell the sink's `onFailure`, if it listens; a report that fails is ignored.
|
|
300
|
+
* A loss caused while `onFailure` runs — until the promise it returns settles, when it returns one —
|
|
301
|
+
* (it wrote to this log again, through the same failing sink) is counted and not reported again: one
|
|
302
|
+
* report deep, never a runaway.
|
|
303
|
+
*/
|
|
304
|
+
lost(sink, entry, error) {
|
|
305
|
+
this.lostEntries = (this.lostEntries ?? 0) + 1;
|
|
306
|
+
if (this.reporting === true)
|
|
307
|
+
return;
|
|
308
|
+
this.reporting = true;
|
|
309
|
+
let pending = false;
|
|
310
|
+
try {
|
|
311
|
+
const reported = sink.onFailure?.({ entry, error });
|
|
312
|
+
if (isThenable(reported)) {
|
|
313
|
+
pending = true;
|
|
314
|
+
const done = () => {
|
|
315
|
+
this.reporting = false;
|
|
316
|
+
};
|
|
317
|
+
new Promise((resolve) => resolve(reported)).then(done, done);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
ignore();
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
if (!pending)
|
|
325
|
+
this.reporting = false;
|
|
326
|
+
}
|
|
99
327
|
}
|
|
100
328
|
/**
|
|
101
329
|
* Every recorded commit bundle, AS SERVED (9.44.2): a new array each call; a bundle freezing sealed
|
|
@@ -119,7 +347,10 @@ export class EventLog {
|
|
|
119
347
|
get length() {
|
|
120
348
|
return this.steps.length;
|
|
121
349
|
}
|
|
122
|
-
/**
|
|
350
|
+
/**
|
|
351
|
+
* Immutable identity of this log: the sink's address when the log was built with one, otherwise
|
|
352
|
+
* absent until the engine binds it.
|
|
353
|
+
*/
|
|
123
354
|
get address() {
|
|
124
355
|
return this.sourceAddress;
|
|
125
356
|
}
|
|
@@ -135,8 +366,16 @@ export class EventLog {
|
|
|
135
366
|
return undefined;
|
|
136
367
|
return Object.freeze({ runId, ...this.sourceAddress, committedThroughIdx: this.steps.length - 1 });
|
|
137
368
|
}
|
|
138
|
-
/**
|
|
369
|
+
/**
|
|
370
|
+
* Wipes history (useful for test resets).
|
|
371
|
+
*
|
|
372
|
+
* @throws Error on a log built with a sink: what the sink stored cannot be wiped, and a second
|
|
373
|
+
* history under the same address would read back as one corrupted log. Start a fresh EventLog.
|
|
374
|
+
*/
|
|
139
375
|
clear() {
|
|
376
|
+
if (this.sink !== undefined) {
|
|
377
|
+
throw new Error('EventLog.clear: this log writes to a sink, and a stored record cannot be wiped — use a new EventLog');
|
|
378
|
+
}
|
|
140
379
|
this.steps = [];
|
|
141
380
|
// A reset prefix cannot reuse old coordinates, even if another stage of
|
|
142
381
|
// the SAME leg binds it again. Keep clear's data behavior, but retire
|
|
@@ -23,10 +23,11 @@
|
|
|
23
23
|
* alias of `foottrace` that is a union of string literals: each one is inside the
|
|
24
24
|
* registry or is named, with its reason, as not an honesty vocabulary.
|
|
25
25
|
*
|
|
26
|
-
* NO OBJECT GAINS A FIELD from being registered.
|
|
27
|
-
* `LogGap` (`'log-gap'`), `CausalNode.incompleteSources` (`'incomplete-sources'`)
|
|
28
|
-
* `FoldedState.redacted` / `redactedPaths` (`'redacted'`)
|
|
29
|
-
*
|
|
26
|
+
* NO OBJECT GAINS A FIELD from being registered. Four signals carry no code of their own — a
|
|
27
|
+
* `LogGap` (`'log-gap'`), `CausalNode.incompleteSources` (`'incomplete-sources'`),
|
|
28
|
+
* `FoldedState.redacted` / `redactedPaths` (`'redacted'`) and an entry of `StoredRecord.unread`
|
|
29
|
+
* (`'unread-entry'`, 1.1.0) — and a `truncated: { byDepth, byNodes }` field means `'truncated'`; they
|
|
30
|
+
* are registered so a reader explains them from the same place.
|
|
30
31
|
*
|
|
31
32
|
* NOT HERE: the two strings a redaction leaves where a value was. The engine writes those on every
|
|
32
33
|
* redacted run; the log's lives in its own leaf, `memory/placeholders.ts` (the scope channel's with
|
|
@@ -75,6 +76,7 @@ export declare const HONESTY_CODES: Readonly<{
|
|
|
75
76
|
'initial+log': string;
|
|
76
77
|
'log-only': string;
|
|
77
78
|
'log-gap': string;
|
|
79
|
+
'unread-entry': string;
|
|
78
80
|
'incomplete-sources': string;
|
|
79
81
|
redacted: string;
|
|
80
82
|
deleted: string;
|
|
@@ -23,10 +23,11 @@
|
|
|
23
23
|
* alias of `foottrace` that is a union of string literals: each one is inside the
|
|
24
24
|
* registry or is named, with its reason, as not an honesty vocabulary.
|
|
25
25
|
*
|
|
26
|
-
* NO OBJECT GAINS A FIELD from being registered.
|
|
27
|
-
* `LogGap` (`'log-gap'`), `CausalNode.incompleteSources` (`'incomplete-sources'`)
|
|
28
|
-
* `FoldedState.redacted` / `redactedPaths` (`'redacted'`)
|
|
29
|
-
*
|
|
26
|
+
* NO OBJECT GAINS A FIELD from being registered. Four signals carry no code of their own — a
|
|
27
|
+
* `LogGap` (`'log-gap'`), `CausalNode.incompleteSources` (`'incomplete-sources'`),
|
|
28
|
+
* `FoldedState.redacted` / `redactedPaths` (`'redacted'`) and an entry of `StoredRecord.unread`
|
|
29
|
+
* (`'unread-entry'`, 1.1.0) — and a `truncated: { byDepth, byNodes }` field means `'truncated'`; they
|
|
30
|
+
* are registered so a reader explains them from the same place.
|
|
30
31
|
*
|
|
31
32
|
* NOT HERE: the two strings a redaction leaves where a value was. The engine writes those on every
|
|
32
33
|
* redacted run; the log's lives in its own leaf, `memory/placeholders.ts` (the scope channel's with
|
|
@@ -81,6 +82,8 @@ export const HONESTY_CODES = /* @__PURE__ */ Object.freeze({
|
|
|
81
82
|
'log-only': 'No initialState travelled with this log (a snapshot stored before 9.17, a redacted snapshot or a hand-built log), so the fold started from an empty object: a key seeded before the run is whole only once a commit since wrote its whole value, and until then it holds just what its merges, appends and nested writes added, or nothing.',
|
|
82
83
|
// ── a row of a stored log that is not a commit bundle — `LogGap` ──────────
|
|
83
84
|
'log-gap': 'A row of the stored log could not be read as a commit bundle, so it keeps its index but contributes no state, and any fold that crosses it is missing whatever that row wrote.',
|
|
85
|
+
// ── an entry a sink stored that no log could hold — `StoredRecord.unread` (1.1.0) ─
|
|
86
|
+
'unread-entry': "An entry a sink stored was not read into any log (it could not be read as an opened, commit, note or closed entry, it lay past one of the reader's limits, or another entry holds its place), so whatever it said (a base, a commit, a note or how the log ended) is not in the record read back.",
|
|
84
87
|
// ── untracked reads a stage also consumed — `CausalNode.incompleteSources` ─
|
|
85
88
|
'incomplete-sources': 'The stage also consumed read paths that bypass read tracking (args, env or an unshadowed silent read), so a slice through it may be incomplete: those reads produce no read-to-write edge to follow.',
|
|
86
89
|
// ── values scrubbed at write time — `FoldedState.redacted` / `redactedPaths`, a value basis ─
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sink.ts — the record's sink port: where a log's entries go AS THEY LAND, so a run that never ends
|
|
3
|
+
* still leaves its record behind.
|
|
4
|
+
*
|
|
5
|
+
* A log that is given a sink (`new EventLog(base, { sink, address })`) hands it four kinds of entry,
|
|
6
|
+
* each stamped with the log's address and the time it was written (`at`, epoch ms):
|
|
7
|
+
*
|
|
8
|
+
* - `opened` — once, from the constructor: the fold base (`initialState`, the sink's own copy);
|
|
9
|
+
* - `commit` — per `record()`, AFTER the bundle is frozen: the log's own bundle, as served;
|
|
10
|
+
* - `note` — per `note(kind, data)`: something the writer wants kept beside the log, as it happens;
|
|
11
|
+
* - `closed` — per `close(outcome)`: how the log ended, and how many commits it held then (`commits`).
|
|
12
|
+
*
|
|
13
|
+
* These are RECORD facts. What the writer means by them rides `meta` (on `opened` / `closed`) and a
|
|
14
|
+
* note's `kind` and `data`: opaque here — stored and handed back, never read or interpreted. Nothing in
|
|
15
|
+
* this file names an engine.
|
|
16
|
+
*
|
|
17
|
+
* THE STORED FORM. A sink stores each entry as a {@link RecordEntry}: the method's entry plus the
|
|
18
|
+
* method's name as `kind`. A note's own kind cannot also be called `kind`, so it is stored as `note`.
|
|
19
|
+
* `memorySink` is the reference adapter; `readStoredRecord` (`foottrace`) reads the entries back.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { EventLog, memorySink } from 'foottrace/write';
|
|
24
|
+
* import { readStoredRecord } from 'foottrace';
|
|
25
|
+
*
|
|
26
|
+
* const sink = memorySink();
|
|
27
|
+
* const log = new EventLog({ count: 0 }, { sink, address: { logRunId: 'run-1', drillPath: [] } });
|
|
28
|
+
* log.note('attempt', { attempt: 1, outcome: 'failed' });
|
|
29
|
+
* // … the process dies here: no `closed` entry …
|
|
30
|
+
* readStoredRecord(sink.entries).root('run-1')?.complete; // false — and its note is still there
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
import type { LogAddress } from './eventPosition.js';
|
|
34
|
+
import type { CommitBundle } from './types.js';
|
|
35
|
+
/**
|
|
36
|
+
* What a writer puts beside the record — on a log's `opened` / `closed` entry, or as a note's data.
|
|
37
|
+
* A plain object that survives `structuredClone`; opaque to foottrace: stored and returned, never read.
|
|
38
|
+
*/
|
|
39
|
+
export type Meta = Readonly<Record<string, unknown>>;
|
|
40
|
+
/** How a log's writer said it ended. A pause ends this log; it is not a failure. */
|
|
41
|
+
export type LogOutcome = 'ok' | 'failed' | 'paused';
|
|
42
|
+
/** The outcomes a `closed` entry may carry, in one place for the writer's and the reader's checks. */
|
|
43
|
+
export declare const LOG_OUTCOMES: readonly LogOutcome[];
|
|
44
|
+
/**
|
|
45
|
+
* One stored entry: what a sink keeps per call, discriminated by `kind` (the method that received it).
|
|
46
|
+
* A note's own kind is stored as `note`.
|
|
47
|
+
*/
|
|
48
|
+
export type RecordEntry = {
|
|
49
|
+
readonly kind: 'opened';
|
|
50
|
+
readonly address: LogAddress;
|
|
51
|
+
readonly at: number;
|
|
52
|
+
/** The fold base — the state before the log's first commit. */
|
|
53
|
+
readonly initialState: Record<string, unknown>;
|
|
54
|
+
readonly meta?: Meta;
|
|
55
|
+
} | {
|
|
56
|
+
readonly kind: 'commit';
|
|
57
|
+
readonly address: LogAddress;
|
|
58
|
+
readonly at: number;
|
|
59
|
+
/** The log's own bundle, frozen, its position stamped as `idx`. */
|
|
60
|
+
readonly bundle: CommitBundle;
|
|
61
|
+
} | {
|
|
62
|
+
readonly kind: 'note';
|
|
63
|
+
readonly address: LogAddress;
|
|
64
|
+
readonly at: number;
|
|
65
|
+
/** The note's own kind, as the writer named it (`note('attempt', …)` → `'attempt'`). */
|
|
66
|
+
readonly note: string;
|
|
67
|
+
readonly data: Meta;
|
|
68
|
+
} | {
|
|
69
|
+
readonly kind: 'closed';
|
|
70
|
+
readonly address: LogAddress;
|
|
71
|
+
readonly at: number;
|
|
72
|
+
readonly outcome: LogOutcome;
|
|
73
|
+
/** The log's length when it closed — how many commits the stored log must hold to be whole. */
|
|
74
|
+
readonly commits: number;
|
|
75
|
+
readonly meta?: Meta;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Where a log's entries go as they land — the port an adapter implements (memory, a file, S3, a table).
|
|
79
|
+
*
|
|
80
|
+
* Every method is called synchronously from the writer; an adapter that writes somewhere slow queues
|
|
81
|
+
* the entry and drains in `flush`. A method may throw, or return a promise that rejects: the log
|
|
82
|
+
* counts it (`EventLog · sinkFailures`), reports it to `onFailure`, and goes on — a failing sink never
|
|
83
|
+
* stops the writer. The entries an adapter is handed are its to keep: the bundle is frozen and shared
|
|
84
|
+
* with the log, everything else is the adapter's own copy.
|
|
85
|
+
*/
|
|
86
|
+
export interface RecordSink {
|
|
87
|
+
/** A log opened at `address` with its fold base. Called once, from the log's constructor. */
|
|
88
|
+
opened(entry: {
|
|
89
|
+
readonly address: LogAddress;
|
|
90
|
+
readonly at: number;
|
|
91
|
+
readonly initialState: Record<string, unknown>;
|
|
92
|
+
readonly meta?: Meta;
|
|
93
|
+
}): void;
|
|
94
|
+
/** A bundle landed in the log at `address` (its position is `bundle.idx`). */
|
|
95
|
+
commit(entry: {
|
|
96
|
+
readonly address: LogAddress;
|
|
97
|
+
readonly at: number;
|
|
98
|
+
readonly bundle: CommitBundle;
|
|
99
|
+
}): void;
|
|
100
|
+
/** The writer noted something beside the log — `kind` and `data` are opaque. */
|
|
101
|
+
note(entry: {
|
|
102
|
+
readonly address: LogAddress;
|
|
103
|
+
readonly at: number;
|
|
104
|
+
readonly kind: string;
|
|
105
|
+
readonly data: Meta;
|
|
106
|
+
}): void;
|
|
107
|
+
/** The log at `address` closed with `outcome`, holding `commits` bundles. */
|
|
108
|
+
closed(entry: {
|
|
109
|
+
readonly address: LogAddress;
|
|
110
|
+
readonly at: number;
|
|
111
|
+
readonly outcome: LogOutcome;
|
|
112
|
+
readonly commits: number;
|
|
113
|
+
readonly meta?: Meta;
|
|
114
|
+
}): void;
|
|
115
|
+
/** Drain whatever the adapter queued. The log never calls it; whoever owns the sink does. */
|
|
116
|
+
flush?(): Promise<void>;
|
|
117
|
+
/** Told of every entry this sink did not take — the entry and what went wrong. A throw here is ignored. */
|
|
118
|
+
onFailure?(failure: {
|
|
119
|
+
readonly entry: RecordEntry;
|
|
120
|
+
readonly error: unknown;
|
|
121
|
+
}): void;
|
|
122
|
+
}
|
|
123
|
+
/** A plain object — not null, not an array, not a primitive. */
|
|
124
|
+
export declare function isEntryObject(value: unknown): value is Record<string, unknown>;
|
|
125
|
+
/**
|
|
126
|
+
* Hand `entry` to the sink's method for its kind, as that method's entry; returns what the method
|
|
127
|
+
* returned (a promise from an async adapter). Throws whatever the method throws — the log catches.
|
|
128
|
+
*/
|
|
129
|
+
export declare function sendEntry(sink: RecordSink, entry: RecordEntry): unknown;
|
|
130
|
+
/**
|
|
131
|
+
* A sink that keeps every entry in memory, in arrival order — the reference adapter: what any adapter
|
|
132
|
+
* must give back for `readStoredRecord` to read the same record. `entries` is a new array on each read.
|
|
133
|
+
* What it keeps is frozen as far as freezing reaches — the base, `meta` and a note's `data` are the
|
|
134
|
+
* copies a log hands it, and the bundle is already frozen — so no reader can rewrite its containers. A
|
|
135
|
+
* Date's time, a Map's or Set's entries and a buffer's bytes are not sealed by freezing (`deepFreeze`'s
|
|
136
|
+
* stated limit). A caller that hands it objects directly hands them over: they are frozen in place.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* const sink = memorySink();
|
|
141
|
+
* const log = new EventLog({}, { sink, address: { logRunId: 'run-1', drillPath: [] } });
|
|
142
|
+
* log.close('ok');
|
|
143
|
+
* sink.entries.map((e) => e.kind); // ['opened', 'closed']
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export declare function memorySink(): RecordSink & {
|
|
147
|
+
readonly entries: readonly RecordEntry[];
|
|
148
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sink.ts — the record's sink port: where a log's entries go AS THEY LAND, so a run that never ends
|
|
3
|
+
* still leaves its record behind.
|
|
4
|
+
*
|
|
5
|
+
* A log that is given a sink (`new EventLog(base, { sink, address })`) hands it four kinds of entry,
|
|
6
|
+
* each stamped with the log's address and the time it was written (`at`, epoch ms):
|
|
7
|
+
*
|
|
8
|
+
* - `opened` — once, from the constructor: the fold base (`initialState`, the sink's own copy);
|
|
9
|
+
* - `commit` — per `record()`, AFTER the bundle is frozen: the log's own bundle, as served;
|
|
10
|
+
* - `note` — per `note(kind, data)`: something the writer wants kept beside the log, as it happens;
|
|
11
|
+
* - `closed` — per `close(outcome)`: how the log ended, and how many commits it held then (`commits`).
|
|
12
|
+
*
|
|
13
|
+
* These are RECORD facts. What the writer means by them rides `meta` (on `opened` / `closed`) and a
|
|
14
|
+
* note's `kind` and `data`: opaque here — stored and handed back, never read or interpreted. Nothing in
|
|
15
|
+
* this file names an engine.
|
|
16
|
+
*
|
|
17
|
+
* THE STORED FORM. A sink stores each entry as a {@link RecordEntry}: the method's entry plus the
|
|
18
|
+
* method's name as `kind`. A note's own kind cannot also be called `kind`, so it is stored as `note`.
|
|
19
|
+
* `memorySink` is the reference adapter; `readStoredRecord` (`foottrace`) reads the entries back.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { EventLog, memorySink } from 'foottrace/write';
|
|
24
|
+
* import { readStoredRecord } from 'foottrace';
|
|
25
|
+
*
|
|
26
|
+
* const sink = memorySink();
|
|
27
|
+
* const log = new EventLog({ count: 0 }, { sink, address: { logRunId: 'run-1', drillPath: [] } });
|
|
28
|
+
* log.note('attempt', { attempt: 1, outcome: 'failed' });
|
|
29
|
+
* // … the process dies here: no `closed` entry …
|
|
30
|
+
* readStoredRecord(sink.entries).root('run-1')?.complete; // false — and its note is still there
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
import { deepFreeze } from '../capture/freeze.js';
|
|
34
|
+
/** The outcomes a `closed` entry may carry, in one place for the writer's and the reader's checks. */
|
|
35
|
+
export const LOG_OUTCOMES = /* @__PURE__ */ Object.freeze(['ok', 'failed', 'paused']);
|
|
36
|
+
/** A plain object — not null, not an array, not a primitive. */
|
|
37
|
+
export function isEntryObject(value) {
|
|
38
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
/** `{ meta }` when there is one, `{}` otherwise — an absent field stays absent. */
|
|
41
|
+
function withMeta(meta) {
|
|
42
|
+
return meta === undefined ? {} : { meta };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Hand `entry` to the sink's method for its kind, as that method's entry; returns what the method
|
|
46
|
+
* returned (a promise from an async adapter). Throws whatever the method throws — the log catches.
|
|
47
|
+
*/
|
|
48
|
+
export function sendEntry(sink, entry) {
|
|
49
|
+
const { address, at } = entry;
|
|
50
|
+
switch (entry.kind) {
|
|
51
|
+
case 'opened':
|
|
52
|
+
return sink.opened({ address, at, initialState: entry.initialState, ...withMeta(entry.meta) });
|
|
53
|
+
case 'commit':
|
|
54
|
+
return sink.commit({ address, at, bundle: entry.bundle });
|
|
55
|
+
case 'note':
|
|
56
|
+
return sink.note({ address, at, kind: entry.note, data: entry.data });
|
|
57
|
+
case 'closed':
|
|
58
|
+
return sink.closed({ address, at, outcome: entry.outcome, commits: entry.commits, ...withMeta(entry.meta) });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A sink that keeps every entry in memory, in arrival order — the reference adapter: what any adapter
|
|
63
|
+
* must give back for `readStoredRecord` to read the same record. `entries` is a new array on each read.
|
|
64
|
+
* What it keeps is frozen as far as freezing reaches — the base, `meta` and a note's `data` are the
|
|
65
|
+
* copies a log hands it, and the bundle is already frozen — so no reader can rewrite its containers. A
|
|
66
|
+
* Date's time, a Map's or Set's entries and a buffer's bytes are not sealed by freezing (`deepFreeze`'s
|
|
67
|
+
* stated limit). A caller that hands it objects directly hands them over: they are frozen in place.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const sink = memorySink();
|
|
72
|
+
* const log = new EventLog({}, { sink, address: { logRunId: 'run-1', drillPath: [] } });
|
|
73
|
+
* log.close('ok');
|
|
74
|
+
* sink.entries.map((e) => e.kind); // ['opened', 'closed']
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function memorySink() {
|
|
78
|
+
const stored = [];
|
|
79
|
+
const keep = (entry) => {
|
|
80
|
+
stored.push(deepFreeze(entry));
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
get entries() {
|
|
84
|
+
return stored.slice();
|
|
85
|
+
},
|
|
86
|
+
opened: ({ address, at, initialState, meta }) => keep({ kind: 'opened', address, at, initialState, ...withMeta(meta) }),
|
|
87
|
+
commit: ({ address, at, bundle }) => keep({ kind: 'commit', address, at, bundle }),
|
|
88
|
+
note: ({ address, at, kind, data }) => keep({ kind: 'note', address, at, note: kind, data }),
|
|
89
|
+
closed: ({ address, at, outcome, commits, meta }) => keep({ kind: 'closed', address, at, outcome, commits, ...withMeta(meta) }),
|
|
90
|
+
flush: () => Promise.resolve(),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* types.ts — the record's types: a bundle, its rows, its patches, its encoding, and the
|
|
3
3
|
* execution tree as the record's readers read it.
|
|
4
4
|
*
|
|
5
|
-
* Imports
|
|
5
|
+
* Imports only the record's leaf attention type. The record names nothing outside itself: the engine's frame
|
|
6
6
|
* types — the stage snapshot, flow messages and retention dials — remain in FootPrint's
|
|
7
7
|
* `frameTypes.ts`. The closed record graph is enforced by scripts/check-layering.mjs.
|
|
8
8
|
*/
|
|
9
|
+
import type { Attention } from '../cause/types.js';
|
|
9
10
|
/** A flat key-value bag representing a state patch (overwrite or merge). */
|
|
10
11
|
export interface MemoryPatch {
|
|
11
12
|
[key: string]: any;
|
|
@@ -194,6 +195,8 @@ export type CommitValuesMode = 'full' | 'delta';
|
|
|
194
195
|
* ```
|
|
195
196
|
*/
|
|
196
197
|
export interface ExecutionTree {
|
|
198
|
+
/** Optional recorded business attention; absence does not assert health. */
|
|
199
|
+
readonly attention?: readonly Attention[];
|
|
197
200
|
/** The stage's id (prefixed inside a subflow). `commitStops` reads the root's to place a subflow's seed. */
|
|
198
201
|
readonly id?: string;
|
|
199
202
|
/** `[subflowPath/]stageId#executionIndex` — what a reader keys on. A node without one is not a stage. */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* types.ts — the record's types: a bundle, its rows, its patches, its encoding, and the
|
|
3
3
|
* execution tree as the record's readers read it.
|
|
4
4
|
*
|
|
5
|
-
* Imports
|
|
5
|
+
* Imports only the record's leaf attention type. The record names nothing outside itself: the engine's frame
|
|
6
6
|
* types — the stage snapshot, flow messages and retention dials — remain in FootPrint's
|
|
7
7
|
* `frameTypes.ts`. The closed record graph is enforced by scripts/check-layering.mjs.
|
|
8
8
|
*/
|
|
@@ -19,6 +19,8 @@ import type { CommitBundle, ExecutionTree } from '../memory/types.js';
|
|
|
19
19
|
import type { LogGap } from './types.js';
|
|
20
20
|
/** A plain object — not null, not an array, not a primitive. */
|
|
21
21
|
export declare function isPlainish(value: unknown): value is Record<string, unknown>;
|
|
22
|
+
/** A row every reader refuses with `reason` — a gap that keeps its index. */
|
|
23
|
+
export declare function absentRow(reason: string): object;
|
|
22
24
|
/**
|
|
23
25
|
* Read a source's `executionTree` as an {@link ExecutionTree}, or as absent.
|
|
24
26
|
*
|
|
@@ -39,6 +39,19 @@ const GAP_BUNDLE = Object.freeze({
|
|
|
39
39
|
export function isPlainish(value) {
|
|
40
40
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Rows that hold an index for a commit the record does not have (1.1.0) — what a STORED log could not
|
|
44
|
+
* supply at a position: no readable commit entry (`storedRecord.ts`). Each
|
|
45
|
+
* carries the reason every reader then reports for its index, so the fold, the cursor and the stored
|
|
46
|
+
* log say the same thing about it. Kept by identity (a JSON copy of one is an ordinary unreadable row).
|
|
47
|
+
*/
|
|
48
|
+
const ABSENT_ROWS = new WeakMap();
|
|
49
|
+
/** A row every reader refuses with `reason` — a gap that keeps its index. */
|
|
50
|
+
export function absentRow(reason) {
|
|
51
|
+
const row = Object.freeze({});
|
|
52
|
+
ABSENT_ROWS.set(row, reason);
|
|
53
|
+
return row;
|
|
54
|
+
}
|
|
42
55
|
/**
|
|
43
56
|
* Read a source's `executionTree` as an {@link ExecutionTree}, or as absent.
|
|
44
57
|
*
|
|
@@ -76,6 +89,9 @@ export function readTree(value) {
|
|
|
76
89
|
* compatibility break dressed up as validation.
|
|
77
90
|
*/
|
|
78
91
|
export function bundleRefusal(row) {
|
|
92
|
+
const absent = typeof row === 'object' && row !== null ? ABSENT_ROWS.get(row) : undefined;
|
|
93
|
+
if (absent !== undefined)
|
|
94
|
+
return absent;
|
|
79
95
|
if (!isPlainish(row)) {
|
|
80
96
|
return `not an object (${row === null ? 'null' : Array.isArray(row) ? 'array' : typeof row})`;
|
|
81
97
|
}
|