Bootstrap state object with multiple async calls using one function
I start with simple code:
class MyState{
constructor( bootstrap = () => {} ) {
this.state = bootstrap();
}
}
The constructor calls the bootstrap function and the state is ready. But in the case of async calls, I'm forced to keep access to the final state object and update it directly when an async call ends.
const state = new MyState(() => {
callAsyncService().then((asyncState) => {
state.state = asyncState
})
return "sync state";
});
It's easy on this example but if have don't keep access to state objects things are more complicated.
The simplest way to handle it is to return a promise or array of promises and resolve those using Promise.all
and/or Promise.resolve
and apply results to the state. This was my first step:
class MyState {
constructor( bootstrap = () => {} ) {
const state = bootstrap();
this.state = {}
if ( Array.isArray(state) ) {
Promise.all( state ).then( (stateParts) => this.state = { ...this.state, ...stateParts })
}else{
Promise.resolve(state).then( (state) => this.state = state )
}
}
}
That way handles one or more promises and in the case of an array, all results will be merged into the state object when the last promise will resolve. It's almost good but what I want is to make the first state without delay, and render the app with loader. The second thought I had was that progress also would be a nice thing.
I thought, what if I will not use Promise.all
but iterate through an array and resolve each promise separately?
class MyState {
constructor( bootstrap = () => {} ) {
const state = bootstrap();
if( Array.isArray(state) ) {
for( let stage of state ) {
Promise.resolve(stage).then( stage => this.state = {
...this.state,
...stage
});
}
}else{
Promise.resolve(state).then( state => this.state = state );
}
}
}
Then I have tested this code with the following bootstrap function:
const sleep = (ms, result) =>
new Promise((done) => setTimeout(() => done(result), ms));
const myBootstrap = () => {
return [
{ state: "loading", progress: 0 },
sleep(1000, { progress: 66 }),
sleep(500, { progress: 100 }),
{ state: "ready" }
];
}
This code didn't keep proper functions call. It's good if there is no matter what result I got first. But the thing it's not I wanted to achieve.
To keep proper sequence I need to check if the first promise is resolved before I will check the second one.
class MyState {
constructor( bootstrap = () => {} ) {
const state = bootstrap();
if( Array.isArray(state) ) {
const step = (i) => {
Promise.resolve(state[i]).then( stage => {
this.state = {
...this.state,
...stage
});
if( state.length > ++i ){
runNext(i);
}
});
};
step(0)
}else{
Promise.resolve(state).then( state => this.state = state );
}
}
}
To simplify this code I will use async
/ await
keywords.
const runBootstrap = async (state, merge) => {
for await ( let stage of state ) {
merge(stage);
}
}
class MyState {
constructor( bootstrap = () => {} ) {
const state = bootstrap();
if( !Array.isArray(state) ) {
state = [state];
}
this.state = {};
runBootstrap( state, (stage) => {
this.state = {
...this.state,
...stage
}
});
}
}
What when one async call in bootstrap depends on other? For example, the first call is for the user session and the next are based on if the user is logged in or not.
const bootstrapApp = async () => {
const user = await api.getCurrentUser();
const resources = await api.getUserResources(user);
return { user, resources };
}
Let's try to use a function generator.
const bootstrapApp = async function* () {
yield { state: "loading", progress: 0 };
const user = await api.getCurrentUser();
if( null !== user ) {
yield { progress: 50, user };
const resources = await api.getUserResources(user);
yield { progress: 100, resources };
}
yield { state: "done" }
}
And resolve it in state constructor.
const runBootstrap = async ( state, merge ) => {
for await ( let stage of state ) {
merge(stage);
}
}
class MyState {
constructor( bootstrap ) {
this.state = {};
runBootstrap(bootstrap(), stage => this.state = {
...this.state,
...stage
});
}
}
I have all, but what if there will be no need to use generator function and simple function call will be enough? Let's try to test this code against those variations:
const bootstrap1 = () => ({
state: "done"
});
const bootstrap2 = () => [
{state: "loading", progress: 0},
sleep(1000, { progress: 66 }),
sleep(500, { progress: 100 }),
{state: "done"}
];
In case of function: bootstrap1
I need to be sure that only asyncIterator
will be passed to loop code. bootstrap2
can be passed to loop code but it's not asyncIterator
and I need to include also simple array type. The whole code will look like this:
const runBootstrap = async ( state, merge ) => {
for await ( let stage of state ) {
merge(stage);
}
}
class MyState {
constructor( bootstrap ) {
this.state = {};
const state = bootstrap();
if(
Array.isArray(state) || // handle bootstrap2
typeof state[Symbol.asyncIterator] !== 'undefined'
) {
runBootstrap(state, stage => this.state = {
...this.state,
...stage
});
} else { // handle bootstrap1
this.state = state;
}
}
}