Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
386 lines
12 KiB
JavaScript
Executable file
386 lines
12 KiB
JavaScript
Executable file
'use strict';
|
|
|
|
/**
|
|
* The core component to sequentialize asynchronous steps.
|
|
* Basically it's an 'interruptable promise', but more than be interrupted,
|
|
* it could 'shift' from one to another phase, with the non-preemptive
|
|
* interrupting model.
|
|
*
|
|
* Example:
|
|
* var process = new Process();
|
|
* process.start() // the default phase is 'start'
|
|
* .next(stepA)
|
|
* .next(stepB)
|
|
* ...
|
|
* // later, some urgent events come
|
|
* process.stop() // one of the default three phases
|
|
* .next(stopStepA)
|
|
* .next(stopStepB)
|
|
* ....
|
|
* // later, some other interrupts come
|
|
* process.shift('stop', 'dizzy')
|
|
* .next(dizzyStepA)
|
|
* .next(dizzyStepB)
|
|
*
|
|
* The phases listed above would immediately interrupt the steps scheduled
|
|
* at the previous phase. However, this is a *non-preemptive* process by
|
|
* default. So, if there is a long-waiting Promise step in the 'start' phase:
|
|
*
|
|
* process.start()
|
|
* .next( longlonglongWaitingPromise ) // <--- now it's waiting this
|
|
* .next( thisStepIsStarving )
|
|
* .next( andThisOneToo )
|
|
* .next( poorSteps )
|
|
* ....
|
|
* // some urgent event occurs when it goes to the long waiting promise
|
|
* process.stop()
|
|
* .next( doThisAsQuickAsPossible )
|
|
*
|
|
* The first step of the 'stop' phase, namely the 'doThisAsQuickAsPossible',
|
|
* would *not* get executed immediately, since the promise is still waiting the
|
|
* last step earlier interruption. So, even the following steps of the 'start'
|
|
* phase would all get dropped, the new phase still need to wait the last one
|
|
* asynchronous step get resolved to get kicked off.
|
|
*
|
|
* ---
|
|
* ## About the non-preemptive model
|
|
*
|
|
* The reason why we can't have a preemptive process is because we couldn't
|
|
* interrupt each single step in the process, so the most basic unit could be
|
|
* interrupted is the step. So, the caveat here is make the step as small as
|
|
* possible, and treat it as some atomic operation that guaranteed to not been
|
|
* interrupted by Process. For example, if we alias 'next' as 'atomic':
|
|
*
|
|
* process.start()
|
|
* .atomic(stepA) // <--- now it's waiting this
|
|
* .atomic(stepB)
|
|
*
|
|
* // some urgent event occurs
|
|
* process.stop()
|
|
* .atomic( doThisAsQuickAsPossible )
|
|
*
|
|
* It would be better than:
|
|
*
|
|
* process.start()
|
|
* .atomic(() => stepA.then(stepB))
|
|
*
|
|
* // some urgent event occurs
|
|
* process.stop()
|
|
* .atomic( doThisAsQuickAsPossible )
|
|
*
|
|
* Since in the second example the first step of the 'stop' phase must wait
|
|
* both the stepA & stepB, while in the first one it only needs to wait stepA.
|
|
* However, this depends on which atomic operations is needed.
|
|
*
|
|
* Nevertheless, user is able to make the steps 'interruptible' via some special
|
|
* methods of the process. That is, to monitor the phase changes to nullify the
|
|
* step:
|
|
*
|
|
* var process = new Process();
|
|
* process.start()
|
|
* .next(() => {
|
|
* var phaseShifted = false;
|
|
* process.until('stop')
|
|
* .next(() => {phaseShifted = true;});
|
|
* return new Promise((r, rj) => {
|
|
* setTimeout(() => {
|
|
* if (phaseShifted) { console.log('do nothing'); }
|
|
* else { console.log('do something'); }
|
|
* }, 1000)
|
|
* });
|
|
* })
|
|
*
|
|
* // some urgent event occurs
|
|
* process.stop()
|
|
* .next( doThisAsQuickAsPossible )
|
|
*
|
|
* So that the first step of the 'stop' phase would execute immediately after
|
|
* the phase shifted, since the last step of the previous phase aborted itself.
|
|
* In future the trick to nullify the last step may be included in as a method
|
|
* of Process, but currently the manual detecting is still necessary.
|
|
*/
|
|
|
|
(function(exports) {
|
|
var Process = function() {
|
|
this.states = {
|
|
phase: null,
|
|
currentPromise: null,
|
|
until: {
|
|
resolver: null,
|
|
phase: null
|
|
},
|
|
// @see: #next
|
|
stepResults: [],
|
|
// @see: #next
|
|
currentPhaseSteps: 0
|
|
};
|
|
};
|
|
|
|
Process.prototype.start = function() {
|
|
this.states.phase = 'start';
|
|
this.states.currentPromise = Promise.resolve();
|
|
return this;
|
|
};
|
|
|
|
Process.prototype.stop = function() {
|
|
return this.shift('start', 'stop');
|
|
};
|
|
|
|
Process.prototype.destroy = function() {
|
|
return this.shift('stop', 'destroy');
|
|
};
|
|
|
|
Process.prototype.shift = function(prev, current) {
|
|
if (prev !== this.states.phase) {
|
|
var error = new Error(`Must be ${prev} before shift to ${current},
|
|
but now it's ${this.states.phase}`);
|
|
console.error(error);
|
|
throw error;
|
|
}
|
|
this.states.phase = current;
|
|
if (this.until.phase === this.states.phase) {
|
|
this.until.resolver();
|
|
}
|
|
// Concat new step to switch to the 'next promise'.
|
|
this.states.currentPromise =
|
|
this.states.currentPromise.catch((err) => {
|
|
if (!(err instanceof Process.InterruptError)) {
|
|
// We need to re-throw it again and bypass the whole
|
|
// phase, until the next phase (final phase) to
|
|
// handle it. Since in Promise, steps after catch would
|
|
// not be affected by the catched error and keep executing.
|
|
throw err;
|
|
}
|
|
// And if it's an interrupt error we do nothing, so that it would
|
|
// make the chain omit this error and execute the following steps.
|
|
});
|
|
// At the moment of shifting, there are no steps belong to the new phase.
|
|
this.states.currentPhaseSteps = 0;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Return a Promise that only be resolved when we get shifed to the
|
|
* target phase.
|
|
*/
|
|
Process.prototype.until = function(phase) {
|
|
var promise = new Promise((res) => {
|
|
this.states.until.resolver = res;
|
|
});
|
|
return promise;
|
|
};
|
|
|
|
/**
|
|
* The 'step' can only be a function return Promise/Process/plain value.
|
|
* No matter a Promise or Process it would return,
|
|
* the chain would concat it as the Promise rule.
|
|
* If it's plain value then this process would ignore it, as
|
|
* what a Promise does.
|
|
*
|
|
* About the resolving values:
|
|
*
|
|
* .next( fnResolveA, fnResolveB ) --> #save [a, b] in this process
|
|
* .next( fnResolveC ) --> #receive [a, b] as first argument
|
|
* .next( fnResolveD ) --> #receive c as first argument
|
|
* .next( fnResolveE, fnResolveF) --> #each of them receive d as argument
|
|
*/
|
|
Process.prototype.next = function(...tasks) {
|
|
if (!this.states.currentPromise) {
|
|
throw new Error('Process should initialize with the `start` method');
|
|
}
|
|
// At definition stage, set it's phase.
|
|
// And check if it's a function.
|
|
tasks.forEach((task) => {
|
|
if ('function' !== typeof task) {
|
|
throw new Error(`The task is not a function: ${task}`);
|
|
}
|
|
task.phase = this.states.phase;
|
|
});
|
|
|
|
// First, concat a 'then' to check interrupt.
|
|
this.states.currentPromise =
|
|
this.states.currentPromise.then(() => {
|
|
// Would check: if the phase it belongs to is not what we're in,
|
|
// the process need to be interrputed.
|
|
for (var task of tasks) {
|
|
if (this.checkInterrupt(task)) {
|
|
throw new Process.InterruptError();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Read it as:
|
|
// 1. execute all tasks to generate resolvable-promises
|
|
// 2. Promise.all(...) to wait these resolvable-promises
|
|
// 3. append a general error handler after the Promise.all
|
|
// so that if any error occurs it would print them out
|
|
// So, including the code above, we would have:
|
|
//
|
|
// currentPromise {
|
|
// [checkInterrupt(tasks)]
|
|
// [Promise.all([taskA1, taskA2...])]
|
|
// [error handler] +}
|
|
//
|
|
// The 'checkInterrupt' and 'error handler' wrap the actual steps
|
|
// to do the necessary checks.
|
|
this.states.currentPromise =
|
|
this.states.currentPromise.then(() => this.generateStep(tasks));
|
|
this.states.currentPromise =
|
|
this.states.currentPromise.catch(this.generateErrorLogger({
|
|
'nth-step': this.currentPhaseSteps
|
|
}));
|
|
|
|
// A way to know if these tasks is the first steps in the current phase,
|
|
// and it's also convenient for debugging.
|
|
this.states.currentPhaseSteps += 1;
|
|
return this;
|
|
|
|
};
|
|
|
|
Process.prototype.rescue = function(handler) {
|
|
this.states.currentPromise =
|
|
this.states.currentPromise.catch((err) => {
|
|
if (err instanceof Process.InterruptError) {
|
|
// Only built-in phase transferring catch can handle interrupts.
|
|
// Re-throw it until we reach the final catch we set.
|
|
throw err;
|
|
} else {
|
|
handler(err);
|
|
}
|
|
});
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* An interface to explicitly put multiple tasks execute at one time.
|
|
**/
|
|
Process.prototype.wait = function() {
|
|
this.next.apply(this, arguments);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* At the preemptive mode a suppressor would be installed as the final step
|
|
* of the previous interrupted Promise, to igore any errors since it has been
|
|
* interrupted.
|
|
*/
|
|
Process.prototype.generateSuppressor = function() {
|
|
return (err) => {};
|
|
};
|
|
|
|
/**
|
|
* Execute task and get Promises or plain values them return,
|
|
* and then return the wrapped Promise as the next step of this
|
|
* process. The name 'step' indicates the generated Promise,
|
|
* which is one step of the main Promise of the current phase.
|
|
*/
|
|
Process.prototype.generateStep = function(tasks) {
|
|
// So we unwrap the task first, and then put it in the array.
|
|
// Since we need to give the 'currentPromise' a function as what the
|
|
// tasks passed here.
|
|
var chains = tasks.map((task) => {
|
|
// Reset the registered results.
|
|
var stepResults = this.states.stepResults;
|
|
this.states.stepResults = [];
|
|
var chain;
|
|
// If it has multiple results, means it's a task group
|
|
// generated results.
|
|
if (this.states.stepResults.length > 1) {
|
|
chain = task(stepResults);
|
|
} else {
|
|
chain = task(stepResults[0]);
|
|
}
|
|
|
|
// Ordinary function returns 'undefine' or other things.
|
|
if (!chain) {
|
|
// It's a plain value.
|
|
// Store it as one of results.
|
|
this.states.stepResults.push(chain);
|
|
return Promise.resolve(chain);
|
|
}
|
|
|
|
if (chain instanceof Process) {
|
|
// Premise: it's a started process.
|
|
return chain.states.currentPromise.then((resolvedValue) => {
|
|
this.states.stepResults.push(resolvedValue);
|
|
});
|
|
} else if (chain.then) {
|
|
// Ordinary promise can be concated immediately.
|
|
return chain.then((resolvedValue) => {
|
|
this.states.stepResults.push(resolvedValue);
|
|
});
|
|
} else {
|
|
// It's a plain value.
|
|
// Store it as one of results.
|
|
this.states.stepResults.push(chain);
|
|
return Promise.resolve(chain);
|
|
}
|
|
});
|
|
return Promise.all(chains);
|
|
};
|
|
|
|
/** We need this to prevent the step() throw errors.
|
|
* In this catch, we distinguish the interrupt and other errors,
|
|
* and then bypass the former and print the later out.
|
|
*
|
|
* The final fate of the real errors is it would be re-throw again
|
|
* after we print the instance out. We need to do that since after an
|
|
* error the promise shouldn't keep executing. If we don't throw it
|
|
* again, since the error has been catched, the rest steps in the
|
|
* promise would still be executed, and the user-set rescues would
|
|
* not catch this error.
|
|
*
|
|
* As a conclusion, no matter whether the error is an interrupt,
|
|
* we all need to throw it again. The only difference is if it's
|
|
* and interrupt we don't print it out.
|
|
*/
|
|
Process.prototype.generateErrorLogger = function(debuginfo) {
|
|
return (err) => {
|
|
if (!(err instanceof Process.InterruptError)) {
|
|
console.error(`ERROR during #${debuginfo['nth-steps']}
|
|
step executes: ${err.message}`, err);
|
|
}
|
|
throw err;
|
|
};
|
|
};
|
|
|
|
Process.prototype.checkInterrupt = function(step) {
|
|
if (step.phase !== this.states.phase) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Because DRY.
|
|
*/
|
|
Process.defer = function() {
|
|
var resolve, reject;
|
|
var promise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
var result = {
|
|
'resolve': resolve,
|
|
'reject': reject,
|
|
'promise': promise
|
|
};
|
|
return result;
|
|
};
|
|
|
|
/* Static version for mimicking Promise.all */
|
|
Process.wait = function(steps) {
|
|
var process = new Process();
|
|
return process.wait(steps);
|
|
};
|
|
|
|
Process.InterruptError = function(message) {
|
|
this.name = 'InterruptError';
|
|
this.message = message || '';
|
|
};
|
|
|
|
Process.InterruptError.prototype = new Error();
|
|
|
|
exports.Process = Process;
|
|
})(window);
|
|
|