present method

void present (
  1. Object expState,
  2. Object what,
  3. {Map stepParms,
  4. Function rejector}
)

Present a transition proposal to this SamModel model.

This method is the linchpin of the whole system. Thank you Jean-Jacques Dubray.

The sequence of the proposal processing is shown below. If there is a failure the rejector method is called. The default is to mark the model as SE.ss_broken after populating SamModel.whyBroken and forcing a render cycle.

  1. The request is subjected to preliminary validation and rejected if it fails.
  2. A SamReq instance is created for the proposal.
  3. If the SamModel is busy the SamReq is queued and an immediate return is made to the proposer.
  4. The SamModel is marked as busy.
  5. If the SamModel.samState is not expState, reject the proposal unless it is an action that can be ignored.
  6. The SamModel.takeStep method is invoked with SamReq as a parameter. Note that any rendering of the outcome is done within takeStep.
  7. If the state has changed and a nap exists for the new state, call the nap function.
  8. If the state has changed raise a signal or weakSignal if they exist in DefState and conditions are valid.
  9. Turn of the busy flag.
  10. if there are queued SamReqs, dequeue the first and resume at step 4.
  11. return to the proposal proposer.

It is assumed this function is serialized and never entered concurrently. This holds true for Dart if Isolates are never employed. It is left as an exercise to the reader to see how a SamModel can work with Isolates in play. I suspect the signal capability would allow signaling between isolates but it may have to be tweaked as the signal presently passes the signaller (child) reference and knows its parents reference (read memory address for reference).

Implementation

void present(Object expState,Object what,{Map stepParms,Function rejector}){
  //log("present $this what=$what? cls=${expState.runtimeType}");
  if (rejector == null) rejector = defReject;
  SamReq req;
  if (expState is SamReq) {
    req = expState; //recursive dequeue
  } else {
    //log("asserting ${what.runtimeType} $_enumType ${SE.sa_change.runtimeType}");
    assert(
    ((_enumTypes.firstWhere((_) => (what.runtimeType == _),orElse:(() => null))) != null) ||
        (what.runtimeType == SE.sa_change.runtimeType),
    "what $what not allowable type ${what.runtimeType}"
    );
    req = SamReq("$expState",rejector,what,stepParms:stepParms);//,#what:what,#stepParms:stepParms,#rejector:rejector,#nap:true,#render:true,#raise:true,#bLog:false};
    if (req.stepParms != null) {
      if (req.stepParms.containsKey('nap') && (req.stepParms['nap'] is bool)) req.nap = req.stepParms['nap'];
      if (req.stepParms.containsKey('render') && (req.stepParms['render'] is bool)) req.render = req.stepParms['render'];
      if (req.stepParms.containsKey('raise') && (req.stepParms['raise'] is bool)) req.raise = req.stepParms['raise'];
      if (req.stepParms.containsKey('bLog') && (req.stepParms['bLog'] is bool)) req.bLog = req.stepParms['bLog'];
    }
    String isWhat = reWhat.firstMatch("$what")?.group(1) ?? "??";
    //log("process $isWhat $what");
    switch(isWhat) {
      case "sa": req.action  = "$what"; break;
      case "ss": req.state   = "$what"; break;
      case "sg": req.signal  =  what;   break;
      default: req.rejector(this,req,"what $what has no sa,ss or sg prefix"); return; // should never happen as reWhat does the work
    }
  }
  if (this._samBusy) {                // ------------ queue up to simplify --------
    this._samQ.add(req);
    return;
  }
  this._samBusy = true;
  if (req.bLog) log("present $this what=${req.state}/${req.action} nap=${req.nap} r=${req.render}");

  if (this._samState != req.expState) { // ------------- validate ------------
    var bIgnore = false;
    if ((req.action != null) && this._ss.ssMap.containsKey(this._samState)) {
      this._sa.applyAction(this,req);
      //var stepObj = this.state.stepMap[this.samState];
      //if ((stepObj.ignore) && (stepObj.ignore.indexOf(stepParms.action) >= 0)) bIgnore = true;
    }
    if (!bIgnore) {
      //console.log("reject %s",req.action);
      req.rejector(this,req,"concurrency error expect=${req.expState} have=${this._samState} req=${req.what}");
    } else {
      // silently ignore
    }
  } else {                            // validated OK. apply transformation
    String oldState = this._samState;
    this._ss.takeStep(this,req);
    if (req.reject != null) {
      req.rejector(this,req,req.reject);
    } else {
      if (req.render) this._sv.render(this,req);
    }
    if (this._samState != oldState) {
      DefState ds = this._ss.ssMap[this._samState];
      if (ds == null) {
        this.broken("reached undefined ${this._samState} $this");
        return;
      }
      if ((ds.fncNap != null) && (req.nap)) {
        ds.fncNap(this,req);
      }
      //var stepObj = this.state.stepMap[this.samState];
      //if (stepObj.nap && req.nap) stepObj.nap(this,req);
      if ((ds.objSignal != null) && req.raise /*&& (this._parent != null)*/) {
        //log('raiseSignal ${ds.strSignal} build par=${this._parent}');
        raiseSignal(ds.objSignal,req);
      }
      if ((ds.objWeakSignal != null) && req.raise && (this._parent != null)) raiseSignal(ds.objWeakSignal,req);
    }
  }
  this._samBusy = false;           // --------- dequeue, process remaining queue ---
  if (req.bLog) {
    //console.log("---- present.exit %s ",this.aaaName,{sm:this,req:req});
  }
  if (this._samQ.length > 0) {
    SamReq qReq = this._samQ.removeAt(0);
    if (qReq.bLog) log("Dequeue $qReq");
    this.present(qReq,null);
  }
  //log("present.done $this what=$what? cls=${expState.runtimeType}");
}