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.
324 lines
9.5 KiB
324 lines
9.5 KiB
4 years ago
|
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;
|
||
|
console.log('Sending message...', this.json);
|
||
|
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() {
|
||
|
console.log('Connecting to socket:', this.url);
|
||
|
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;
|
||
|
}
|
||
|
}
|
||
|
}
|