-
Notifications
You must be signed in to change notification settings - Fork 1
UNISYS Reference
NOTE 2018 AUG 26 This documentation is out of date with regards to REACT.Component, LIFECYCLE startup order, expanded Local/Network messaging, and State handling for object/array properties.
UNISYS is the name of our modular application data+communications library. It implements message-based lifecycle, event, and state management for web apps. It was designed to make communication between different module types (i.e. REACT and D3) and hierarchically nested components (REACT child components) as direct as possible while still imposing a logical structure that can be extended across a network.
UNISYS is comprised of three code objects: a main system library located in system/unisys.js
and two helper objects. All three objects provide access to a particular aspect of UNISYS.
const UNISYS = require(`system/unisys`); // main system library
var UMOD = UNISYS.NewModule(module.id); // module w/ unique unisys ID
const UDATA = UNISYS.NewDataLink(UMOD); // messaging and state access
Through these modules, UNISYS provides the following support:
-
UMOD
LIFECYCLE: Phase-based Code Execution - Code modules can register to system events such as INITIALIZE and LOADASSETS to run code at particular times during the application lifecycle. Code can be assured that EVERY module will have completely finished executing the previous phase, so dependencies are reliable. -
UDATA
GLOBAL STATE: Namespace-addressible State Change Notification - Code modules can access a shared state object and subscribe to changes in sets of properties. REACT components can use this subscription mechanism, along with its ownsetState()
method, to access a shared application state without the contortions of forwarding props or using ReactRedux. Other components outside the REACT component ecosystem can access the same state and subscription mechanism, enabling cross-module communication. -
UDATA
GLOBAL EVENTS: Message-based event passing - Code modules can define their own events easily, for invocation by other UNISYS subscribers, using a simple syntax. -
UDATA
GLOBAL REMOTE PROCEDURES: Message-based asynchronous callback chains - Code modules can register message handlers that receive adata
object, optionally returning a response to the original caller asynchronously with support for chains of transactions. This also works across the network, simplify inter-application network communication.
There is also UNISYS.Component
, a direct replacement for React.Component
. It handles the UMOD and UDATA modules creation and exposes the pertinent methods so React components can participate in the UNISYS Lifecycle in a predictable manner.
UNISYS defines several "lifecycle phases" to help coordinate how multiple modules synchronize their internal state during initialization, configuration, setting changes, and so forth. UNISYS controls the entire application lifecycle. UNISYS requires every registered hook function to complete before going to the next phase.
For regular javascript modules, core functionality of lifecycle is accessed through the UMOD.Hook()
method, which accepts a 'phasename' and an arrow function. The Hook()
method is available on the UMOD object. Here is a simple example.
UMOD.Hook('INITIALIZE',()=>{
// your initialization code here
});
For React: UNISYS.Component
classes, a subset of hooks are exposed through a set of event handlers. Furthermore, not all lifecycle hooks are available to React, as our React "views" are initialized later in the application startup.
The lifecycle phase names are defined in system/event/lifecycle.js
:
'TEST_CONF' - boot: reserved for test code initialization
'INITIALIZE' - boot: initialize empty data structures
'LOADASSETS' - boot: load any required assets into data
'CONFIGURE' - boot: finalize internal data state
'DOM_READY' - boot: root AppView has finished componentDidMount
'RESET' - setup: select runtime data structures and runmode
'START' - setup: any modules may start talking to each other
'APP_READY' - setup: tell network app is ready
'RUN' - setup: system has started running
'UPDATE' - run: receives an update tick
'PREPAUSE' - run: should prepare to pause operation
'PAUSE' - run: is paused
'POSTPAUSE' - run: should prepare to resume RUN lifecycle
'STOP' - stop: should handle shutdown gracefully
'UNLOADASSETS' - stop: can release any loaded assets
'SHUTDOWN' - stop: application has stopped
NOTE: NetCreate currently doesn't use the RUN or STOP lifecycle hooks; they are used for realtime simulation apps like STEP.
Regular modules can use the UNISYS.Hook('phasename')
method. React components that need UNISYS support should extend from UNISYS.Component
and use one of the named hook events (e.g. this.OnStart()
). Note that a React component does NOT have access to any hooks fired before DOM_READY.
Both UNISYS.Hook()
and UNISYS.Component.On<HookEvent>()
accept a listener function that will be called by the system when that hook fires. The listener function can run immediately OR return a "Promise" for asynchronous operations.
EXAMPLE: RUN IMMEDIATELY
// immediate return example (does not block code)
UNISYS.Hook('INITIALIZE', function() {
console.log('returns immediately');
});
EXAMPLE: RETURN PROMISE (ASYNCHRONOUS)
// blocking example (uses Promise)
// waits 1000 milliseconds before completing, holding-up
// the INITIALIZE phase
UNISYS.Hook('INITIALIZE', function () {
let p = new Promise(function (resolve,reject) {
setTimeout(
() => {
resolve(1);
console.log('returns after 1000ms');
},
1000
); // setTimeout
}); // new Promise
return p;
}); // Hook('INITIALIZE'...)
While it looks complicated, it allows any module implementing a LIFECYCLE phase to perform an asynchronous operation and be assured that it finishes before the next phase is entered. Used correctly, this eliminates race conditions and out-of-order code initialization execution.
NOTE: Promises and async/await are confusing topics. You can think of a Promise as a kind of branching queue, with 'resolve' (success) and 'reject' (failure) conditions that are used to advance or stop the queue. The queue is created by creating at least one Promise-wrapped function. If the Promise-wrapped function returns ANOTHER Promise, it adds on to the queue of Promises. If the Promise-wrapped function returns a value (including void), this is the same as resolve(value)
and the promise is fulfilled (it's a magic assumption). Not all promises need to return a value either, if you are just using it for asynchronous control.
NOTE: If your function wants to access other variables outside the function declaration, you may need to be aware of Javascript's closure rules (or use just use an arrow function) to ensure that this data is still accessible when the function is executed; if you notice that you are getting undefined values when your code runs (particularly when using this
) that is probably what is going on.
UNISYS maintains a state dictionary that stores shallow objects containing properties, keyed by a 'namespace'.
A programmer can organize groups of related properties by namespace, using a setter function to update the object and a subscriber function to request updates about a namespace's state changes. UDATA.SetState()
is modeled closely after REACT's use of setState()
.
// set state in 'namespace' by adding new values
UDATA.SetAppState( 'namespace', { prop:'value'} );
// retrieve the full state object in 'namespace'
let state = UDATA.AppState('namespace');
// subscribe/unsubscribe to changes to 'namespace'
let stateHandler = (stateChangeObj)=>{ /*code*/ };
UDATA.OnAppStateChange( 'namespace', stateHandler );
UDATA.AppStateChangeOff( 'namespace', stateHandler );
React components that are extended from UNISYS.Component
has access to this.OnAppStateChange()
, etc.
To register your own application events, use the UDATA datalink object.
var UMOD = UNISYS.NewModule( module.id );
const UDATA = UNISYS.NewDataLink( UMOD );
// event handler function
let MyHandler = (eventName, data)=>{
switch (eventName) {
case 'myeventname':
console.log('received my event!');
break;
default :
throw Error(`unhandled event <${eventName}>`);
}
};
// create event handler
// note: multiple event handlers can be registered in any code module
// for the same event
UDATA.On('myeventname', MyHandler);
// emit the event ()
UDATA.Emit('myeventname', { hello:'this is my data'});
UDATA.Broadcast('myeventname', { hello:'this is also my data'});
// disable the handler
UDATA.Off('myeventname', MyHandler);
Emit()
will ensure that the message does not also get sent BACK to the originating UDATA source. This is useful for code modules that both send and receive the same event with peers.
Broadcast()
will send the message to all implementors, even the originating code module. This is useful for using events to separating the setting and saving of state, or for general status events.
A variation of the Global Events system is to define remote procedure calls that can als return values asynchronously. This works not only within the application, but across the network as well without specifying a network address. "The message is the address" is how you can think about it.
The semantics are similar to Global Events, but use a different set of calls and parameters.
var UMOD = UNISYS.NewModule( module.id );
const UDATA = UNISYS.NewDataLink( UMOD );
// example handler for receiving messages
function ExampleHandler (data) {
console.log('received data',data);
data.fish = data.fish || 'added a fish';
return data;
}
// enable message receving for 'MY_MESSAGE'
UDATA.Register('MY_MESSAGE',ExampleHandler);
// disable message receiving
UDATA.Unregister('MY_MESSAGE',ExampleHandler);
// send a message
UDATA.Call('MY_MESSAGE', { hi: 'hello' });
// send a message and receive a response
UNODE.Call('MY_MESSAGE', { hi: 'hello' }, function (data) {
console.log('received data back',data);
})
Currently networked remote procedure calls is not implemented, but is next in line. This requires porting of old code both to the client and server, updating it to use our streamlined semantics and data structures.
The remote procedure call system currently accepts a single callback. Our old implementation supports "sequences", but today we would want to use Promises instead. This will simplify some complicated inter-application synchronization issues (e.g. saving a file to the server, and then refreshing the display when the file has been reported saved).
Handling conditional rendering of UI features is an extension of a well-ordered application state manager, so providing utility methods for detecting/setting/switching these conditions AND receiving notification will further the GUI logic by avoiding "magic flags" buried in a code module.
Application modes are a superset of conditional rendering, determining what sets of features are enabled/disabled under what conditions. Utility methods that help detect and manage the data structures that must be updated during mode changes, and integrating this into the regular LIFECYCLE management, may help reduce code complexity further.
For multi-step UI operations, transaction/workflow support would provide a way to manage the setting and data elements of "conditional rendering" and "application mode logic" in a managed way. This would hypothetically help with more complicated interactions with a combination of modal and non-modal steps to achieve a certain end. This is a form of modeling the interaction possibilities of a well-ordered object-manipulation interface that by necessity has some verb-driven elements.