/*______________
| ______ | 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;
}
});