Skip to content

This site describes solve-engine as it is on main: 2.43.0, which npm does not have yet. npm installs 2.40.0, so a page may show an answer that version does not give yet.

AsyncResolutionBatcher

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:78

Micro-batches async resolution completions into a single DAG walk + re-evaluation pass.

When multiple promises resolve within the same event-loop tick (e.g., 3 currency rates: USD→GBP, USD→EUR, USD→JPY all return within 2ms), this batcher collapses them into ONE DAG walk and ONE re-execution pass instead of 3 separate ones.

Lifecycle:

  • Engine calls add() after each resolveAsync() completes
  • queueMicrotask() schedules flush() for the end of the current tick
  • flush() deduplicates queryKeys, walks DAG once for all resolved keys, topologically sorts affected lines, re-executes, and fires listener events

Perf: For N resolutions in a tick, reduces DAG walks from N to 1 and re-executions from N*avgAffected to totalAffected.

* Events are published to a native ReadableStream. Stream-based
  • consumers read from the stream via getReader(), gaining built-in
  • cancellation (reader.cancel()), proper resource cleanup
  • (reader.releaseLock()), and the ability to
  • pipeTo() / pipeThrough() / tee() the event flow.

The stream uses a configurable CountQueuingStrategy with a default highWaterMark of 64 events to limit the internal buffer size.

new AsyncResolutionBatcher(
dag,
lineCache,
vm,
highWaterMark?
): AsyncResolutionBatcher;

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:261

ParameterTypeDefault value
dagDependencyGraphundefined
lineCacheLineCacheundefined
vmVMundefined
highWaterMarknumberAsyncResolutionBatcher.DEFAULT_HIGH_WATER_MARK

AsyncResolutionBatcher

_testCaptures:
| AsyncResolutionEvent[]
| null = null;

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:123

Test-only synchronous capture array. When enabled (non-null), every event is synchronously pushed here in addition to the stream. Tests read from this array to avoid async stream reader timing issues.


checkpointer: VMCheckpointer | null = null;

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:168

The checkpoint chain to rebuild variable state from, when there is one.

A re-run reads whatever the VM currently holds, which after a full pass is the state at the END of the document. That is the right answer only for a name written once. :x = <fetched> above :x = 99 left the fetched value standing at every line below the redefinition, because the line that redefines it was not itself affected and so was not re-run: a line reading x below it answered with the value from the top of the document.

With a chain, the batch is run as a sweep through the document instead: restore to the line before the first affected one, then walk forward, executing the affected lines and applying the recorded bindings of every writing line passed on the way. Null leaves the previous behaviour exactly as it was, which is what a host driving the batcher on its own gets.


isAwaited: (() => boolean) | null = null;

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:256

Answers whether someone is waiting on the engine’s settle(), set by the owning engine. A caller awaiting settle() re-evaluates once it resolves, which is receiving the result, so the unwired warning does not apply to it. Null reads as nobody waiting.


keepResult:
| ((lineNumber, value, writeVariable) => Value)
| null = null;

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:150

Bounds what a re-run keeps, set by the owning engine: the answer as it is, or the refusal when keeping it would take the note past vm.maxRetainedElements (#694), with the name the line assigned let go. Null keeps every answer.


onLineResult: ((lineNumber, value) => void) | null = null;

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:142

Called for each line whose result is patched after an async resolution, on both the main-thread and worker-pool paths.

A host that displays async results must set this. It is the only mechanism that moves a resolved value out of the LineCache and into the host’s own document state. The engine cannot do it itself: it does not own a document, the host does, and the batcher has no reference to one.

Nullable rather than a constructor parameter because it is cleared by clearAll() and re-wired on re-subscribe, so it cannot be readonly. That makes it easy to miss, which is why warnIfUnwired exists: leaving it unset, with nobody reading getEventStream either, means async values resolve into the cache and are never shown, with nothing to indicate why. A host that genuinely does not want async results should not register async resolvers at all.


queryClient: QueryClient | null = null;

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:186

The query cache of the engine that owns this batcher, handed to a re-run line in its execution context (LineExecutionContext.queryClient).

A package’s plugin function reads a resolved value back from that context. It used to read one module-level slot the engine set when it ran a line, and a re-run here that left the slot as it was read whichever cache was published last: for an engine whose first line fetches, none at all, and with a second engine in the process that engine’s cache, so the other engine’s price was reported as this line’s answer (#710).

Set by the engine at construction. Null falls back to the cache on the VM’s own context, which is null for a host driving the batcher on its own with no cache of its own.

get dedupCount(): number;

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:396

Number of resolutions this tick collapsed into one already pending for the same (packageId, queryKey). Counted as they are refused: the batch itself never holds a repeat, so recounting it here always gave zero.

number


get listenerCount(): number;

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:405

Whether the internal event stream currently has an active reader. 1 if a consumer has called getEventStream().getReader() (or otherwise locked the stream) and not released it, 0 otherwise.

number


get pendingCount(): number;

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:387

Number of resolutions currently queued for the next flush.

number


get workerOffloadCount(): number;

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:421

Number of flushes dispatched to a worker pool: always 0.

A re-run of more than fifty lines used to go to the internal execution pool, whose workers run each line in a bare VM with no variables, no packages and no line context, so price * qty came back as “Undefined variable: price” once a refresh touched enough lines (#661). Every re-run is on the main thread now. Kept so the Workers diagnostic tab and BatcherMetrics keep their shape.

number

add(entry): void;

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:328

Add a resolved query key to the pending batch.

Called by ExpressionEngine.resolveAsync() after a promise resolves or errors. If this is the first entry in the current tick, schedules a microtask flush.

ParameterType
entryBatchEntry

void


clearAll(): void;

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:426

Remove all listeners and cancel pending batch. Called on engine clear.

void


getEventStream(): ReadableStream<AsyncResolutionEvent>;

Defined in: packages/engine/src/engine/AsyncResolutionBatcher.ts:382

Get the native event stream for stream-based consumers.

Use this for backpressure, cancellation, or the ability to pipeTo() / pipeThrough() the event flow.

ReadableStream<AsyncResolutionEvent>

A ReadableStream that emits AsyncResolutionEvent items as the batcher processes async resolutions.