@synerty/vortexjs
Version:
Custom observable data serialisation and routing based on Angular 2+
1,451 lines (1,436 loc) • 212 kB
JavaScript
import * as pako from 'pako';
import * as base64 from 'base-64';
import { __decorate, __param } from 'tslib';
import deepEqual from 'deep-equal';
import * as i0 from '@angular/core';
import { EventEmitter, NgZone, Injectable, Inject, Component, forwardRef } from '@angular/core';
import { Subject, BehaviorSubject, firstValueFrom, debounceTime } from 'rxjs';
import { Network } from '@capacitor/network';
import { takeUntil, first, filter, distinctUntilChanged, skip } from 'rxjs/operators';
import { Capacitor } from '@capacitor/core';
import { Device } from '@capacitor/device';
import { CapacitorSQLite, SQLiteConnection } from '@capacitor-community/sqlite';
import stringify from 'json-stable-stringify';
import PromiseWorker from 'webworker-promise';
/**
* Created by Jarrod Chesney / Synerty on 22/11/16.
*/
// ----------------------------------------------------------------------------
/**
* Keys from Object
*
* Extract an array of keys from a json object.
* This will not include keys starting with an underscore.
*
* @param obj: The object to get the keys from.
* @param includeUnderscore: Should keys with underscores be included?
* @return A list of keys from the object.
*/
function dictKeysFromObject(obj, includeUnderscore = false) {
let keys = [];
for (let k in obj) {
if ((!k.startsWith("_") || includeUnderscore)
&& obj.hasOwnProperty(k) && typeof k !== "function") {
keys.push(k);
}
}
return keys;
}
// ----------------------------------------------------------------------------
class AssertException {
message;
constructor(message) {
let self = this;
self.message = message;
}
toString() {
let self = this;
return "AssertException: " + self.message;
}
}
/**
* A simple assert statement
* @param exp : The boolean to assert
* @param message : The message to log when the assertion fails.
*/
function assert(exp, message = null) {
if (exp)
return;
console.trace();
throw new AssertException(message);
}
// ----------------------------------------------------------------------------
/**
* Create url encoded arguments
*
* @param filter : The object containing the key:value pairs to convert into a url
*
*/
function getFiltStr(filter) {
let filtStr = "";
for (let key in filter) {
if (!filter.hasOwnProperty(key))
continue;
filtStr += (filtStr.length ? "&" : "?") + key + "=" + filter[key];
}
return filtStr;
}
// ----------------------------------------------------------------------------
/**
* Date String
*
* @return A date and time formatted to a string for log messages.
*/
function dateStr() {
let d = new Date();
return d.toTimeString()
.split(" ")[0] + "." + d.getUTCMilliseconds() + ": ";
}
// ----------------------------------------------------------------------------
/**
* Bind a function
* @param obj : The object to bind the function for.
* @param method : The method to bind onto to the object.
*
* @return A callable function that will call the method correctly bound to "this"
*/
function bind(obj, method) {
return function () {
return method.apply(obj, arguments);
};
}
// ----------------------------------------------------------------------------
/**
* Bind a function
* @param err : The err object to convert to a string.
*
* @return A callable function that will call the method correctly bound to "this"
*/
function errToStr(err) {
if (err.message != null)
return err.message;
try {
let jsonStr = JSON.stringify(err);
if (jsonStr != "{}")
return jsonStr;
}
catch (ignore) {
}
return err.toString();
}
/** Deep Clone
* @param data: Deep Clone an entire JSON data structure
* @param ignoreFieldNames: An array of field names not to copy.
*
* @return A clone of the data
*/
function deepCopy(data, ignoreFieldNames = null) {
const dict = {};
if (ignoreFieldNames != null
&& Object.prototype.toString.call(ignoreFieldNames)
.slice(8, -1) == "Array") {
for (const fieldName of ignoreFieldNames)
dict[fieldName] = true;
}
return _deepCopy(data, dict);
}
function _deepCopy(data, ignoreFieldNames) {
// If the data is null or undefined then we return undefined
if (data === null || data === undefined)
return undefined;
// Get the data type and store it
const dataType = Object.prototype.toString.call(data)
.slice(8, -1);
// DATE
if (dataType == "Date") {
const clonedDate = new Date();
clonedDate.setTime(data.getTime());
return clonedDate;
}
// OBJECT
if (dataType == "Object") {
let copiedObject = {};
for (const key of Object.keys(data)) {
if (ignoreFieldNames != null && ignoreFieldNames[key] === true)
continue;
copiedObject[key] = _deepCopy(data[key], ignoreFieldNames);
}
return copiedObject;
}
// ARRAY
if (dataType == "Array") {
let copiedArray = [];
for (const item of data)
copiedArray.push(_deepCopy(item, ignoreFieldNames));
return copiedArray;
}
return data;
}
// ----------------------------------------------------------------------------
/* Add a imports for these requires */
// Declare the TypeScript for Declaration Merging
// https://www.typescriptlang.org/docs/handbook/declaration-merging.html
// Start the javascript type patching
if (Array.prototype.diff == null) {
Array.prototype.diff = function (a) {
return this.filter(function (i) {
return !(a.indexOf(i) > -1);
});
};
}
if (Array.prototype.intersect == null) {
Array.prototype.intersect = function (a) {
return this.filter(function (i) {
return (a.indexOf(i) > -1);
});
};
}
if (Array.prototype.remove == null) {
Array.prototype.remove = function (objectOrArray) {
if (objectOrArray == null)
return this;
if (objectOrArray instanceof Array) {
return this.diff(objectOrArray);
}
else {
let index = this.indexOf(objectOrArray);
if (index !== -1)
this.splice(index, 1);
return this;
}
};
}
if (Array.prototype.add == null) {
Array.prototype.add = function (objectOrArray) {
if (objectOrArray == null)
return this;
// If for some reasons they are trying to add us to our self, throw an exception.
if (objectOrArray === this)
throw new Error("Array.add, I was passed myself, i can't add my self to myself.");
if (objectOrArray instanceof Array) {
for (let i = 0; i < objectOrArray.length; ++i)
this.push(objectOrArray[i]);
return this;
}
this.push(objectOrArray);
return this;
};
}
if (Array.prototype.equals == null) {
Array.prototype.equals = function (array) {
// if the other array is a false value, return
if (array == null)
return false;
// compare object instances
if (this === array)
return true;
// compare lengths - can save a lot of time
if (this.length !== array.length)
return false;
for (let i = 0; i < this.length; i++) {
// Check if we have nested arrays
if (this[i] instanceof Array && array[i] instanceof Array) {
// recurse into the nested arrays
if (!this[i].compare(array[i]))
return false;
}
else if (this[i] !== array[i]) {
// Warning - two different object instances will never be equal: {x:20} !=
// {x:20}
return false;
}
}
return true;
};
}
if (Array.prototype.bubbleSort == null) {
Array.prototype.bubbleSort = function (compFunc) {
let self = this;
function merge(left, right) {
let result = [];
while (left.length && right.length) {
if (compFunc(left[0], right[0]) <= 0) {
result.push(left.shift());
}
else {
result.push(right.shift());
}
}
while (left.length)
result.push(left.shift());
while (right.length)
result.push(right.shift());
return result;
}
if (self.length < 2)
return self.slice(0, self.length);
let middle = parseInt((self.length / 2).toString());
let left = self.slice(0, middle);
let right = self.slice(middle, self.length);
return merge(left.bubbleSort(compFunc), right.bubbleSort(compFunc));
};
}
// ============================================================================
// Array.indexOf prptotype function
// Add if this browser (STUPID IE) doesn't support it
if (Array.prototype.indexOf == null) {
Array.prototype.indexOf = function (item) {
let len = this.length >>> 0;
let from = Number(arguments[1]) || 0;
from = (from < 0) ? Math.ceil(from) : Math.floor(from);
if (from < 0)
from += len;
for (; from < len; from++) {
if (from in this && this[from] === item)
return from;
}
return -1;
};
}
// ----------------------------------------------------------------------------
// Typescript date - date fooler
function now$2() {
return new Date();
}
function logLong(message, start, payload = null) {
let duration = now$2() - start;
let desc = "";
// You get 5ms to do what you need before i call the performance cops.
if (duration < 10)
return;
if (payload != null) {
desc = ", " + JSON.stringify(payload.filt);
}
// console.log(`${message}, took ${duration}${desc}`);
}
// ----------------------------------------------------------------------------
class PayloadDelegateABC {
}
function btoa(data) {
try {
return window["btoa"](data);
}
catch (e) {
return base64.encode(data);
}
}
function atob(data) {
try {
return window["atob"](data);
}
catch (e) {
return base64.decode(data);
}
}
class PayloadDelegateInMainWeb extends PayloadDelegateABC {
deflateAndEncode(payloadJson) {
return new Promise((resolve, reject) => {
const compressedData = pako.deflate(payloadJson);
const compressedDataStr = new Uint8Array(compressedData).reduce((acc, curr, i) => acc + String.fromCharCode(curr), "");
const encodedData = btoa(compressedDataStr);
resolve(encodedData);
});
}
encodeEnvelope(payloadEnvelopeJson) {
return new Promise((resolve, reject) => {
const vortexMsg = btoa(payloadEnvelopeJson);
resolve(vortexMsg);
});
}
decodeAndInflate(vortexStr) {
return new Promise((resolve, reject) => {
const compressedData = Uint8Array.from(atob(vortexStr), (v) => v.charCodeAt(0));
const payloadJson = pako.inflate(compressedData, { to: "string" });
resolve(payloadJson);
});
}
decodeEnvelope(vortexStr) {
return new Promise((resolve, reject) => {
let payloadJson = atob(vortexStr);
resolve(payloadJson);
});
}
}
/*
* ###############################################################################
* Common Serialisation functions
* ###############################################################################
*/
class SerialiseUtil {
static T_RAPUI_TUPLE = "rt";
static T_RAPUI_PAYLOAD = "rp";
static T_RAPUI_PAYLOAD_ENVELOPE = "rpe";
static T_GENERIC_CLASS = "gen"; // NOT SUPPORTED
static T_FLOAT = "float";
static T_INT = "int";
static T_STR = "str";
static T_BYTES = "bytes";
static T_BOOL = "bool";
static T_DATETIME = "datetime";
static T_DICT = "dict";
static T_LIST = "list";
static V_NULL = "null";
static V_TRUE = "1";
static V_FALSE = "0";
static ISO8601_PY = "%Y-%m-%d %H:%M:%S.%f%z";
static ISO8601 = "YYYY-MM-DD HH:mm:ss.SSSSSSZZ";
// Rapui Serialised Type - Shortened for memory constraints.
__rst;
toStr(obj) {
let self = this;
if (obj["toISOString"] != null)
// instanceof Date or moment
return obj.toISOString().replace("Z", "000+0000").replace("T", " ");
if (typeof obj.constructor === "boolean")
return obj ? SerialiseUtil.V_TRUE : SerialiseUtil.V_FALSE;
if (typeof obj.constructor === "string")
return obj;
return obj.toString();
}
fromStr(val, typeName) {
let self = this;
if (typeName === SerialiseUtil.T_STR)
return val;
if (typeName === SerialiseUtil.T_BYTES)
return base64.decode(encodeURI(val));
if (typeName === SerialiseUtil.T_BOOL)
return val === SerialiseUtil.V_TRUE;
if (typeName === SerialiseUtil.T_FLOAT ||
typeName === SerialiseUtil.T_INT)
return parseFloat(val);
if (typeName === SerialiseUtil.T_DATETIME)
return new Date(val);
alert("fromStr - UNKNOWN TYPE");
}
toRapuiType(value) {
let self = this;
if (value == null)
return SerialiseUtil.V_NULL;
if (value.__rst === SerialiseUtil.T_RAPUI_TUPLE)
return SerialiseUtil.T_RAPUI_TUPLE;
if (value.__rst === SerialiseUtil.T_RAPUI_PAYLOAD)
return SerialiseUtil.T_RAPUI_PAYLOAD;
if (value.__rst === SerialiseUtil.T_RAPUI_PAYLOAD_ENVELOPE)
return SerialiseUtil.T_RAPUI_PAYLOAD_ENVELOPE;
if (value instanceof Date)
return SerialiseUtil.T_DATETIME;
if (value.constructor === Number)
return SerialiseUtil.T_FLOAT;
if (value.constructor === String)
return SerialiseUtil.T_STR;
if (value.constructor === Boolean)
return SerialiseUtil.T_BOOL;
if (value.constructor === Array)
return SerialiseUtil.T_LIST;
if (value.constructor === Object)
return SerialiseUtil.T_DICT;
alert("toRapuiType - UNKNOWN TYPE");
}
rapuiEquals(obj1, obj2, obj1FieldNames, obj2FieldNames) {
let self = this;
let fieldNames1 = obj1FieldNames;
fieldNames1.sort();
let fieldNames2 = obj2FieldNames;
fieldNames2.sort();
if (!fieldNames1.equals(fieldNames2))
return false;
// Create the <items> base element
for (let fieldIndex = 0; fieldIndex < fieldNames1.length; ++fieldIndex) {
let name = fieldNames1[fieldIndex];
let field1 = obj1[name];
let field2 = obj2[name];
if (field1 === undefined && field2 === undefined)
continue;
else if (field1 === undefined || field2 === undefined)
return false;
let type1 = self.toRapuiType(field1);
let type2 = self.toRapuiType(field2);
if (type1 !== type2)
return false;
if (type1 === SerialiseUtil.T_RAPUI_TUPLE ||
type1 === SerialiseUtil.T_RAPUI_PAYLOAD ||
type1 === SerialiseUtil.T_RAPUI_PAYLOAD_ENVELOPE) {
if (!field1.equals(field2))
return false;
}
else if (type1 === SerialiseUtil.T_LIST) {
let indexes = [];
for (let index = 0; index < field1.length; index++) {
indexes.push(index);
}
let isEqual = self.rapuiEquals(field1, field2, indexes, indexes);
if (!isEqual)
return false;
}
else if (type1 === SerialiseUtil.T_DICT) {
let isEqual = self.rapuiEquals(field1, field2, dictKeysFromObject(field1), dictKeysFromObject(field2));
if (!isEqual)
return false;
}
else if (type1 === SerialiseUtil.T_DATETIME) {
if (field1.getTime() !== field2.getTime())
return false;
}
else {
if (field1 !== field2)
return false;
}
}
return true;
}
}
// Declare the TypeScript for Declaration Merging
// https://www.typescriptlang.org/docs/handbook/declaration-merging.html
if (String.prototype.replaceAll == null) {
String.prototype.replaceAll = function (stringToFind, stringToReplace) {
let temp = this;
while (temp.indexOf(stringToFind) !== -1)
temp = temp.replace(stringToFind, stringToReplace);
return temp;
};
}
if (String.prototype.format == null) {
String.prototype.format = function () {
let args = arguments;
return this.replace(/{(\d+)}/g, function (match, num) {
return typeof args[num] !== "undefined" ? args[num] : match;
});
};
}
if (String.prototype.trim == null) {
String.prototype.trim = function () {
return String(this)
.replace(/^\s+|\s+$/g, "");
};
}
if (String.prototype.startsWith == null) {
// see below for better implementation!
String.prototype.startsWith = function (str) {
return this.slice(0, str.length) === str;
};
}
if (String.prototype.endsWith == null) {
String.prototype.endsWith = function (pattern) {
let d = this.length - pattern.length;
return d >= 0 && this.lastIndexOf(pattern) === d;
};
}
if (String.prototype.isPrintable == null) {
String.prototype.isPrintable = function () {
let re = /^[\x20-\x7e]*$/;
return re.test(this);
};
}
const JSONABLE_TYPES = {};
function addJsonableType(jsonableType) {
return function (_Class) {
JSONABLE_TYPES[jsonableType] = _Class;
return _Class;
};
}
/**
* ############################################################################### #
* JSON Serialisation functions
* ###############################################################################
*/
class Jsonable extends SerialiseUtil {
_tupleType;
_rawJonableFields = null;
static JSON_CLASS_TYPE = "_ct";
// private static readonly JSON_CLASS = "_c";
static JSON_TUPLE_TYPE = "_c";
static JSON_FIELD_TYPE = "_ft";
static JSON_FIELD_DATA = "_fd";
constructor() {
super();
/*
* Jsonable This class gives simple objects suport for serialising to/from json.
* It handles Number, String, Array and Date. It doesn't handle more complex
* structures (hence why Payloads have their own functions to do this)
*/
let self = this;
self.__rst = SerialiseUtil.T_GENERIC_CLASS;
}
_isRawJsonableField(name) {
if (name == null || name.length == 0) {
return false;
}
if (this._rawJonableFields == null) {
return false;
}
return this._rawJonableFields.indexOf(name) != -1;
}
_fieldNames() {
let self = this;
let keys = [];
for (let k in self) {
if (!k.startsWith("_") && self.hasOwnProperty(k)) {
keys.push(k);
}
}
return keys;
}
equals(other) {
let self = this;
return self.rapuiEquals(self, other, self._fieldNames(), other._fieldNames());
}
toRestfulJsonDict() {
return this._tupleToJsonDict(true);
}
toJsonDict() {
return this._tupleToJsonDict(false);
}
_tupleToJsonDict(useShortNames) {
let self = this;
let jsonDict = {};
if (!useShortNames) {
jsonDict[Jsonable.JSON_CLASS_TYPE] = self.__rst;
}
if (!useShortNames && self._tupleType != null) {
jsonDict[Jsonable.JSON_TUPLE_TYPE] = self._tupleType;
}
/* This is in the PY version
else
jsonDict[JSON_CLASS] = className(self)
*/
let fieldNames = self._fieldNames();
// fieldNames.sort(); // Why?
// Create the <items> base element
for (let i = 0; i < fieldNames.length; ++i) {
let name = fieldNames[i];
self.toJsonField(self[name], jsonDict, name, useShortNames);
}
return jsonDict;
}
fromJsonDict(jsonDict) {
/*
* From Json Returns and instance of this object populated with data from the
* json dict
*
*/
for (const name of Object.keys(jsonDict)) {
if (name.startsWith("_")) {
continue;
}
if (this._isRawJsonableField(name)) {
this[name] = jsonDict[name];
}
else {
this[name] = this.fromJsonField(jsonDict[name]);
}
}
// This is only required for unit tests new Tuple().fromJsonDict(..)
if (jsonDict[Jsonable.JSON_CLASS_TYPE] == SerialiseUtil.T_RAPUI_TUPLE) {
this._tupleType = jsonDict[Jsonable.JSON_TUPLE_TYPE];
}
return this;
}
toJsonField(value, jsonDict = null, name = null, useShortNames = false) {
let self = this;
let convertedValue = null;
let valueType = value == null ? SerialiseUtil.V_NULL : self.toRapuiType(value);
if (this._isRawJsonableField(name)) {
convertedValue = deepCopy(value);
}
else if (valueType === SerialiseUtil.T_RAPUI_TUPLE ||
valueType === SerialiseUtil.T_RAPUI_PAYLOAD ||
valueType === SerialiseUtil.T_RAPUI_PAYLOAD_ENVELOPE) {
if (useShortNames) {
convertedValue = value.toRestfulJsonDict();
}
else {
convertedValue = value.toJsonDict();
}
}
else if (valueType === SerialiseUtil.T_DICT) {
// Treat these like dicts
convertedValue = {};
for (const keyName of Object.keys(value)) {
if (useShortNames) {
self.toJsonField(value[keyName], convertedValue, keyName, true);
}
else {
self.toJsonField(value[keyName], convertedValue, keyName);
}
}
}
else if (valueType === SerialiseUtil.T_LIST) {
convertedValue = [];
// List
for (let i = 0; i < value.length; ++i) {
let element;
if (useShortNames) {
element = self.toJsonField(value[i], null, null, true);
}
else {
element = self.toJsonField(value[i]);
}
convertedValue.push(element);
}
}
else if (valueType === SerialiseUtil.T_FLOAT ||
valueType === SerialiseUtil.T_INT ||
valueType === SerialiseUtil.T_BOOL ||
valueType === SerialiseUtil.T_STR) {
convertedValue = value;
}
else if (valueType === SerialiseUtil.V_NULL) {
convertedValue = null;
}
else {
convertedValue = self.toStr(value);
}
// Non standard values need a dict to store their value type attributes
// Create a sub dict that contains the value and type
let jsonStandardTypes = [
SerialiseUtil.T_FLOAT,
SerialiseUtil.T_STR,
SerialiseUtil.T_INT,
SerialiseUtil.V_NULL,
SerialiseUtil.T_BOOL,
SerialiseUtil.T_LIST,
SerialiseUtil.T_DICT,
];
if (jsonStandardTypes.indexOf(valueType) === -1 &&
!(value instanceof Jsonable)) {
let typedData = {};
typedData[Jsonable.JSON_FIELD_TYPE] = valueType;
typedData[Jsonable.JSON_FIELD_DATA] = convertedValue;
convertedValue = typedData;
}
/* Now assign the value and it's value type if applicable */
if (name != null && jsonDict != null) {
jsonDict[name] = convertedValue;
}
return convertedValue;
}
// ----------------------------------------------------------------------------
fromJsonField(value, valueType = null) {
let self = this;
if (valueType === SerialiseUtil.V_NULL || value == null) {
return null;
}
if (valueType === SerialiseUtil.T_INT) {
return value;
}
if (value[Jsonable.JSON_CLASS_TYPE] != null) {
valueType = value[Jsonable.JSON_CLASS_TYPE];
}
// JSON handles these types natively,
// if there is no type then these are the right types
if (valueType == null) {
valueType = self.toRapuiType(value);
if ([
SerialiseUtil.T_BOOL,
SerialiseUtil.T_FLOAT,
SerialiseUtil.T_INT,
SerialiseUtil.T_STR,
].indexOf(valueType) !== -1) {
return value;
}
}
if (value[Jsonable.JSON_FIELD_TYPE] != null) {
return self.fromJsonField(value[Jsonable.JSON_FIELD_DATA], value[Jsonable.JSON_FIELD_TYPE]);
}
// Tuple
if (valueType === SerialiseUtil.T_RAPUI_TUPLE) {
let Tuple = JSONABLE_TYPES[SerialiseUtil.T_RAPUI_TUPLE];
let tupleType = value[Jsonable.JSON_TUPLE_TYPE];
let newTuple = Tuple.create(tupleType);
return newTuple.fromJsonDict(value);
}
// Handle the case of payloads within payloads
if (valueType === SerialiseUtil.T_RAPUI_PAYLOAD) {
return new Payload().fromJsonDict(value);
}
// Payload Endpoint
if (valueType === SerialiseUtil.T_RAPUI_PAYLOAD_ENVELOPE) {
return new PayloadEnvelope().fromJsonDict(value);
}
/* SKIP T_GENERIC_CLASS */
if (valueType === SerialiseUtil.T_DICT) {
let restoredDict = {};
for (const subName of Object.keys(value)) {
restoredDict[subName] = self.fromJsonField(value[subName]);
}
return restoredDict;
}
if (valueType === SerialiseUtil.T_LIST) {
let restoredList = [];
for (let i = 0; i < value.length; ++i) {
restoredList.push(self.fromJsonField(value[i]));
}
return restoredList;
}
// Handle single value
return self.fromStr(value, valueType);
}
}
var Tuple_1;
/** Tuples implementation details.
*
* We're not going to have fully fledged tuples in the browser. As far as the
* browser is concerned, it will recieve tuples which will have all the fields
* and then it will create tuples to send back, populating the fields it needs.
*
* There should be some checks when it gets back to the server to ensure the
* populated fields exist in the tuples when it deserialises it.
*
*/
let Tuple = Tuple_1 = class Tuple extends Jsonable {
// Change Tracking Enabled - Shortened for memory conservation
_ct = false;
// Change Tracking Reference State - Shortened for memory conservation
_ctrs = null;
constructor(tupleType = null) {
super();
let self = this;
self.__rst = SerialiseUtil.T_RAPUI_TUPLE;
self._tupleType = tupleType;
}
static create(tupleType) {
if (TUPLE_TYPES[tupleType] == null) {
return new Tuple_1(tupleType);
}
else {
// Tuples set their own types, don't pass anything to the constructor
return new TUPLE_TYPES[tupleType]();
}
}
_tupleName() {
return this._tupleType;
}
// ---------------
// Start change detection code
_setChangeTracking(on = true) {
this._ctrs = new Tuple_1();
this._ctrs.fromJsonDict(this.toJsonDict());
this._ct = on;
}
_detectedChanges(reset = true) {
let changes = [];
for (let key of dictKeysFromObject(this)) {
let old_ = this._ctrs[key];
let new_ = this[key];
if (deepEqual(old_, new_)) {
continue;
}
changes.push({
fieldName: key,
oldValue: old_,
newValue: new_,
});
}
if (reset) {
this._setChangeTracking(true);
}
return changes;
}
};
Tuple = Tuple_1 = __decorate([
addJsonableType(SerialiseUtil.T_RAPUI_TUPLE)
], Tuple);
let TUPLE_TYPES = {};
function addTupleType(_Class) {
let inst = new _Class();
TUPLE_TYPES[inst._tupleType] = _Class;
}
// ----------------------------------------------------------------------------
// Payload class
/**
*
* This class is serialised and transferred over the vortex to the server.
*/
class PayloadEnvelope extends Jsonable {
static workerDelegate = new PayloadDelegateInMainWeb();
static vortexUuidKey = "__vortexUuid__";
static vortexNameKey = "__vortexName__";
filt;
data;
result = null;
date = null;
/**
* Payload Envelope
* This class is serialised and tranferred over the vortex to the server.
* @param filt The filter that the server handler is listening for
* @param data: The encoded payload to go into this envelope
* different location @depreciated
* @param date The date for this envelope, it should match the payload.
*/
constructor(filt = {}, data = null, date = null) {
super();
this.__rst = SerialiseUtil.T_RAPUI_PAYLOAD_ENVELOPE;
this.filt = filt;
this.data = data;
this.date = date == null ? new Date() : this.date;
}
static setWorkerDelegate(delegate) {
PayloadEnvelope.workerDelegate = delegate;
}
get encodedPayload() {
if (!this.data?.length) {
return null;
}
if (typeof this.data !== "string") {
throw new Error("PayloadEnvelope: encodedPayload is not an array");
}
// noinspection UnnecessaryLocalVariableJS
const str = this.data;
return str;
}
set encodedPayload(val) {
if (!val?.length || typeof val !== "string") {
throw new Error("PayloadEnvelope: val is not null or string");
}
this.data = val;
}
// -------------------------------------------
// Envelope method
isEmpty() {
// Ignore the connection start vortexUuid value
// It's sent as the first response when we connect.
for (let property in this.filt) {
if (property === PayloadEnvelope.vortexUuidKey)
continue;
// Anything else, return false
return false;
}
return ((this.encodedPayload == null || this.encodedPayload.length === 0) &&
this.result == null);
}
async decodePayload() {
if (this.encodedPayload == null) {
throw new Error("PayloadEnvelope: decodePayload, data is null");
}
return await Payload.fromEncodedPayload(this.encodedPayload);
}
// -------------------------------------------
// JSON Related method
_fromJson(jsonStr) {
return Promise.resolve(JSON.parse(jsonStr)) //
.then((jsonDict) => {
assert(jsonDict[Jsonable.JSON_CLASS_TYPE] === this.__rst);
return this.fromJsonDict(jsonDict);
});
}
_toJson() {
return Promise.resolve(this.toJsonDict()) //
.then((jsonDict) => JSON.stringify(jsonDict));
}
static fromVortexMsg(vortexStr) {
// Websockets do not require base64 encoding
if (vortexStr[0] === "{") {
// noinspection UnnecessaryLocalVariableJS
const jsonStr = vortexStr;
return new PayloadEnvelope()._fromJson(jsonStr);
}
// noinspection UnnecessaryLocalVariableJS
const result = PayloadEnvelope.workerDelegate
.decodeEnvelope(vortexStr)
.then((jsonStr) => new PayloadEnvelope()._fromJson(jsonStr));
return result;
}
toVortexMsg() {
return this._toJson() //
.then((jsonStr) => PayloadEnvelope.workerDelegate.encodeEnvelope(jsonStr));
}
}
// ----------------------------------------------------------------------------
// Payload class
/**
*
* This class is serialised and transferred over the vortex to the server.
*/
class Payload extends Jsonable {
static workerDelegate = new PayloadDelegateInMainWeb();
filt;
tuples;
date = null;
/**
* Payload
* This class is serialised and tranferred over the vortex to the server.
* @param filt The filter that the server handler is listening for
* @param tuples: The tuples to init the Payload with
* different location @depreciated
* @param date The date for this envelope, it should match the payload.
*/
constructor(filt = {}, tuples = [], date = null) {
super();
this.__rst = SerialiseUtil.T_RAPUI_PAYLOAD;
this.filt = filt;
this.tuples = tuples;
this.date = date == null ? new Date() : this.date;
}
static setWorkerDelegate(delegate) {
Payload.workerDelegate = delegate;
}
// -------------------------------------------
// JSON Related method
_fromJson(jsonStr) {
return Promise.resolve(JSON.parse(jsonStr)).then((jsonDict) => {
assert(jsonDict[Jsonable.JSON_CLASS_TYPE] === this.__rst);
return this.fromJsonDict(jsonDict);
});
}
async _toJson() {
const jsonDict = await this.toJsonDict();
return await JSON.stringify(jsonDict);
}
static async fromEncodedPayload(encodedPayloadStr) {
const jsonStr = await Payload.workerDelegate.decodeAndInflate(encodedPayloadStr);
return await new Payload()._fromJson(jsonStr);
}
async toEncodedPayload() {
const jsonStr = await this._toJson();
return await Payload.workerDelegate.deflateAndEncode(jsonStr);
}
async makePayloadEnvelope() {
const encodedThis = await this.toEncodedPayload();
return new PayloadEnvelope(this.filt, encodedThis, this.date);
}
}
let STOP_PROCESSING = "STOP_PROCESSING";
class PayloadIO {
_endpoints;
constructor() {
let self = this;
self._endpoints = [];
}
add(endpoint) {
let self = this;
self._endpoints.add(endpoint);
}
remove(endpoint) {
let self = this;
self._endpoints.remove(endpoint);
}
process(payloadEnvelope) {
let self = this;
// Make a copy of the endpoints array, it may change endpoints
// can remove them selves during iteration.
let endpoints = self._endpoints.slice(0);
for (let i = 0; i < endpoints.length; ++i) {
if (endpoints[i].process(payloadEnvelope) === STOP_PROCESSING)
break;
}
}
}
let payloadIO = new PayloadIO();
class PayloadEndpoint {
_filt;
_lastPayloadDate;
_processLatestOnly;
constructor(component, filter, processLatestOnly = false) {
let self = this;
self._filt = filter;
self._lastPayloadDate = null;
self._processLatestOnly = processLatestOnly === true;
assert(self._filt != null, "Payload filter is null");
if (self._filt.key == null) {
let e = new Error(`There is no 'key' in the payload filt \
, There must be one for routing - ${JSON.stringify(self._filt)}`);
console.log(e);
throw e;
}
payloadIO.add(self);
// Add auto tear downs for angular scopes
let subscription = component.onDestroyEvent.subscribe(() => {
this.shutdown();
subscription.unsubscribe();
});
this._observable = new Subject();
}
_observable;
get observable() {
return this._observable;
}
/**
* Process Payload
* Check if the payload is meant for us then process it.
*
* @return null, or if the function is overloaded, you could return STOP_PROCESSING
* from PayloadIO, which will tell it to stop processing further endpoints.
*/
process(payloadEnvelope) {
if (!this.checkFilt(this._filt, payloadEnvelope.filt))
return null;
if (!this.checkDate(payloadEnvelope))
return null;
try {
this._observable.next(payloadEnvelope);
}
catch (e) {
// NOTE: Observables automatically remove observers when the raise exceptions.
console.log(`${dateStr()} ERROR: PayloadEndpoint.process, observable has been removed
${e.toString()}
${JSON.stringify(payloadEnvelope.filt)}`);
}
return null;
}
shutdown() {
let self = this;
payloadIO.remove(self);
if (this._observable["observers"] != null) {
for (let observer of this._observable["observers"]) {
observer["unsubscribe"]();
}
}
}
checkFilt(leftFilt, rightFilt) {
for (let key of dictKeysFromObject(leftFilt, true)) {
if (!rightFilt.hasOwnProperty(key))
return false;
let left = leftFilt[key];
let right = rightFilt[key];
// Handle the case of null !== undefined
if (left == null && right == null)
return true;
if (typeof left !== typeof right)
return false;
// Handle special case for Arrays using our equals method in ArrayUtil
if (left instanceof Array) {
if (left.sort().equals(right.sort()))
continue;
else
return false;
}
// Handle special case for Arrays using our equals method in ArrayUtil
if (left instanceof Object) {
if (this.checkFilt(left, right))
continue;
else
return false;
}
if (left !== right)
return false;
}
return true;
}
checkDate(payload) {
if (this._processLatestOnly) {
if (this._lastPayloadDate == null ||
this._lastPayloadDate < payload.date)
this._lastPayloadDate = payload.date;
else
return false;
}
return true;
}
}
/**
* The file defines some commonly used filter keys
*/
let rapuiServerEcho = "rapuiServerEcho";
let rapuiClientEcho = "rapuiClientEcho";
let rapuiVortexUuid = "rapuiVortexUuid";
let plIdKey = "id";
let plDeleteKey = "delete";
/**
* Server response timeout in milliseconds
* @type {number}
*/
let SERVER_RESPONSE_TIMEOUT_SECONDS = 20.0;
var VortexClientStateE;
(function (VortexClientStateE) {
VortexClientStateE[VortexClientStateE["Idle"] = 0] = "Idle";
VortexClientStateE[VortexClientStateE["Connecting"] = 1] = "Connecting";
VortexClientStateE[VortexClientStateE["Online"] = 2] = "Online";
VortexClientStateE[VortexClientStateE["Closing"] = 3] = "Closing";
VortexClientStateE[VortexClientStateE["Closed"] = 4] = "Closed";
VortexClientStateE[VortexClientStateE["Shutdown"] = 5] = "Shutdown";
})(VortexClientStateE || (VortexClientStateE = {}));
class VortexClientABC {
vortexStatusService;
HEART_BEAT_PERIOD_SECONDS = 10.0;
HEART_BEAT_TIMEOUT_SECONDS = 180.0;
RECONNECT_BACKOFF_SECONDS = 10.0;
beatTimer = null;
_uuid;
_name;
_url;
_vortexState = VortexClientStateE.Idle;
serverVortexUuid = null;
serverVortexName = null;
processingNetworkStateChange = false;
/**
* RapUI VortexService, This class is responsible for sending and receiving payloads to/from
* the server.
*/
constructor(vortexStatusService, url, vortexClientName) {
this.vortexStatusService = vortexStatusService;
this._uuid = VortexClientABC.makeUuid();
this._name = vortexClientName;
this._url = url;
// If the user switches network types, then reset the abort timer
Network.addListener("networkStatusChange", (status) => {
// We only want to do something if we DISconnect
if (status.connected) {
return;
}
// If we're already doing something, then do nothing
if (this.processingNetworkStateChange) {
return;
}
this.processingNetworkStateChange = true;
this.shutdown()
.catch((e) => console.log(`ERROR: VortexClientABC - Network State Change failed - ${e}`))
.then(() => (this.processingNetworkStateChange = false));
});
}
static makeUuid() {
function func(c) {
let r = (Math.random() * 16) | 0, v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
}
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, func);
}
get url() {
return this._url;
}
get uuid() {
return this._uuid;
}
get name() {
return this._name;
}
get isClosed() {
return this._vortexState === VortexClientStateE.Closed;
}
get isClosing() {
return this._vortexState === VortexClientStateE.Closing;
}
get isOnline() {
return this._vortexState === VortexClientStateE.Online;
}
get isShutdown() {
return this._vortexState === VortexClientStateE.Shutdown;
}
async close() {
this._vortexState = VortexClientStateE.Closing;
this.vortexStatusService.logInfo(`VortexClientABC shutting down vortex`);
this.clearBeatTimer();
await this.shutdown();
this._vortexState = VortexClientStateE.Shutdown;
}
setOnline() {
this._vortexState = VortexClientStateE.Online;
this.vortexStatusService.setOnline(true);
this.restartTimer();
}
setClosing() {
this._vortexState = VortexClientStateE.Closing;
this.vortexStatusService.setOnline(false);
}
setClosed() {
this._vortexState = VortexClientStateE.Closed;
this.vortexStatusService.setOnline(false);
if (!this.isShutdown)
this.restartTimer();
}
reconnect() {
this._vortexState = VortexClientStateE.Idle;
this.send(new PayloadEnvelope());
this.restartTimer();
return Promise.resolve();
}
beat() {
// We may still get a beat before the connection closes
if (!this.isOnline)
return;
this.restartTimer();
}
restartTimer() {
this.clearBeatTimer();
// If we're online, then use the heartbeat timeout
// If we're not online, then use the reconnect backoff
const timerSeconds = this.isOnline
? this.HEART_BEAT_TIMEOUT_SECONDS
: this.RECONNECT_BACKOFF_SECONDS;
this.beatTimer = setTimeout(() => {
if (this.isShutdown)
return;
this.dead();
this.reconnect()
.then(() => {
this.restartTimer();
})
.catch((e) => {
this.vortexStatusService.logError(`restartTimer ${e}`);
});
}, timerSeconds * 1000);
}
clearBeatTimer() {
if (this.beatTimer != null) {
clearTimeout(this.beatTimer);
this.beatTimer = null;
}
}
dead() {
this.vortexStatusService.setOnline(false);
this.vortexStatusService.logInfo(`VortexService server heartbeats have timed out : ${this._url}`);
}
send(payloadEnvelope) {
if (this.isShutdown) {
let msg = dateStr() +
"VortexService is closed, Probably due to a login page reload";
console.log(msg);
throw new Error("An attempt was made to reconnect a closed vortex");
}
let payloadEnvelopes = [];
if (payloadEnvelope instanceof Array)
payloadEnvelopes = payloadEnvelope;
else
payloadEnvelopes = [payloadEnvelope];
for (let p of payloadEnvelopes) {
// Empty payloadEnvelopes are like heart beats, don't check them
if (!p.isEmpty() && p.filt["key"] == null) {
throw new Error("There is no 'key' in the payloadEnvelopes filt" +
", There must be one for routing");
}
}
let vortexMsgs = [];
let promises = [];
for (let payloadEnvelope of payloadEnvelopes) {
promises.push(payloadEnvelope
.toVortexMsg()
.then((vortexMsg) => vortexMsgs.push(vortexMsg)));
}
return Promise.all(promises)
.then(() => this.sendVortexMsg(vortexMsgs))
.catch((e) => {
let msg = `ERROR VortexClientABC: ${e.toString()}`;
console.log(msg);
throw new Error(msg);
});
}
/**
* Receive
* This should only be called only from VortexConnection
* @param payloadEnvelope {Payload}
*/
receive(payloadEnvelope) {
this.beat();
if (payloadEnvelope.filt.hasOwnProperty(rapuiClientEcho)) {
delete payloadEnvelope[rapuiClientEcho];
this.send(payloadEnvelope);
}
if (payloadEnvelope.isEmpty()) {
if (payloadEnvelope.filt[PayloadEnvelope.vortexUuidKey] != null)
this.serverVortexUuid =
payloadEnvelope.filt[PayloadEnvelope.vortexUuidKey];
if (payloadEnvelope.filt[PayloadEnvelope.vortexNameKey] != null)
this.serverVortexName =
payloadEnvelope.filt[PayloadEnvelope.vortexNameKey];
return;
}
// console.log(dateStr() + "Received payloadEnvelope with filt : " + JSON.stringify(payloadEnvelope.filt));
// TODO, Tell the payloadIO the vortexUuid
payloadIO.process(payloadEnvelope);
}
}
// ------------------
// Some private structures
var TupleLoaderEventEnum;
(function (TupleLoaderEventEnum) {
TupleLoaderEventEnum[TupleLoaderEventEnum["Load"] = 0] = "Load";
TupleLoaderEventEnum[TupleLoaderEventEnum["Save"] = 1] = "Save";
TupleLoaderEventEnum[TupleLoaderEventEnum["Delete"] = 2] = "Delete";
})(TupleLoaderEventEnum || (TupleLoaderEventEnum = {}));
/**
* TupleLoader for Angular2 + Synerty Vortex
*
* @param: vortex The vortex instance to send via.
*
* @param: component The component to register our events on.
*
* @param: filterUpdateCallable A IFilterUpdateCallable callable that returns null
* or an IPayloadFilter
*
* Manual changes can be triggerd as follows.
* * "load()"
* * "save()"
* * "del()"
*/
class TupleLoader {
vortex;
vortexStatusService;
component;
event = new EventEmitter();
filterUpdateCallable;
lastPayloadFilt = null;
lastTuples = null;
timer = null;
lastPromise = null;
endpoint = null;
constructor(vortex, vortexStatusService, component, filterUpdateCallable) {
this.vortex = vortex;
this.vortexStatusService = vortexStatusService;
this.component = component;
if (filterUpdateCallable instanceof Function) {
this.filterUpdateCallable = filterUpdateCallable;
}
else {
this.filterUpdateCallable = () => {
return filterUpdateCallable;
};
}
// Regiseter for the angular docheck
this.component.doCheckEvent
.pipe(takeUntil(this.component.onDestroyEvent))
.subscribe(() => this.filterChangeCheck());
// Create the observable object
this._observable = new Subject();
// Remove all observers when the component is destroyed.
this.component.onDestroyEvent
.pipe(first())
.subscribe(() => this._observable.complete());
}
_observable;
/**
* @property: The tuple observable to subscribe to.
*/
get observable() {
return this._observable;
}
filterChangeCheck() {
if (!this.vortexStatusService.snapshot.isOnline) {
return;
}
// Create a copy
let newFilter = Object.assign({}, this.filterUpdateCallable());
if (newFilter == null) {
if (this.endpoint != null) {
this.endpoint.shutdown();
this.endpoint = null;
}
this.lastTuples = null;
this.lastPayloadFilt = null;
return;
}
if (this.lastPayloadFilt != null &&
deepEqual(newFilter, this.lastPayloadFilt, { strict: true })) {
return;
}
this.lastPayloadFilt = newFilter;
this.endpoint = new PayloadEndpoint(this.component, this.lastPayloadFilt, true);
this.endpoint.observable.subscribe((payloadEnvelope) => {
this.processPayloadEnvelope(payloadEnvelope);
});
this.vortex.send(new PayloadEnvelope(this.lastPayloadFilt));
}
/**
* Load Loads the data from a server
*
* @returns: Promise<Payload>, which is called when the load succeeds or fails.
*
*/
load() {
return this.saveOrLoad(TupleLoaderEventEnum.Load);
}
/**
* Save
*
* Collects the data from the form, into the tuple and sends it through the
* vortex.
*
* @param: tuples The tuples to save, if tuples is null, the last load