flairjs
Version:
True Object Oriented JavaScript
1,147 lines (1,052 loc) • 369 kB
JavaScript
/**
* @preserve
* Flair.js
* True Object Oriented JavaScript
*
* Assembly: flair
* File: ./flair.js
* Version: 0.59.75
* Wed, 25 Sep 2019 04:56:26 GMT
*
* (c) 2017-2019 Vikas Burman
* MIT
*/
(function(root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) { // AMD support
define(() => { return factory(); });
} else if (typeof exports === 'object') { // CommonJS and Node.js module support
let fo = factory();
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = fo; // Node.js specific `module.exports`
}
module.exports = exports = fo; // CommonJS
} else { // expose as global on window
root.flair = factory();
}
})((this || globalThis), function() {
'use strict';
/* eslint-disable no-unused-vars */
// locals
let isServer = new Function("try {return this===global;}catch(e){return false;}")(),
isWorker = false,
sym = [],
symKey = 'FLAIR_SYMBOLS',
symString = '',
meta = Symbol('[meta]'),
modulesRootFolder = 'modules',
disposers = [],
options = {},
flairTypes = ['class', 'enum', 'interface', 'mixin', 'struct'],
flairInstances = ['instance', 'sinstance'],
settings = {},
config = {},
envX = null,
envProps = {},
isAppStarted = false;
/* eslint-enable no-unused-vars */
// worker setting
if (isServer) {
try {
let worker_threads = require('worker_threads');
isWorker = worker_threads.isMainThread;
} catch (err) { // eslint-disable-line no-unused-vars
isWorker = false;
}
} else { // client
isWorker = (typeof WorkerGlobalScope !== 'undefined' ? true : false);
}
// flairapp bootstrapper
let flair = async (arg1, arg2) => {
let ADO = null,
options = null;
if (typeof arg1 === 'string') { // just the entry point is specified
options = { main: arg1 };
} else if (arg1.main && arg1.module && arg1.engine) { // this is start options object
options = arg1;
} else {
ADO = arg1;
}
if (options) {
if (typeof arg2 === 'string') { options.config = arg2; } // config is also given
if (!isAppStarted) {
// boot
isAppStarted = await flair.AppDomain.boot(options);
}
// return
return flair.AppDomain.app();
} else if (ADO) {
flair.AppDomain.registerAdo(ADO);
}
};
// read symbols from environment
// symbols can be pass in variety of formats:
// server: command line args (process.argv), environment variables (process.env.FLAIR_SYMBOLS)
// worker-server: get whatever symbols collection server main thread had - passed as workerData.symbols
// client: global variable (window.FLAIR_SYMBOLS)
// worker-client: get whatever symbols collection client main thread had - set in WorkerGlobalScope
if (isServer) {
if (isWorker) {
// from workerData.symbols
let workerData = require('worker_threads').workerData;
symString = workerData.symbols || '';
} else {
// from process.argv
let idx = process.argv.findIndex((item) => { return (item.startsWith(`--${symKey}`) ? true : false); });
if (idx !== -1) { symString = process.argv[idx].substr(2).split('=')[1]; }
// from process.env
if (process.env[symKey]) { // add to list
if (symString) { symString += ','; }
symString += process.env[symKey];
}
}
} else { // client
if (isWorker) {
symString = WorkerGlobalScope[symKey] || '';
} else {
// from window
symString += window[symKey] || '';
}
}
if (symString) { sym = symString.split(',').map(item => item.trim()); }
//
options.symbols = Object.freeze(sym);
options.env = Object.freeze({
type: (isServer ? 'server' : 'client'),
isServer: isServer,
isClient: !isServer,
isWorker : isWorker,
isMain: !isWorker,
cores: ((isServer ? (require('os').cpus().length) : window.navigator.hardwareConcurrency) || 4),
isCordova: (!isServer && !!window.cordova),
isNodeWebkit: (isServer && process.versions['node-webkit']),
isProd: ((sym.indexOf('PROD') !== -1 || sym.indexOf('STAGE') !== -1) && sym.indexOf('DEV') === -1),
isStage: (sym.indexOf('STAGE') !== -1 && sym.indexOf('DEV') === -1),
isDev: (sym.indexOf('DEV') !== -1),
isLocal: ((isServer ? require('os').hostname() : self.location.host).indexOf('local') !== -1),
isDebug: (sym.indexOf('DEBUG') !== -1),
isTest: (sym.indexOf('TEST') !== -1),
isAppMode: () => { return isAppStarted; },
x: (once) => {
if (!envX && once) { envX = Object.freeze(once); } // set once - extra env properties are added here during runtime, generally via reading from a config file - once
return envX || {};
},
props: (ns, key, value) => {
if (typeof value === 'undefined') {
if (typeof key === 'undefined') {
return envProps[ns] || {};
} else {
return (envProps[ns] ? envProps[ns][key] : null);
}
} else {
envProps[ns] = envProps[ns] || {};
if (value === null) {
delete envProps[ns][key];
} else {
envProps[ns][key] = value;
}
}
}
});
// Prod / Stage vs Dev are mutually exclusive environments
// Prod is set to true when either PROD or STAGE or both are present and DEV is not present
// Stage is true only when STAGE is present and DEV is not present
// Dev is true only when DEV is present even if PROD / STAGE is also present
// Local, Debug and Test can be true in any of these environments
// flair
flair.members = [];
flair.options = Object.freeze(options);
flair.env = flair.options.env; // direct env access as well
const a2f = (name, obj, disposer) => {
flair[name] = Object.freeze(obj);
flair.members.push(name);
if (typeof disposer === 'function') { disposers.push(disposer); }
};
// members
/**
* @name noop
* @description No Operation function
* @example
* noop()
* @params
* @returns
*/
const _noop = () => {};
// attach to flair
a2f('noop', _noop);
/**
* @name nip
* @description Not Implemented Property
* @example
* nip()
* @params
* @returns
*/
const _nip = {
get: () => { throw _Exception.NotImplemented('prop', _nip.get); },
set: () => { throw _Exception.NotImplemented('prop', _nip.set); }
};
_nip.ni = true; // a special flag to quick check that this is a not-implemented object
// attach to flair
a2f('nip', _nip);
/**
* @name nim
* @description Not Implemented Method
* @example
* nim()
* @params
* @returns
*/
const _nim = () => { throw _Exception.NotImplemented('func', _nim); };
_nim.ni = true; // a special flag to quick check that this is a not-implemented object
// attach to flair
a2f('nim', _nim);
/**
* @name Exception
* @description Lightweight Exception class that extends Error object and serves as base of all exceptions
* @example
* Exception()
* Exception(type)
* Exception(type, stStart)
* Exception(error)
* Exception(error, stStart)
* Exception(type, message)
* Exception(type, message, stStart)
* Exception(type, error)
* Exception(type, error, stStart)
* Exception(type, message, error)
* Exception(type, message, error, stStart)
* @params
* type: string - error name or type
* message: string - error message
* error: object - inner error or exception object
* stStart: function - hide stack trace before this function
* @constructs Exception object
*/
const _Exception = function(arg1, arg2, arg3, arg4) {
let _this = new Error(),
stStart = _Exception;
switch(typeof arg1) {
case 'string':
_this.name = arg1;
switch(typeof arg2) {
case 'string':
_this.message = arg2;
switch(typeof arg3) {
case 'object':
_this.error = arg3;
if (typeof arg4 === 'function') { stStart = arg4; }
break;
case 'function':
stStart = arg3;
break;
}
break;
case 'object':
_this.message = arg2.message || '';
_this.error = arg2;
if (typeof arg3 === 'function') { stStart = arg3; }
break;
case 'function':
stStart = arg2;
break;
}
break;
case 'object':
_this.name = arg1.name || 'Unknown';
_this.message = arg1.message || '';
_this.error = arg1;
if (typeof arg2 === 'function') { stStart = arg2; }
break;
}
_this.name = _this.name || 'Undefined';
if (!_this.name.endsWith('Exception')) { _this.name += 'Exception'; }
// limit stacktrace
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(_this, stStart);
}
// add hint of error
if (_this.error) {
_this.message += '[' + _this.error + ']';
}
// return
return Object.freeze(_this);
};
// all inbuilt exceptions
_Exception.OperationFailed = (name, error, stStart = _Exception.OperationFailed) => { return new _Exception('OperationFailed', `Operation failed with error. (${name})`, error, stStart); }
_Exception.OperationConflict = (name, stStart = _Exception.OperationConflict) => { return new _Exception('OperationConflict', `Operation failed with conflict. (${name})`, stStart); }
_Exception.Unauthorized = (name, stStart = _Exception.Unauthorized) => { return new _Exception('Unauthorized', `Access is not authorized. (${name})`, stStart); }
_Exception.Circular = (name, stStart = _Exception.Circular) => { return new _Exception('Circular', `Circular calls found. (${name})`, stStart); }
_Exception.Continue = (name, stStart = _Exception.Continue) => { return new _Exception('Continue', `Continue requested. (${name})`, stStart); }
_Exception.Redirect = (name, stStart = _Exception.Continue) => { return new _Exception('Redirect', `Redirect requested. (${name})`, stStart); }
_Exception.Duplicate = (name, stStart = _Exception.Duplicate) => { return new _Exception('Duplicate', `Item already exists. (${name})`, stStart); }
_Exception.NotFound = (name, stStart = _Exception.NotFound) => { return new _Exception('NotFound', `Item not found. (${name})`, stStart); }
_Exception.InvalidArgument = (name, stStart = _Exception.InvalidArgument) => { return new _Exception('InvalidArgument', `Argument type is invalid. (${name})`, stStart); }
_Exception.InvalidDefinition = (name, stStart = _Exception.InvalidDefinition) => { return new _Exception('InvalidDefinition', `Item definition is invalid. (${name})`, stStart); }
_Exception.InvalidOperation = (name, stStart = _Exception.InvalidOperation) => { return new _Exception('InvalidOperation', `Operation is invalid in current context. (${name})`, stStart); }
_Exception.NotImplemented = (name, stStart = _Exception.NotImplemented) => { return new _Exception('NotImplemented', `Member is not implemented. (${name})`, stStart); }
_Exception.NotDefined = (name, stStart = _Exception.NotDefined) => { return new _Exception('NotDefined', `Member is not defined or is not accessible. (${name})`, stStart); }
_Exception.NotAvailable = (name, stStart = _Exception.NotAvailable) => { return new _Exception('NotAvailable', `Feature is not available. (${name})`, stStart); }
_Exception.NotSupported = (name, stStart = _Exception.NotSupported) => { return new _Exception('NotSupported', `Operation is not supported. (${name})`, stStart); }
_Exception.NotAllowed = (name, stStart = _Exception.NotAllowed) => { return new _Exception('NotAllowed', `Operation is not allowed. (${name})`, stStart); }
_Exception.NoContent = (name, stStart = _Exception.NoContent) => { return new _Exception('NoContent', `No content to return. (${name})`, stStart); }
_Exception.NotModified = (name, stStart = _Exception.NotModified) => { return new _Exception('NotModified', `Content is not changed. (${name})`, stStart); }
// attach to flair
a2f('Exception', _Exception);
const guid = () => {
return '_xxxxxxxx_xxxx_4xxx_yxxx_xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
const which = (def) => {
// full blown def can be:
// envProp::mainThreadOnServer{.min}.xyz ~ envProp::workerThreadOnServer{.min}.xyz | envProp::mainThreadOnClient{.min}.xyz ~ envProp::workerThreadOnClient{.min}.xyz
let item = def,
items = null,
envProp = null;
if (item.indexOf('|') !== -1) { // server | client
items = item.split('|');
if (options.env.isServer) { // left is server
item = items[0].trim();
} else { // right is client
item = items[1].trim();
}
if (item === 'x') { item = ''; } // special case to explicitly mark absence of a type
}
// worker environment specific pick
if (item.indexOf('~') !== -1) { // main thread ~ worker thread
items = item.split('~');
if (!options.env.isWorker) { // left is main thread
item = items[0].trim();
} else { // right is worker thread
item = items[1].trim();
}
if (item === 'x') { item = ''; } // special case to explicitly mark absence of a type
}
// environment specific condition
if (item.indexOf('::') !== -1) { // isVue::./flair.ui.vue{.min}.js
items = item.split('::'),
envProp = items[0].trim();
item = items[1].trim();
if (!(options.env[envProp] || options.env.x()[envProp])) { // if envProp is NOT defined neither at root env nor at extended env, OR defined but is false / falsy
item = ''; // special case to dynamically mark absence of a type
}
}
// debug/prod specific pick
if (item.indexOf('{.min}') !== -1) {
if (options.env.isDebug) {
item = item.replace('{.min}', ''); // a{.min}.js => a.js
} else {
item = item.replace('{.min}', '.min'); // a{.min}.js => a.min.js
}
}
return item; // modified or as is or empty
};
const isArrow = (fn) => {
return (!(fn).hasOwnProperty('prototype') && fn.constructor.name === 'Function');
};
const isASync = (fn) => {
return (fn.constructor.name === 'AsyncFunction');
};
const findIndexByProp = (arr, propName, propValue) => {
return arr.findIndex((item) => {
return (item[propName] === propValue ? true : false);
});
};
const findItemByProp = (arr, propName, propValue) => {
let idx = arr.findIndex((item) => {
return (item[propName] === propValue ? true : false);
});
if (idx !== -1) { return arr[idx]; }
return null;
};
const splitAndTrim = (str, splitChar) => {
if (!splitChar) { splitChar = ','; }
return str.split(splitChar).map((item) => { return item.trim(); });
};
const escapeRegExp = (string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // eslint-disable-line no-useless-escape
};
const replaceAll = (string, find, replace) => {
return string.replace(new RegExp(escapeRegExp(find), 'g'), replace);
};
const stuff = (str, args) => {
if (typeof str === 'string' && Array.isArray(args) && args.length > 0) {
let idx = 0;
for(let arg of args) {
str = replaceAll(str, `%${++idx}`, arg);
}
}
return str;
};
const shallowCopy = (target, source, overwrite, except) => {
if (!except) { except = []; }
for(let item in source) {
if (source.hasOwnProperty(item) && except.indexOf(item) === -1) {
if (!overwrite) { if (item in target) { continue; }}
target[item] = source[item];
}
}
return target;
};
const loadFile = async (file) => { // text based file loading operation - not a general purpose fetch of any url (it assumes it is a phycical file)
let loader = null;
if (isServer) {
loader = _Port('serverFile');
} else { // client
loader = _Port('clientFile');
}
return await loader(file);
};
const loadModule = async (module, globalObjName, isDelete) => {
if (isServer) {
return await _Port('serverModule').require(module);
} else { // client
let obj = await _Port('clientModule').require(module);
if (!obj && typeof globalObjName === 'string') {
if (isWorker) {
obj = WorkerGlobalScope[globalObjName] || null;
if (isDelete) { delete WorkerGlobalScope[globalObjName]; }
} else {
obj = window[globalObjName] || null;
if (isDelete) { delete window[globalObjName]; }
}
}
if (obj) { return obj; }
}
};
const lens = (obj, path) => path.split(".").reduce((o, key) => o && o[key] ? o[key] : null, obj);
const globalSetting = (path, defaultValue, globalRoot = 'global') => {
// any global setting (i.e., outside of a specific assembly setting) can be defined at:
// "global" root node (or anything else that is given) in appConfig/webConfig file
// Each setting can be at any depth inside "global" (or anything else given) and its generally a good idea to namespace intelligently to
// avoid picking someone else' setting
let _globalSettings = _AppDomain.config() ? (_AppDomain.config()[globalRoot] || {}) : {};
return lens(_globalSettings, path) || defaultValue;
};
const sieve = (obj, props, isFreeze, add) => {
let _props = props ? splitAndTrim(props) : Object.keys(obj); // if props are not give, pick all
const extract = (_obj) => {
let result = {};
if (_props.length > 0) { // copy defined
for(let prop of _props) { result[prop] = _obj[prop]; }
} else { // copy all
for(let prop in obj) {
if (obj.hasOwnProperty(prop)) { result[prop] = obj[prop]; }
}
}
if (add) { for(let prop in add) { result[prop] = add[prop]; } }
if (isFreeze) { result = Object.freeze(result); }
return result;
};
if (Array.isArray(obj)) {
let result = [];
for(let item of obj) { result.push(extract(item)); }
return result;
} else {
return extract(obj);
}
};
const b64EncodeUnicode = (str) => { // eslint-disable-line no-unused-vars
// first we use encodeURIComponent to get percent-encoded UTF-8,
// then we convert the percent encodings into raw bytes which
// can be fed into btoa.
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode('0x' + p1);
}));
};
const b64DecodeUnicode = (str) => {
// Going backwards: from bytestream, to percent-encoding, to original string.
return decodeURIComponent(atob(str).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
};
const uncacheModule = (module) => {
if (isServer) {
_Port('serverModule').undef(module);
} else {
_Port('clientModule').undef(module);
}
};
const deepMerge = (objects, isMergeArray = true) => { // credit: https://stackoverflow.com/a/48218209
const isObject = obj => obj && typeof obj === 'object';
return objects.reduce((prev, obj) => {
Object.keys(obj).forEach(key => {
const pVal = prev[key];
const oVal = obj[key];
if (Array.isArray(pVal) && Array.isArray(oVal)) {
if (isMergeArray) {
prev[key] = pVal.concat(...oVal); // merge array
} else {
prev[key] = [].concat(...oVal); // overwrite as new array
}
} else if (isObject(pVal) && isObject(oVal)) {
prev[key] = deepMerge([pVal, oVal], isMergeArray);
} else {
prev[key] = oVal;
}
});
return prev;
}, {});
};
const getLoadedScript = (...scriptNames) => {
if (isServer || isWorker) { return ''; }
let scriptFile = '',
baseUri = '',
el = null;
for(let scriptName of scriptNames) {
for(let script of window.document.scripts) {
if (script.src.endsWith(scriptName)) {
el = window.document.createElement('a');
el.href = script.src;
baseUri = el.protocol + '//' + el.host + '/';
el = null;
scriptFile = './' + script.src.replace(baseUri, '');
break;
}
}
if (scriptFile) { break; }
}
return scriptFile;
};
/**
* @name typeOf
* @description Finds the type of given object in flair type system
* @example
* typeOf(obj)
* @params
* obj: object - object that needs to be checked
* @returns string - type of the given object
* it can be following:
* > special ones like 'undefined', 'null', 'NaN', infinity
* > special javascript data types like 'array', 'date', etc.
* > inbuilt flair object types like 'class', 'struct', 'enum', etc.
* > native regular javascript data types like 'string', 'number', 'function', 'symbol', etc.
*/
const _typeOf = (obj) => {
let _type = '';
// undefined
if (typeof obj === 'undefined') { _type = 'undefined'; }
// null
if (!_type && obj === null) { _type = 'null'; }
// infinity
if (!_type && typeof obj === 'number' && isFinite(obj) === false) { _type = 'infinity'; }
// array
if (!_type && Array.isArray(obj)) { _type = 'array'; }
// date
if (!_type && (obj instanceof Date)) { _type = 'date'; }
// flair types
if (!_type && obj[meta]) { _type = obj[meta].type; }
// native javascript types
if (!_type) { _type = typeof obj; }
// return
return _type;
};
// attach to flair
a2f('typeOf', _typeOf);
/**
* @name is
* @description Checks if given object is of a given type
* @example
* is(obj, type)
* @params
* obj: object - object that needs to be checked
* type: string OR type - type to be checked for, it can be following:
* > expected native javascript data types like 'string', 'number', 'function', 'array', 'date', etc.
* > 'function' - any function, cfunction' - constructor function and 'afunction - arrow function
* > any 'flair' object or type, 'flairtype' - only flair types and 'flairinstance' - only flair instances
* > inbuilt flair object types like 'class', 'struct', 'enum', etc.
* > custom flair object instance types which are checked in following order:
* >> for class instances:
* isInstanceOf given as type
* isImplements given as interface
* isMixed given as mixin
* >> for struct instances:
* isInstance of given as struct type
* @returns boolean - true/false
*/
const _is = (obj, type) => {
// NOTE: in all 'check' type functions, Args() is not to be used, as Args use them itself
// obj may be undefined or null or false, so don't check for validation of that here
if (type[meta]) { type = type[meta].name || type[meta].Type.getName(); } // since it can be a type as well
if (_typeOf(type) !== 'string') { throw _Exception.InvalidArgument('type', _is); }
let isMatched = false;
if (obj) {
switch(type) {
case 'NaN':
isMatched = isNaN(obj); break;
case 'infinity':
isMatched = (typeof obj === 'number' && isFinite(obj) === false); break;
case 'array':
case 'Array':
isMatched = Array.isArray(obj); break;
case 'date':
case 'Date':
isMatched = (obj instanceof Date); break;
case 'flairtype':
isMatched = (obj[meta] && flairTypes.indexOf(obj[meta].type) !== -1); break;
case 'flairinstance':
isMatched = (obj[meta] && flairInstances.indexOf(obj[meta].type) !== -1); break;
case 'flair':
// presence ot meta symbol means it is flair type/instance
isMatched = typeof obj[meta] !== 'undefined'; break;
case 'cfunction':
isMatched = (typeof obj === 'function' && !isArrow(obj)); break;
case 'afunction':
isMatched = (typeof obj === 'function' && isArrow(obj)); break;
default:
// native javascript types (including simple 'function')
if (!isMatched) { isMatched = (typeof obj === type); }
if (!isMatched && obj[meta]) {
// flair types
if (!isMatched) { isMatched = (type === obj[meta].type); }
// flair instance check (instance)
if (!isMatched && flairInstances.indexOf(obj[meta].type) !== -1) { isMatched = _isInstanceOf(obj, type); }
// flair type check (derived from)
if (!isMatched && obj[meta].type === 'class') { isMatched = _isDerivedFrom(obj, type); }
// flair type check (direct name)
if (!isMatched && flairTypes.indexOf(obj[meta].type) !== -1) { isMatched = (obj[meta].name === type); }
}
}
} else {
switch(type) {
case 'undefined':
isMatched = (typeof obj === 'undefined'); break;
case 'null':
isMatched = (obj === null); break;
case 'NaN':
isMatched = isNaN(obj); break;
}
}
// return
return isMatched;
};
// attach to flair
a2f('is', _is);
/**
* @name is
* @description Checks if given object has specified member defined
* @example
* isDefined(obj, memberName)
* @params
* obj: object - object that needs to be checked
* memberName: string - name of the member to check
* @returns boolean - true/false
*/
const _isDefined = (obj, memberName) => {
// NOTE: in all 'check' type functions, Args() is not to be used, as Args use them itself
let isErrorOccured = false;
try {
obj[memberName]; // try to access it, it will throw error if not defined on an object which is a flair-object
// if error does not occur above, means either member is defined or it was not a flairjs object, in that case check for 'undefined'
isErrorOccured = (typeof obj[memberName] === 'undefined');
} catch (err) {
isErrorOccured = true;
}
// return
return !isErrorOccured;
};
// attach to flair
a2f('isDefined', _isDefined);
/**
* @name Args
* @description Lightweight args pattern processing that returns a validator function to validate arguments against given arg patterns
* @example
* Args(...patterns)
* @params
* patterns: string(s) - multiple pattern strings, each representing one pattern set
* each pattern set can take following forms:
* 'type, type, type, ...' OR 'name: type, name: type, name: type, ...'
* type: can be following:
* > special types: 'undefined' - for absence of a passed value
* > expected native javascript data types like 'string', 'number', 'function', 'array', etc.
* > 'function' - any function, cfunction' - constructor function and 'afunction - arrow function
* > inbuilt flair object types like 'class', 'struct', 'enum', etc.
* > custom flair object instance types which are checked in following order:
* >> for class instances:
* isInstanceOf given as type
* isImplements given as interface
* isMixed given as mixin
* >> for struct instances:
* isInstance of given as struct type
* name: argument name which will be used to store extracted value by parser
* @returns function - validator function that is configured for specified patterns
*/
const _Args = (...patterns) => {
if (patterns.length === 0) { throw _Exception.InvalidArgument('patterns', _Args); }
/**
* @description Args validator function that validates against given patterns
* @example
* (...args)
* @params
* args: any - multiple arguments to match against given pattern sets
* @returns object - result object, having:
* raw: (array) - original arguments as passed
* index: (number) - index of pattern-set that matches for given arguments, -1 if no match found
* if more than one patterns may match, it will stop at first match
* types: (string) - types which matched - e.g., string_string
* this is the '_' joint string of all type definition part from the matching pattern-set
* isInvalid: (boolean) - to check if any match could not be achieved
* <name(s)>: <value(s)> - argument name as given in pattern having corresponding argument value
* if a name was not given in pattern, a default unique name will be created
* special names like 'raw', 'index' and 'isInvalid' cannot be used.
*/
let _args = (...args) => {
// process each pattern - exit with first matching pattern
let types = null, items = null,
name = '', type = '',
pIndex = -1, aIndex = -1, // pattern index, argument index
matched = false,
mCount = 0, // matched arguments count of pattern
faliedMatch = '',
result = {
raw: args || [],
index: -1,
isInvalid: false,
error: null,
values: {}
};
if (patterns) {
for(let pattern of patterns) { // pattern
pIndex++; aIndex=-1; matched = false; mCount = 0;
types = pattern.split(',');
for(let item of types) {
aIndex++;
items = item.split(':');
if (items.length !== 2) {
name = `_${pIndex}_${aIndex}`; // e.g., _0_0 or _1_2, etc.
type = item.trim() || '';
} else {
name = items[0].trim() || '',
type = items[1].trim() || '';
}
if (aIndex > result.raw.length) { matched = false; break; }
if (!_is(result.raw[aIndex], type)) { matched = false; faliedMatch = name; break; }
result.values[name] = result.raw[aIndex]; matched = true; mCount++;
}
if (matched && mCount === types.length) {result.index = pIndex; break; }
}
}
// set state
result.isInvalid = (result.index === -1 ? true : false);
result.error = (result.isInvalid ? _Exception.InvalidArgument(faliedMatch) : null);
// throw helper
result.throwOnError = (stStart) => {
if (result.error) { throw new _Exception(result.error, stStart || _args); }
};
// return
return Object.freeze(result);
};
// return freezed
return Object.freeze(_args);
};
// attach to flair
a2f('Args', _Args);
/**
* @name event
* @description Event marker
* @example
* event()
* @params
* argsProcessor - args processor function, if args to be processed before event is raised
* @returns
* function - returns given function or a noop function as is with an event marked tag
*/
const _event = (argsProcessor) => {
let args = _Args('argsProcessor: undefined',
'argsProcessor: afunction')(argsProcessor); args.throwOnError(_event);
argsProcessor = argsProcessor || ((...eventArgs) => { return eventArgs; });
argsProcessor.event = true; // attach tag
return argsProcessor;
}
// attach to flair
a2f('event', _event);
/**
* @name nie
* @description Not Implemented Event
* @example
* nie()
* @params
* @returns
*/
const _nie = _event(() => { throw _Exception.NotImplemented('event', _nie); });
_nie.ni = true; // a special flag to quick check that this is a not-implemented object
// attach to flair
a2f('nie', _nie);
/**
* @name Dispatcher
* @description Event dispatching.
*/
const Dispatcher = function(eventHost) {
let events = {};
eventHost = eventHost || '';
// add event listener
this.add = (event, handler) => {
let args = _Args('event: string, handler: afunction')(event, handler); args.throwOnError(this.add);
if (!events[event]) { events[event] = []; }
events[event].push(handler);
};
// remove event listener
this.remove = (event, handler) => {
let args = _Args('event: string, handler: afunction')(event, handler); args.throwOnError(this.remove);
if (events[event]) {
let idx = events[event].indexOf(handler);
if (idx !== -1) { events[event].splice(idx, 1); }
}
};
// dispatch event
this.dispatch = (event, eventArgs) => {
let args = _Args('event: string')(event); args.throwOnError(this.dispatch); // note: no check for eventArgs, as it can be anything
if (events[event]) {
events[event].forEach(handler => {
// NOTE: any change here should also be done in SharedChannel where progress event is being routed across threads
setTimeout(() => { handler(Object.freeze({ host: eventHost, name: event, args: eventArgs || [] })); }, 0); // <-- event handler will receive this
});
}
};
// get number of attached listeners
this.count = (event) => {
let args = _Args('event: string')(event); args.throwOnError(this.count);
return (events[event] ? events[event].length : 0);
};
// clear all handlers for all events associated with this dispatcher
this.clear = () => {
events = {};
};
};
/**
* @name InjectedArg
* @description An argument that is injected by a custom attribute OR an advise
* @example
* InjectedArg(value);
* @params
* @returns
*/
const InjectedArg = function(value) {
this.value = value;
};
InjectedArg.filter = (args) => {
// return all plain args, leaving all injected args
let filteredArgs = [];
if (args) {
for(let a of args) {
if (!(a instanceof InjectedArg)) { filteredArgs.push(a); }
}
}
return filteredArgs;
};
InjectedArg.extract = (args) => {
// return all raw injected args, in reverse order
let injectedArgs = [];
if (args) {
for(let a of args) {
if (a instanceof InjectedArg) { injectedArgs.push(a.value); }
}
}
return injectedArgs.reverse();
};
// attach to flair
a2f('InjectedArg', Object.freeze(InjectedArg));
/**
* @name Port
* @description Customize configurable functionality of the core. This gives a way to configure a different component to
* handle some specific functionalities of the core, e.g., fetching a file on server, or loading a module on
* client, or handling sessionStorage, to name a few.
* Ports are defined by a component and handlers of required interface types can be supplied from outside
* as per usage requirements
* @example
* Port(name) // @returns handler/null - if connected returns handler else null
* Port.define(name, members, intf) // @returns void
* Port.connect(name, handler) // @returns void
* Port.disconnect(name) // @returns void
* Port.disconnect.all() // @returns void
* Port.isDefined(name) // @returns boolean - true/false
* Port.isConnected(name) // @returns boolean - true/false
* @params
* name: string - name of the port
* members: array of strings - having member names that are checked for their presence
* handler: function - a factory that return the actual handler to provide named functionality for current environment
* inbuilt: function - an inbuilt factory implementation of the port functionality, if nothing is configured, this implementation will be returned
* NOTE: Both handler and inbuilt are passed flair.options.env object to return most suited implementation of the port
* @returns handler/boolean/void - as specified above
*/
let ports_registry = {};
const _Port = (name) => {
if (typeof name !== 'string') { throw _Exception.InvalidArgument('name', _Port); }
if (ports_registry[name]) {
return (ports_registry[name].handler ? ports_registry[name].handler : ports_registry[name].inbuilt); // inbuilt could also be null if not inbuilt implementation is given
}
return null;
};
_Port.define = (name, members, inbuilt) => {
let args = _Args('name: string, members: array, inbuilt: afunction',
'name: string, inbuilt: afunction',
'name: string, members: array',
'name: string')(name, members, inbuilt); args.throwOnError(_Port.define);
if (ports_registry[name]) { throw _Exception.Duplicate(name, _Port.define); }
ports_registry[name] = {
type: (args.values.members ? 'object' : 'function'), // a port handler can be
members: args.values.members || null,
handler: null,
inbuilt: (args.values.inbuilt ? args.values.inbuilt(options.env) : null)
};
};
_Port.connect = (name, handler) => {
let args = _Args('name: string, handler: afunction')(name, handler); args.throwOnError(_Port.connect);
if (!ports_registry[name]) { throw _Exception.NotFound(name, _Port.connect); }
let actualHandler = handler(options.env); // let it return handler as per context
if (typeof actualHandler !== ports_registry[name].type) { throw _Exception.InvalidArgument('handler', _Port.connect); }
let members = ports_registry[name].members;
if (members) {
for(let member of members) {
if (typeof actualHandler[member] === 'undefined') { throw _Exception.NotImplemented(member, _Port.connect); }
}
}
ports_registry[name].handler = actualHandler;
};
_Port.disconnect = (name) => {
if (typeof name !== 'string') { throw _Exception.InvalidArgument('name', _Port.disconnect); }
if (ports_registry[name]) {
ports_registry[name].handler = null;
}
};
_Port.isDefined = (name) => {
if (typeof name !== 'string') { throw _Exception.InvalidArgument('name', _Port.isDefined); }
return (ports_registry[name] ? true : false);
};
_Port.isConnected = (name) => {
if (typeof name !== 'string') { throw _Exception.InvalidArgument('name', _Port.isConnected); }
return (ports_registry[name] && ports_registry[name].handler ? false : true);
};
// attach to flair
a2f('Port', _Port, () => {
// disconnect all ports
for(let port in ports_registry) {
if (ports_registry.hasOwnProperty(port)) {
ports_registry[port].handler = null;
}
}
// clear registry
ports_registry = {};
});
/**
* @name AssemblyLoadContext
* @description The isolation boundary of type loading across assemblies.
*/
const AssemblyLoadContext = function(name, domain, defaultLoadContext, currentContexts, contexts) {
let alcTypes = {},
alcResources = {},
alcRoutes = {},
instances = {},
asmFiles = {},
asmNames = {},
namespaces = {},
isUnloaded = false,
currentAssembliesBeingLoaded = [],
onLoadCompleteFunctions = {};
// context
this.name = name;
this.domain = domain;
this.isUnloaded = () => { return isUnloaded || domain.isUnloaded(); };
this.unload = () => {
if (!isUnloaded) {
// mark unloaded
isUnloaded = true;
// delete from domain registry
delete contexts[name];
// dispose all active instances
for(let instance in instances) {
if (instance.hasOwnProperty(instance)) {
_dispose(instances[instance]);
}
}
// clear registries
alcTypes = {};
asmFiles = {};
asmNames = {};
alcResources = {};
alcRoutes = {};
instances = {};
namespaces = {};
}
};
this.current = () => {
if (this.isUnloaded()) { throw _Exception.InvalidOperation(`Context is already unloaded. (${this.name})`, this.current); }
if (currentContexts.length === 0) {
return defaultLoadContext || this; // the first content created is the default context, so in first case, it will come as null, hence return this
} else { // return last added context
// when a context load any assembly, it pushes itself to this list, so
// that context become current context and all loading types will attach itself to this
// new context, and as soon as load is completed, it removes itself.
// Now, if for some reason, an assembly load operation itself (via some code in index.js)
// initiate another context load operation, that will push itself on top of this context and
// it will trickle back to this context when that secondary load is done
// so always return from here the last added context in list
return currentContexts[currentContexts.length - 1];
}
};
// types
this.registerType = (Type) => {
if (this.isUnloaded()) { throw _Exception.InvalidOperation(`Context is already unloaded. (${this.name})`, this.registerType); }
// certain types are built as instances, like interface and enum
let name = '',
type = '',
typeMeta = Type[meta];
if (typeMeta.Type) {
name = typeMeta.Type[meta].name;
type = typeMeta.Type[meta].type;
} else {
name = typeMeta.name;
type = typeMeta.type;
}
// only valid types are allowed
if (flairTypes.indexOf(type) === -1) { throw _Exception.InvalidArgument('Type', this.registerType); }
// namespace name is already attached to it, and for all '(root)'
// marked types' no namespace is added, so it will automatically go to root
let ns = name.substr(0, name.lastIndexOf('.')),
onlyName = name.replace(ns + '.', '');
// check if already registered
if (alcTypes[name]) { throw _Exception.Duplicate(name, this.registerType); }
if (alcResources[name]) { throw _Exception.Duplicate(`Already registered as Resource. (${name})`, this.registerType); }
if (alcRoutes[name]) { throw _Exception.Duplicate(`Already registered as Route. (${name})`, this.registerType); }
// register
alcTypes[name] = Type;
// register to namespace as well
if (ns) {
if (!namespaces[ns]) { namespaces[ns] = {}; }
namespaces[ns][onlyName] = Type;
} else { // root
namespaces[onlyName] = Type;
}
// return namespace where it gets registered
return ns;
};
this.getType = (qualifiedName) => {
if (this.isUnloaded()) { throw _Exception.InvalidOperation(`Context is already unloaded. (${this.name})`, this.getType); }
if (typeof qualifiedName !== 'string') { throw _Exception.InvalidArgument('qualifiedName', this.getType); }
return alcTypes[qualifiedName] || null;
};
this.ensureType = async (qualifiedName) => {
if (this.isUnloaded()) { throw _Exception.InvalidOperation(`Context is already unloaded. (${this.name})`); }
if (typeof qualifiedName !== 'string') { throw _Exception.InvalidArgument('qualifiedName'); }
let Type = this.getType(qualifiedName);
if (!Type) {
let asmFile = domain.resolve(qualifiedName);
if (asmFile) {
await this.loadAssembly(asmFile);
Type = this.getType(qualifiedName);
if (!Type) { throw _Exception.OperationFailed(`Assembly could not be loaded. (${asmFile})`); }
} else {
throw _Exception.NotFound(qualifiedName);
}
}
return Type;
};
this.allTypes = () => {
if (this.isUnloaded()) { throw _Exception.InvalidOperation(`Context is already unloaded. (${this.name})`, this.allTypes); }
return Object.keys(alcTypes);
};
this.execute = async (info, progressListener) => {
// NOTE: The logi