VulpesOS/gaia/apps/system/lockscreen/js/stream/process.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

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);