Source: lib/core/trace/reactive.js

/**
 * @file reactive.js
 * @description Trace hooks for the reactive system.
 *
 * Two things are recorded here, and the split matters:
 *
 * - A **write** is a logical mutation: one property, at one path, changing
 *   from one value to another. It is recorded in the proxy traps, where the
 *   mutation actually happens, rather than in `trigger()` — `trigger` walks up
 *   the `parentMap` re-firing itself for every ancestor, so recording there
 *   would log one write per level of nesting for a single assignment.
 *
 * - A **watcher wake** is a consequence of a write. It is recorded in
 *   `trigger()`, inside the write's causal scope, which is what produces
 *   "cart.items.2.qty changed, and that woke CartItem#3's render".
 *
 * Placing this in its own module rather than inlining it in `proxyHandler.js`
 * keeps `capture` and `recorder` out of the reactive system's import graph
 * from the reactive side, so the dependency runs one way only.
 * @module lib/core/trace/reactive
 */

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

/**
 * Opens a write node and makes it the causal parent of the reactive work the
 * caller is about to trigger.
 *
 * Callers must guard with `tracer.on` and must pass the returned token to
 * `tracer.leave()` in a `finally`.
 * @param {object} target - The raw object that was mutated.
 * @param {string|symbol} key - The mutated key.
 * @param {any} oldValue - The value before the mutation.
 * @param {any} newValue - The value after the mutation.
 * @param {string} [op] - What kind of mutation this was, when it was not a plain assignment.
 * @returns {number} A restore token for `tracer.leave`, or -1 when tracing is off.
 */
export function traceWrite(target, key, oldValue, newValue, op) {
  const recorder = tracer.sink;
  if (!recorder) {
    return -1;
  }

  const path = getPropertyPath(target, key);
  const node = {
    path,
    from: recorder.capture(oldValue, path),
    to: recorder.capture(newValue, path),
  };
  if (op) {
    node.op = op;
  }
  return tracer.enter(TraceNodeType.WRITE, node);
}

/**
 * Opens a write node for a mutation whose before/after values are not a single
 * pair — an array method, a `Map.clear()`, a `Set.add()`.
 *
 * The collection's size is recorded instead of its contents: capturing a whole
 * array on every `push` would make tracing a list quadratic.
 * @param {object} target - The raw collection.
 * @param {string|symbol} key - The method or key that changed.
 * @param {string} op - The operation name, e.g. `push` or `clear`.
 * @param {number} [size] - The collection's size after the mutation.
 * @returns {number} A restore token for `tracer.leave`, or -1 when tracing is off.
 */
export function traceCollectionWrite(target, key, op, size) {
  if (!tracer.sink) {
    return -1;
  }
  const path = getPropertyPath(target, typeof key === 'symbol' ? undefined : key);
  return tracer.enter(TraceNodeType.WRITE, { path, op, size });
}

/**
 * Opens a watcher node for a watcher about to re-run because of a write.
 *
 * Named watchers carry their own identity (`CartItem#render`, `Resource#users`,
 * a computed key); anonymous ones are still worth recording, because the shape
 * of the propagation is the answer to "why did this update".
 * @param {object} watcher - The `AvenxWatcher` about to run.
 * @returns {number} A restore token for `tracer.leave`, or -1 when tracing is off.
 */
export function traceWatcher(watcher) {
  if (!tracer.sink) {
    return -1;
  }
  const name = (watcher && watcher.name) || 'anonymous';
  const node = { name };

  // `Component#render` is the single most common watcher and the one a reader
  // most wants to see attributed, so its component is lifted out of the name.
  const renderMatch = /^(.+)#render$/.exec(name);
  if (renderMatch) {
    node.kind = 'render';
    node.component = renderMatch[1];
  } else if (watcher && watcher.options && watcher.options.isComputed) {
    node.kind = 'computed';
  } else {
    node.kind = watcher && watcher.isEffect ? 'effect' : 'watch';
  }

  return tracer.enter(TraceNodeType.WATCHER, node);
}