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:
Element B is a subscriber to element A; A is notified of changes to B's value
Element A is a publisher to element B; A notifies B of changes to its value
Element D is a subscriber to both elements B and C
Element D is a publisher to both elements E and F
Each subscriber can implement a transformation / combinator function to derive its value from its publishers.
Element B's value is a 10 multiple of element A's value
Element D's value is combined by addition of the values of B and C
By default a subscriber takes the value of its publisher (identity function)
The value of an element can be queried, or if so configured can emit it's value on change.
Element G's value is 34
Element H is Live and will emit its value anytime its publisher updates
Publisher / Subscriber change propagation
Change propagation through the hierarchy
Changes to a publisher will propagate to its subscribers.
Element C's value has changed to 5
Element C's subscriber D recalculates its value
Element D is also a publisher and notifies E and F of the change
Following the hierarchy, elements E, F, G, and H also recalculate their respective values
Element H is Live and will emit the new value
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.
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:
The fullName Trax node illustrates how a Trax node can subscribe to multiple publishers.
The new fct() API explicitly provides the required logic to combine the three publisher values.
A Trax node will only contain a single value (be it a primitive or any JS Object). The fct() is used derive that single value out of multiple publishers.
Note that the publishers aren't restricted to Trax nodes, but can be any javascript value.
Also, the fullName Trax node has two subscribers that will react to fullName changes.
The fct() API can also be used on single publisher Trax nodes to perform a transform on the value that will become the Trax node's value.
In this example we transform the full name to all caps in one case and to all lower caps in the other.
The default (and hidden) fct() API is an identity function that simply copies the first publisher value.
You can try this out by editing the above code directly in the browser. Remove the fct() from the fullName Trax node and see what happens.
What do you think will happen if you add the line
fullName( first, last, "Sr.")
to the end of the code above?
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:
When a function returns the magic Trax.HALT (*) the event is hidden from downstream dependent subscribers (and from the onChange handler).
Instead of providing a onChange function we can also simply set it to true (***) and build a side-effect into the fct (**).
We do this because we want to have access to both the clickEvent itself as well as the everySecond interval handle so we can clearInterval after 8 seconds.
The onChange handler only has access to the (new) value of the Trax node and not the two publisher values.
Another way we could have solved this is to have the function create a two-element tuple (an array with two values, or an object with two attributes)
capturing the two values. The onChange function could then use the tuple information to implement the clearInterval functionality.
Do you want to have a go and try this approach in the code above?
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() )() );
(*): 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].
(*): 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...
As a first step, subscriber element caches are recursively invalidated
If the cache is already invalidated we know that there is no Live element down that branch and all subscriber caches will already be invalidated
Conversely, if a cache is valid we either have a Live node down that branch, or we recently queried an element - either way all dependent element caches will be invalidated
When we come across a Live element, a reference to that element will be put aside for subsequent processing
Recalculating the value of Live elements
Live element refresh
For all Live element references that were put aside we recalculate their value.
Just like for a query, we recursively traverse the element's publishers and use their values to derive this element's value
When a publisher has a valid cached value, we can use that
Otherwise we continue the recursion
The handling of the Live element references can either be done synchronously or asynchronously.
In the case of synchronous handling the Live node refresh is done right away after the cache invalidation step completes
For asynchronous handling the refresh logic is deferred until the next Javascript event loop
By deferring the refresh logic any other updates driven by the main program logic are batched and will be handled together - this can avoid the cost of redundant updates to the same branch
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.
The onChange function works by side-effect to accomplish the desired external behavior
Note that the fct()s along the change notification branch are also fired to recalculate the nodes values - fct()s may also implement side-effects if desired
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.