footprintjs 9.40.0 → 9.41.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/CLAUDE.md +3 -3
- package/dist/esm/lib/builder/FlowChartBuilder.d.ts +10 -3
- package/dist/esm/lib/builder/FlowChartBuilder.js +93 -86
- package/dist/esm/lib/capture/freeze.d.ts +1 -1
- package/dist/esm/lib/capture/freeze.js +2 -2
- package/dist/esm/lib/engine/traversal/FlowchartTraverser.d.ts +12 -0
- package/dist/esm/lib/engine/traversal/FlowchartTraverser.js +49 -15
- package/dist/esm/lib/memory/StageContext.d.ts +16 -0
- package/dist/esm/lib/memory/StageContext.js +35 -40
- package/dist/esm/lib/reactive/types.js +4 -4
- package/dist/esm/lib/recorder/hooks.d.ts +2 -2
- package/dist/esm/lib/recorder/hooks.js +2 -2
- package/dist/esm/lib/recorder/snapshot.d.ts +1 -1
- package/dist/esm/lib/recorder/snapshot.js +2 -2
- package/dist/esm/lib/runner/FlowChartExecutor.d.ts +93 -377
- package/dist/esm/lib/runner/FlowChartExecutor.js +184 -1059
- package/dist/esm/lib/runner/attach.d.ts +95 -0
- package/dist/esm/lib/runner/attach.js +247 -0
- package/dist/esm/lib/runner/checkpoint.d.ts +69 -0
- package/dist/esm/lib/runner/checkpoint.js +254 -0
- package/dist/esm/lib/runner/index.d.ts +1 -1
- package/dist/esm/lib/runner/index.js +1 -1
- package/dist/esm/lib/runner/options.d.ts +79 -0
- package/dist/esm/lib/runner/options.js +31 -0
- package/dist/esm/lib/runner/resume.d.ts +73 -0
- package/dist/esm/lib/runner/resume.js +301 -0
- package/dist/esm/lib/runner/snapshot.d.ts +41 -0
- package/dist/esm/lib/runner/snapshot.js +93 -0
- package/dist/lib/builder/FlowChartBuilder.js +93 -86
- package/dist/lib/capture/freeze.js +2 -2
- package/dist/lib/engine/traversal/FlowchartTraverser.js +49 -15
- package/dist/lib/memory/StageContext.js +35 -40
- package/dist/lib/reactive/types.js +4 -4
- package/dist/lib/recorder/hooks.js +2 -2
- package/dist/lib/recorder/snapshot.js +2 -2
- package/dist/lib/runner/FlowChartExecutor.js +182 -1057
- package/dist/lib/runner/attach.js +251 -0
- package/dist/lib/runner/checkpoint.js +258 -0
- package/dist/lib/runner/index.js +1 -1
- package/dist/lib/runner/options.js +35 -0
- package/dist/lib/runner/resume.js +307 -0
- package/dist/lib/runner/snapshot.js +98 -0
- package/dist/types/lib/builder/FlowChartBuilder.d.ts +10 -3
- package/dist/types/lib/capture/freeze.d.ts +1 -1
- package/dist/types/lib/engine/traversal/FlowchartTraverser.d.ts +12 -0
- package/dist/types/lib/memory/StageContext.d.ts +16 -0
- package/dist/types/lib/recorder/hooks.d.ts +2 -2
- package/dist/types/lib/recorder/snapshot.d.ts +1 -1
- package/dist/types/lib/runner/FlowChartExecutor.d.ts +93 -377
- package/dist/types/lib/runner/attach.d.ts +95 -0
- package/dist/types/lib/runner/checkpoint.d.ts +69 -0
- package/dist/types/lib/runner/index.d.ts +1 -1
- package/dist/types/lib/runner/options.d.ts +79 -0
- package/dist/types/lib/runner/resume.d.ts +73 -0
- package/dist/types/lib/runner/snapshot.d.ts +41 -0
- package/package.json +1 -1
- package/dist/esm/lib/runner/checkpointSanitize.d.ts +0 -44
- package/dist/esm/lib/runner/checkpointSanitize.js +0 -133
- package/dist/lib/runner/checkpointSanitize.js +0 -138
- package/dist/types/lib/runner/checkpointSanitize.d.ts +0 -44
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
* // 2-param form (accepts a ScopeFactory directly, for backward compatibility):
|
|
15
15
|
* const executor = new FlowChartExecutor(chart, myFactory);
|
|
16
16
|
*
|
|
17
|
+
*
|
|
18
|
+
* The executor composes four modules, one job each (F9): `attach.ts` (who
|
|
19
|
+
* observes a run), `resume.ts` (plan and announce a re-entry), `checkpoint.ts`
|
|
20
|
+
* (the pause checkpoint), `snapshot.ts` (what `getSnapshot()` serves). What
|
|
21
|
+
* stays here is the per-run state and the run lifecycle that threads it.
|
|
17
22
|
* const result = await executor.run({ input: data, env: { traceId: 'req-123' } });
|
|
18
23
|
*/
|
|
19
24
|
import type { FlowChart } from '../builder/types.js';
|
|
@@ -21,123 +26,39 @@ import type { CombinedNarrativeRecorderOptions } from '../engine/narrative/Combi
|
|
|
21
26
|
import type { CombinedNarrativeEntry } from '../engine/narrative/narrativeTypes.js';
|
|
22
27
|
import type { ManifestEntry } from '../engine/narrative/recorders/ManifestFlowRecorder.js';
|
|
23
28
|
import type { FlowRecorder } from '../engine/narrative/types.js';
|
|
24
|
-
import { type ExecutorResult, type RunOptions, type ScopeFactory, type SerializedPipelineStructure, type StageNode, type
|
|
25
|
-
import type { RunDials } from '../memory/runPolicy.js';
|
|
29
|
+
import { type ExecutorResult, type RunOptions, type ScopeFactory, type SerializedPipelineStructure, type StageNode, type SubflowResult } from '../engine/types.js';
|
|
26
30
|
import type { CommitValuesMode, ReadTrackingMode, WriteTrackingMode } from '../memory/types.js';
|
|
27
31
|
import type { FlowchartCheckpoint } from '../pause/types.js';
|
|
28
32
|
import type { CombinedRecorder } from '../recorder/CombinedRecorder.js';
|
|
29
33
|
import type { EmitRecorder } from '../recorder/EmitRecorder.js';
|
|
30
|
-
import type { ScopeProtectionMode } from '../scope/protection/types.js';
|
|
31
34
|
import type { RedactionPolicy, RedactionReport, ScopeRecorder } from '../scope/types.js';
|
|
32
|
-
import {
|
|
35
|
+
import type { AttachRecorderOptions, ObserverDrainResult } from './DeferredObserverTier.js';
|
|
33
36
|
import { type RuntimeSnapshot } from './ExecutionRuntime.js';
|
|
34
|
-
|
|
35
|
-
* Options object for `FlowChartExecutor` — preferred over positional params.
|
|
36
|
-
*
|
|
37
|
-
* ```typescript
|
|
38
|
-
* const ex = new FlowChartExecutor(chart, {
|
|
39
|
-
* scopeFactory: myFactory,
|
|
40
|
-
* defaultValuesForContext: { ... },
|
|
41
|
-
* });
|
|
42
|
-
* ```
|
|
43
|
-
*
|
|
44
|
-
* **Sync note for maintainers:** Every field added here must also appear in the
|
|
45
|
-
* `flowChartArgs` private field type and in the constructor's options-resolution
|
|
46
|
-
* block (the `else if` branch that reads from `opts`). Missing any one of the
|
|
47
|
-
* three causes silent omission — the option is accepted but never applied.
|
|
48
|
-
* The observability DIALS are the exception: they come from `RunDials`
|
|
49
|
-
* (memory/runPolicy.ts) and travel as one `dials` field, so a new dial is
|
|
50
|
-
* added there and nowhere here.
|
|
51
|
-
*
|
|
52
|
-
* **TScope inference note:** When using the options-object form with a custom scope,
|
|
53
|
-
* TypeScript cannot infer `TScope` through the options object. Pass the type
|
|
54
|
-
* explicitly: `new FlowChartExecutor<TOut, MyScope>(chart, { scopeFactory })`.
|
|
55
|
-
*/
|
|
56
|
-
export interface FlowChartExecutorOptions<TScope = any> extends RunDials {
|
|
57
|
-
/** Custom scope factory. Defaults to TypedScope or ScopeFacade auto-detection. */
|
|
58
|
-
scopeFactory?: ScopeFactory<TScope>;
|
|
59
|
-
/**
|
|
60
|
-
* Default values pre-populated into the shared context before **each** stage
|
|
61
|
-
* (re-applied every stage, acting as baseline defaults).
|
|
62
|
-
*/
|
|
63
|
-
defaultValuesForContext?: unknown;
|
|
64
|
-
/**
|
|
65
|
-
* Initial context values merged into the shared context **once** at startup
|
|
66
|
-
* (applied before the first stage, not repeated on subsequent stages).
|
|
67
|
-
* Distinct from `defaultValuesForContext`, which is re-applied every stage.
|
|
68
|
-
*/
|
|
69
|
-
initialContext?: unknown;
|
|
70
|
-
/** Read-only input accessible via `scope.getArgs()` — never tracked or written. */
|
|
71
|
-
readOnlyContext?: unknown;
|
|
72
|
-
/**
|
|
73
|
-
* Custom error classifier for throttling detection. Return `true` if a fork
|
|
74
|
-
* child's error represents a rate-limit or backpressure condition; the
|
|
75
|
-
* executor then fires `FlowRecorder.onThrottled` for that child (9.39.0 —
|
|
76
|
-
* an EVENT, not a state key: the `monitor.isThrottled` write it replaced
|
|
77
|
-
* never landed). The child's failure is otherwise handled as before.
|
|
78
|
-
* Defaults to no throttling classification.
|
|
79
|
-
*/
|
|
80
|
-
throttlingErrorChecker?: (error: unknown) => boolean;
|
|
81
|
-
/** Handlers for streaming stage lifecycle events (see `addStreamingFunction`). */
|
|
82
|
-
streamHandlers?: StreamHandlers;
|
|
83
|
-
/** Scope protection mode for TypedScope direct-assignment detection. */
|
|
84
|
-
scopeProtectionMode?: ScopeProtectionMode;
|
|
85
|
-
}
|
|
37
|
+
import { type FlowChartExecutorOptions } from './options.js';
|
|
86
38
|
export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
87
39
|
private traverser;
|
|
88
|
-
/** Shared execution counter
|
|
40
|
+
/** Shared execution counter and per-stage visit counts (loopIteration) — shared BY
|
|
41
|
+
* REFERENCE with every traverser of the run; survive pause/resume, reset on run(). */
|
|
89
42
|
private _executionCounter;
|
|
90
|
-
/** Shared per-run visit counts (by stageId) driving TraversalContext.loopIteration.
|
|
91
|
-
* Twin of _executionCounter: survives pause/resume, reset on fresh run(). */
|
|
92
43
|
private _visitCounts;
|
|
93
|
-
/**
|
|
94
|
-
* through every TraversalContext so recorders can scope state to a single
|
|
95
|
-
* run. See `runId.ts`. */
|
|
44
|
+
/** Fresh per run() and per resume() — stamped on every TraversalContext (`runId.ts`). */
|
|
96
45
|
private _currentRunId;
|
|
97
|
-
|
|
98
|
-
private
|
|
99
|
-
private combinedRecorder;
|
|
100
|
-
private flowRecorders;
|
|
101
|
-
private scopeRecorders;
|
|
102
|
-
/**
|
|
103
|
-
* RFC-001 deferred-observer wiring — created LAZILY on the first
|
|
104
|
-
* `delivery: 'deferred'` attach. `undefined` for every executor that never
|
|
105
|
-
* opts in: zero allocation, zero per-event cost, byte-identical behavior
|
|
106
|
-
* (the emit fast-path precedent).
|
|
107
|
-
*/
|
|
108
|
-
private deferredTier?;
|
|
46
|
+
/** Who observes the run — narrative, inline lists, deferred tier (`attach.ts`). */
|
|
47
|
+
private readonly observers;
|
|
109
48
|
private redactionPolicy;
|
|
110
|
-
/**
|
|
111
|
-
* The run's ONE redaction rule (`memory/redaction.ts`): built fresh per
|
|
112
|
-
* `createTraverser` (per-call marks reset per run and per resume, as the
|
|
113
|
-
* shared set always did), installed on the runtime root so every context
|
|
114
|
-
* retains under it, and read by every `ScopeFacade` through its context.
|
|
115
|
-
*/
|
|
49
|
+
/** The run's ONE redaction rule (`memory/redaction.ts`), built fresh per leg. */
|
|
116
50
|
private redactionRule;
|
|
117
51
|
private lastCheckpoint;
|
|
118
52
|
/**
|
|
119
|
-
* `true` once
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* continuity: execution tree, recorders, narrative
|
|
124
|
-
* accumulate across pause/resume cycles)
|
|
125
|
-
* • false → seed a fresh runtime from `checkpoint.sharedState`
|
|
126
|
-
* (cross-executor / cross-process resume: new instance
|
|
127
|
-
* reconstructed from a serialized checkpoint)
|
|
128
|
-
*
|
|
129
|
-
* Without this flag, fresh executors silently discarded the
|
|
130
|
-
* checkpoint's sharedState and resume handlers couldn't read pre-pause
|
|
131
|
-
* scope. See `test/lib/pause/cross-executor-resume.test.ts`.
|
|
53
|
+
* `true` once run() (or a resume) executed here. `resume()` branches on it:
|
|
54
|
+
* true → reuse the runtime (execution tree, recorders, narrative accumulate);
|
|
55
|
+
* false → seed a fresh runtime from `checkpoint.sharedState` (cross-executor
|
|
56
|
+
* resume). See `test/lib/pause/cross-executor-resume.test.ts`.
|
|
132
57
|
*/
|
|
133
58
|
private _hasRunBefore;
|
|
134
59
|
/**
|
|
135
|
-
* Re-entrancy guard
|
|
136
|
-
*
|
|
137
|
-
* recorders — a second concurrent entry on the SAME executor would
|
|
138
|
-
* interleave runIds and cross-contaminate recorder/narrative state, and
|
|
139
|
-
* `getCheckpoint()` would return whichever run paused last. One executor =
|
|
140
|
-
* one in-flight execution; create an executor per concurrent run.
|
|
60
|
+
* Re-entrancy guard: run()/resume() mutate per-run state (traverser, runId,
|
|
61
|
+
* counters, checkpoint, recorders) — one executor = one in-flight execution.
|
|
141
62
|
* See docs/guides/execution-model.md.
|
|
142
63
|
*/
|
|
143
64
|
private _isExecuting;
|
|
@@ -166,28 +87,11 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
166
87
|
* Must be called before run().
|
|
167
88
|
*/
|
|
168
89
|
setRedactionPolicy(policy: RedactionPolicy): void;
|
|
169
|
-
/**
|
|
170
|
-
* Set the read-tracking policy for `StageSnapshot.stageReads` (#14).
|
|
171
|
-
* Must be called before run(). Equivalent to the `readTracking`
|
|
172
|
-
* constructor option — see {@link FlowChartExecutorOptions.readTracking}
|
|
173
|
-
* for the mode semantics ('full' default / 'summary' / 'off').
|
|
174
|
-
*/
|
|
90
|
+
/** The `readTracking` dial (see {@link FlowChartExecutorOptions.readTracking}). Call before run(). */
|
|
175
91
|
setReadTracking(mode: ReadTrackingMode): void;
|
|
176
|
-
/**
|
|
177
|
-
* Set the write-tracking policy for `StageSnapshot.stageWrites` (#13c-A).
|
|
178
|
-
* Must be called before run(). Equivalent to the `writeTracking`
|
|
179
|
-
* constructor option — see {@link FlowChartExecutorOptions.writeTracking}
|
|
180
|
-
* for the mode semantics ('full' default / 'summary' / 'off'), the
|
|
181
|
-
* onCommit-payload consequence, and the redaction-precedence rule.
|
|
182
|
-
*/
|
|
92
|
+
/** The `writeTracking` dial (see {@link FlowChartExecutorOptions.writeTracking}). Call before run(). */
|
|
183
93
|
setWriteTracking(mode: WriteTrackingMode): void;
|
|
184
|
-
/**
|
|
185
|
-
* Set the commit-values encoding policy for the commit log (#13c-B).
|
|
186
|
-
* Must be called before run(). Equivalent to the `commitValues`
|
|
187
|
-
* constructor option — see {@link FlowChartExecutorOptions.commitValues}
|
|
188
|
-
* for the mode semantics ('full' default / 'delta'), the verb-qualified
|
|
189
|
-
* `overwrite` consequence, and the `commitValueAt` migration helper.
|
|
190
|
-
*/
|
|
94
|
+
/** The `commitValues` dial (see {@link FlowChartExecutorOptions.commitValues}). Call before run(). */
|
|
191
95
|
setCommitValues(mode: CommitValuesMode): void;
|
|
192
96
|
/**
|
|
193
97
|
* Returns a compliance-friendly report of all redaction activity from the
|
|
@@ -198,12 +102,9 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
198
102
|
* Returns the checkpoint from the most recent paused execution, or `undefined`
|
|
199
103
|
* if the last run completed without pausing.
|
|
200
104
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* pause time (see `buildPauseCheckpoint`). Holding, mutating, or persisting
|
|
205
|
-
* it cannot affect the executor, and a later same-executor resume cannot
|
|
206
|
-
* mutate a checkpoint you already stored.
|
|
105
|
+
* JSON-serializable (store it in Redis, Postgres, localStorage…) and fully
|
|
106
|
+
* DETACHED from engine state (`checkpoint.ts`): mutating or persisting it
|
|
107
|
+
* cannot affect the executor, nor a later resume a checkpoint you stored.
|
|
207
108
|
*
|
|
208
109
|
* @example
|
|
209
110
|
* ```typescript
|
|
@@ -226,38 +127,23 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
226
127
|
*
|
|
227
128
|
* Returns 0 before any run; after, returns the cumulative commit
|
|
228
129
|
* count across the executor's lifetime (including resumes).
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* which is the same value as `getSnapshot().commitLog.length`. The
|
|
232
|
-
* naming asymmetry is historical — the underlying `EventLog` field
|
|
233
|
-
* is named `executionHistory` but stores the `CommitBundle[]` that
|
|
234
|
-
* `commitLog` exposes. They always report the same LENGTH (verified by the
|
|
235
|
-
* "matches commitLog.length" integration test); since 9.17.0 the snapshot
|
|
236
|
-
* serves a detached frozen COPY of the array, not the live one.
|
|
130
|
+
* Always equals `getSnapshot().commitLog.length` (the log is the
|
|
131
|
+
* `EventLog`'s `executionHistory`; the snapshot serves a frozen copy).
|
|
237
132
|
*/
|
|
238
133
|
getCommitCount(): number;
|
|
239
134
|
/**
|
|
240
135
|
* Resume a paused flowchart from a checkpoint.
|
|
241
136
|
*
|
|
242
|
-
* Restores the scope state,
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* (`checkpoint.pendingPauses`), the run pauses again with the next sibling's
|
|
249
|
-
* question once this one's branch is done; the fan-out's join runs after the
|
|
250
|
-
* last answer. See `ResumeEntry` (`footprintjs/advanced`).
|
|
137
|
+
* Restores the scope state, runs the paused stage's `resumeFn` with the input
|
|
138
|
+
* (an `interrupt()` pause re-runs the stage with the answer), then continues
|
|
139
|
+
* with whatever ran after that stage on the run — its `next`, or its
|
|
140
|
+
* dispatcher's continuation, at every level of the pause path. Parallel
|
|
141
|
+
* siblings that paused together (`checkpoint.pendingPauses`) are asked in
|
|
142
|
+
* turn; the join runs after the last answer. See `ResumeEntry`.
|
|
251
143
|
*
|
|
252
|
-
* The checkpoint
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* **Recorder/narrative state depends on the resume mode.** Resuming on the SAME
|
|
256
|
-
* executor that ran preserves and accumulates narrative/metrics/debug across the
|
|
257
|
-
* pause/resume cycle (preserveRecorders). Resuming on a FRESH executor
|
|
258
|
-
* (reconstructed from a stored checkpoint) starts with empty recorder state —
|
|
259
|
-
* collect what you need before discarding the paused executor. A fresh `runId`
|
|
260
|
-
* is generated either way.
|
|
144
|
+
* The checkpoint comes from `getCheckpoint()` or from storage. On the SAME
|
|
145
|
+
* executor, narrative/recorder state accumulates across the pause; on a
|
|
146
|
+
* FRESH executor it starts empty. A fresh `runId` either way.
|
|
261
147
|
*
|
|
262
148
|
* @example
|
|
263
149
|
* ```typescript
|
|
@@ -273,139 +159,37 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
273
159
|
*/
|
|
274
160
|
resume(checkpoint: FlowchartCheckpoint, resumeInput?: unknown, options?: Pick<RunOptions, 'signal' | 'env' | 'maxDepth' | 'maxIterations'>): Promise<ExecutorResult>;
|
|
275
161
|
/**
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* checkpoint
|
|
280
|
-
*
|
|
281
|
-
* - `sharedState` IS `SharedMemory`'s current generation — never edited,
|
|
282
|
-
* but (copy-on-write, 9.29.0) every later generation shares its
|
|
283
|
-
* unchanged subtrees, so a checkpoint that aliased it would alias the
|
|
284
|
-
* resumed run's state too.
|
|
285
|
-
* - `executionTree` nodes are fresh, but their `logs`/`errors`/`metrics`/
|
|
286
|
-
* `evals`/`stageReads`/`flowMessages` fields reference live
|
|
287
|
-
* `DiagnosticCollector` bags that keep accumulating on same-executor
|
|
288
|
-
* resume.
|
|
289
|
-
* - `subflowStates` values are shallow copies whose NESTED objects alias
|
|
290
|
-
* subflow memory, and they get seeded back into live runtimes on resume.
|
|
291
|
-
* - `subflowResults` values stay referenced by the traverser's results map.
|
|
292
|
-
*
|
|
293
|
-
* The checkpoint is persisted by contract ("store in Redis/Postgres") — it
|
|
294
|
-
* must never share structure with the engine. Pause is not a hot path; the
|
|
295
|
-
* clone cost is irrelevant.
|
|
296
|
-
*
|
|
297
|
-
* The JSON-safe checkpoint contract (no functions, no class instances)
|
|
298
|
-
* governs CONSUMER-owned data — but the executionTree's diagnostic bags
|
|
299
|
-
* accept ANY value at write time without cloning ($debug/$error/$metric/
|
|
300
|
-
* $eval store raw references), so a contract-compliant run can still carry
|
|
301
|
-
* a non-cloneable diagnostic. Observability side-bags never abort traversal
|
|
302
|
-
* anywhere else in the library, so they must not abort the pause either:
|
|
303
|
-
* on clone failure we sanitize the diagnostic bags (non-cloneable values
|
|
304
|
-
* become '[non-serializable: …]' markers — the live engine bags are never
|
|
305
|
-
* touched) and retry. If the retry STILL fails, the violation is in
|
|
306
|
-
* consumer-owned data (realistically `pauseData` — a function can never
|
|
307
|
-
* reach shared state in the first place: TransactionBuffer clones every
|
|
308
|
-
* written value at write time, so the offending write already rejected)
|
|
309
|
-
* and we throw a DESCRIPTIVE contract error naming the offending
|
|
310
|
-
* checkpoint field(s). A naked DataCloneError never escapes.
|
|
311
|
-
*
|
|
312
|
-
* Subflow scope capture (`subflowStates`) survives ONLY on the signal — the
|
|
313
|
-
* nested runtimes are GC'd as the stack unwinds. Promoting it onto the
|
|
314
|
-
* checkpoint here lets cross-executor resume restore pre-pause subflow
|
|
315
|
-
* scope (e.g. an Agent's `scope.history`). Empty `{}` for root-level pauses.
|
|
316
|
-
*/
|
|
317
|
-
private buildPauseCheckpoint;
|
|
318
|
-
/**
|
|
319
|
-
* Find a StageNode in the compiled graph by ID.
|
|
320
|
-
* Handles subflow paths by drilling into registered subflows.
|
|
162
|
+
* The settle of a leg that threw: flush the deferred tier (the OUTERMOST
|
|
163
|
+
* handler — a pause re-throws through subflow traversers without exit
|
|
164
|
+
* events, so per-traverser hooks would miss it), then turn a pause into a
|
|
165
|
+
* detached checkpoint (`checkpoint.ts`) and a `PausedResult`; anything else
|
|
166
|
+
* is rethrown.
|
|
321
167
|
*/
|
|
322
|
-
private
|
|
323
|
-
/**
|
|
324
|
-
private
|
|
168
|
+
private pausedOrThrow;
|
|
169
|
+
/** The re-entrancy guard: one executor = one in-flight execution. */
|
|
170
|
+
private assertIdle;
|
|
325
171
|
/**
|
|
326
|
-
* Attach a
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* it is replaced (not duplicated). This prevents double-counting when both
|
|
332
|
-
* a framework and the user attach the same recorder type.
|
|
333
|
-
*
|
|
334
|
-
* Built-in recorders use auto-increment IDs (`metrics-1`, `debug-1`, ...) by
|
|
335
|
-
* default, so multiple instances with different configs coexist. To override
|
|
336
|
-
* a framework-attached recorder, pass the same well-known ID.
|
|
172
|
+
* Attach a ScopeRecorder to observe data operations (reads, writes, commits)
|
|
173
|
+
* on every stage scope. Call before run(). **Idempotent by `id`** (replaced,
|
|
174
|
+
* never duplicated). `{ delivery: 'deferred' }` moves it off the hot path —
|
|
175
|
+
* delivered at the next microtask checkpoint; re-attaching an id on the
|
|
176
|
+
* other tier SWAPS tiers (`docs/guides/observers-deferred.md`).
|
|
337
177
|
*
|
|
338
178
|
* @example
|
|
339
179
|
* ```typescript
|
|
340
|
-
* // Multiple recorders with different configs — each gets a unique ID
|
|
341
180
|
* executor.attachScopeRecorder(new MetricRecorder());
|
|
342
|
-
* executor.attachScopeRecorder(new
|
|
343
|
-
*
|
|
344
|
-
* // Override a framework-attached recorder by passing its well-known ID
|
|
345
|
-
* executor.attachScopeRecorder(new MetricRecorder('metrics'));
|
|
346
|
-
*
|
|
347
|
-
* // Attaching twice with same ID replaces (no double-counting)
|
|
348
|
-
* executor.attachScopeRecorder(new MetricRecorder('my-metrics'));
|
|
349
|
-
* executor.attachScopeRecorder(new MetricRecorder('my-metrics')); // replaces previous
|
|
181
|
+
* executor.attachScopeRecorder(new MetricRecorder('my-metrics')); // replaces same id
|
|
350
182
|
* ```
|
|
351
|
-
*
|
|
352
|
-
* **Delivery tier (RFC-001):** pass `{ delivery: 'deferred' }` to take the
|
|
353
|
-
* recorder out of the engine's hot path — events are captured into a
|
|
354
|
-
* bounded queue and delivered at the next microtask checkpoint ("one beat
|
|
355
|
-
* behind"). Omitting `delivery` keeps the historical synchronous call,
|
|
356
|
-
* byte-identical to previous releases. Re-attaching the same `id` with a
|
|
357
|
-
* different tier SWAPS tiers cleanly — never double delivery. See
|
|
358
|
-
* `docs/guides/observers-deferred.md`.
|
|
359
183
|
*/
|
|
360
184
|
attachScopeRecorder(recorder: ScopeRecorder, options?: AttachRecorderOptions): void;
|
|
361
|
-
/**
|
|
362
|
-
* Lazily create the executor's ONE deferred-observer tier (one merged
|
|
363
|
-
* queue, total event order across all three channels). The FIRST deferred
|
|
364
|
-
* attach's options configure the dispatcher; later differing options are
|
|
365
|
-
* dev-warned and ignored (see `AttachRecorderOptions`).
|
|
366
|
-
*/
|
|
367
|
-
private ensureDeferredTier;
|
|
368
|
-
/**
|
|
369
|
-
* Detach a child flowchart on the given driver and return a `DetachHandle`
|
|
370
|
-
* the caller can `wait()` on (Promise) or read `.status` from (sync).
|
|
371
|
-
*
|
|
372
|
-
* The driver is a REQUIRED first argument — there is no library-default,
|
|
373
|
-
* to keep the engine free of driver imports and to make the choice of
|
|
374
|
-
* scheduling algorithm explicit at the call site.
|
|
375
|
-
*
|
|
376
|
-
* @example
|
|
377
|
-
* ```typescript
|
|
378
|
-
* import { microtaskBatchDriver } from 'footprintjs/detach';
|
|
379
|
-
*
|
|
380
|
-
* const exec = new FlowChartExecutor(parentChart);
|
|
381
|
-
* const handle = exec.detachAndJoinLater(microtaskBatchDriver, telemetryChart, { event: 'x' });
|
|
382
|
-
* await handle.wait(); // optional
|
|
383
|
-
* ```
|
|
384
|
-
*/
|
|
385
|
-
detachAndJoinLater(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): import('../detach/types.js').DetachHandle;
|
|
386
|
-
/**
|
|
387
|
-
* Detach a child flowchart on the given driver and DISCARD the handle.
|
|
388
|
-
* Use for telemetry exports / fire-and-forget side effects where the
|
|
389
|
-
* caller doesn't care about the result.
|
|
390
|
-
*
|
|
391
|
-
* Errors raised by the child still land on the (discarded) handle — they
|
|
392
|
-
* go silent unless surfaced through a recorder. For observable detach,
|
|
393
|
-
* prefer `detachAndJoinLater` and surface failures via `.wait().catch()`.
|
|
394
|
-
*/
|
|
395
|
-
detachAndForget(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): void;
|
|
396
185
|
/** Detach all scope Recorders with the given ID — both delivery tiers. */
|
|
397
186
|
detachScopeRecorder(id: string): void;
|
|
398
187
|
/** Returns a defensive copy of attached scope Recorders (both tiers). */
|
|
399
188
|
getScopeRecorders(): ScopeRecorder[];
|
|
400
189
|
/**
|
|
401
|
-
* Attach a FlowRecorder to observe control flow events.
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* **Idempotent by ID:** replaces existing recorder with same `id`.
|
|
406
|
-
*
|
|
407
|
-
* **Delivery tier (RFC-001):** pass `{ delivery: 'deferred' }` for
|
|
408
|
-
* next-checkpoint delivery off the hot path — see `attachScopeRecorder`.
|
|
190
|
+
* Attach a FlowRecorder to observe control flow events. Automatically
|
|
191
|
+
* enables narrative. Must be called before run(). Idempotent by `id`;
|
|
192
|
+
* `{ delivery: 'deferred' }` as for `attachScopeRecorder`.
|
|
409
193
|
*/
|
|
410
194
|
attachFlowRecorder(recorder: FlowRecorder, options?: AttachRecorderOptions): void;
|
|
411
195
|
/** Detach all FlowRecorders with the given ID — both delivery tiers. */
|
|
@@ -413,39 +197,11 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
413
197
|
/** Returns a defensive copy of attached FlowRecorders (both tiers). */
|
|
414
198
|
getFlowRecorders(): FlowRecorder[];
|
|
415
199
|
/**
|
|
416
|
-
* Attach a recorder
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
* separately, because forgetting one of the two is a silent foot-gun —
|
|
422
|
-
* half your events never fire and there is no runtime warning. With
|
|
423
|
-
* `attachCombinedRecorder` the library guarantees the recorder's declared
|
|
424
|
-
* methods all fire, and adds no overhead versus two explicit calls.
|
|
425
|
-
*
|
|
426
|
-
* ## Idempotency
|
|
427
|
-
*
|
|
428
|
-
* Idempotent by `id` across ALL channels — re-attaching with the same `id`
|
|
429
|
-
* replaces the previous instance everywhere it was registered. Mixing
|
|
430
|
-
* `attachCombinedRecorder(x)` with a prior `attachScopeRecorder(y)` or
|
|
431
|
-
* `attachFlowRecorder(y)` that share `x.id === y.id` is also safe: the
|
|
432
|
-
* combined attach replaces the single-channel registration on whichever
|
|
433
|
-
* channel(s) `x` has methods for. No duplicate firings occur.
|
|
434
|
-
*
|
|
435
|
-
* ## Narrative activation
|
|
436
|
-
*
|
|
437
|
-
* If the recorder has any control-flow methods, `enableNarrative()` is
|
|
438
|
-
* called as a side effect (the narrative subsystem is required to emit
|
|
439
|
-
* control-flow events). Data-flow-only recorders do NOT activate the
|
|
440
|
-
* narrative.
|
|
441
|
-
*
|
|
442
|
-
* ## Detection rule
|
|
443
|
-
*
|
|
444
|
-
* Only **own** event methods count (see `hasRecorderMethods`). Methods
|
|
445
|
-
* inherited via the prototype chain are ignored — this protects against
|
|
446
|
-
* accidental `Object.prototype` pollution attaching handlers you never
|
|
447
|
-
* declared. A recorder that provides only `clear`/`toSnapshot` is a
|
|
448
|
-
* no-op and emits a dev-mode warning to surface the likely mistake.
|
|
200
|
+
* Attach a recorder to every channel (scope, flow, emit) it has OWN `on*`
|
|
201
|
+
* methods for — preferred over single-channel calls, where forgetting one
|
|
202
|
+
* silently loses events. Idempotent by `id` across all channels; a flow
|
|
203
|
+
* method enables the narrative; `delivery` comes from the options or the
|
|
204
|
+
* recorder's own field.
|
|
449
205
|
*
|
|
450
206
|
* @example
|
|
451
207
|
* ```typescript
|
|
@@ -458,24 +214,11 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
458
214
|
* ```
|
|
459
215
|
*/
|
|
460
216
|
attachCombinedRecorder(recorder: CombinedRecorder, options?: AttachRecorderOptions): void;
|
|
461
|
-
/**
|
|
462
|
-
* Detach a combined recorder from all channels it was attached to.
|
|
463
|
-
* Safe to call if the recorder was only on one channel or never attached.
|
|
464
|
-
*/
|
|
217
|
+
/** Detach a combined recorder from every channel it was attached to (safe if never attached). */
|
|
465
218
|
detachCombinedRecorder(id: string): void;
|
|
466
219
|
/**
|
|
467
|
-
* Attach an `EmitRecorder`
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
* Internally, emit recorders share the scope-recorder channel because
|
|
471
|
-
* emit events fire from inside `ScopeFacade` during stage execution,
|
|
472
|
-
* same timing as `onRead`/`onWrite`. This method is a convenience that
|
|
473
|
-
* delegates to `attachScopeRecorder` — consumers can also use
|
|
474
|
-
* `attachScopeRecorder` directly for a recorder that implements BOTH
|
|
475
|
-
* `onWrite` and `onEmit`. Either approach places the recorder on the
|
|
476
|
-
* same underlying list, so `onEmit` fires exactly once per event.
|
|
477
|
-
*
|
|
478
|
-
* **Idempotent by `id`:** replaces existing recorder with same `id`.
|
|
220
|
+
* Attach an `EmitRecorder` for `scope.$emit(name, payload)` events. It rides
|
|
221
|
+
* the scope list, so `onEmit` fires exactly once per event. Idempotent by `id`.
|
|
479
222
|
*
|
|
480
223
|
* @example
|
|
481
224
|
* ```typescript
|
|
@@ -490,25 +233,34 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
490
233
|
attachEmitRecorder(recorder: EmitRecorder, options?: AttachRecorderOptions): void;
|
|
491
234
|
/** Detach an `EmitRecorder` by id. Safe to call if never attached. */
|
|
492
235
|
detachEmitRecorder(id: string): void;
|
|
236
|
+
/** Returns a defensive copy of attached recorders (both tiers) that implement `onEmit`. */
|
|
237
|
+
getEmitRecorders(): EmitRecorder[];
|
|
493
238
|
/**
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
239
|
+
* Detach a child flowchart on the given driver and return a `DetachHandle`
|
|
240
|
+
* the caller can `wait()` on (Promise) or read `.status` from (sync). The
|
|
241
|
+
* driver is REQUIRED — there is no library default.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```typescript
|
|
245
|
+
* import { microtaskBatchDriver } from 'footprintjs/detach';
|
|
246
|
+
*
|
|
247
|
+
* const exec = new FlowChartExecutor(parentChart);
|
|
248
|
+
* const handle = exec.detachAndJoinLater(microtaskBatchDriver, telemetryChart, { event: 'x' });
|
|
249
|
+
* await handle.wait(); // optional
|
|
250
|
+
* ```
|
|
497
251
|
*/
|
|
498
|
-
|
|
252
|
+
detachAndJoinLater(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): import('../detach/types.js').DetachHandle;
|
|
499
253
|
/**
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* if a flat `string[]` is needed locally.
|
|
254
|
+
* Detach a child flowchart on the given driver and DISCARD the handle — for
|
|
255
|
+
* fire-and-forget side effects. A child's error lands on the discarded handle;
|
|
256
|
+
* for observable detach prefer `detachAndJoinLater` + `.wait().catch()`.
|
|
504
257
|
*/
|
|
505
|
-
|
|
258
|
+
detachAndForget(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): void;
|
|
506
259
|
/**
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
* entries inline). Plus any user-attached recorders.
|
|
260
|
+
* Structured narrative entries (type, text, depth) — the one public narrative
|
|
261
|
+
* API; `.map(e => e.text)` for a flat `string[]`.
|
|
510
262
|
*/
|
|
511
|
-
|
|
263
|
+
getNarrativeEntries(): CombinedNarrativeEntry[];
|
|
512
264
|
/**
|
|
513
265
|
* Execute the chart. Resolves when the run finishes — or pauses, if a
|
|
514
266
|
* pausable stage returned data (check `isPaused()` afterward).
|
|
@@ -528,13 +280,10 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
528
280
|
*/
|
|
529
281
|
run(options?: RunOptions): Promise<ExecutorResult>;
|
|
530
282
|
/**
|
|
531
|
-
* Flush the deferred-observer backlog, then await async listener
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
* when no deferred observer was ever attached. `pending === 0` means a
|
|
536
|
-
* full drain; a non-zero `pending` reports continuations (plus any queued
|
|
537
|
-
* events) still outstanding at the deadline — honest, never silent.
|
|
283
|
+
* Flush the deferred-observer backlog, then await async listener completions
|
|
284
|
+
* under a deadline — call before a serverless process freezes or exits.
|
|
285
|
+
* Zeros when no deferred observer was attached; `pending > 0` reports what
|
|
286
|
+
* was still outstanding at the deadline.
|
|
538
287
|
*/
|
|
539
288
|
drainObservers(opts?: {
|
|
540
289
|
timeoutMs?: number;
|
|
@@ -542,50 +291,17 @@ export declare class FlowChartExecutor<TOut = any, TScope = any> {
|
|
|
542
291
|
/**
|
|
543
292
|
* Returns the runtime snapshot.
|
|
544
293
|
*
|
|
545
|
-
* @param options.redact
|
|
546
|
-
* redacted mirror
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
* redacted mirror is not maintained, so this flag is a no-op —
|
|
550
|
-
* `sharedState` is the raw working memory either way. Default `false`.
|
|
551
|
-
*
|
|
552
|
-
* The commit log is already redacted at write-time regardless of this
|
|
553
|
-
* flag, and the execution tree carries only structural metadata.
|
|
554
|
-
* `subflowResults[*].treeContext.globalContext` follows the flag too
|
|
555
|
-
* (9.20.0): under `redact: true` it is each subflow's own redacted
|
|
556
|
-
* mirror; plain, the subflow's live heap.
|
|
294
|
+
* @param options.redact `true` serves `sharedState` (and each subflow's
|
|
295
|
+
* `globalContext`) from the redacted mirror — the safe view to export. A
|
|
296
|
+
* no-op without a redaction policy. The commit log is redacted at write
|
|
297
|
+
* time regardless. Default `false`.
|
|
557
298
|
*
|
|
558
299
|
* **Treat `sharedState` as READ-ONLY.** In production it is a live view of
|
|
559
|
-
*
|
|
560
|
-
* engine state. In dev mode (`enableDevMode()`) it is a deep-frozen CLONE,
|
|
561
|
-
* so any consumer mutation throws loudly instead of corrupting silently.
|
|
300
|
+
* engine memory; in dev mode a deep-frozen clone, so a mutation throws.
|
|
562
301
|
*/
|
|
563
302
|
getSnapshot(options?: {
|
|
564
303
|
redact?: boolean;
|
|
565
304
|
}): RuntimeSnapshot;
|
|
566
|
-
/**
|
|
567
|
-
* Collect `toSnapshot()` bundles from every attached recorder — ONE entry
|
|
568
|
-
* per recorder id, across all channels and both delivery tiers.
|
|
569
|
-
*
|
|
570
|
-
* The dedupe is load-bearing, not tidiness. A recorder that implements a
|
|
571
|
-
* shared-name hook (`onError` / `onPause` / `onResume` — declared on BOTH
|
|
572
|
-
* the scope and flow interfaces) is legitimately registered on BOTH inline
|
|
573
|
-
* lists by `attachCombinedRecorder`, and that is by design: each channel
|
|
574
|
-
* calls the hook with its own payload variant. `MetricRecorder.onPause` is
|
|
575
|
-
* the everyday case. Walking the lists without a shared `seen` set turned
|
|
576
|
-
* that into a DUPLICATED snapshot entry (same id twice), which breaks every
|
|
577
|
-
* consumer that indexes `snapshot.recorders` by id.
|
|
578
|
-
*
|
|
579
|
-
* Ordering is scope list → flow list → deferred tier, and the FIRST bundle
|
|
580
|
-
* for an id wins. A recorder with no `toSnapshot` never claims an id, so it
|
|
581
|
-
* cannot shadow a same-id recorder that does have one.
|
|
582
|
-
*
|
|
583
|
-
* The row is REBUILT field by field by the one copier (`recorder/snapshot.ts
|
|
584
|
-
* · copyBundle`, shared with `CompositeRecorder`) rather than spread, so a
|
|
585
|
-
* recorder cannot smuggle an `id` of its own choosing into the snapshot (the
|
|
586
|
-
* id is the executor's, and consumers index by it).
|
|
587
|
-
*/
|
|
588
|
-
private collectRecorderSnapshots;
|
|
589
305
|
/** @internal */
|
|
590
306
|
getRuntime(): import("../engine/types.js").IExecutionRuntime;
|
|
591
307
|
/** @internal */
|