Source: lib/core/trace/transaction.js

/**
 * @file transaction.js
 * @description Trace hooks for Avenx Rewind.
 *
 * A value that changes back on its own is the single most confusing thing a
 * trace can contain. Without a node for it, a rewind shows up as a run of
 * writes with no cause — exactly the situation Trace exists to eliminate.
 *
 * The node is opened *around* the rewind, so every restoring write lands
 * underneath it as an ordinary write and the causal tree reads:
 *
 *   ▸ click <button.qty-inc> CartItem
 *     └─ action CartItem.incQty()
 *        └─ rewind CartItem.incQty — 3 restored  [safe]
 *           ├─ write cart.items.0.qty 2 → 1
 *           └─ write CartItem.busy true → false
 *
 * Kept out of `journal.js` for the same reason `trace/reactive.js` is kept out
 * of `proxyHandler.js`: the reactive system should not import the recorder.
 * @module lib/core/trace/transaction
 */

import { tracer } from './tracer.js';
import { TraceNodeType } from './schema.js';

/**
 * Opens a rewind node and makes it the causal parent of the restoring writes.
 *
 * Callers must guard with `tracer.on` and pass the returned token to
 * `tracer.leave()` in a `finally`.
 * @param {object} frame - The journal frame about to be played backwards.
 * @returns {{token: number, id: number|null}} The restore token for
 *   `tracer.leave`, and the node's id so the outcome can be filled in later.
 *   `{token: -1, id: null}` when tracing is off.
 */
export function traceRewindStart(frame) {
  if (!tracer.sink) {
    return { token: -1, id: null };
  }
  const token = tracer.enter(TraceNodeType.REWIND, {
    action: frame.owner ? `${frame.owner}.${frame.name}` : frame.name,
    policy: frame.onConflict,
    // Filled in by `traceRewindOutcome` once the rewind has run: the counts
    // are not known until it has, and the node has to exist first so the
    // restoring writes are recorded underneath it.
    restored: 0,
    conflicts: 0,
  });
  return { token, id: tracer.current() };
}

/**
 * Records what the rewind managed to do, on the node opened for it.
 * @param {number|null} id - The node id `traceRewindStart` returned.
 * @param {object} outcome - What `JournalFrame#rewind` reported.
 * @returns {void}
 */
export function traceRewindOutcome(id, outcome) {
  if (id === null || !tracer.sink || typeof tracer.sink.annotate !== 'function') {
    return;
  }
  tracer.sink.annotate(id, {
    restored: outcome.restored,
    conflicts: outcome.conflicts.length,
    ...(outcome.conflicts.length > 0
      ? { conflictPaths: outcome.conflicts.map((conflict) => conflict.path) }
      : {}),
    ...(outcome.unrewindable.length > 0 ? { unrewindable: outcome.unrewindable } : {}),
  });
}