All files / tao-utils/src wire.js

100% Statements 24/24
100% Branches 37/37
100% Functions 8/8
100% Lines 24/24

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 174 175 176 177 178 179 180 181                            2x   2x   12x                 2x                                             11x                               24x             14x   10x                                           12x 12x                                                                                               22x 3x       19x 19x       4x       15x 3x   12x 12x       14x 7x       12x     7x 1x      
import { AppCtx } from '@tao.js/core';
 
/**
 * Any surface the utils surface-resolution convention resolves to a
 * `Network` (`typeof x.enter === 'function' ? x : x._network`): a bare
 * `Network` (exposing `enter` + `decorate`), or a Kernel-shaped wrapper
 * exposing `_network`.
 *
 * @typedef {Object} NetworkSurface
 * @property {Function} [enter] - present on a bare Network (and on Channel's channel-scoped gate)
 * @property {Function} [decorate] - present on a bare Network
 * @property {import('@tao.js/core').Network} [_network] - present on Kernel-shaped wrappers
 */
 
const DEFAULT_TRANSPORT = 'WIRE';
 
let transportInstance = 0;
function transportName(name) {
  return name || `${DEFAULT_TRANSPORT}${++transportInstance}`;
}
 
/**
 * Wire-envelope version. Receivers ignore wire envelopes with an unknown
 * version (treated as absent) rather than fail — see ENVELOPE-SPEC.md §9.
 *
 * @type {number}
 */
export const WIRE_VERSION = 1;
 
/**
 * The portable part of a dispatch envelope — what a transport serializes
 * alongside a signal's trigram + data (ENVELOPE-SPEC.md §9).
 *
 * @typedef {Object} WireEnvelope
 * @property {number} v - wire-envelope version (`WIRE_VERSION`)
 * @property {(Object|null)} chain - the sending hop's `envelope.chain`,
 *           verbatim (JSON-clean by construction), or `null`
 */
 
/**
 * The portable part of a dispatch envelope, for serializing alongside a
 * signal's trigram + data. Only `chain` crosses a process boundary:
 * `cascade` holds live references and process-local affinity, and `hop` is
 * boundary-local (ENVELOPE-SPEC.md §9).
 *
 * @param {Object} [envelope] - a dispatch envelope (`{ cascade, hop, chain }`)
 * @returns {WireEnvelope} `{ v: WIRE_VERSION, chain }` — `chain` is `null`
 *          when the envelope is absent or carries none
 */
export function wireEnvelope(envelope) {
  return {
    v: WIRE_VERSION,
    chain: envelope && envelope.chain ? envelope.chain : null,
  };
}
 
/**
 * Extract the continuable chain from a received wire envelope. Absent,
 * unversioned, or unknown-version envelopes yield `null` (fresh chain) —
 * one-sided backward compatibility with pre-wire senders. A `chain` that is
 * not a plain object (arrays included) also yields `null`.
 *
 * @param {WireEnvelope} [wire] - a received `{ v, chain }` wire envelope
 * @returns {(Object|null)} the received chain to continue, or `null`
 */
export function chainFromWire(wire) {
  if (
    !wire ||
    wire.v !== WIRE_VERSION ||
    !wire.chain ||
    typeof wire.chain !== 'object' ||
    Array.isArray(wire.chain)
  ) {
    return null;
  }
  return wire.chain;
}
 
/**
 * Enter a signal received from a remote process: stamps the transport's own
 * hop-scope origin marker (echo suppression) and continues the received
 * chain through the local reducers.
 *
 * Surface resolution follows the utils convention
 * (`typeof network.enter === 'function' ? network : network._network`).
 *
 * @param {NetworkSurface} network - any surface exposing `enter` (a
 *        `Network` or `Channel`) or a Kernel-shaped wrapper exposing
 *        `_network`
 * @param {Object} tao - trigram (short or long keys; long-form keys win)
 * @param {*} data - datagram(s)
 * @param {(WireEnvelope|undefined)} wire - the received `{ v, chain }` wire
 *        envelope; pass `undefined` when the sender included none
 * @param {string} sourceName - this transport's origin marker
 * @returns {void}
 */
export function enterFromWire(network, tao, data, wire, sourceName) {
  const net = typeof network.enter === 'function' ? network : network._network;
  net.enter(
    new AppCtx(
      tao.term || tao.t,
      tao.action || tao.a,
      tao.orient || tao.o,
      data,
    ),
    {
      hop: { source: sourceName },
      chain: chainFromWire(wire),
    },
  );
}
 
/**
 * Duplex (Source-shaped) transport helper: emits every hop on the network —
 * with its wire envelope — except hops that arrived FROM this transport
 * (suppression applies to the arriving hop only, so chained responses flow
 * back out: the bidirectional reflex), and enters received signals with the
 * origin marker + continued chain.
 *
 * Everything transport-specific — connection lifecycle, routing, delivery
 * semantics, serialization of the emitted values — stays with the caller:
 * supply `send(tao, data, wire)` and invoke `receive(tao, data, wire)` for
 * inbound signals.
 *
 * Emission is phase-blind (`onDispatch`), matching Source's historical
 * semantics. For a veto-respecting emitter (e.g. a per-client reply path),
 * decorate with `onProceed` directly — see `@tao.js/socket.io`.
 *
 * @param {NetworkSurface} kernel - the Kernel (or bare Network) to bridge.
 *        Not a Channel: a duplex transport spans the whole kernel;
 *        channel-scoped reply paths belong to phase-gated emitters (see
 *        `@tao.js/socket.io`)
 * @param {Object} [opts]
 * @param {string} [opts.name] - origin-marker name (auto-generated if omitted)
 * @param {function(Object, *, WireEnvelope): void} [opts.send] -
 *        `(tao, data, wire) => void` outbound emitter (required at runtime:
 *        omitting it throws the setup Error below)
 * @returns {{ name: string, receive: function(Object, *, WireEnvelope=): void, dispose: function(): void }}
 *          the transport handle: `name` is the origin marker, `receive`
 *          enters an inbound signal (as `enterFromWire`), `dispose`
 *          detaches the emitting decoration
 * @throws {Error} when `kernel` is missing, when the resolved network lacks
 *         envelope support (`enter` + `decorate`) - upgrade `@tao.js/core`,
 *         or when `send` is not a function
 */
export function createTransport(kernel, { name, send } = {}) {
  if (!kernel || (typeof kernel.enter !== 'function' && !kernel._network)) {
    throw new Error(
      'must provide `kernel` to attach the transport to a network',
    );
  }
  const network = typeof kernel.enter === 'function' ? kernel : kernel._network;
  if (
    typeof network.enter !== 'function' ||
    typeof network.decorate !== 'function'
  ) {
    throw new Error(
      'createTransport requires a @tao.js/core version with envelope support - upgrade @tao.js/core',
    );
  }
  if (typeof send !== 'function') {
    throw new Error('must provide `send` to emit signals to the wire');
  }
  const transport = transportName(name);
  const undecorate = network.decorate({
    // Stryker disable next-line StringLiteral: decoration name is a diagnostic label with no observable behavior
    name: `transport:${transport}`,
    onDispatch: (ac, envelope) => {
      if (envelope.hop.source !== transport) {
        send(ac.unwrapCtx(), ac.data, wireEnvelope(envelope));
      }
    },
  });
  return {
    name: transport,
    receive: (tao, data, wire) =>
      enterFromWire(network, tao, data, wire, transport),
    dispose: () => undecorate(),
  };
}