All files / tao-utils/src Source.js

100% Statements 31/31
100% Branches 26/26
100% Functions 10/10
100% Lines 30/30

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 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173        3x   3x   12x       6x                                                                                                                 19x 2x       17x 17x       3x       14x 2x   12x 3x 3x   12x 12x     12x 7x 1x   6x   11x     14x                       25x                             19x 14x 7x                           19x 3x                 6x                         2x      
import { AppCtx } from '@tao.js/core';
 
/** @typedef {import('@tao.js/core').Kernel} Kernel */
 
const DEFAULT_SOURCE = 'FROM';
 
let sourceInstance = 0;
function sourceName(name) {
  return name || `${DEFAULT_SOURCE}${++sourceInstance}`;
}
 
function sourceControl(source) {
  return { source };
}
 
/**
 * A trigram in short (`t`/`a`/`o`) or long (`term`/`action`/`orient`) keys;
 * long-form keys win.
 *
 * @typedef {Object} Trigram
 * @property {string} [t] - term (short key)
 * @property {string} [term] - term (long key)
 * @property {string} [a] - action (short key)
 * @property {string} [action] - action (long key)
 * @property {string} [o] - orient (short key)
 * @property {string} [orient] - orient (long key)
 */
 
/**
 * Bridges a Network to an external source of AppCons (e.g. a socket): emits
 * every AppCon on the network to the source, except those that arrived FROM
 * the source — suppression applies to the arriving hop only, so AppCons
 * chained in response are emitted back out (the bidirectional reflex).
 *
 * Implemented as a Network decoration; the origin marker rides the
 * envelope's hop scope (entry hop only) — see ENVELOPE-SPEC.md.
 * Decoration: `onDispatch` (phase-blind emission), self-filtered on
 * `envelope.hop.source`.
 *
 * @export
 * @class Source
 */
export default class Source {
  /**
   * Creates an instance of Source.
   *
   * Note the Source resolves its network from `kernel._network` only (it
   * does not apply the utils surface-resolution convention, so a bare
   * `Network` is not accepted); the resolved network must support `enter`
   * and `decorate`.
   *
   * @param {Kernel} kernel - Kernel-shaped wrapper (exposing `_network`) to attach the Source to
   * @param {function(Object, *): void} toSrc - outbound emitter called with
   *        `(tao, data)` — the unwrapped `{ t, a, o }` trigram and the
   *        datagram(s) — for every AppCon except those arriving from this Source
   * @param {(string|function(function(Trigram, *): void): void)} [name] -
   *        the Source's name, used as its hop-scope origin marker
   *        (auto-generated `FROM<n>` when omitted); passing a `function`
   *        here is the `fromSrc` overload
   * @param {function(function(Trigram, *): void): void} [fromSrc] - binder for
   *        the inbound side: called once with a setter `(tao, data)` that
   *        enters received signals stamped with this Source's origin marker
   * @throws {Error} when `kernel` (or its `_network`) is missing, when the
   *         network lacks envelope support (`enter` + `decorate`) - upgrade
   *         `@tao.js/core`, when `toSrc` is missing, or when `fromSrc` is
   *         given but not a function
   * @memberof Source
   */
  constructor(kernel, toSrc, name, fromSrc) {
    if (!kernel || !kernel._network) {
      throw new Error(
        'must provide `kernel` to attach the Source to a network',
      );
    }
    this._network = kernel._network;
    if (
      typeof this._network.enter !== 'function' ||
      typeof this._network.decorate !== 'function'
    ) {
      throw new Error(
        'Source requires a @tao.js/core version with envelope support - upgrade @tao.js/core',
      );
    }
    if (!toSrc) {
      throw new Error('must provide `toSrc` way to send ACs to the source');
    }
    if (typeof name === 'function') {
      fromSrc = name;
      name = null;
    }
    this._toSrc = toSrc;
    this._name = sourceName(name);
    // Make fromSrc optional for binding a handler
    // if not passed it is a function exposed by the Source i.e. setCtx
    if (fromSrc) {
      if (typeof fromSrc !== 'function') {
        throw new Error('optional `fromSrc` must be a function');
      }
      fromSrc((tao, data) => this._enter(tao, data));
    }
    this._undecorate = this._network.decorate({
      // Stryker disable next-line StringLiteral: decoration name is a diagnostic label with no observable behavior
      name: `source:${this._name}`,
      onDispatch: (ac, envelope) => this.handleAppCon(ac, envelope),
    });
  }
 
  /**
   * The Source's name — the hop-scope origin marker it stamps on entries
   * and filters emission on.
   *
   * @type {string}
   * @memberof Source
   */
  get name() {
    return this._name;
  }
 
  /**
   * `onDispatch` decoration callback: emits every dispatched AppCon to the
   * source except those whose arriving hop carries this Source's origin
   * marker. Suppression reads the hop scope only — hops after the entry
   * reset to `{}` — so AppCons chained in response are emitted back out
   * (the bidirectional reflex).
   *
   * @param {AppCtx} ac
   * @param {Object} envelope - the dispatch envelope `{ cascade, hop, chain }`
   * @returns {void}
   * @memberof Source
   */
  handleAppCon = (ac, envelope) => {
    if (envelope.hop.source !== this.name) {
      this._toSrc(ac.unwrapCtx(), ac.data);
    }
  };
 
  /**
   * Enters a signal received from the source: builds an AppCtx (long-form
   * trigram keys win) and enters it with this Source's origin marker on the
   * entry hop, suppressing the echo back to the source.
   *
   * @param {Trigram} trigram
   * @param {*} data - datagram(s) for the signal
   * @returns {void}
   * @memberof Source
   */
  setCtx = ({ t, term, a, action, o, orient }, data) => {
    this._enter({ t, term, a, action, o, orient }, data);
  };
 
  /**
   * @param {Trigram} trigram
   * @param {*} data
   * @private
   */
  _enter({ t, term, a, action, o, orient }, data) {
    this._network.enter(new AppCtx(term || t, action || a, orient || o, data), {
      hop: sourceControl(this.name),
    });
  }
 
  /**
   * Detach this Source's decoration from the network: stops emitting to the
   * source. Inbound entries via `setCtx`/`fromSrc` still dispatch.
   *
   * @returns {void}
   * @memberof Source
   */
  dispose() {
    this._undecorate();
  }
}