All files / lib/core/trace transaction.js

94.28% Statements 66/70
37.5% Branches 3/8
100% Functions 2/2
94.28% Lines 66/70

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 424x 2x     2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 424x 424x 424x 424x 424x 424x 424x 424x 2x     2x 2x 2x 2x 2x 2x 2x 2x 2x  
/**
 * @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 } : {}),
  });
}