[lib.client.client-driver][lib.client.xhr][lib.client.ws] made more generic with trying to re-configure connection (bind) if connection lost and trying to send() a request

This commit is contained in:
xdrm-brackets 2018-03-24 15:13:27 +01:00
parent 1cedd6284a
commit a1a5610f2c
6 changed files with 372 additions and 255 deletions

View File

@ -1,3 +1,4 @@
import {APIClient} from './lib/api-client'
import {GlobalStore} from './lib/gstore'
import VueRouter from 'vue-router'
import routes from './routes'
@ -6,8 +7,14 @@ import {ContentController} from './lib/content-controller'
import {RoomController} from './lib/room-controller'
import {ChannelController} from './lib/channel-controller'
import XHRClientDriver from './lib/client/xhr.js'
import WebSocketClientDriver from './lib/client/ws.js'
window.gs = new GlobalStore();
window.api = new APIClient('localhost', null, false);
window.xhrcd = XHRClientDriver;
window.wscd = WebSocketClientDriver;
/* (1) Global data
---------------------------------------------------------*/

View File

@ -1,81 +1,142 @@
/* classe API */
export class APIClient{
constructor(target){
this.target = target;
this.xhr = []; // tableau d'objets pour les requêtes ajax
this.buffer = null; // Contiendra le buffer pour debugger si erreur de parsage
this.error = { // error constants
'-1': 'Invalid target format: "METHOD module/method"',
'-2': 'XHR error',
'-3': 'Invalid JSON response',
/* (1) Constructs an API client manager
*
* @_hostname<String> Server hostname (without http, port number, etc)
* @_baseuri<String> Server base URI
* @_ssl<bool> [OPT] Whether SSL is activated (http vs https) (default: https)
* @_port<int> [OPT] optional HTTP port (default: none)
*
* @return http_url<String> Built http_url
*
---------------------------------------------------------*/
constructor(_hostname, _baseuri, _ssl=true, _port=null){
/* (1) Build URL parts */
this.http_parts = {
ssl: _ssl === true,
hostname: _hostname,
baseuri: this.fix_uri(_baseuri),
port: !isNaN(_port) ? _port: null
};
this.xhr_stack = []; // Ajax request stack
this.buffer = null; // Last request buffer
}
fix_uri(_uri){
if( typeof _uri !== 'string' )
return '';
return _uri.split('/').filter((r) => r.trim().length).join('/')
}
/* (2) HTTP url dynamic getter
*
* @_uri<String> [OPT] optional URI string
* @_token<String> [OPT] optional HTTP token
*
* @return http_url<String> Built http_url
*
---------------------------------------------------------*/
build_url(_uri, _token=null){
/* (1) Initialize URL buffer */
let bufurl = 'http';
/* (2) Manage @ssl */
this.http_parts.ssl && ( bufurl = bufurl.concat('s') );
bufurl = bufurl.concat('://');
/* (3) Manage token */
( typeof _token === 'string' ) && ( bufurl = bufurl.concat(`${_token}@`) );
/* (4) Manage hostname */
bufurl = bufurl.concat(this.http_parts.hostname);
/* (5) Manage port */
( this.http_parts.port !== null ) && ( bufurl = bufurl.concat(`:${this.http_parts.port}`) );
/* (6) Base uri */
bufurl = bufurl.concat(`/${this.http_parts.baseuri}/`);
/* (7) Manage URI */
bufurl = bufurl.concat( this.fix_uri(_uri) );
return bufurl;
}
/* transaction avec le serveur (http://{host}/api/)
/* Server Transaction
*
* @param pTarget<String> URI cible, format "HTTP_METHOD uri/uri/uri"
* @param pArgs<Object> Le tableu d'arguments passé en POST (attribut<->postfield) à http://{host}/api/
* @param pHandler<Function> Fonction qui s'éxécutera lors de la réponse (1 argument -> réponse<Object>)
* @param pToken<String> Optionnel, token d'auth pour l'api
* @param _path<String> target path (format "HTTP_METHOD uri/uri/uri")
* @param _args<Object> formdata object (as raw object)
* @param _callback<Function> Response callback
* @param _token<String> [OPT] http token
*
***************************************************************************************************
*
* @usecase
* 1. api.call(
* 2. "PUT newspaper/article/4"
* 2. { content: "New article content" },
* 3. function(resp){
* 4. alert(resp.error);
* 5. }
* 2. 'PUT newspaper/article/4'
* 3. { content: "new content" },
* 4. (r) => alert(r.error),
* 5. 'sometoken'
* 6. );
*
*/
call(pTarget, pArgs, pHandler, pToken=null){
call(_path, _args, _callback, _token=null){
/* (1) Check @pHandler (for dispatching errors)
/* (1) Argument management
---------------------------------------------------------*/
/* (1) Check if is a Function */
if( !(pHandler instanceof Function) )
throw new Error("3rd argument must be a function, but is of type '"+typeof(pHandler)+"' !");
/* (1) Set default callback if @callback not callable */
if( !(_callback instanceof Function) )
_callback = function(r){ console.warn('The API callback function is missing, default callback set.', 'Response', r); };
/* (2) Check @pTarget
---------------------------------------------------------*/
/* (1) Check format */
if( !/^([A-Z]+) (.+)/i.test(pTarget) ){
pHandler({ error: -1, ErrorDescription: this.error['-1'] });
/* (2) Check @path format */
if( !/^([A-Z]+) (.+)/i.test(_path) ){
_callback({ error: -1 });
return false;
}
/* (2) Store locally data */
var lHttpMethod = RegExp.$1;
var lUri = RegExp.$2;
var http_method = RegExp.$1;
var http_uri = RegExp.$2;
/* (3) Default @_token */
if( typeof _token !== 'string' )
_token = null;
/* (3) Create form data
---------------------------------------------------------*/
/* (1) Create virtual form */
var lForm = new FormData();
var form_data = new FormData();
/* (2) Add attributes */
for( var key in pArgs ){
for( var key in _args ){
// {2.1} If a file -> send as it //
if( pArgs[key] instanceof File )
lForm.append(key, pArgs[key]);
if( _args[key] instanceof File )
form_data.append(key, _args[key]);
// {2.2} Else -> JSON stringify //
else
lForm.append(key, JSON.stringify(pArgs[key]));
form_data.append(key, JSON.stringify(_args[key]));
}
@ -84,74 +145,69 @@ export class APIClient{
/* (4) Create XHR request
---------------------------------------------------------*/
/* (1) Clean ended requests */
for( var i = this.xhr.length-1 ; i >= 0 ; i-- ){
for( var i = this.xhr_stack.length-1 ; i >= 0 ; i-- ){
if( this.xhr[i] != null )
if( this.xhr_stack[i] != null )
break;
this.xhr.pop();
this.xhr_stack.pop();
}
/* (2) Push a new entry -> fetch its index */
i = this.xhr.push(null) - 1;
i = this.xhr_stack.push(null) - 1;
/* (3) Create XHR object */
this.xhr[i] = (window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHttpRequest');
this.xhr_stack[i] = (window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHttpRequest');
/* (5) Bind response event
---------------------------------------------------------*/
var self = this; // to access the buffer
this.xhr[i].onreadystatechange = function(i){
this.xhr_stack[i].onreadystatechange = function(i, parent){
/* (1) If request over */
if( this.xhr[i].readyState == 4 ){
if( this[i].readyState === 4 ){
/* (2) Update buffer (for debug) */
self.buffer = this.xhr[i].responseText;
parent.buffer = this[i].responseText;
/* (3) If request success */
if( [0, 200, 417].indexOf(this.xhr[i].status) > -1 ){
if( [0, 200].indexOf(this[i].status) > -1 ){
/* (3.1) Create default response (if JSON error) */
var response = {error:-3, ErrorDescription: self.error['-3']};
var response = { error: -2 };
/* (3.2) Try to parse JSON */
try{ response = JSON.parse(this.xhr[i].responseText); }catch(e){}
try{ response = JSON.parse(this[i].responseText); }catch(e){}
/* (3.3) Launch @pHandler with response */
pHandler(response);
/* (3.3) Launch @_callback with response */
_callback(response);
/* (4) If request error */
}else
pHandler({ error:-2, ErrorDescription: self.error['-2'] });
_callback({ error: -3 });
/* (5) Notify current xhr instance is done */
this.xhr[i] = null;
this[i] = null;
}
}.bind(this, i);
}.bind(this.xhr_stack, i, this);
/* (6) Finish & send request
---------------------------------------------------------*/
/* (1) Open the XHR */
this.xhr[i].open(lHttpMethod, this.target+lUri, true);
console.log(http_method, this.build_url(http_uri, _token));
this.xhr_stack[i].open(http_method, this.build_url(http_uri, _token), true);
/* (2) Manage optional token */
if( pToken != null )
this.xhr[i].setRequestHeader('Authorization', 'Digest '+pToken);
/* (2) Custom header to notify we're using Ajax */
this.xhr_stack[i].setRequestHeader('X-Requested-With', 'XMLHttpRequest');
/* (3) Custom header to notify we're using Ajax */
this.xhr[i].setRequestHeader('X-Requested-With', 'XMLHttpRequest');
/* (4) Make the call */
this.xhr[i].send( lForm );
/* (3) Make the call */
this.xhr_stack[i].send(form_data);
return true;

View File

@ -1,15 +1,15 @@
export class ClientDriver{
export default class ClientDriver{
/* (0) Constants & Enums
---------------------------------------------------------*/
static get STATE(){
return Object.freeze({
OPENING: 0,
OPENED: 1,
CLOSED: 0,
READY: 1,
CONNECTING: 2,
CONNECTED: 3,
CLOSED: 4
TRANFERING: 4
});
}
@ -22,27 +22,81 @@ export class ClientDriver{
---------------------------------------------------------*/
constructor(_resource){
/* (1) Initialize driver
---------------------------------------------------------*/
/* (1) Default attributes values */
this.error = false;
this.state = ClientDriver.STATE.OPENING;
this.force_close = false;
this.state = ClientDriver.STATE.READY;
this.stack = [];
/* (2) Default User callbacks */
this.callback = {
onready: function(){},
onconnected: function(){},
onclose: function(){},
onreceive: function(){}
};
/* (2) Fail: invalid _resource */
this.event = {
/* (3) Manage connection opened */
onconnected: function(){
// update state
this.state = ClientDriver.STATE.CONNECTED;
// call callback
this.callback.onconnected();
// if request(s) in stack -> pop & send them
while( this.stack.length > 0 )
this.send(this.stack.shift());
}.bind(this),
/* (4) Manage connection closed */
onclose: function(){
// if called close() -> CLOSED
if( this.force_close )
this.state = ClientDriver.STATE.CLOSED;
// else set state back to READY
else
this.state = ClientDriver.STATE.READY;
// call callback
this.callback.onclose();
}.bind(this),
/* (5) Manage response received */
onreceive: function(_response){
// set state from TRANSFERING to CONNECTED
this.state = ClientDriver.STATE.CONNECTED;
// call callback
this.callback.onreceive(_response);
}.bind(this)
};
/* (2) Manage errors
---------------------------------------------------------*/
/* (1) Fail: invalid _resource */
if( typeof _resource !== 'string' )
return ( this.error = true );
/* (3) Set explicit attributes */
/* (2) Set explicit attributes */
this.resource = _resource;
return;
}
/* (2) Binds the client to the resource
*
* @return bound<boolean> Whether the binding has been successful
@ -50,17 +104,43 @@ export class ClientDriver{
---------------------------------------------------------*/
bind(){
/* (1) Fail: not OPENING OR error */
if( this.error || this.state !== ClientDriver.STATE.OPENING )
/* (1) force_close -> fail */
if( this.force_close )
return false;
/* (2) By default return success */
/* (2) Fail: not READY */
if( this.state !== ClientDriver.STATE.READY )
return false;
/* (3) Update state */
this.state = ClientDriver.STATE.CONNECTING;
/* (4) Child dispatch */
return true;
}
/* (3) Send request
/* (3) Closes the connection
*
* @return closed<boolean> Whether the connection has been closed
*
---------------------------------------------------------*/
close(){
/* (1) set force_close */
this.force_close = true;
/* (2) Update state */
this.state = ClientDriver.STATE.CLOSED;
/* (4) Child dispatch */
return true;
}
/* (4) Send request
*
* @_request<Object> Request data
*
@ -69,111 +149,99 @@ export class ClientDriver{
---------------------------------------------------------*/
send(_request){
/* (1) Fail: if error */
if( this.error )
/* (1) force_close -> fail */
if( this.force_close )
return false;
/* (2) Fail: invalid _request */
/* (2) bufferise 'state' */
let state = this.state;
/* (3) Fail: invalid _request */
if( !(_request instanceof Object) )
return false;
/* (3) If not already connected -> stack request */
if( this.state < ClientDriver.STATE.CONNECTED ){
this.stack.push(_request);
/* (4) CLOSED -> abort */
if( state === ClientDriver.STATE.CLOSED )
return false;
}
/* (5) READY -> stack message + bind() [in case connection failed)] */
if( state === ClientDriver.STATE.READY )
return ( this.stack.push(_request) > -1 ) && this.bind();
/* (6) CONNECTING -> stack message */
if( state === ClientDriver.STATE.CONNECTING )
return ( this.stack.push(_request) > -1 );
/* (4) By default return success */
/* (7) update state */
this.state = ClientDriver.STATE.TRANFERING;
/* (8) Child dispatch */
return true;
}
/* (4) Bind event to connection opened
/* (5) Bind event to connection successfully opened
*
* @_callback<Function> Callback launched when connection is opened
*
* @return bound<boolean> Whether the callback has successfully been bound
*
---------------------------------------------------------*/
onready(_callback){
set onconnected(_callback){
/* (1) Fail: already CONNECTED OR error */
if( this.error || this.state >= ClientDriver.STATE.CONNECTED )
return false;
/* (2) Fail: invalid _callback */
/* (1) Fail: invalid _callback */
if( !(_callback instanceof Function) )
return false;
/* (3) Register _callback */
this.callback.onready = function(cback){
/* (2) Register _callback */
this.callback.onconnected = _callback;
// call callback
cback();
// if request(s) in stack -> pop & send them
while( this.stack.length > 0 )
this.send(this.stack.shift());
}.bind(this, _callback);
/* (5) By default return success */
/* (3) By default return success */
return true;
}
/* (5) Bind event to connection closed
/* (6) Bind event to connection closed
*
* @_callback<Function> Callback launched when connection is closed
*
* @return bound<boolean> Whether the callback has successfully been bound
*
---------------------------------------------------------*/
onclose(_callback){
set onclose(_callback){
/* (1) Fail: already CLOSED OR error */
if( this.error || this.state === ClientDriver.STATE.CLOSED )
return false;
/* (2) Fail: invalid _callback */
/* (1) Fail: invalid _callback */
if( !(_callback instanceof Function) )
return false;
/* (3) Register _callback */
/* (2) Register _callback */
this.callback.onclose = _callback;
/* (4) By default return success */
/* (3) By default return success */
return true;
}
/* (6) Bind event to message reception
/* (7) Bind event to message reception
*
* @_callback<Function> Callback launched when a message is received
*
* @return bound<boolean> Whether the callback has successfully been bound
*
---------------------------------------------------------*/
onreceive(_callback){
set onreceive(_callback){
/* (1) Fail: already CONNECTED OR error */
if( this.error || this.state >= ClientDriver.STATE.CONNECTED )
return false;
/* (2) Fail: invalid _callback */
/* (1) Fail: invalid _callback */
if( !(_callback instanceof Function) )
return false;
/* (3) Register _callback */
/* (2) Register _callback */
this.callback.onreceive = _callback;
/* (4) By default return success */
/* (3) By default return success */
return true;
}

View File

@ -1,6 +1,6 @@
import {ClientDriver} from './client-driver.js'
import ClientDriver from './client-driver.js'
export class WebSocketClientDriver extends ClientDriver{
export default class WebSocketClientDriver extends ClientDriver{
/* (1) Creates a client driver
@ -21,9 +21,6 @@ export class WebSocketClientDriver extends ClientDriver{
this.ws = null;
this.buffer = null; // useful when waiting for WebSocket to open
/* (N) Set all is ok */
this.error = false;
}
@ -42,28 +39,16 @@ export class WebSocketClientDriver extends ClientDriver{
this.ws = new WebSocket(this.resource);
/* (2) Bind callback.onready */
this.ws.onopen = function(){
this.state = ClientDriver.STATE.CONNECTED;
this.callback.onready();
}.bind(this);
this.ws.onopen = this.event.onconnected;
/* (3) Bind callback.onclose */
this.ws.onclose = function(){
this.state = ClientDriver.STATE.CLOSED;
this.callback.onclose();
}.bind(this);
this.ws.onclose = this.event.onclose;
/* (4) Bind callback.onerror */
this.ws.onerror = function(){
this.state = ClientDriver.STATE.CLOSED;
this.callback.onclose();
}.bind(this);
this.ws.onerror = this.event.onclose;
/* (5) Bind callback.onreceive */
this.ws.onmessage = function(response){
this.state = ClientDriver.STATE.CONNECTED;
this.callback.onreceive(response);
}.bind(this);
this.ws.onmessage = (_message_event) => this.event.onreceive(_message_event.data);
/* (6) Return success */
return true;
@ -85,11 +70,12 @@ export class WebSocketClientDriver extends ClientDriver{
return false;
/* (1) Error: invalid _request.buffer */
if( typeof _request.buffer !== 'string' )
if( typeof _request.buffer !== 'string' ){
this.state = ClientDriver.STATE.CONNECTED;
return false;
}
/* (2) Send message */
console.log('sent', _request.buffer);
this.ws.send(_request.buffer);
/* (3) Return success */
@ -98,50 +84,22 @@ export class WebSocketClientDriver extends ClientDriver{
}
/* (4) Bind event to connection opened
/* (3) Closes the connection
*
* @_callback<Function> Callback launched when connection is opened
*
* @return bound<boolean> Whether the callback has successfully been bound
* @return closed<boolean> Whether the connection has been closed
*
---------------------------------------------------------*/
onready(_callback){
close(){
/* (0) Parent check */
if( !super.onready(_callback) )
if( !super.close() )
return false;
}
/* (1) Close websocket */
this.ws.close();
/* (5) Bind event to connection closed
*
* @_callback<Function> Callback launched when connection is closed
*
* @return bound<boolean> Whether the callback has successfully been bound
*
---------------------------------------------------------*/
onclose(_callback){
/* (0) Parent check */
if( !super.onclose(_callback) )
return false;
}
/* (6) Bind event to message reception
*
* @_callback<Function> Callback launched when a message is received
*
* @return bound<boolean> Whether the callback has successfully been bound
*
---------------------------------------------------------*/
onreceive(_callback){
/* (0) Parent check */
if( !super.onreceive(_callback) )
return false;
/* (2) Return success */
return true;
}

View File

@ -1,6 +1,6 @@
import {ClientDriver} from './client-driver.js'
import ClientDriver from './client-driver.js'
export class XHRClientDriver extends ClientDriver{
export default class XHRClientDriver extends ClientDriver{
/* (1) Creates a client driver
@ -20,9 +20,6 @@ export class XHRClientDriver extends ClientDriver{
/* (2) Create useful attributes */
this.xhr = null;
/* (N) Set all is ok */
this.error = false;
}
@ -51,17 +48,13 @@ export class XHRClientDriver extends ClientDriver{
// Got response
if( this.xhr.status === 200 ){
this.callback.onreceive(this.xhr.responseText);
this.state = ClientDriver.STATE.CLOSED;
this.callback.onclose();
this.event.onreceive(this.xhr.responseText);
this.event.onclose();
// Request error
}else{
}else
this.event.onclose();
this.state = ClientDriver.STATE.CLOSED;
this.callback.onclose();
}
break;
}
@ -69,11 +62,11 @@ export class XHRClientDriver extends ClientDriver{
}.bind(this);
/* (3) State is now CONNECTED (wrong but ..) */
/* (3) State is now CONNECTED (ready to send) */
this.state = ClientDriver.STATE.CONNECTED;
/* (4) Bind to callback onready() */
this.callback.onready();
/* (4) Bind to callback onconnected() */
this.event.onconnected();
/* (5) Return success */
return true;
@ -97,14 +90,18 @@ export class XHRClientDriver extends ClientDriver{
return false;
/* (2) Error: invalid _request.path */
if( typeof _request.path !== 'string' )
if( typeof _request.path !== 'string' ){
this.state = ClientDriver.STATE.CONNECTED;
return false;
}
/* (2) Manage _request.path argument
---------------------------------------------------------*/
/* (1) Error: invalid path format */
if( !/^([A-Z]+) (.+)/i.test(_request.path) )
if( !/^([A-Z]+) (.+)/i.test(_request.path) ){
this.state = ClientDriver.STATE.CONNECTED;
return;
}
/* (2) Extract path data */
@ -145,54 +142,4 @@ export class XHRClientDriver extends ClientDriver{
}
/* (4) Bind event to connection opened
*
* @_callback<Function> Callback launched when connection is opened
*
* @return bound<boolean> Whether the callback has successfully been bound
*
---------------------------------------------------------*/
onready(_callback){
/* (0) Parent check */
if( !super.onready(_callback) )
return false;
}
/* (5) Bind event to connection closed
*
* @_callback<Function> Callback launched when connection is closed
*
* @return bound<boolean> Whether the callback has successfully been bound
*
---------------------------------------------------------*/
onclose(_callback){
/* (0) Parent check */
if( !super.onclose(_callback) )
return false;
}
/* (6) Bind event to message reception
*
* @_callback<Function> Callback launched when a message is received
*
* @return bound<boolean> Whether the callback has successfully been bound
*
---------------------------------------------------------*/
onreceive(_callback){
/* (0) Parent check */
if( !super.onreceive(_callback) )
return false;
}
}

81
webpack/mockup/api.list Normal file
View File

@ -0,0 +1,81 @@
=== USER ===
>>> POST /user/
{
username: <String>
password: <String>
}
>>> GET /user/:uid
{}
>>> PUT /user/:uid
{
username: <?String>
passowrd: <?String>
}
>>> DELETE /user/:uid
{}
=== USER AUTHENTICATION ===
>>> POST /user/token
{
username: <String>
password: <String>
}
=== CHANNEL ===
>>> POST @token:/channel/
{
link: <String>
name: <String>
}
>>> GET @token:/channel/:cid --> channel specific data
>>> GET @token:/channel/ --> all available channels for user
{}
>>> PUT @token:/channel/:cid
{
link: <?String>
name: <?String>
}
>>> DELETE @token:/channel/:cid
{}
=== CHANNEL <> USER ===
>>> POST @token:/channel/:cid/subscribe
{}
>>> DELETE @token:/channel/:cid/subscribe
{}
=== ROOM ===
>>> POST @token:/channel/:cid/room/:type/
{
name: <String>
}
>>> GET @token:/channel/:cid/room/:type/ --> all channel's rooms of this type
>>> GET @token:/channel/:cid/room/:type/:rid --> room information of this type
>>> PUT @token:/channel/:cid/room/:type/:rid
{
name: <String>
}
>>> DELETE @token:/channel/:cid/room/:type/:rid --> remove room