Agent

Agent class in CONNECTIVE is the base abstraction of a reactive flow. Regardless of how it works internally, any agent (or any reactive flow) can be thought of as something that can respond to some events/data (sources, input pins, etc) and its response is typically emitting some other events/data (its sinks, output pins, etc).

Agents are to be used to modularize your reactive flows. For example, any repeating sub-graph of a reactive flow is, by definition outlined above, an agent, and so you could replace it with an agent instead of repeating yourself constantly, the same way that you would encapsulate repeated logic in a function and/or a class:
  /*!*/import { Agent, sink, map, block } from '@connectv/core';

/*!*/export class Multi extends Agent {
  private k = 1;

  constructor() {
/*!*/    super({
/*!*/      inputs: ['n', 'k'], //--> we have two inputs: 'n', and 'k'
/*!*/      outputs: ['o']      //--> and one output: 'o'
/*!*/    });

    //
    //--> get every value from input 'n', multiply it by 'k',
    //--> and pass it to the output.
    //
/*!*/    this.in('n').to(map(n => n * this.k)).to(this.out('o'));

    //
    //--> `block()` is used here to make the output dependent on
    //--> the input 'k', without the input directly passing any value
    //--> to the output. basically this just ensures that when the output
    //--> is bound, so is the input 'k'.
    //
/*!*/    this.in('k').to(sink(k => this.k = k)).to(block()).to(this.out('o'));
  }
}
Our custom agent here has two inputs: 'n' and 'k', and one output, 'o'. It will multiply anything that comes through 'n' by this.k (default 1) and send it to 'o', and values sent to 'k' will update this.k.

Now that we have our custom agent defined, we can use it in our other reactive flows like this:
  import { wrap, map } from '@connectv/core';
import { fromEvent } from 'rxjs';
/*!*/import { Multi } from './multi';

let n = document.getElementById('n') as HTMLInputElement;
let k = document.getElementById('k') as HTMLInputElement;
let p = document.getElementById('p');
/*!*/let m = new Multi();

/*!*/wrap(fromEvent(n, 'input')).to(map(() => n.value)).to(m.in('n'));
/*!*/wrap(fromEvent(k, 'input')).to(map(() => k.value)).to(m.in('k'));
/*!*/m.out('o').subscribe(v => p.innerHTML = v);
NOTICE: This is just an example of how you could define custom agents and it is NOT the recommended method. The recommended way of creating your custom agents is using composition() as it also takes care of a lot of boilerplate stuff related to custom agents such as memory management.

Signature

Each agent has a signature, which is an object indicating the name of the input and output pins of the agent. In the case of our example custom agent, this bit of code outlines the signature:
    constructor() {
    super({
      inputs: ['n', 'k'], //--> we have two inputs: 'n', and 'k'
      outputs: ['o']      //--> and one output: 'o'
    });

    ...
The signature object needs to be passed to Agent constructor. It has two arrays of strings: So in case of our example, any instance of Multi will have two inputs, n and k, and one output, o. You can access these pins (both from within the agent and from outside) using .in() and .out() methods:
  // within the agent ...
this.in('k');
this.out('o');

// from outside ...
let m = new Multi();
m.in('n');
m.out('o');
You can also access the signature object of each agent via its .signature property.

Control

An agent might have an extra control pin accessible via its .control property. This pin is typically used to control the behavior of the agent, like prevent it from responding except when certain criteria is met, or signal it to reset its internal state.

Note that .control is not mentioned in an agent's signature.

Implicit connection

Take another look at how we used the agent defined in the first example:
let m = new Multi();

wrap(fromEvent(n, 'input')).to(map(() => n.value)).to(m.in('n'));
wrap(fromEvent(k, 'input')).to(map(() => k.value)).to(m.in('k'));
m.out('o').subscribe(v => p.innerHTML = v);
Because we needed to connect different input and output pins of our agent to different parts of our reactive flow, we had to store it in a variable m, and then connect each pin to the flow in a separate statement.

This method of injecting an agent into a reactive flow can be cumbersome and lead to clutter, specifically when you have large flows with multiple agents. To resolve the issue, an agent can support implicit connection, which looks like this:
group(
  wrap(fromEvent(n, 'input')).to(map(() => n.value)),
  wrap(fromEvent(k, 'input')).to(map(() => k.value))
).serialTo(new Multi()).subscribe(v => p.innerHTML = v);
This is an implicit connection, because we have not explicitly mentioned which inputs of new Multi() we want to serially connect to with this code:
group(...).serialTo(new Multi())
similarly we are not explicitly mentioning which outputs we want to connect from (or in this case, subscribe to) with this:
...serialTo(new Multi()).subscribe(...)
For this to work, our agent would need to specify which inputs and outputs are to be considered when doing implicit connection (and in which order):
import { Agent, sink, map, block } from '@connectv/core';

export class Multi extends Agent {
  private k = 1;

  constructor() {
    super({
      inputs: ['n', 'k'],
      outputs: ['o']
    });

    this.in('n').to(map(n => n * this.k)).to(this.out('o'));
    this.in('k').to(sink(k => this.k = k)).to(block()).to(this.out('o'));
  }

/*!*/  createEntries() { return [this.in('n'), this.in('k')]; } //--> use n & k for implicit connection
/*!*/  createExits() { return [this.out('o')]; }                //--> use o for implicit connection
}
which in turn would allow us to simplify how we re-use this agent with implicit connection:
import { wrap, map, group } from '@connectv/core';
import { fromEvent } from 'rxjs';
import { Multi } from './multi';

let n = document.getElementById('n') as HTMLInputElement;
let k = document.getElementById('k') as HTMLInputElement;
let p = document.getElementById('p');

/*!*/group(
/*!*/  wrap(fromEvent(n, 'input')).to(map(() => n.value)),
/*!*/  wrap(fromEvent(k, 'input')).to(map(() => k.value))
/*!*/).serialTo(new Multi()).subscribe(v => p.innerHTML = v);
Almost all default agents in CONNECTIVE support implicit connection, including custom agents created by composition(), expr(), or node().

Clearing up

All agents are equipped with a .clear() method that will clear out the internal machinery of the agent. It will clear up all internal subscriptions, pins, agents, etc. Similar to pin's .clear(), this method will render the agent useless, so use it only when you are done with the agent.

For your custom agents, you must also be careful to properly implement .clear(). It is highly recommended to use the Composition class as it will manage this (along other stuff) for you.

Further reading




Copied to Clipboard!