Skip to main content

dryRun and explain

Two preview facades let callers see what a mutation would do before committing.

FacadeReturnsUse for
engine.dryRun(directives)Full per-method results, the RFC 6902 patch, and what topics would have firedPreview a multi-method executionQueue
engine.explain(method, params){ wouldSucceed, reason?, willEmitTopics, touchesPaths } projection over a single methodUI gates and tooltips

Both run the real engine machinery against a snapshot and then always restore state at the end. Subscribers are NOT notified — the notice buffer is captured into the result and drained.

engine.dryRun(directives)

Same shape as executionQueue — accepts a directive list — but never persists side effects and never fires subscribers.

const { wouldSucceed, patch, willEmitNotices, results } = tournamentEngine.dryRun([
{ method: 'deleteDrawDefinition', params: { drawId } },
]);

if (wouldSucceed) {
console.log(`Would change ${patch.length} fields`);
console.log(`Would emit ${willEmitNotices.map((n) => n.topic).join(', ')}`);
} else {
showWarning(`Cannot delete: ${results.at(-1)?.error?.message}`);
}

DryRunResult shape

type DryRunResult = {
wouldSucceed: boolean;
results: any[]; // per-directive results (same shape executionQueue returns)
patch: JsonPatch; // RFC 6902 ops between pre-state and would-be post-state
willEmitNotices: EmittedNotice[]; // { topic, payloads[] } for each topic
error?: any; // set when a directive returned `{ error }`
rolledBack: true; // always true — dryRun never persists
};

The patch is the same shape generated by jsonPatch. It's safe to display, log, store, or apply via any RFC 6902 patcher.

pipe semantics

dryRun honors the same pipe mechanism executionQueue does — chained directives thread results through without the caller materializing them:

tournamentEngine.dryRun([
{ method: 'addEvent', params: { event: { eventType: 'SINGLES' } } },
{
method: 'generateDrawDefinition',
params: { drawSize: 16 },
pipe: { event: true }, // event from previous result
},
]);

Errors

Three guard paths return { wouldSucceed: false, error } without dispatching:

Triggererror.message
directives is not an array'directives must be an array'
Any directive's params is not an object'params must be an object'
A directive references a method not registered on the engine'method '<name>' not found'

Method-level errors (the dispatched method returned { error }) populate the top-level error and stop the loop — subsequent directives don't run. The post-state still reflects whatever earlier directives modified before the error, and the snapshot still restores everything.

engine.explain(method, params)

Projection over dryRun for the common "would this single call succeed?" question. Returns the four signals UI code actually wants and tucks the full dryRun under detail:

const { wouldSucceed, reason, willEmitTopics, touchesPaths } = tournamentEngine.explain('deleteDrawDefinition', {
drawId,
});

const tooltip = wouldSucceed
? `Will remove ${touchesPaths.length} fields and fire ${willEmitTopics.length} notices`
: `Cannot: ${reason?.message}`;

ExplainResult shape

type ExplainResult = {
wouldSucceed: boolean;
reason?: { code?: string; message?: string; info?: string };
willEmitTopics: string[]; // topic names — not payloads
touchesPaths: string[]; // JSON Pointer paths from the patch
detail: DryRunResult; // full dryRun if you need it
};

reason is the legacy POJO shape, deliberately — so unwrap() and POJO-aware consumers can branch on it without an extra adapter.

Why both?

explain is a hot-path-friendly façade: one method, simple projection, no need to materialize the directive list. UI tooltips and per-button-state gates call it every render.

dryRun is the full-fidelity tool: see every per-method result, every topic + payload, every changed field. Preflight workflows, audit-log previews, and "show me the diff" panes call it once on demand.

Performance

One makeDeepCopy of tournamentRecords up front (the same cost rollbackOnError already pays) and one O(n) tree-walk diff at the end. For typical state sizes — single tournament with a few draws — both facades return in well under 10 ms. For very large state (50+ events, full draws) it's 50–200 ms.

Safe for dev/preflight callers; not for hot scoring paths. The planned "no-rollback explain" optimization (run validators only, skip mutation + diff) is future work; ships when the engine exposes per-method validator metadata.