Trax a reactive pub/sub framework

Inspired by solutions like RxJS, MOBX, Flyd, signals, streams, observables, mono/flux ... Trax is a minimal yet sufficient reactive framework.

Trax was built with the following goals:

Trax is a companion module of Trix, a reactive UI framework. Trax can be used by itself or together with Trix.

Table of contents

  1. Overview
    1. Publisher / Subscriber hierarchy
    2. Publisher / Subscriber change propagation
    3. Usage
    4. Source Code
  2. Tutorial
    1. A Trax node
    2. Setting and getting the Trax node value
    3. Pub/sub
    4. Live handling of events
    5. Multiple subscribers, multiple publishers
    6. Filtering events
    7. Async/sync
    8. Fire
    9. Custom events
    10. Summary
    11. Blank canvas
  3. Companion modules
    1. Trex
      1. APPLY
      2. IF
      3. NOT
      4. CHOOSE
      5. ARRAYLENGTH
      6. FILTER
      7. INC
      8. TOGGLE
    2. Trux
      1. pipe, compose, when
      2. reconcileArrays
      3. diffMapper
      4. prettyJSON
  4. Trax APIs
    1. trax( ...args )
    2. ( ...args )
    3. ( )
    4. fct( callback )
    5. fct callback function
    6. onChange( callback )
    7. onchange callback function
    8. fire( name?, arg? )
    9. handler( name, callback )
    10. event handler callback function
    11. deregister( recursive )
    12. prune( )
    13. id( name )
    14. log( callback )
    15. Trax.log( callback )
    16. logger callback function
    17. pubs( levels )
    18. subs( levels )
    19. Trax.onChange( mode )
  5. Algorithmic approach
    1. Deferred propagation
    2. Change propagation for Live elements
    3. Recalculating the value of Live elements
    4. Live elements onChange triggered
  6. Acknowledgements

Overview

Publisher / Subscriber hierarchy

A Pub / Sub hierarchy of elements

Trax elements either directly wrap a value or reference a list of publishers from which the element's value can be derived.

A Trax value can be any javascript value: primitive, object, array, function, ...

The elements A through H are organized in a publisher / subscriber hierarchy. For example:

Each subscriber can implement a transformation / combinator function to derive its value from its publishers.

The value of an element can be queried, or if so configured can emit it's value on change.

Publisher / Subscriber change propagation

Change propagation through the hierarchy

Changes to a publisher will propagate to its subscribers.

Instead of using the imperative logic d = b + c, we would declaratively create the pub / sub relationship where d is a subscriber to b and c. After that, Whenever b or c changes, d is notified and recalculates its value.

The principle advantage of the pub / sub declarative approach is that new subscribers can be added to existing hierarchies without touching the existing hierarchy and no risk of affecting the existing logic. The dependency logic is also elevated to a first class citizen and the made more explicit and crisp.

The most notable disadvantage with this declarative approach is that one is "one removed" from the code. Not having direct language support makes it harder to troubleshoot when things go wrong - as they invariably do. Also, for us imperative programmers, this is an unfamiliar programming paradigm and will require some getting used to.

Usage

import { trax, Trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'

Create subscriber elements using trax( a,b,c ) with a,b,c being javascript values (primitives, objects, functions, ...) or other trax elements acting as publishers. The list of publishers can also be set or updated using t( x,y,z ) where t is a trax element and x,y,z is the new list of publishers.

Define a transformation/aggregation function using t.fct( transformFct ) with t being a trax element and transformFct being the transformation/aggregation taking the list of publisher values.

The t.onChange( effectFct ) method will make the trax element Live and will call the effectFct to perform the desired side effects when publishers notify for updates.

Source code

The source code is available in github under https://github.com/0x1F528/Trax

Tutorial

Trax works by declaratively creating and wiring up Trax nodes in a pub/sub dependency graph. Value changes to publishers propagate from to any subscribers. Subscribers may also function as publishers and pass the update further down the line. The current value can be queried from the Trax element or can be emitted via side effect on update events.

A Trax node

Let's look at an example. Creating a Trax node is as simple as

Creating a Trax node
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                
                    var node = trax();
                
            

The node we just created has no value. A value can be set during initialization or later.

Setting and getting the Trax node value

The Trax nodes follow a setter/getter functional pattern where parameters passed in are set to the node - when no parameters are passed in the function simply returns its current value.

Setting a value
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    var node = trax( 4 );
                    var oden = trax();
                    oden( "Hello world!" );     // setter

                    console.log( node() );
                    console.log( oden() );      // getter
                
            

It might be helpful to think of the value being set to the Trax node as a publisher for this node. (And that this node is subscribing to the value being set)

Pub/sub

The publisher/subscriber linkage becomes clearer when we wire up several Trax nodes

Setting a value
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    var one = trax( 4 );
                    var two = trax( one );
                    var three = trax( two );
                    
                    console.log( "one: "+ one() );
                    console.log( "three: "+ three() );
                    console.log( "----" );
                    
                    one( 56 ); // setting one to a new value 56
                    
                    console.log( "one: "+ one() );
                    console.log( "three: "+ three() );
                
            

You can see how the value of "one" propagates through "two" and then "three". A change to "one" results in a change in "three".

Often we are not just passively interested the current value of a subscriber, but we also want to be able to act upon the change event itself.

Live handling of events

You can access live Trax updates by adding an "onChange" function.

Setting a value
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    var one = trax( 4 );
                    var two = trax( one );
                    var three = trax( two )
                        .onChange( (x) => console.log("LIVE three: "+ x) );
                    
                    console.log( "one: "+ one() );
                    console.log( "three: "+ three() );
                    console.log( "----" );
                    
                    one( 56 ); // setting one to a new value 56
                    
                    console.log( "one: "+ one() );
                    console.log( "three: "+ three() );
                
            

Run the code to see how the onChange function is called immediately after "one" is updated to 56.

Note the fluent API chaining. Trax APIs will update the node configuration and then return the node itself for further configuration. The only obvious exception is that when calling the Trax node getter (calling the function without a parameter) the value is returned, not the node.

Multiple subscribers, multiple publishers

We would be severely limited if could only create linear dependencies between Trax nodes.

Trax nodes can depend on multiple publishers and have multiple subscribers!

Let's try this!

Multiple subscribers, multiple publishers
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    var first = trax( "John" );
                    var last = trax( "Doe" );
                    var fullName = trax( first, last, "Jr." )       // <-- multiple subscribers
                        .fct( (x,y, z) => x + " " + y + " " + z);   // <-- fct to combine values
                    var allCaps = trax( fullName )
                        .fct( (x) => x.toUpperCase() );             // <-- transform value
                    var lowerCase = trax( fullName )
                        .fct( (x) => x.toLowerCase() )
                        .onChange( (x) => console.log("LIVE lowerCase: "+ x) );
                    
                    console.log( "fullName: "+ fullName() );
                    console.log( "allCaps: "+ allCaps() );
                    console.log( "lowerCase: "+ lowerCase() );
                    console.log( "----" );
                    
                    first( "Jane" ); // setting first to the new value "Jane"
                    
                    console.log( "fullName: "+ fullName() );
                    console.log( "allCaps: "+ allCaps() );
                    console.log( "lowerCase: "+ lowerCase() );
                
            

This latest example introduces several new features:

Filtering events

Imagine you have some periodic event (for example mouse clicks) and you want to filter them out if they are too close together (debounce), or only want to allow a sampling of events to get through. Trax allows you to filter events along the dependency chain so that the higher up nodes never see the event.

Filter events
                import { trax, Trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    var clickEvent = trax(0);
                    var intervalHandle = setInterval( () => { clickEvent( clickEvent() + 1 ) }, 1000);
                    
                    var everyClick = trax( clickEvent )
                        .onChange( (x) => console.log("every click: "+x) );
                    var everySecondClick = trax( clickEvent )
                        .fct( (x) => (x%2) ? x : Trax.HALT )                    // (*) filter out the even clicks
                        .onChange( (x) => console.log("every second click: "+x) );
                    
                    var makeItStop = trax( intervalHandle, clickEvent )
                        .fct( (i,e) => { if (e > 8) clearInterval(i); } )       // (**) only for 8 seconds
                        .onChange( true );                                      // (***)
                
            

Again, we discover a few new features:

Async/sync

Trax can handle live updates either synchronously or asynchronously. In the synchronous case the event is handled right then and there. In the asynchronous case the live updates are batched and handled as a group at the end.

Asynchronous mode might make more sense in the case of a web site where we want to reflect the final outcome and not flash all the intermediate results. It would also be more efficient as any side-effects are acted upon only once.

Synchronous mode would be for cases where every event is important (say counting events) whereas batched events may only keep the last event.

Async / sync handling of events
                import { trax, Trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    Trax.onChange(Trax.MODE.SYNC);
                    var event = trax(0).onChange( (x) => console.log( "--> "+ x ) );
                    event(22);
                    event(3001);
                    event(7);
                    
                    console.log( "final value: "+ event() );
                
            

Change the setting on the first line from Trax.MODE.SYNC to Trax.MODE.ASYNC and rerun.

In Async mode the events 22 and 3001 are superseded by the 7 event and lost

In general I would recommend Async mode unless there is a reason to go with Sync mode.

Fire

Often we might be more interested in the actual event than the value.

Button clicks, mouse clicks, and interval expirations are examples of events without meaningful values.

We could use trax('whatever') call to trigger an update event that would propagate to the subscribers, but Trax implements a fire() method dedicated to this use case.

Fire
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    var event = trax();
                    var subscriber1 = trax( event );
                    var subscriber2 = trax( subscriber1 )
                        .onChange( (x) => console.log( "Oh look, an event happened" ));
                    event.fire();
                    setTimeout( () => event.fire(), 1000 );
                    setTimeout( () => event.fire(), 2000 );
                
            

Try changing the code above to use event('') instead of fire().

Would using event() work?

Custom events

Piggybacking on the fire() API we can also create custom events (as opposed to the normal update events).

Custom events will propagate along the dependency tree just like the update events, but they will NOT change the value of the Trax node.

Custom events
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    var event = trax(0);
                    var subscriber1 = trax( event );
                    var subscriber2 = trax( subscriber1 )
                        .handler('whazzup', (value,thiz,arg1,arg2) => (arg1+" "+arg2).toUpperCase() );
                    var subscriber3 = trax( subscriber2 );
                    var subscriber4 = trax( subscriber3 )
                        .handler('whazzup', (value,thiz,arg1) => console.log( 'How are you '+arg1+'?' ))
                        .onChange( true );
                    event.fire('whazzup', 'John','Henry');
                
            

As you can see, intermediary Trax nodes can transform the (optional) custom event parameters. They could also return Trax.HALT to keep the event from bubbling further up the subscriber chain. Unlike update events, the event propagation only works if a higher-level node is Live. There is no way to query for a custom event. The value transmitted up the chain is transient and not cached or stored.

Summary

Did this feel like a lot?

Quite a few details, but conceptually the behavior is consistent and is accomplished by just a handful of primitives.

"t" shall denote a Trax instances in this table
API Purpose Params Result
trax( ...args ) Create a new Trax node Optional initial values/Trax nodes t
t( ...args ) Set the Trax node to args Values/Trax nodes t
t( ) Get the Trax node value N/A value
t.fct( f ) Set the transformation function that defines this Trax node's value A function that takes args number of parameters and returns a single value t
t.onChange( f ) Set the onChange function to handle any side effects A function that takes a single parameter t
t.fire( name, ...args ) Fire an event An optional event name and optional event parameters; if neither is present will be handled as a default update event t
t.handler( name, f ) Set an event handler Event name and function to handle the event; the response is passed up the subscriber chain t
Trax.HALT When returned by a fct or handler function, blocks the event from further propagating N/A N/A

There are a few cleanup and trouble shooting APIs that have not been covered in this tutorial. You can read more about them in the API documentation

Here is a blank canvas for you to try things out :-)

Have fun
                import { trax, Trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    // add your code here











                
            

Other Modules

Trax has two optional companion modules, Trex and Trux

Trex

Trex provides a number of prebuilt Trax functions. You can use them directly in your code or use them as inspiration for your own special cases.

APPLY
Apply a function to an element. APPLY( observed: T, applier: (v: T) => V ): Trax<V>
observed
T (*)
The input value to be applied
applier
(v: T) => V (*)
Function that will be applied to the observed
Returns
Trax<V>
Trax holding the result
(*): can always be <type> or Trax<type> as Trax will unwrap the value for evaluation

APPLY is syntactic sugar to create a new Trax instance and add a transform fct to it. This is mostly useful to inline these steps into a parameter.

APPLY
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                import { APPLY } from 'https://0x1f528.github.io/Trax/modules/trex.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    let observed = trax("flintstone");
                    console.log( APPLY( observed, (x) => x.toUpperCase() )() );
                
            
IF
Implements If/Else. IF( criteria: boolean, yes: any, no: any ): Trax<any>
criteria
boolean (*)
criteria to choose yes or no
yes
any (*)
Value to be returned if criteria is true
no
any (*)
Value to be returned if criteria is false
Returns
Trax<any>
Trax holding the yes or no result
(*): can always be <type> or Trax<type> as Trax will unwrap the value for evaluation
IF
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                import { APPLY,IF } from 'https://0x1f528.github.io/Trax/modules/trex.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    let count = trax(8);
                    let result = IF(APPLY(count, (cnt) => cnt > 10), "count is greater than 10", "count is less or equal to 10" );
                    console.log( result() );
                    
                    count(11);
                    console.log( result() );
                
            
NOT
Apply NOT to a boolean value. NOT( criteria: boolean ): Trax<boolean>
criteria
boolean (*)
criteria to choose yes or no
Returns
Trax<boolean>
Trax holding the negation of the input criteria
(*): can always be <type> or Trax<type> as Trax will unwrap the value for evaluation
NOT
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                import { APPLY,IF,NOT } from 'https://0x1f528.github.io/Trax/modules/trex.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    let count = trax(8);
                    let result = IF(NOT(APPLY(count, (cnt) => cnt > 10)), "count is less or equal to 10", "count is greater than 10" );
                    console.log( result() );
                    
                    count(11);
                    console.log( result() );
                
            
CHOOSE
Choose an entry based on a key. CHOOSE( key: any, lookup: Array<any> | Object, otherwise: any ): Trax<any>
key
any (*)
Key that will be applied to the lookup for evaluation
lookup
Array<any> | Object (*)
Used as the lookup table/object
otherwise
any (*)
Value to be returned if the key was not found in lookup
Returns
Trax<any>
Trax holding the result of evaluating lookup[key]
(*): can always be <type> or Trax<type> as Trax will unwrap the value for evaluation

CHOOSE behaves a bit like a case statement. We can select a result based on an input key.

The lookup selection can be an array or an object; anything that can evaluate lookup[key].

CHOOSE
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                import { CHOOSE } from 'https://0x1f528.github.io/Trax/modules/trex.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    let key = trax();
                    let lookup = trax(["zero","one","two","three","four"]);
                    CHOOSE(key, lookup, "failed").
                        onChange( (x) => console.log(x));
                    key(4);
                    key(11);
                    key(1);
                    console.log('--------------')
                    
                    lookup(
                        {
                            "zero":"null",
                            "one":"eins",
                            "two":"zwei",
                            "three":"drei"
                            ,"four":"vier"
                        }
                    )
                    key("four");
                    key("eleven");
                    key("one");
                
            

Pop Quiz: Why do we get that extra "failed" output before the "vier" result?

ARRAYLENGTH
Get the length of an array. ARRAYLENGTH( arr: Array<any> ): Trax<number>
arr
Array<any> (*)
Input array
Returns
Trax<number>
Trax holding the length of the array
(*): can always be <type> or Trax<type> as Trax will unwrap the value for evaluation
ARRAYLENGTH
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                import { ARRAYLENGTH } from 'https://0x1f528.github.io/Trax/modules/trex.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    let arr = trax([1,16,6,3,88]);
                    let display = trax(arr, ARRAYLENGTH(arr))
                        .fct( (a,l) => "["+a+"] has "+l+" elements");
                    console.log( display() );
                
            
FILTER
Filter an array FILTER( arr: Array<T>, filter: (T) => boolean ): Trax<Array<T>>
arr
Array<T> (*)
The array to be filtered
filter
(T) => boolean (*)
Function that will used to filter the input array
Returns
Trax<Array<T>>
Trax holding the filtered array
(*): can always be <type> or Trax<type> as Trax will unwrap the value for evaluation
FILTER
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                import { FILTER } from 'https://0x1f528.github.io/Trax/modules/trex.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    let arr = trax([1,16,6,3,88]);
                    let filterFct = trax( (x) => x%2 );
                    let display = FILTER( arr, filterFct );
                    console.log( display() );
                    
                    filterFct( (x) => x%4===0 );
                    console.log( display() );
                
            

Note that a Trax node can even hold a function.

INC
Increments a counter on every invocation. INC( event: Trax, initial: number, increment?: number, max?: number ): Trax<number>
event
Trax
Trax events to be counted
initial
number (*)
Initial count
increment?
number (*)
By how much the count should increase on each event (optional, defaults to 1)
max?
number (*)
Maximum count value; count will wrap when greater than max using a modulo operator (optional, defaults to no max value)
Returns
Trax<number;>
Trax holding the current count of events
(*): can always be <type> or Trax<type> as Trax will unwrap the value for evaluation
INC
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                import { INC } from 'https://0x1f528.github.io/Trax/modules/trex.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    let event = trax();
                    let increment = trax(2);
                    INC(event, 3, increment, 5)
                        .onChange( (x) => console.log(x) );
                    event.fire();
                    event.fire();
                    event.fire();
                    event.fire();
                    event.fire();
                    event.fire();
                    
                    console.log( '---------' );
                    increment(3);           // Note: changing the increment will also fire the INC.
                    event.fire();
                    event.fire();
                
            

Can you write your own INC that won't fire INC when changing the increment? Look at Trux.diffMapper for inspiration.

TOGGLE
Toggles a boolean value. TOGGLE( initial: boolean ): Trax<boolean>
initial
boolean
Initial value
Returns
Trax<boolean>
Trax holding the current toggle value; will negate on every evaluation/call
TOGGLE
                import { trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                import { TOGGLE } from 'https://0x1f528.github.io/Trax/modules/trex.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    let event = trax(true);
                    TOGGLE(event)
                        .onChange( (x) => console.log(x) );
                    event.fire();
                    event.fire();
                    event.fire();
                    event.fire();
                
            
XHR
Make a XMLHttpRequest. XHR( url: Trax<string>, traxNode: Trax<string>, req?: XMLHttpRequest ): Trax
url
Trax<string>
Data URL
traxNode
Trax<string>
Trax output node to accept the XMLHttpRequest response
req?
XMLHttpRequest
XMLHttpRequest instance; optional: if not provided a XMLHttpRequest instance will be created
Returns
Trax
Trax node that will perform the XMLHttpRequest on url update
XHR
                import { trax, Trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                import { XHR } from 'https://0x1f528.github.io/Trax/modules/trex.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    let companyId = 1;
                    let companyLookup = trax().fct( (id) => (id) ? 'sample_data/company'+id : Trax.HALT);
                    let accountId = trax();
                    XHR(companyLookup, accountId);
                    
                    let accountLookup = trax(accountId).fct( (id) => (id) ? 'sample_data/account'+id : Trax.HALT);
                    let employeeName = trax().onChange( (n) => console.log(n) );
                    XHR(accountLookup, employeeName);
                    
                    companyLookup(companyId); // this starts the lookup chain
                
            

The contents of data1 is "313"

The contents of data313 is "Fred Flintstone"

In the example you can see how to chain multiple requests. After the accountId is returned the employee data will be fetched and available in the employeeName Trax instance.

If you want to try out something more, there is a data2 url with contents "313|212"

and a data212 url with contents "Barney Rubble"

Note: Using fetch and passing promises as values into the Trax nodes would be more elegant. Do you want to give that a try?

Trux

To keep dependencies on other libraries low I put some useful code snippets used in demo/test code into an independent Trux utility module.

You may not feel any great need to use Trux, as you could easily create your own based on your needs. For completeness a quick overview of the methods provided...

pipe, compose, when

A bit of support for functional programming allowing you to execute a sequence of functions, the result of one feeding into the next.

pipe( ...arr: (any) => any ): (any) => any
...arr
(any) => any
List of input functions to be evaluated sequentially
Returns
(any) => any
Function that will apply the list of input functions sequentially
compose( ...arr: (any) => any ): (any) => any
...arr
(any) => any
List of input functions to be evaluated in reverse order
Returns
(any) => any
Function that will apply the list of input functions in reverse order

The when() function allows you to conditionally execute based on some key value. when( key: any, fct: () => any ): (any) => any
key
(any) => any
Key value criteria to determine if the fct will be evaluated
fct
(any) => any
Conditionally evaluated function
Returns
(any) => any
Function that will apply the fct if the input matches the key
Pipe, compose, when
                import { pipe, compose, when } from 'https://0x1f528.github.io/Trax/modules/trux.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    let seq = pipe(
                        (x) => x + 2,
                        (x) => x * 2,
                        (x) => x - 2
                    );
                    console.log( seq(4) );
                    let seq2 = (x) => (((x + 2) * 2) - 2);
                    console.log( seq2(4) );
                    
                    let rev = compose(
                        (x) => x + 2,
                        (x) => x * 2,
                        (x) => x - 2
                    );
                    console.log( rev(4) );
                    let rev2 = (x) => (((x - 2) * 2) + 2);
                    console.log( rev2(4) );
                    
                    let choice = pipe(
                        when('A', () => console.log( 'Say A' )),
                        when('B', () => console.log( 'Say B' )),
                        when('C', () => console.log( 'Say C' )),
                    );
                    choice('C');
                    choice('A');
                
            
reconcileArrays

This method allows you to sync up a destination array to a source array in place. The two arrays don't have to contain the same type of data.

Existing destination array elements are moved within the array as needed, not destroyed and recreated

reconcileArrays( source: Array<S>, destination: Array<D>, equals: (S,D) => boolean, generate: (S) => D ): void
source
Array<S>
Input array whose elements must be matched by the destination
destination
Array<T>
Input/Output array whose elements will be rearranged to match the source array
equals
(S,D) => boolean
Returns true if the source element of type S matches destination element of type D
generate
(S) => D
Given an element of type S, create a new matching element of type D
Returns
void
Array reconciliation
                import { reconcileArrays } from 'https://0x1f528.github.io/Trax/modules/trux.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    let sourceArray = [8,5,12,16];
                    let destinationArray = [];
                    // mapping 1 -> A, 2 -> B, 3 -> C, ...
                    let createFromSourceValue = (s) => String.fromCharCode(64 + s);
                    let testForEquality = (s,d) => (createFromSourceValue(s) === d);
                    
                    reconcileArrays(sourceArray, destinationArray, testForEquality, createFromSourceValue);
                    
                    console.log(destinationArray);
                    sourceArray[3] = 12;
                    sourceArray.push(15);
                    
                    reconcileArrays(sourceArray, destinationArray, testForEquality, createFromSourceValue);
                    
                    console.log(destinationArray);
                
            
diffMapper

The diffMapper function is one that you might actually find useful.

A feature omitted from Trax for performance reasons is the ability to know which of multiple publishers has fired/changed. You may want to implement different behavior depending on which publisher actually changed.

diffMapper( source: Array<S>, destination: Array<D>, equals: (S,D) => boolean, generate: (S) => D ): void
TODO: should diffMapper be fixed?
Detecting which elements have changed
                import { trax, Trax } from 'https://0x1f528.github.io/Trax/modules/trax.js'
                import { diffMapper } from 'https://0x1f528.github.io/Trax/modules/trux.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    let event = trax();
                    let increment = trax(1);
                    let current = trax(4)
                        .onChange( (x) => console.log(x) );
                    let incrementor = trax(event, increment, current, diffMapper())
                        .fct( (e,i,c,d) => 
                                d([i])[0] ? Trax.HALT :     // test if i has changed
                                c + i )                     // increment
                        .onChange( (x) => current(x) )      // set current to the new value
                    event.fire();
                    event.fire();
                    increment(5);
                    event.fire();
                
            

The diffMapper takes an array of values and returns a boolean array with true for all values that have changed since the last time diffMapper was called. In the example above we only test on changes for i. If the increment has changed we don't want to increment or fire the onChange event. We don't bother testing the event value as the value is always the same; here we are interested in the event, not the value.

Try commenting out the "test if i has changed" code and run again. What happens now?

prettyJSON

This code snippet takes a JSON string and formats it into a more readable form.

prettyJSON( jsonString: string): string
jsonString
string
Valid JSON string
Returns
string
Pretty printed JSON string equivalent to the input string
JSON prettifier
                import { prettyJSON } from 'https://0x1f528.github.io/Trax/modules/trux.js'
                console.log = (x) => {
                    const body = document.getElementsByTagName("body")[0];
                    const text = document.createTextNode(x);
                    const br = document.createElement("br");
                    body.appendChild(text);
                    body.appendChild(br);
                }
                
                    const body = document.getElementsByTagName("body")[0];
                    const pre = document.createElement("pre");
                    
                    let json = '{"one":1,"two":2,"arr":[{"three":4,"four":{"five":5}},{"six":6}]}';
                    let pretty = prettyJSON(json);
                    pre.innerHTML = pretty;
                    body.appendChild(pre);
                
            

This will be useful for some of the Trax troubleshooting APIs (pubs(), subs())

Trax APIs

trax( ...args )
Creates a new Trax instance trax( ...args?: any[]): Trax
...args?
any[]
Any number of publishers for this trax instance
Returns
Trax
A Trax instance subscribed to the passed in publishers
( ...args )
Calling the Trax instance (a function) with arguments. Subscribes a Trax instance to the passed in publishers; replaces any previous subscribed values. ( ...args: any[]): Trax
...args
any[]
Any number of publishers for this trax instance
Returns
Trax
This Trax instance now subscribed to a new list of publishers (fluent interface)
( )
Calling the Trax instance (a function) with no arguments. Returns the current Trax value. (): Trax
No arguments
Returns
Trax
Returns the value of the Trax instance
fct( callback )
Assign a transform function to the Trax instance. fct( transformation: (...args: any[], value?: T, this?: Trax<T>) => T ): Trax<T>
transformation
(...args: any[], value?: T, this?: Trax<T>) => T
Transformation function (see below)
Returns
Trax<T>
Returns the Trax instance (fluent interface)
fct callback function
Syntax of the transformation callback function to be assigned to the Trax instance. This function will determine the value of this Trax instance given the current publisher values. (...args: any[], value?: T, this?: Trax<T>) => T
...args
any[]
List of the values of each of the publishers in order of the Trax publishers declaration
value?
T
The current value of the Trax instance; optional as the callback function may choose not to use this
this?
Trax<T>
The Trax instance itself; optional as the callback function may choose not to use this
Returns
T
Returns the new value for this Trax instance; if Trax.HALT returned event propagation will be halted and Trax value will not be updated
onChange( callback )
Set the onChange configuration to the Trax instance. onChange( configuration: boolean | (value: T, this?: Trax<T>) => void ): Trax<T>
configuration
boolean | (value: T, this?: Trax<T>) => void
If boolean value passed in will be live or not depending on the value. Otherwise will be live and use the onChange function as described below
Returns
Trax<T>
Returns the Trax instance (fluent interface)
onChange callback function
Syntax of the onChange callback function. This function will handle any required side-effects. (value: T, this?: Trax<T>) => void
value
T
The current Trax value after any updates
this?
Trax<T>
The Trax instance itself; optional as the callback function may choose not to use this
Returns
void
Return value not used by Trax
fire( name?, arg? )
Generate an event that will propagate to any Live subscribers. fire( name?: string | symbol, arg?: any ): Trax<T>
name?
string | symbol
Name of the event; if omitted will be the standard Update event, custom event otherwise
arg?
any
Optional argument that will be passed to the event handlers; omitted for the standard Update event
Returns
Trax<T>
Returns the Trax instance (fluent interface)
handler( name, callback )
Function to handle any publisher custom events handler( name: string | symbol, action? : (value: T, arg?: any, this?: Trax<T>) => V ): Trax<T>
name
string | symbol
Name of the custom event
action?
(value: T, arg?: any, Trax<T>) => V
Event handler; described below
Returns
Trax<T>
Returns the Trax instance (fluent interface)
event handler callback function
Callback function to handle custom events. (value: T, arg?: any, this?: Trax<T>) => V
value
T
Current value of the Trax instance
arg?
any
Event argument passed up from the triggered publisher; optional as the callback function may choose not to use this
this?
Trax<T>
The Trax instance; optional as the callback function may choose not to use this
Returns
V
Event argument that will be passed to any live subscribers; if Trax.HALT returned event propagation will be halted
deregister( recursive )
Deregister/unhook this Trax instance from all publishers. This would be used during decommissioning of a node to avoid memory leaks. deregister( recursive?: boolean ): Trax<T>
recursive?
boolean
If set to true any orphaned publishers (no subscribers) will also be deregistered
Returns
Trax<T>
Returns the Trax instance (fluent interface)
prune( )
Similar to deregister(), prune will recursively deregister from any publishers AND recursively unhook any subscribers. prune( ): Trax<T>
Returns
Trax<T>
Returns the Trax instance (fluent interface)
id( name )
Provide the trax instance with an id/name. This can be helpful when debugging/troubleshooting and is required for the logging APIs. id( name?: string ): Trax<T> | string
name?
string
Name to set on this Trax instance; if the optional parameter is omitted will return the current name
Returns
Trax<T> | string
Returns the Trax instance (fluent interface) OR the current id if name? parameter was omitted
log( callback )
Configure a logging function on this Trax instance. Will log change events. log( logger: false | (id: string, from: string, to: string) => string ): Trax<T>
logger
false | (id: string, from: string, to: string) => string
false to turn off, or logging callback function
Returns
string
Returns the Trax instance (fluent interface)
Trax.log( callback )
Configure a global logging function for all Trax instances (unless overridden by the above). Will log change events. log( logger: (id: string, from: string, to: string) => string ): void
logger
false | (id: string, from: string, to: string) => string
false to turn off, or logging callback function
Returns
void
No return value
logger callback function
Syntax of logger callback function (id: string, from: string, to: string) => string
id
string
Name of Trax node being updated
from
string
Previous value of Trax node
to
string
New (and current) value of Trax node
Returns
string
New (and current) value of Trax node
pubs( levels )
For troubleshooting: returns a recursive JSON string of the publisher nodes of this instance pubs( levels?: number ): string
levels?
number
How many levels deep to recurse on the publisher chain; if omitted will recurse the full depth
Returns
string
Returns a JSON string of all publishers levels deep
subs( levels )
For troubleshooting: returns a recursive JSON string of the subscriber nodes of this instance subs( levels?: number ): string
levels?
number
How many levels deep to recurse on the subscriber chain; if omitted will recurse the full depth
Returns
string
Returns a JSON string of all subscribers levels deep
Trax.onChange( mode )
Globally set the onChange behavior to synchronous or asynchronous Trax.onChange( mode: <Trax.MODE> ): void
mode
<Trax.MODE>
Trax.MODE.SYNC or Trax.MODE.ASYNC: SYNC triggers onChange events inline; ASYNC batches onChange events until the next available JS event loop
Returns
void

Algorithmic approach

While Trax behaves like updates are propagated right then and there this is not actually what happens under the hood. To optimize performance Trax only propagates change events when necessary to maintain the illusion of immediacy.

Deferred propagation

A Pub / Sub hierarchy of elements

Trax nodes maintain a cache of their value. The cache can be in an invalidated state or valid state.

In general, requesting an element's value will trigger a recursive traversal down the dependency tree. If the cached value is valid, that value will be used as the element's value. Otherwise the immediate publishers will be queried for their values and will be used to derive the current element's value and cached in turn.

For Live Trax nodes we except them to react immediately to upstream changes. In this case propagation cannot be deferred.

Change propagation for Live elements

Change propagation

When a publisher is updated with a new value...

Recalculating the value of Live elements

Live element refresh

For all Live element references that were put aside we recalculate their value.

The handling of the Live element references can either be done synchronously or asynchronously.

Live elements onChange triggered

Emitting the Live update

As the final step, the updated Live elements will emit their new value by calling the onChange function.

Acknowledgements

(‡) The code editor is provided by CodeFlask. The accompanying REPL was built using Trix and Trax.

A special shout out to Flyd for providing me with the inspiration to start this journey so many years ago.