UNPKG

@synerty/vortexjs

Version:

Custom observable data serialisation and routing based on Angular 2+

1,437 lines (1,421 loc) 237 kB
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 { NgZone, Injectable, Inject, EventEmitter, Component, forwardRef } from '@angular/core'; import { Subject, BehaviorSubject, firstValueFrom as firstValueFrom$1, debounceTime } from 'rxjs'; import { Network } from '@capacitor/network'; import { takeUntil, first, filter, distinctUntilChanged, skip } from 'rxjs/operators'; import { Http } from '@capacitor-community/http'; import { HttpHeaders } from '@angular/common/http'; 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"; // Node compatibility const logDebug = console.debug ? bind(console, console.debug) : bind(console, console.log); const logInfo = bind(console, console.log); const logError = console.error ? bind(console, console.error) : bind(console, console.log); const logWarning = console.warn ? bind(console, console.warn) : bind(console, console.log); /** * Enum representing the different states of the Vortex application. * Disabled: The vortex if offline and disconnected * Reconnecting: The vortex is not online, but is in the reconnecting state * machine. * Online: The websocket is online and connected * NoNetwork: The network is offline * NetworkOnlineNoWebsocketResource: * The HTTP service is online, but not serving * the websocket upgrades. * This usually means the service is logged out. */ var VortexStateEnum; (function (VortexStateEnum) { VortexStateEnum[VortexStateEnum["Disabled"] = 1] = "Disabled"; VortexStateEnum[VortexStateEnum["Reconnecting"] = 2] = "Reconnecting"; VortexStateEnum[VortexStateEnum["Online"] = 3] = "Online"; VortexStateEnum[VortexStateEnum["NoNetwork"] = 4] = "NoNetwork"; VortexStateEnum[VortexStateEnum["NetworkOnlineNoWebsocketResource"] = 5] = "NetworkOnlineNoWebsocketResource"; })(VortexStateEnum || (VortexStateEnum = {})); class VortexStatusService { zone; _websocketState$ = new BehaviorSubject(VortexStateEnum.NoNetwork); _isOnline$ = new BehaviorSubject(false); debug = new Subject(); info = new Subject(); warning = new Subject(); errors = new Subject(); connectionInfo = new Subject(); connectionError = new Subject(); constructor(zone) { this.zone = zone; } get snapshot() { return { isOnline: this.websocketState == VortexStateEnum.Online, queuedActionCount: this.lastQueuedTupleActions, }; } get websocketState() { return this._websocketState$.getValue(); } get websocketStateObservable() { return this._websocketState$.asObservable(); } get isOnline() { return this._isOnline$.asObservable(); } setVortexState(state) { logDebug(dateStr() + "VortexStatusService.setWebsocketState " + ` old state = ${this.websocketState}` + ` new state = ${state}`); // Don't let reconnecting clobber NoNetwork if (this.websocketState === VortexStateEnum.NoNetwork && state === VortexStateEnum.Reconnecting) { logDebug(dateStr() + "VortexStatusService.setWebsocketState skipping change from" + " NoNetwork to Reconnecting"); } else { this._websocketState$.next(state); } const isOnline = state === VortexStateEnum.Online; if (this._isOnline$.getValue() !== isOnline) { this._isOnline$.next(isOnline); } } queuedActionCount = new Subject(); lastQueuedTupleActions = 0; incrementQueuedActionCount() { this.setQueuedActionCount(this.lastQueuedTupleActions + 1); } decrementQueuedActionCount() { this.setQueuedActionCount(this.lastQueuedTupleActions - 1); } setQueuedActionCount(count) { if (count === this.lastQueuedTupleActions) return; this.lastQueuedTupleActions = count; this.queuedActionCount.next(count); } logDebug(message) { logDebug(dateStr() + "Vortex Status - debug: " + message); this.debug.next(message); } logInfo(message) { logInfo(dateStr() + "Vortex Status - info: " + message); this.info.next(message); } logWarning(message) { logWarning(dateStr() + "Vortex Status - warning: " + message); this.warning.next(message); } logError(message) { logError(dateStr() + "Vortex Status - error: " + message); this.errors.next(message); } logConnectionInfo(message) { logInfo(dateStr() + "Vortex Connection Status - info: " + message); this.connectionInfo.next(message); } logConnectionError(message) { logError(dateStr() + "Vortex Connection Status - error: " + message); this.connectionError.next(message); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VortexStatusService, deps: [{ token: NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VortexStatusService, providedIn: "root" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VortexStatusService, decorators: [{ type: Injectable, args: [{ providedIn: "root", }] }], ctorParameters: function () { return [{ type: i0.NgZone, decorators: [{ type: Inject, args: [NgZone] }] }]; } }); /** * 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 = {})); class VortexClientABC { vortexStatusService; headers; 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; _isShutdown = false; 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, headers) { this.vortexStatusService = vortexStatusService; this.headers = headers; 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 isConnecting() { return this._vortexState === VortexClientStateE.Connecting; } 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._isShutdown; } async close() { this._vortexState = VortexClientStateE.Closing; this.vortexStatusService.logInfo(`VortexClientABC shutting down vortex`); this.clearBeatTimer(); await this.shutdown(); } setConnecting() { this.vortexStatusService.setVortexState(VortexStateEnum.Reconnecting); this._vortexState = VortexClientStateE.Connecting; this.restartTimer(); } setOnline() { this.vortexStatusService.logConnectionInfo("Vortex Online"); this._vortexState = VortexClientStateE.Online; this.vortexStatusService.setVortexState(VortexStateEnum.Online); this.restartTimer(); } setClosing() { this._vortexState = VortexClientStateE.Closing; this.vortexStatusService.setVortexState(VortexStateEnum.Reconnecting); } setClosed() { this._vortexState = VortexClientStateE.Closed; this.vortexStatusService.setVortexState(this.isShutdown ? VortexStateEnum.Disabled : VortexStateEnum.Reconnecting); if (!this.isShutdown) this.restartTimer(); } setShutdown() { this._isShutdown = true; } async reconnect() { this._vortexState = VortexClientStateE.Idle; await this.send(new PayloadEnvelope()); this.restartTimer(); } 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.setVortexState(VortexStateEnum.Reconnecting); this.vortexStatusService.logInfo(`VortexService server heartbeats have timed out : ${this._url}`); } async send(payloadEnvelope) { if (this.isShutdown) { let msg = dateStr() + "VortexService is closed, Probably due to a login page reload"; console.log(msg);