Skip to content

UI Dataflow Design

Flynn Duniho edited this page May 29, 2024 · 5 revisions

NETCREATE is a networked single-page Javascript webapp. One of the complications of this kind of program is sequencing operations that depend on previous operations to complete; you can't easily write procedural code that does this and it's easy to write crappy code that is difficult to debug.

In the past we've created modules that handle difficult event-like asynchronous sequential operations (e.g. reliable network messaging), and we are going to generalize the mechanism for other common scenarios. UNISYS will take on UISTATE and SYSLOOP features with a more consistent syntax so you don't have to remember so many things. We're also going to make use of new features of ES6 and ES7 Javascript to write more robust and compact code.

Common Patterns of Asynchronous Use

There are several patterns that we (will) commonly see in NetCreate:

  • Browser Level 2 Event handlers, using <DOM_Element>.addListener()
  • Application-defined Events handled by EventEmitter pattern using UNISYS.On() and fired using UNISYS.Emit()
  • Application Lifecycle Events hooked by UNISYS.Hook() (supercedes SYSLOOP module)
  • Application State change events via UNISYS.SetState() and UNISYS.OnStateChange() (supercedes UISTATE module)
  • Network Message handlers defined by UNISYS.RegisterMessage() and invoked by UNISYS.Call() and UNISYS.Broadcast().
  • (TBD) Mode changes events and Setting Sets associated with mode hierarchies
  • (TBD) Stream events (includes WebSockets)

For our own modules that implement API functions of an asynchronous nature, we'll be using both Promises and ES7 asynch/await. Promises gives us the ability to chain asynchronous operations, whereas Asynch/Await provides an alternative syntax for using Promises that makes code easier to read.

Asynchronous Results

Here's how our asynchronous code used to work compared to how it works now. Note the then() that wraps the anonymous function in the second form

// 1. old callback-style return
// note: MOD.GetAsyncData() calls provided callback with result
MOD.GetAsyncData(function(data){
  console.log('GetAsyncData returned',data);
});

// 2. new Promise-based return
// note: MOD.GetAsyncData() returns a Promise, which implements then()
MOD.GetAsyncData().then(function(data){
  console.log('GetAsyncData returned',data);
});

Promises are a little difficult to grasp at first, but I'll talk about that later. For now, know this: the advantage of Promises is that you can chain then() one after the other, with each anonymous function returning either a value that is passed to the next then():

// 3. chain of Promise-based returns
// note: MOD.GetAsyncData() returns a Promise
MOD.GetAsyncData()
.then(function(data){
  // decorate data from GetAsyncData()
  data = Object.assign(data,{filename:'hi'});
  return data;
})
.then(function(data)){
  // GetMoreAsyncData() returns a Promise
  let moreData = MOD.GetMoreAsyncData(data.filename);
  return moreData; // this is a promise, but magic happens
})
.then(function(data){
  // data contains the result of GetMoreAsyncData(), which was
  // returned above as 'moreData'
  console.log('got moreData',data);
});

At this point, the confusing issue is that there is internal code magic that makes it so the then() chain functions receive data instead of the previously-returned Promise object. This obscures the logic and requires you to know what a Promise does. It's confusing and stupid, but it's vastly improved with the ES7 Javascript standard's new keywords await and async. Now you can write code that looks synchronous and resembles sensible logic. The Promise chain above turns into this (note async in front of function and await keywords in front of the MOD calls)

// 4. await/async version of Example 3
// first define an async function
async function ChainOp() {
  let data = await MOD.GetAsynchData(); // still returns a Promise
  data.Object.assign(data,{filename:'hi'});
  moreData = await MOD.GetMoreAsyncData(data.fileName); // still returns a Promise
  console.log('got',moreData);
}
// now call it
ChainOp();

This looks much more compact, and the great thing is that program execution halts until the awaited Promise completes. This makes the logic of the operation much easier to follow. Not only that, but you can also use try/catch blocks to handle errors thrown by the Promises.

There is more to say about how Promises actually work, including error handling and how to convert old-style callbacks into new Promises. A Promise is sort of like a magic asynchronous task queue that implements chains of "success" and "failure" functions that execute one-after-the other. It makes it possible to write asynchronous code by avoiding nested callback "pyramid of doom".


MORE RAW NOTES

To make a UNISYS MESSAGE CALL

// MOD was created with UNISYS.NewModule('name');
// MOD.Call() returns a Promise
MOD.Call('MESSAGENAME', data).then((data)=###{
  // data received back
})
.catch((data)=###{
  // error occurs if MESSAGENAME doesn't exist
  // or if there are more than one handler
});

To make a UNISYS MESSAGE CALL that has MULTIPLE IMPLEMENTORS

MOD.CallCollect('MESSAGENAME',data).then((result)=###{
  // result contains the results from ALL
  // implementors of this event across the network
  // as a list of data objects
});

// a message might be locally handled or
// remotely handled. The message is the address
// with UNISYS, so name them accordingly for
// uniqueness!

To make a UNISYS MESSAGE CHAIN CALL with Promises

// MOD was created with UNISYS.NewModule('name');
// MOD.Call() returns a Promise
MOD.Call('MESSAGENAME', data) // returns a Promise
.then((data)=###{
  // do something cool
  return data;
})
.then(data)=###{
  // data is passed from previous
  // do something else cool
});

To make a UNISYS MESSAGE CHAIN CALL with async/await

// MOD was created with UNISYS.NewModule('name');
// MOD.Call() returns a Promise
async function GetSomething() {
  let data;
  try {
    // synchronous
    data = await MOD.Call('Event',data);
    data = await MOD.Call('AnotherEvent',data);
    return data;
    } catch (err) {
      // err is thrown by MOD.Call's promise rejection
    }
}
// get something twice (these are running in parallel)
console.log('data',GetSomething());
console.log('data',GetSomething());
// beware asynchronous side effects due to the way Promises
// run the executor function IMMEDIATELY, so if it depends
// on values set outside the function there may be weirdness

To BROADCAST A UNISYS MESSAGE

// MOD was created with UNISYS.NewModule('name');
// MOD.Broadcast() returns a Promise
MOD.Broadcast('MESSAGENAME',data); // sends out a message to all listeners

To HANDLE EVENT across the entire webapp

// MOD was created with UNISYS.NewModule('name');
MOD.On('EVENTNAME',(data)=###{
  // event handler
});
// note that events do not have "duration", so they don't chain
// the way a promise chain would

To INVOKE EVENT across the entire webapp

// MOD was created with UNISYS.NewModule('name');
MOD.Emit('EVENTNAME',data);

To implement EVENTS AS SEQUENCEABLE PROGRAM CALLS

Promises can be used to fire START and STOP events on local objects.
Promises can also queue additional success/fail conditions.
Thinking of EVENTS as something happened and then MESSAGES as the action-oriented aspect
might bethe pattern:

  1. detect the event
    2A. do a message sequence
    2B. call "action queues" on modules/objects that support them

An action queue is the animation languages where you can load a series of named actions
that use update cycles in a gameloop. This is a different thing though.

Component UNISYS data binding

Components register named UISTATE objects with UNISYS. .. multiple instances of a type of component should be in a list. .. user interface "mode" is a UISTATE property. Component registers for named UISTATE updates to redraw UI, calling local SetState() to redraw. Component updates self through setState(), and updates UISTATE through another SetState() call.

Non-Component UNISYS data binding

Module registers for changes to named UISTATE objects
Module changes UISTATE through SetState() call

APPSTATE and LIFECYCLE higher-order data binding

There is GLOBAL APP STATE (data) that can be managed by UNISYS
There is also LIFECYCLE APP STATE that is loaded/changed by entering/exciting lifecycles
Certain operations may be affected by the user interface mode and other settings, so need a way to check
There are SETS OF MODES that can also be registered with UNISYS
LIFECYCLE is a special type of MODE
CHANGES IN MODES can fire HOOKS that change GLOBAL APP STATE and also MODAL APP STATE
You can set MODE HIEARCHY by retrieving the MODALITIES and setting a new LIST that determines order of logic.
Use javascript Set() and Map() and WeakMap() to implement these modalities
We'll use similar enter, update, exit terminology
sets of modes and submodes are a thing too. Some modes may not have all submodes.

Clone this wiki locally