All files / lib/core/runtime atomic.js

100% Statements 89/89
87.5% Branches 7/8
100% Functions 3/3
100% Lines 89/89

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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 9048x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 48x 5x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x 4x 4x 5x 48x 48x 48x 48x 48x 48x 48x 91x 91x 48x 48x 48x 48x 48x 48x 48x 2x 2x  
/**
 * @file atomic.js
 * @description The `atomic()` marker for bridge actions.
 *
 * A component declares a transaction in its template — `<action name="inc"
 * atomic>` — because that is where its actions are declared. A bridge is an
 * ordinary ES module, so there is no attribute to hang the declaration on and
 * it takes a wrapper instead:
 *
 *   import { bridge, atomic } from 'avenx-core/runtime';
 *
 *   export default bridge({
 *     state: { items: [] },
 *     addQty: atomic(function (id, n) { ... }),
 *   });
 *
 * The wrapper does two jobs at once. At runtime it marks the function so the
 * Bridge factory knows to run it inside a transaction. At compile time it is
 * the syntax `BridgeParser` recognises, which is what puts the action's write
 * set into Atlas and its effects into the AVX_W43 report.
 *
 * It returns the same function rather than a new one, so a bridge action keeps
 * its name, its arity and its identity — a wrapper that changed any of those
 * would show up in stack traces and in `fn.length` checks for no reason.
 * @module lib/core/runtime/atomic
 */
 
import { AvenxError, AvenxErrorCodes } from './AvenxError.js';
 
/**
 * Marks a bridge action as transactional.
 *
 * A symbol keeps the mark out of the member namespace that `ownKeys` and
 * template scopes see.
 * @type {symbol}
 */
export const ATOMIC_MARK = Symbol.for('avenx.atomic');
 
/**
 * Declares a bridge action transactional.
 *
 * Every state write the action makes — its own and those of anything it calls
 * — is journaled. If the action throws, or returns a promise that rejects,
 * the journal is played backwards and the state is what it was before the
 * action ran.
 * @template {Function} T
 * @param {T} fn - The action implementation.
 * @param {object} [options] - Transaction options.
 * @param {'safe'|'force'|'abort'} [options.onConflict] - What a rewind does
 *   when it finds a value this transaction did not write. Defaults to the
 *   project's `rewind.onConflict`.
 * @returns {T} The same function, marked.
 * @throws {AvenxError} When given something that is not a function.
 */
export function atomic(fn, options = {}) {
  if (typeof fn !== 'function') {
    throw new AvenxError(
      AvenxErrorCodes.BRIDGE_INVALID_MEMBER,
      'atomic()',
      fn === null ? 'null' : typeof fn,
      'atomic',
    );
  }
  Object.defineProperty(fn, ATOMIC_MARK, {
    value: { onConflict: options.onConflict },
    enumerable: false,
    configurable: true,
    writable: false,
  });
  return fn;
}
 
/**
 * Reads the transaction options off a marked function.
 * @param {Function} fn - A possible atomic action.
 * @returns {{onConflict: string=}|null} Its options, or null when unmarked.
 */
export function atomicOptions(fn) {
  return (typeof fn === 'function' && fn[ATOMIC_MARK]) || null;
}
 
/**
 * Whether a function was declared atomic.
 * @param {Function} fn - A possible atomic action.
 * @returns {boolean} True when the function carries the mark.
 */
export function isAtomic(fn) {
  return atomicOptions(fn) !== null;
}