568 lines
24 KiB
JavaScript
Executable File
568 lines
24 KiB
JavaScript
Executable File
'use strict'
|
|
/**
|
|
___ ____ __ ____ ___
|
|
/ __)( _ \ /__\ ( _ \ / __)
|
|
___\__ \ )___//(__)\ ) /( (__
|
|
(_______/(__) (__)(__)(_)\_) \___) SPA Rational Code
|
|
By Mike & Nike
|
|
|
|
This file is part of Sparc by Mike & Nike.
|
|
Sparc is free software:
|
|
- you can redistribute it and/or modify it under the terms of the GNU General Public License,
|
|
as published by the Free Software Foundation, either version 3 of the License,
|
|
or (at your option) any later version.
|
|
- Sparc is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
|
|
See the GNU General Public License for more details.
|
|
Get your copy of the GNU General Public License at <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
* @category Core
|
|
* @hideconstructor
|
|
* @tutorial app-01-overview
|
|
*/
|
|
class Sparc {
|
|
/**
|
|
* Genaral app configuration, as soon as it is loaded
|
|
* @tutorial app-02-configuration
|
|
* */
|
|
config = {};
|
|
/** Classes definitions for MVC + Sparc libs follow the rule script-name = instantiable class name */
|
|
LoadedClasses = {};
|
|
/** Thirdparties and other libs don't necessarily have a class, or this naming conv. then store path only. */
|
|
LoadedScripts = [];
|
|
/** @type User */
|
|
User = null
|
|
/** @type Assets */
|
|
Assets = null
|
|
/** @type Router */
|
|
Router = null
|
|
/** @type Events */
|
|
events = null
|
|
/** @type Array<Object> */
|
|
latestErrors = [];
|
|
|
|
|
|
/**
|
|
*
|
|
* @param {*} ref
|
|
* @param {*} cls
|
|
* @param {*} replace
|
|
*/
|
|
registerClass(ref, cls, replace=false) {
|
|
if(replace || (!this.LoadedClasses.hasOwnProperty(ref))){
|
|
this.LoadedClasses[ref] = cls;
|
|
} else {
|
|
console.warn('Attempting to register already defined class ', ref);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
onBootReady() {
|
|
console.log('Sparc boot loaded...');
|
|
|
|
// Instances shortcuts
|
|
this.User = new this.LoadedClasses.User(); // This is User base class (no login)
|
|
|
|
// Static classes shortcuts
|
|
this.Assets = Assets;
|
|
|
|
// General app config are loaded here
|
|
this.Assets.loadJson({ 'name': 'config.json',
|
|
'path': '/app/config/',
|
|
}).then(this.onConfigLoaded.bind(this));
|
|
// Instances shortcuts
|
|
this.events = new this.LoadedClasses.Events(); // This is User base class (no login)
|
|
this.addEvent = this.events.addEvent.bind(this.events);
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
onWindowReady() {
|
|
// MVC not yet loaded here, see remark below about loading base classes.
|
|
Loader.loadScripts({ 'basepath':'/core/libs/',
|
|
'scripts': ['Router', 'Logger', 'Assets','Events'],
|
|
'dependencies':{ 'Router': ['../baseClasses/User'] },
|
|
}).then(this.onBootReady.bind(this));
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {*} config
|
|
*/
|
|
onConfigLoaded(config){
|
|
console.log('App config loaded...');
|
|
if(!config) {
|
|
console.error('Could not load SPARC config file !?');
|
|
return;
|
|
}
|
|
this.config = config;
|
|
if(('logger' in config) && ('enabled' in config.logger) && (config.logger.enabled)
|
|
&& ('levels' in config.logger) && ('postUrl' in config.logger)){
|
|
window.console = this.logger(window.console, config.logger);
|
|
}
|
|
|
|
this.Assets.defaults = config.assets;
|
|
|
|
if( ('userLib' in config) && ('className' in config.userLib) && (config.userLib.className!='')) {
|
|
Loader.loadScripts({ 'basepath':'/app/libs/',
|
|
'scripts': [config.userLib.className],
|
|
'dependencies':{ }, // Dependencies of the User class from config ?
|
|
}).then(()=>{
|
|
this.User = new this.LoadedClasses.User(); // Re-instantiate extnded class
|
|
// Upon creation, we are ourselves, but it might change later. (no OTS yet) Philosophical, isn't it :D
|
|
this.currentUser = this.User;
|
|
this.onUserClassReady();
|
|
});
|
|
} else {
|
|
this.onUserClassReady();
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
onUserClassReady(){
|
|
this.User.checkAuthenticated(()=>{
|
|
console.log('back from checkAuthenticated')
|
|
if(this.User.isAuthenticated) {
|
|
console.log('authenticated OK')
|
|
window.onbeforeunload = () => "Do you really want to leave EISMEA application ?";
|
|
|
|
// Loading base Classes, who can now use app.config.
|
|
// If later we do multi-app then one config per app, and baseclass instantiated with its own config.
|
|
Loader.loadScripts({
|
|
'basepath':'/core/baseClasses/',
|
|
'scripts': [
|
|
'Model',
|
|
'View',
|
|
'MasterController'
|
|
], // Look momy: sounds just like MVC ;-)
|
|
'dependencies': {
|
|
'MasterController': ['Controller']
|
|
}, // Dependencies of the base classes ?
|
|
}).then(this.loadLibraries.bind(this));
|
|
|
|
// Now load & start messageBus (must be authenticated, but can by done aside of loading the MVC core)
|
|
if((this.config.messageBus.enabled) && ("Worker" in window) && ('WebSocket' in window)) {
|
|
Loader.loadScripts({ 'basepath':'/core/libs/',
|
|
'scripts': ['MessageBus'],
|
|
}).then(() => {
|
|
this.MessageBus = new this.LoadedClasses.MessageBus(this.config.messageBus, this.User.getMessageBusUserInfo());
|
|
});
|
|
} else if(this.config.messageBus.enabled){
|
|
console.warn("Could not register MessageBus worker: Browser too old ?");
|
|
}
|
|
} else {
|
|
console.log('User is not Authenticated, redirect to Login page');
|
|
this.User.gotoLogin();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
loadLibraries(){
|
|
// Loading custom libs & intermediary classes
|
|
let allPromises = [];
|
|
let lib, libclass;
|
|
if('libsBaseClasses' in this.config.router){
|
|
for(lib of this.config.router.libsBaseClasses) {
|
|
if(Array.isArray(lib.onlyIfClasses) && !lib.onlyIfClasses.every(cls => (cls in this.LoadedClasses))) continue
|
|
libclass = lib.classes[0]; // Let all assets be dependencies of the first script (if many)
|
|
Loader.AssetsDependencies[libclass] = lib.assets;
|
|
allPromises.push( Loader.loadScripts({
|
|
'basepath':'/app/libs'+lib.path,
|
|
'scripts': lib.classes,
|
|
'dependencies': lib.dependencies,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
Promise.allSettled(allPromises).then(this.onMVCReady.bind(this));
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
onMVCReady() {
|
|
// Now that we are authenticated, we have the config and the MVC
|
|
// load the top routes and then route !
|
|
|
|
this.Assets.loadJson({ 'name': 'baseRoutes.json', 'path': '/app/config/' })
|
|
.then(
|
|
(topRoutes) =>{
|
|
if(!this.Router) {
|
|
//console.log('Top routes loaded...', app.config);
|
|
var routerOptions = {
|
|
'defaults' : this.config.router,
|
|
'routes': topRoutes,
|
|
'callback': () =>{ } // Called when *routed* (not when instanciated)
|
|
};
|
|
if('getRolesFrom' in this.config.router){ //Role can be an array of string or string representing a function
|
|
var isCallable = Function([],"return(typeof("+this.config.router.getRolesFrom+")=='function');")
|
|
if(isCallable()) routerOptions['roles'] = Function([],"return("+this.config.router.getRolesFrom+"());");
|
|
else routerOptions['roles'] = this.config.router.getRolesFrom;
|
|
} else routerOptions['roles']='';
|
|
this.Router = new Router(routerOptions);
|
|
}
|
|
this.Router.route();
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
// Let the browser cache boot loader components or not ? (true in prod or if you don't work on core)
|
|
const bootBrowserCache = false;
|
|
|
|
/* Here is Sparc bootstrap, depends only on Loader core class defined below */
|
|
if(typeof(app)=='undefined') var app = new Sparc();
|
|
|
|
window.onload = app.onWindowReady.bind(app);
|
|
|
|
|
|
|
|
/**
|
|
___ _____ __ ____ ____ ____
|
|
( ) ( _ ) /__\ ( _ \( ___)( _ \
|
|
) (_ )(_)( /(__)\ )(_) ))__) ) /
|
|
(____)(_____)(__)(__)(____/(____)(_)\_) for SPARC
|
|
By Mike & Nike
|
|
This static class contains the mechanics for loading javascripts. <br>
|
|
It will load any needed dependancies for you (with the help of the dependancies file) <br>
|
|
It can be called just boot-load you application and all its dependancies , <br>
|
|
and/or, alternatively, you can load widgets yourself anytime.
|
|
|
|
* @static
|
|
* @hideconstructor
|
|
* @category Core
|
|
* @todo found some direct reference to the app global var which is a bit sloppy
|
|
*/
|
|
class Loader {
|
|
static Dependencies = {};
|
|
static LoadedDepFiles = [];
|
|
static AssetsDependencies = {};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
static resolveDeps(imps, deps){
|
|
const leveler = (levels, root, l, trail=[]) => {
|
|
if(deps.hasOwnProperty(root)){
|
|
for(var dep of deps[root]){
|
|
if(!levels.hasOwnProperty(dep)) levels[dep] = -1;
|
|
if(levels[dep]<l) levels[dep]=l;
|
|
if(trail.indexOf(dep)<0){ // detect circular dependency to avoid infinite recursion
|
|
let curtrail = [...trail];
|
|
trail.push(dep);
|
|
leveler(levels, dep , l+1, trail);
|
|
trail = curtrail;
|
|
} else console.error('Circular depencency with '+dep, trail);
|
|
}
|
|
}
|
|
}
|
|
var levels={};
|
|
// Build the dep tree of things we need, noting the depth.
|
|
for(var imp of imps) { leveler(levels, imp, 1); } // Level starts at 1, zero reserved for roots
|
|
// Add requested imports as last, unless already with higher priority (tree shaking)
|
|
for(var imp of imps) { if(!levels.hasOwnProperty(imp)) levels[imp] = 0; }
|
|
// Group by levels, as their loading can be paralellized (whereas on level must wait on the prev. one)
|
|
var name, level; var byLevel = {};
|
|
for([name, level] of Object.entries(levels)) {
|
|
if(!byLevel.hasOwnProperty(level)) { byLevel[level]=[]; }
|
|
byLevel[level].push(name);
|
|
}
|
|
return(byLevel);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
static Import(scriptNames, deps, importIdPrfx, basePath = './'){ // Will start with real import (_import) only when / if dependencies are loaded.
|
|
var CurrentImportChain = {};
|
|
if(typeof(deps)=='string') { // deps is a path to a json file containing the dep tree
|
|
if( (deps!='') && (Loader.LoadedDepFiles.indexOf(deps)<0) ){
|
|
Loader.DependenciesLoader = new Promise((resolve, reject) => {
|
|
fetch(basePath+deps, { headers: { "Content-Type": "application/json; charset=utf-8" }})
|
|
.then(res => res.json())
|
|
.then(response => {
|
|
Loader.Dependencies = { ...Loader.Dependencies, ...response}; // Merge with known deps
|
|
Loader.LoadedDepFiles.push(deps);
|
|
resolve();
|
|
})
|
|
.catch(err => { console.error('Error loading dependencies'); Loader.Dependencies = {}; reject(); });
|
|
});
|
|
} else {
|
|
Loader.DependenciesLoader = new Promise((resolve, reject) => { resolve(); }); //Already loaded, nothing to do here
|
|
}
|
|
} else if(typeof(deps)=='object') { // deps is the dep-tree object
|
|
Loader.DependenciesLoader = new Promise((resolve, reject) => {
|
|
Loader.Dependencies = { ...Loader.Dependencies, ...deps }; // Merge with known deps
|
|
resolve(); // already ready ! ;-)
|
|
});
|
|
} else { console.error('Bad dependencies !?'); return(false); }
|
|
// Prepare a synchronous chain of asynchronous loads
|
|
Loader.DependenciesLoader.then(()=>{
|
|
var byLevelImps = Loader.resolveDeps(scriptNames, Loader.Dependencies);
|
|
var total = Object.values(byLevelImps).reduce((tot,v)=> (tot+v.length), 0);
|
|
document.dispatchEvent(new CustomEvent("LoaderProgressAddTodo", {'detail': { 'importID':importIdPrfx, 'addValue': total }}));
|
|
for(var key in Object.getOwnPropertyNames(byLevelImps).sort().reverse()){
|
|
CurrentImportChain[importIdPrfx+'_'+key] = {'imps': byLevelImps[key], 'path':basePath };
|
|
}
|
|
var firstImportId = importIdPrfx+'_'+key;
|
|
document.addEventListener('LoaderAllReady', function(evt) { // Next async imports in the sync chain
|
|
var nextImpNb = evt.detail.substr(0,evt.detail.lastIndexOf('_')+1)+(parseInt(evt.detail.substr(evt.detail.lastIndexOf('_')+1))-1);
|
|
if(evt.detail != importIdPrfx+'_0'){
|
|
if(CurrentImportChain.hasOwnProperty(nextImpNb)) {
|
|
Loader._import(CurrentImportChain[nextImpNb]['imps'], nextImpNb,CurrentImportChain[nextImpNb]['path']);
|
|
}
|
|
}
|
|
});
|
|
// First load all assets
|
|
Loader.LoadAssetDependencies(byLevelImps).then( () => {
|
|
// Start the synchronous chain
|
|
Loader._import(CurrentImportChain[firstImportId]['imps'], firstImportId,CurrentImportChain[firstImportId]['path']);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {*} byLevelImps
|
|
* @returns {Array<Promise>}
|
|
*/
|
|
static LoadAssetDependencies(byLevelImps){
|
|
let allImports = [];
|
|
for(var level in byLevelImps) allImports=[...allImports, ...byLevelImps[level]];
|
|
allImports = Array.from(new Set(allImports)); //remove dups
|
|
allImports = allImports.map(this.path2ClassName);
|
|
let allPromises = [];
|
|
for(var classname of allImports){
|
|
for(var assetType in Loader.AssetsDependencies[classname]){
|
|
if(!Array.isArray(Loader.AssetsDependencies[classname][assetType])) {
|
|
console.warn(`Bad config: Asset dependencies for ${classname} - ${assetType} is not an array !?`)
|
|
continue;
|
|
}
|
|
for(var assetObj of Loader.AssetsDependencies[classname][assetType]){
|
|
if((typeof(assetObj) != 'object') || (!('name' in assetObj))) {
|
|
console.warn(`Bad config: One asset dependency for ${classname} - ${assetType} has no name !?`)
|
|
continue;
|
|
}
|
|
switch(assetType){
|
|
case 'fonts': allPromises.push(app.Assets.loadFont(assetObj));
|
|
break;
|
|
case 'styles': allPromises.push(app.Assets.loadCss(assetObj));
|
|
break;
|
|
case 'html': allPromises.push(app.Assets.loadHtml(assetObj));
|
|
break;
|
|
case 'views': assetObj.path = '/app/views/';
|
|
allPromises.push(app.Assets.loadHtml(assetObj));
|
|
break;
|
|
case 'images': allPromises.push(app.Assets.getImage(assetObj));
|
|
break;
|
|
case 'json': allPromises.push(app.Assets.loadJson(assetObj));
|
|
break;
|
|
case 'sfx': allPromises.push(app.Assets.loadSound(assetObj));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return(Promise.allSettled(allPromises));
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {*} src
|
|
* @returns {Promise}
|
|
*/
|
|
static loadScript(src) {
|
|
return new Promise((resolve, reject) => {
|
|
const script = document.createElement('script')
|
|
script.type = 'text/javascript'
|
|
script.onload = resolve
|
|
script.onerror = reject
|
|
script.src = src
|
|
document.head.append(script)
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
static path2ClassName(scriptName){
|
|
scriptName = scriptName.substr(scriptName.lastIndexOf('/')+1);
|
|
if(scriptName.indexOf('.')==-1) scriptName+='.'; // also pure scriptname or path without extension compatible !
|
|
return(scriptName.substr(0,scriptName.lastIndexOf('.')));
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
static url2Path(url){
|
|
var x = new URL(url);
|
|
return(x.pathname);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
static _import(names, importID, basePath){ // Here, all imports are made asynchronously, in parallel.
|
|
var allLoaded = true;
|
|
var nbLeft = names.length;
|
|
for(var name of names){
|
|
var className = this.path2ClassName(name);
|
|
if(name.substring(0,4)!='http') var scriptFp = (basePath + '/' + name).replace(/\/+/g,'/')+'.js';
|
|
else var scriptFp = name;
|
|
if( (!(className in app.LoadedClasses)) && (app.LoadedScripts.indexOf(scriptFp)<0) ) {
|
|
allLoaded = false;
|
|
if( !bootBrowserCache ) scriptFp += '?'+importID;
|
|
Loader.loadScript(scriptFp)
|
|
.then((result) => {
|
|
var scriptName= this.url2Path(result.currentTarget.src);
|
|
// MCV classes and internal ones put themselves in app.loadedClasses,
|
|
// Non-Sparc scripts don't so save their path.
|
|
if( ('router' in app.config) &&
|
|
!(scriptName.startsWith(app.config.router.controllersPath)) &&
|
|
!(scriptName.startsWith(app.config.router.modelsPath)) &&
|
|
!(scriptName.startsWith(app.config.router.viewsPath)) &&
|
|
!(scriptName.startsWith('/core/'))
|
|
) app.LoadedScripts.push(scriptName);
|
|
var className = this.path2ClassName(result.currentTarget.src);
|
|
document.dispatchEvent(new CustomEvent('Loader'+className+'Ready', { 'detail': importID}));
|
|
document.dispatchEvent(new CustomEvent("LoaderProgressIncrement", {'detail': { 'importID': importID, 'success': true }}));
|
|
nbLeft--;
|
|
if(nbLeft<=0){
|
|
document.dispatchEvent(new CustomEvent("LoaderAllReady", { 'detail': importID }));
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
if((typeof(error)!='undefined') && (typeof(error.currentTarget)!='undefined')){
|
|
var scriptName= error.currentTarget.src;
|
|
var className = this.path2ClassName(error.currentTarget.src);
|
|
console.error('Could not import Class '+className);
|
|
document.dispatchEvent(new CustomEvent("LoaderError", { 'detail': {'importID': importID, 'className': className }}));
|
|
} else console.error('Script loading error '+error);
|
|
document.dispatchEvent(new CustomEvent("LoaderProgressIncrement", {'detail': { 'importID': importID, 'success': false }}));
|
|
nbLeft--;
|
|
});
|
|
} else nbLeft--;
|
|
}
|
|
if(allLoaded){
|
|
for(var name of names) document.dispatchEvent(new CustomEvent('Class'+name+'Ready', { 'detail':importID }));
|
|
document.dispatchEvent(new CustomEvent("LoaderAllReady", { 'detail':importID }));
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Load a list of scripts.
|
|
* @static
|
|
* @param {string} basepath - base path (something like /sccripts/)
|
|
* @param {Array<string>} scripts - array of scripts to load
|
|
* @param {(string|object)} dependencies - Last minute Dependancies :tree object ( like {'scriptName': ['deps1', 'dep2,...]} ) or path to its json file.
|
|
* @param {function} callback - Called when everything is loaded
|
|
* @returns {Promise} A promise resolved whn-en all is loaded
|
|
*/
|
|
static loadScripts(args){
|
|
// Start by filtering out scripts we already have
|
|
if(!args.refresh) {
|
|
args.scripts = args.scripts.filter((sname)=>{
|
|
var className = this.path2ClassName(sname);
|
|
return(!(className in app.LoadedClasses) && (!(sname in app.LoadedScripts)));
|
|
});
|
|
if(args.scripts.length==0) {
|
|
if( (args.hasOwnProperty('callback')) && (typeof(args.callback)=='function') ){
|
|
args.callback({});
|
|
}
|
|
return(new Promise((resolve) => { resolve(); }));
|
|
}
|
|
}
|
|
var myImportID = crypto.randomUUID();
|
|
if(!args.hasOwnProperty('dependencies')) args.dependencies = [];
|
|
if(!args.hasOwnProperty('basepath')) args.basepath = '';
|
|
return(new Promise((resolve, fail) => {
|
|
document.addEventListener('LoaderAllReady', (evt) => { if(evt.detail==myImportID+'_0') resolve(); } );
|
|
document.addEventListener('LoaderError', (evt) => { if(evt.detail==myImportID+'_0') fail(); } );
|
|
Loader.Import(args.scripts, args.dependencies, myImportID, args.basepath);
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Load a list of views.
|
|
* @static
|
|
* @param {array} Views - array of Views to load
|
|
* @returns {string} The unique ID of the import
|
|
*/
|
|
static async loadViews(views){
|
|
// Start with views templates
|
|
let allPromises = []
|
|
let templates = []
|
|
let scripts = []
|
|
let dependencies = {}
|
|
for(var dep of views){
|
|
if(typeof(dep) == 'string') {// just add view as dependency of controller, with correct path
|
|
templates.push(dep+'.html')
|
|
scripts.push(dep)
|
|
} else if(typeof(dep) == 'object'){ // This view depends on other views
|
|
scripts.push(dep.view);
|
|
dependencies[dep.view]=dep.dependencies//.map(x=>'/app/views/'+x)
|
|
templates.push(dep.view+'.html')
|
|
}
|
|
}
|
|
|
|
for(let view of templates){
|
|
let assetObj = { path: '/app/views/', name: view }
|
|
allPromises.push(app.Assets.loadHtml(assetObj));
|
|
}
|
|
await Promise.allSettled(allPromises);
|
|
|
|
// Finish with views scripts
|
|
return(this.loadScripts({
|
|
basepath:'/app/views/',
|
|
scripts: scripts,
|
|
dependencies: dependencies,
|
|
}));
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* POLYFILLS
|
|
*/
|
|
|
|
if(typeof(crypto.randomUUID)!='function'){
|
|
crypto.randomUUID = () => {
|
|
var buf = new Uint8Array(14);
|
|
crypto.getRandomValues(buf);
|
|
var uuid = Array.from(buf, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
|
|
return(
|
|
uuid.substring(0,8) + '-' +
|
|
uuid.substring(10,4) + '-' +
|
|
uuid.substring(14,4) + '-' +
|
|
uuid.substring(18,4) + '-' +
|
|
uuid.substring(22)
|
|
);
|
|
}
|
|
}
|
|
|
|
if(typeof(typeof(Promise.allSettled))!='function'){
|
|
Promise.allSettled = ((promises) => Promise.all(
|
|
promises.map(p => p
|
|
.then(value => ({
|
|
status: "fulfilled",
|
|
value
|
|
}))
|
|
.catch(reason => ({
|
|
status: "rejected",
|
|
reason
|
|
}))
|
|
)
|
|
));
|
|
} |