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.