Skip to content

Using PhaseMachine

DSri Seah edited this page May 20, 2024 · 1 revision

Warning

This class needs refactoring to clarify its semantics before being added to @ursys/core. See issue #4

PhaseMachine is an URSYS class that implements a sequence of operations using hooks. It's similar conceptually to a simple state machine, an "action hook" system, or "run levels" in unix-like systems. Originally, it was used for application lifecycle management.

Within the context of URSYS WebApps, the PhaseMachine is a primitive control mechanism similar to an EventEmitter. It allows one to declare a number of named phase groups that contain an array of phases in each instance of PhaseMachine. You can then attach code for execution during one of the named phases using syntax like this:

UR.Hook('UR/LOAD_ASSETS', ()=>{
  return new Promise( (resolve,reject)=>{
    // do something that takes time
  });
});

UR.Hook('UR/APP_READY', ()=>{
  console.log('app is initialized');
});

So long as you make the Hook call before the phase runs, you are guaranteed that your code will run sometime during that phase. You can use the Hook() call on module load.

Behind the scenes, URSYS WebApps will then run every phase in the PhaseMachine by group through a call that looks like this:

(async ()=>{
  await UR.Exec('UR:INIT');    
  await UR.Exec('UR:CONNECT');
  await UR.Exec('UR:LOAD');
  await UR.Exec('UR:CONFIG');
  await UR.Exec('UR:READY');
  await UR.Exec('UR:RUN');
})();

This ensures that each phase of phase group "INIT" of the "UR" PhaseMachine instance will have its collection of code called, and all of them must complete phase-by-phase.

Initialization

The initialization of PhaseMachine instance looks like:

const APP_PHASES = {
  INIT:    [ 'SYS_INIT', 'DOM_READY' ],
  CONNECT: [ 'NET_CONNECT', 'NET_READY'],
  LOAD:    [ 'LOAD_DB', 'LOAD_CONFIG', 'LOAD_ASSETS' ],
  CONFIG:  [ 'APP_CONFIGURE' ],
  READY:   [ 'APP_READY' ],
  RUN:     [ 'APP_STAGE','APP_START','APP_RUN', 'APP_TICK' ],
  RESET:   [ 'APP_RESET', 'APP_RESTAGE' ],
  PAUSE:   [ 'APP_PAUSE', 'APP_PAUSED', 'APP_UNPAUSE' ],
  UNLOAD:  [ 'APP_STOP', 'APP_UNLOAD', 'APP_SHUTDOWN' ],
  REBOOT:  [ 'APP_REBOOT' ]
};
const APP_LIFECYCLES = new PhaseMachine('UR',APP_PHASES);

Internal Operation

The global UR library exposes the Hook() and Exec() methods. Under the hood, they are using these PhaseMachine methods.

  • hook(phaseOp, opFunc) - subscribe opFunc to the named phase operation
  • execute(phaseOp) - call all functions subscribed to the single named phase operation
  • executePhase(phaseGroup) - call all functions in the phaseGroup for each phase, one after the other
  • executePhaseParallel(phaseGroup) - call all functions in the phaseGroup, allowing them to complete in any order

Other Uses of PhaseMachine

PhaseMachine can also be used for managing any kind of application loop that involves code stages to be run in a very particular order. For example, a simple video game loop might look like this:

const GAMELOOP_PHASE = {
  LOAD: [ 'LOAD_ASSETS', 'RESET', 'INIT', 'READY' ],
  RUN: [
    // get state and queue derived state
    'INPUTS_UPDATE', 'PHYSICS_UPDATE',
    // agent/groups autonomous updates
    'AGENTS_UPDATE', 'AGENTS_EVENT','GROUPS_UPDATE',
    // agent/groups script execution and queue actions
    'GROUPS_THINK', 'AGENTS_THINK', 'GROUPS_VETO',
    // agent/groups execute queue actions
    'FEATURES_EXEC', 'AGENTS_EXEC', 'GROUPS_EXEC',
    // simulation
    'SIM_EVAL', 'REFEREE_EVAL',
    // display output
    'GUI_UPDATE', 'VIS_UPDATE', 'VIS_RENDER'
  ]
};

This is the ideal order of operations because each subsequent phase depends on the completion of the prior one. Of particular note in the above is the RUN phase group, which shows the idealized loop that runs every simulation tick.