/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/Services.jsm"); this.EXPORTED_SYMBOLS = ["StateMachine"]; const DEBUG = false; this.StateMachine = function(aDebugTag) { function debug(aMsg) { dump('-------------- StateMachine:' + aDebugTag + ': ' + aMsg); } var sm = {}; var _initialState; var _curState; var _prevState; var _paused; var _eventQueue = []; var _deferredEventQueue = []; var _defaultEventHandler; // Public interfaces. sm.setDefaultEventHandler = function(aDefaultEventHandler) { _defaultEventHandler = aDefaultEventHandler; }; sm.start = function(aInitialState) { _initialState = aInitialState; sm.gotoState(_initialState); }; sm.sendEvent = function (aEvent) { if (!_initialState) { if (DEBUG) { debug('StateMachine is not running. Call StateMachine.start() first.'); } return; } _eventQueue.push(aEvent); asyncCall(handleFirstEvent); }; sm.getPreviousState = function() { return _prevState; }; sm.getCurrentState = function() { return _curState; }; // State object maker. // @param aName string for this state's name. // @param aDelegate object: // .handleEvent: required. // .enter: called before entering this state (optional). // .exit: called before exiting this state (optional). sm.makeState = function (aName, aDelegate) { if (!aDelegate.handleEvent) { throw "handleEvent is a required delegate function."; } var nop = function() {}; return { name: aName, enter: (aDelegate.enter || nop), exit: (aDelegate.exit || nop), handleEvent: aDelegate.handleEvent }; }; sm.deferEvent = function (aEvent) { // The definition of a 'deferred event' is: // We are not able to handle this event now but after receiving // certain event or entering a new state, we might be able to handle // it. For example, we couldn't handle CONNECT_EVENT in the // diconnecting state. But once we finish doing "disconnecting", we // could then handle CONNECT_EVENT! // // So, the deferred event may be handled in the following cases: // 1. Once we entered a new state. // 2. Once we handled a regular event. if (DEBUG) { debug('Deferring event: ' + JSON.stringify(aEvent)); } _deferredEventQueue.push(aEvent); }; // Goto the new state. If the current state is null, the exit // function won't be called. sm.gotoState = function (aNewState) { if (_curState) { if (DEBUG) { debug("exiting state: " + _curState.name); } _curState.exit(); } _prevState = _curState; _curState = aNewState; if (DEBUG) { debug("entering state: " + _curState.name); } _curState.enter(); // We are in the new state now. We got a chance to handle the // deferred events. handleDeferredEvents(); sm.resume(); }; // No incoming event will be handled after you call pause(). // (But they will be queued.) sm.pause = function() { _paused = true; }; // Continue to handle incoming events. sm.resume = function() { _paused = false; asyncCall(handleFirstEvent); }; //---------------------------------------------------------- // Private stuff //---------------------------------------------------------- function asyncCall(f) { Services.tm.currentThread.dispatch(f, Ci.nsIThread.DISPATCH_NORMAL); } function handleFirstEvent() { var hadDeferredEvents; if (0 === _eventQueue.length) { return; } if (_paused) { return; // The state machine is paused now. } hadDeferredEvents = _deferredEventQueue.length > 0; handleOneEvent(_eventQueue.shift()); // The handler may defer this event. // We've handled one event. If we had deferred events before, now is // a good chance to handle them. if (hadDeferredEvents) { handleDeferredEvents(); } // Continue to handle the next regular event. handleFirstEvent(); } function handleDeferredEvents() { if (_deferredEventQueue.length && DEBUG) { debug('Handle deferred events: ' + _deferredEventQueue.length); } for (let i = 0; i < _deferredEventQueue.length; i++) { handleOneEvent(_deferredEventQueue.shift()); } } function handleOneEvent(aEvent) { if (DEBUG) { debug('Handling event: ' + JSON.stringify(aEvent)); } var handled = _curState.handleEvent(aEvent); if (undefined === handled) { throw "handleEvent returns undefined: " + _curState.name; } if (!handled) { // Event is not handled in the current state. Try handleEventCommon(). handled = (_defaultEventHandler ? _defaultEventHandler(aEvent) : handled); } if (undefined === handled) { throw "handleEventCommon returns undefined: " + _curState.name; } if (!handled) { if (DEBUG) { debug('!!!!!!!!! FIXME !!!!!!!!! Event not handled: ' + JSON.stringify(aEvent)); } } return handled; } return sm; };