SOURCE CODE: Uize.Service
VIEW REFERENCE

/*______________
|       ______  |   U I Z E    J A V A S C R I P T    F R A M E W O R K
|     /      /  |   ---------------------------------------------------
|    /    O /   |    MODULE : Uize.Service Class
|   /    / /    |
|  /    / /  /| |    ONLINE : http://www.uize.com
| /____/ /__/_| | COPYRIGHT : (c)2012 UIZE
|          /___ |   LICENSE : Available under MIT License or GNU General Public License
|_______________|             http://www.uize.com/license.html
*/

/* Module Meta Data
  type: Class
  importance: 7
  codeCompleteness: 0
  testCompleteness: 0
  docCompleteness: 5
*/

/*?
  Introduction
    The =Uize.Service= module defines a base class from which classes that define services can inherit.

    *DEVELOPERS:* `Chris van Rensburg`
*/

Uize.module ({
  name:'Uize.Service',
  superclass:'Uize.Class',
  builder:function (_superclass) {
    /*** Variables for Scruncher Optimization ***/
      var
        _undefined,
        _false = false,
        _true = true,
        _Uize = Uize,

        /*** constants ***/
          _SERVICE_TAKING_TOO_LONG = 5000
      ;

    /*** Class Constructor ***/
      var
        _class = _superclass.subclass (
          function () {this._serviceMethods = []}
        ),
        _classPrototype = _class.prototype
      ;

    /*** Utility Functions ***/
      function _log (_message) {
        typeof console != 'undefined' && typeof console.log == 'function' &&
          console.log (_message)
        ;
      }

      function _methodCaller (_this,_methodName) {
        return function () {return _this [_methodName].apply (_this,arguments)};
      }

    /*** Private Static Properties ***/
      _class._serviceMethods = [];

    /*** Private Instance Methods ***/
      _classPrototype._warn = function (_message) {
        _log ('SERVICE WARNING: ' + _message);
      };

    /*** Public Static Methods ***/
      _class.declareServiceMethods = function (_serviceMethods) {
        var _this = this;

        if (arguments.length != 1 || typeof _serviceMethods != 'object')
          _serviceMethods = [].slice.call (arguments)
        ;

        var _serviceMethodPublicWrappers = {};
        function _declareServiceMethod (_methodName,_methodProfile) {
          var _isInitMethod = _methodName == 'init';

          function _errorMessage (_message) {return '<< ' + _methodName + ' >> ' + _message}

          if (_this.prototype [_methodName])
            throw new Error (
              _errorMessage ('You may not override a non-service public method with a service method')
            )
          ;

          _serviceMethods [_methodName] = _methodProfile || (_methodProfile = {});

          // normalize method profile
          // eg.
          // {
          //    async: true (default) | false,
          //    params: {...}
          // }
          _methodProfile.async = _methodProfile.async !== _false;

          _serviceMethodPublicWrappers [_methodName] = function (_params,_callback) {
            var
              _this = this,
              _adapter = _this.get ('adapter'),
              _methodIsAsync = _methodProfile.async
            ;
            if (!_methodIsAsync) {
              if (!_adapter)
                throw new Error (
                  _errorMessage (
                    'To call a synchronous service method, a service adapter must be set and the service must be initialized'
                  )
                )
              ;
              if (!_isInitMethod && !_this.get ('initialized'))
                throw new Error (
                  _errorMessage (
                    'In order to call a synchronous service method, the service must already be initialized'
                  )
                )
              ;
            }
            if (_params == _undefined) {
              _params = {};
            } else if (typeof _params != 'object') {
              throw new Error (_errorMessage ('First argument (params) must be an object, null, or undefined'));
            }
            var
              _adapterMethodWasAsync = _false,
              _timeCalled,
              _timeReturned,
              _takingTooLongTimeout,
              _result,
              _error,
              _handleReturnFromAdapterMethod = function () {
                _takingTooLongTimeout && clearTimeout (_takingTooLongTimeout);
                function _callCallback () {
                  var _onSuccess, _onError;
                  if (_callback) {
                    var _typeofCallback = typeof _callback;
                    if (_typeofCallback == 'function') {
                      _onSuccess = _callback;
                    } else if (_typeofCallback == 'object') {
                      _onSuccess = _callback.onSuccess;
                      _onError = _callback.onError;
                    }
                  }
                  if (_error) {
                    if (_onError) {
                      _onError (_error);
                    } else {
                      typeof _error == 'string'
                        ? (_error = new Error (_errorMessage (_error)))
                        : (_error.message = _errorMessage (_error.message))
                      ;
                      throw _error;
                    }
                  } else {
                    _isInitMethod && _this.set ('initialized',_true);
                    _onSuccess && _onSuccess (_result);
                  }
                }
                if (_timeReturned !== _undefined) {
                  throw new Error (_errorMessage ('Service adapter method should only return once'));
                } else {
                  _timeReturned = Uize.now ();
                  var _adapterMethodDuration = _timeReturned - _timeCalled;
                  _adapterMethodDuration > _SERVICE_TAKING_TOO_LONG &&
                    _this._warn (
                      _errorMessage(
                        'Service adapter method took too long to return (' + _adapterMethodDuration + 'ms)'
                      )
                    )
                  ;
                  if (_methodProfile.async) {
                    _adapterMethodWasAsync ? _callCallback () : setTimeout (_callCallback,0);
                  } else {
                    if (_adapterMethodWasAsync) {
                      throw new Error (
                        _errorMessage (
                          'Service method is declared as synchronous, but implementation in adapter is asynchronous'
                        )
                      );
                    } else {
                      _callCallback ();
                    }
                  }
                }
              },
              _callAdapterMethod = function () {
                if (
                  _adapter [_methodName] (
                    _params,
                    function (_response) {
                      _result = _response;
                      _handleReturnFromAdapterMethod ();
                    },
                    function (_errorResponse) {
                      _error = _errorResponse || {};
                      _handleReturnFromAdapterMethod ();
                    }
                  ) !== _undefined
                )
                  throw new Error (
                    _errorMessage (
                      'Service adapter method should always provide its result through a callback, not a return statement'
                    )
                  )
                ;
                _adapterMethodWasAsync = _true;
              }
            ;

            // now ready to start the call
            _timeCalled = Uize.now ();
            if (_methodIsAsync)
              _takingTooLongTimeout = setTimeout (
                function () {
                  var _initialized = _this.get ('initialized');
                  _this._warn (
                    _errorMessage (
                      _adapter && _initialized
                        ? 'Service adapter method taking too long to return'
                        : (
                          'Taking too long to be ready to call service adapter method (' +
                            'adapter ' + (_adapter ? '' : 'not ') + 'set, ' +
                            (_initialized ? '' : 'not ') + 'initialized' +
                          ')'
                        )
                    )
                  );
                },
                _SERVICE_TAKING_TOO_LONG
              )
            ;
            if (_isInitMethod) {
              _params.serviceInterface = {
                fire:_methodCaller (_this,'fire'),
                wire:_methodCaller (_this,'wire'),
                set:_methodCaller (_this,'set'),
                get:_methodCaller (_this,'get')
              };
              _params.service = _this;
              _this.once ('adapter',_callAdapterMethod);
            } else {
              if (!_adapter) {
                _this._warn (_errorMessage ('Adapter is not yet set when service method is called'));
              } else if (!_this.get ('initialized')) {
                _this._warn (
                  _errorMessage (
                    'Service adapter is set but not yet initialized when service method is called'
                  )
                );
              }
              _this.once (
                'adapter',
                function () {
                  _adapter = _this.get ('adapter');
                  _this.once ('initialized',_callAdapterMethod);
                }
              );
            }
            return _result;
          };
        }
        Uize.forEach (
          _serviceMethods,
          Uize.isArray (_serviceMethods)
            ? function (_methodName) {_declareServiceMethod (_methodName)}
            : function (_methodProfile,_methodName) {_declareServiceMethod (_methodName,_methodProfile)}
        );
        Uize.copyInto (_this.prototype,_serviceMethodPublicWrappers);
        /*?
          Static Methods
            Uize.Service.declareServiceMethods
              document...

              EXAMPLE
              ..........................................
              var FileSystem = Uize.Service.subclass ();
              FileSystem.declareServiceMethods ({
                readFile:{
                  async:false
                },
                writeFile:{
                  async:false
                },
                getFiles:{
                  async:false
                },
                getFolder:{
                  async:false
                },
                // etc.
                // etc.
              });
              ..........................................
        */
      };

    /*** Register Properties ***/
      _class.registerProperties ({
        _adapter:{
          name:'adapter',
          conformer:function (_adapter) {
            if (typeof _adapter == 'string') {
              var _adapterClass = eval (_adapter);
              if (_adapterClass) {
                _adapter = new _adapterClass;
              } else {
                throw new Error (
                  _errorMessage (
                    'The adapter module ' + _adapter + ' must be required and loaded first if you wish to set an adapter by module name'
                  )
                );
              }
            }
            if (_adapter != _undefined) {
              // validate the service adapter
              var _missingServiceMethods = [];
              Uize.forEach (
                this.constructor._serviceMethods,
                function (_serviceMethod) {
                  if (
                    !_adapter.hasOwnProperty (_serviceMethod) ||
                    typeof _adapter [_serviceMethod] != 'function'
                  ) {
                    _missingServiceMethods.push (_serviceMethod);
                  }
                }
              );
              if (_missingServiceMethods.length) {
                _adapter = _undefined;
                throw new Error (
                  _errorMessage (
                    'Service module adapter is missing implementations for service methods: ' + _missingServiceMethods.sort ().join (', ')
                  )
                );
              }
            }
            return _adapter;
          },
          onChange:function () {this.set ({initialized:_false})}
          /*?
            State Properties
              adapter
                document...
          */
        },
        _initialized:{
          name:'initialized',
          value:_false
          /*?
            State Properties
              initialized
                document...
          */
        }
      });

    return _class;
  }
});