You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
frontend/src/app/flitter-socket.ts

322 lines
9.4 KiB

import {uuid_v4} from './utility';
const _FWS = WebSocket;
export class FlitterSocketTransaction {
connection: any;
type: string;
id: string;
resolved = false;
socket: any;
outgoing: any;
incoming: any;
connectionId: string;
sent = false;
received = false;
// tslint:disable-next-line:variable-name
_status = 200;
// tslint:disable-next-line:variable-name
_message = '';
endpoint = '';
json = '';
constructor(data, conn) {
this.connection = conn;
this.type = data.type;
this.id = data.transaction_id ? data.transaction_id : conn.uuid();
this.socket = conn.socket;
this.outgoing = data.outgoing ? data.outgoing : {};
this.incoming = data.incoming ? data.incoming : {};
this.connectionId = conn.id;
}
resolve() {
this.resolved = true;
}
status(code: number = null) {
if ( code ) {
this._status = code;
return this;
}
return this._status;
}
message(msg: string = null) {
if ( msg ) {
this._message = msg;
return this;
}
return this._message;
}
send(data: any = null) {
if ( this.type === 'response' ) {
if ( this.resolved ) { throw new Error(`Transaction can only be sent once per request. (ID: ${this.id})`); }
const obj = {
status: this._status,
transaction_id: this.id,
type: 'response',
... (this._message && {message: this._message}),
... (data && {data}),
... ((!data && this.outgoing) && {data: this.outgoing})
};
this.json = JSON.stringify(obj);
this.resolve();
} else if ( this.type === 'request' ) {
if ( this.sent ) { throw new Error(`Request can only be sent once per Transaction. (ID: ${this.id})`); }
const obj = {
endpoint: this.endpoint,
transaction_id: this.id,
type: 'request',
... (data && {data}),
... ((!data && this.outgoing) && {data: this.outgoing})
};
this.json = JSON.stringify(obj);
}
this.sent = true;
return this.socket.send(this.json);
}
}
export class FlitterSocketClientServerTransaction extends FlitterSocketTransaction {
// tslint:disable-next-line:variable-name
_handler: any;
constructor(data, conn) {
if ( data.data ) { data.outgoing = data.data; }
super(data, conn);
this.type = 'request';
if ( data.endpoint ) { this.endpoint = data.endpoint; }
}
handler(fn) {
if ( !fn ) { return this._handler; }
this._handler = fn;
return this;
}
receipt(data) {
this.received = true;
return this._handler(this, this.socket, data);
}
}
export class FlitterSocketServerClientTransaction extends FlitterSocketTransaction {
constructor(data, conn) {
if ( data.data ) { data.incoming = data.data; }
super(data, conn);
this.type = 'response';
if ( data.endpoint ) { this.endpoint = data.endpoint; }
}
set(key, value = null) {
if ( value && typeof this.outgoing === 'object' ) { this.outgoing[key] = value; } else { this.outgoing = key; }
return this;
}
}
export class FlitterSocketConnection {
url: string;
open = false;
activeTransactions: any = {};
id: string;
// tslint:disable-next-line:variable-name
_controller: any = {};
closeResolves: any[] = [];
closeCallbacks: any[] = [];
openResolves: any[] = [];
openCallbacks: any[] = [];
socket: any;
constructor(url) {
this.url = url;
this.open = false;
this.activeTransactions = {};
this.id = this.uuid();
this._controller = {};
this._create_socket();
this.closeResolves = [];
this.closeCallbacks = [];
this.openResolves = [];
this.openCallbacks = [];
}
on_close(callback = false) {
if ( !callback ) {
return new Promise(resolve => {
this.closeResolves.push(resolve);
});
} else {
this.closeCallbacks.push(callback);
}
}
on_open(callback = false) {
if ( !callback ) {
return new Promise(resolve => {
this.openResolves.push(resolve);
});
} else {
this.openCallbacks.push(callback);
}
}
_create_socket() {
this.socket = new _FWS(this.url);
this.socket.onopen = (e) => {
this.open = true;
this.openResolves.forEach(r => r(this));
this.openCallbacks.forEach(c => c(this));
this.openResolves = [];
this.openCallbacks = [];
};
this.socket.onmessage = this._on_message.bind(this);
this.socket.onclose = (e) => {
this.open = false;
this.closeResolves.forEach(r => r(this));
this.closeCallbacks.forEach(c => c(this));
this.closeResolves = [];
this.closeCallbacks = [];
};
}
_on_message(event) {
const data = this.validate_message(event.data);
if ( !data ) { return; }
if ( data.type === 'response' ) {
if ( Object.keys(this.activeTransactions).includes(data.transaction_id) ) {
const t = this.activeTransactions[data.transaction_id];
t.receipt(data.data);
if ( t.resolved ) { delete this.activeTransactions[t.id]; }
} else { throw new Error('Response: transaction ID not found. It\'s possible that the transaction was already resolved.'); }
} else if ( data.type === 'request' ) {
const t = new FlitterSocketServerClientTransaction(data, this);
this.activeTransactions[t.id] = t;
this._controller[t.endpoint](t, this.socket);
if ( t.resolved ) { delete this.activeTransactions[t.id]; }
}
}
request(endpoint, data, handler) {
const t = new FlitterSocketClientServerTransaction({data, endpoint}, this);
t.handler(handler);
this.activeTransactions[t.id] = t;
t.send();
}
async asyncRequest(endpoint, data): Promise<[any, any, any]> {
return new Promise((res, rej) => {
try {
this.request(endpoint, data, (...args: any) => {
res(args);
});
} catch (e: unknown) {
rej(e);
}
});
}
validate_message(msg) {
let fail = false;
let validTid = true;
let error = '';
let code = 400;
// check if valid JSON
if ( !this.is_json(msg) ) {
fail = true;
error = 'Incoming message must be valid FSP JSON object.';
validTid = false;
}
let data;
if ( !fail ) { data = JSON.parse(msg); }
// check for required fields: transaction_id, type
if ( !fail && !Object.keys(data).includes('transaction_id') ) {
fail = true;
error = 'Incoming message must include universally-unique transaction_id.';
validTid = false;
}
if ( !fail && (!Object.keys(data).includes('type') || !(['request', 'response'].includes(data.type))) ) {
fail = true;
error = 'Incoming message must include valid type, which may be one of: request, response.';
}
// if request, check for required fields: endpoint
if ( !fail && data.type === 'request' && !Object.keys(data).includes('endpoint') ) {
fail = true;
error = 'Incoming request message must include a valid endpoint.';
}
// if request, check if transaction_id is unique
if ( !fail && data.type === 'request' && Object.keys(this.activeTransactions).includes(data.transaction_id) ) {
fail = true;
error = 'Incoming request message must have a universally-unique, NON-EXISTENT transaction_id.';
validTid = false;
}
// if request, check for valid endpoint
if ( !fail && data.type === 'request' && !(typeof this._controller[data.endpoint] === 'function') ) {
fail = true;
error = 'The requested endpoint does not exist or is invalid.';
code = 404;
}
// if response, check if transaction_id exists
if ( !fail && data.type === 'response' && !Object.keys(this.activeTransactions).includes(data.transaction_id)) {
fail = true;
error = 'The specified transaction_id does not exist. It\'s possible that this transaction has already resolved.';
}
if ( fail ) {
const sendData = {
transaction_id: validTid ? data.transaction_id : 'unknown',
message: error,
status: code,
type: 'response'
};
this.send_raw(sendData);
} else { return data; }
}
send_raw(data) {
this.socket.send(JSON.stringify(data));
}
uuid() {
return uuid_v4();
}
controller(set: any = false) {
if ( !set ) { return this._controller; }
this._controller = set;
return this;
}
is_json(str) {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
}