98
}
99
}
101
>
102
>
// ============================================================================
103
>
// Step outcome — what the pure state machine tells the trampoline
104
>
// ============================================================================
105
>
106
>
type StepOutcome =
107
>
/** Either a virtual event was executed, or a run was rejected for a
108
>
* bookkeeping reason (depth/event overflow). The trampoline should let
109
>
* the embedding decide how to reach the next step. */
110
>
| 'progress'
111
>
/** No actionable event under any active deadline. The trampoline should
112
>
* park until something wakes the processor. */
113
>
| 'park'
114
>
/** No active runs. The trampoline should stop driving. */
115
>
| 'quiesce';
116
>
117
>
// ============================================================================
118
>
// VirtualTimeProcessor
119
>
// ============================================================================
120
>
121
>
export interface VirtualTimeProcessorOptions {
122
>
readonly defaultMaxEvents?: number;
123
>
}
124
>
125
>
/**
126
>
* # VirtualTimeProcessor
127
>
*
128
>
* Drives a {@link VirtualClock} from the host event loop. This is the
129
>
* **embedding** of a small virtual event loop into the host event loop.
130
>
*
131
>
* ## Responsibilities, separated
132
>
*
133
>
* - {@link _step} is a *pure* state-machine advance. It reads the clock,
134
>
* decides what to do, optionally executes one virtual event, and returns
135
>
* a {@link StepOutcome}. It never touches host time.
136
>
*
137
>
* - {@link _drive} is the *trampoline*. It calls `_step` and lets the
138
>
* {@link Embedding} decide whether to loop in place (`'continueSync'`)
139
>
* or schedule the next iteration on the host (`'cbScheduled'`). It is
140
>
* the only code that touches host time.
141
>
*
142
>
* - {@link Run} carries the user's termination predicate. Runs are pure
143
>
* over `_step`'s observations; they never schedule.
144
>
*
145
>
* ## Invariants
146
>
*
147
>
* 1. **Single driver.** At any moment at most one `_drive` invocation is
148
>
* active per processor (the `_inDrive` guard).
149
>
*
150
>
* 2. **Step is pure w.r.t. host time.** `_step` only reads the clock,
151
>
* mutates the run set via settling, and synchronously runs at most one
152
>
* virtual event. It never calls into a host time API.
153
>
*
154
>
* 3. **Embedding chooses the host primitive.** Whether the next step runs
155
>
* inline, after a microtask drain, or on a paint frame is entirely the
156
>
* embedding's decision — *per event*.
157
>
*
158
>
* 4. **Park is breakable.** While parked, the processor wakes on
159
>
* {@link VirtualClock.onEventScheduled}, on a token cancellation, and
160
>
* on a new run being added.
161
>
*
162
>
* 5. **Disposal is terminal.** After dispose, all runs are rejected and
163
>
* `_step`/`_drive` short-circuit to `'quiesce'`.
164
>
*
165
>
* ## On the trace-reset sink
166
>
*
167
>
* The trace context's deferred reset (see {@link TraceContext.runAsHandler})
168
>
* needs a "fire after the microtask closure" primitive. The processor passes
169
>
* its *own* {@link nextMacrotask} as that sink, so the reset goes through
170
>
* the same primitive the embedding uses for its own host hops. This removes
171
>
* any race between the processor's hops and the trace-reset timer.
172
>
*/
173
>
export class VirtualTimeProcessor extends Disposable {
174
>
175
>
private readonly _runs = new Map<Run, IDisposable>();
176
>
private readonly _history: VirtualEvent[] = [];
177
>
private _executedTotal = 0;
178
>
private _disposed = false;
179
>
180
>
private _inDrive = false;
181
>
private _parkCleanup: IDisposable | undefined;
182
>
183
>
private readonly _defaultMaxEvents: number;
184
>
185
>
public get history(): readonly VirtualEvent[] { return this._history; }
186
>
public get executedTotal(): number { return this._executedTotal; }
187
>
188
>
constructor(
189
private readonly _clock: VirtualClock,
190
private readonly _embedding: Embedding,