timeout-flow
Version:
Fluent, composable, pauseable JavaScript timers and time control flows — plus RAF utilities for frame-based logic.
228 lines (187 loc) • 6.2 kB
JavaScript
// ./src/TimeoutFlow.js
import { after } from './after.js';
import { every } from './every.js';
/**
* Creates a fluent scheduler.
* Returns a chainable timeline.
*/
export function flow() {
const steps = [];
let currentIndex = 0;
let isPaused = false;
let isCancelled = false;
let loopEnabled = false;
let loopLimit = Infinity;
let loopCount = 0;
let onFinishCallback = null;
const labelMap = new Map();
let jumpTarget = null;
let conditionStack = [];
let skipMode = false;
let whileCondition = null;
let doWhileCondition = null;
const runner = {
after(duration, fn) {
steps.push({ type: 'after', duration, fn });
return runner;
},
every(duration, fn, times = Infinity) {
steps.push({
type: 'every',
duration,
fn,
times,
whileCondition,
doWhileCondition
});
whileCondition = null;
doWhileCondition = null;
return runner;
},
loop(n = true) {
loopEnabled = true;
loopLimit = n === true ? Infinity : n;
return runner;
},
onFinish(cb) {
onFinishCallback = cb;
return runner;
},
start() {
isCancelled = false;
isPaused = false;
currentIndex = 0;
loopCount = 0;
executeNext();
return runner;
},
pause() {
if (isPaused || isCancelled) return;
isPaused = true;
steps[currentIndex]?.controller?.pause?.();
},
resume() {
if (!isPaused || isCancelled) return;
isPaused = false;
steps[currentIndex]?.controller?.resume?.();
},
cancel() {
isCancelled = true;
const step = steps[currentIndex];
step?.controller?.cancel?.();
step.controller = null;
},
reset() {
runner.cancel();
steps.length = 0;
currentIndex = 0;
loopCount = 0;
onFinishCallback = null;
return runner;
},
get isPaused() {
return isPaused;
},
label(name) {
labelMap.set(name, steps.length); // point to next step index
return runner;
},
jumpTo(name) {
jumpTarget = name;
return runner;
},
if(predicate) {
conditionStack.push(() => (typeof predicate === 'function' ? predicate() : !!predicate));
return runner;
},
unless(predicate) {
conditionStack.push(() => !(typeof predicate === 'function' ? predicate() : !!predicate));
return runner;
},
while(predicate) {
whileCondition = () => (typeof predicate === 'function' ? predicate() : !!predicate);
return runner;
},
doWhile(predicate) {
doWhileCondition = () => (typeof predicate === 'function' ? predicate() : !!predicate);
return runner;
}
};
function increment_step(step = null) {
if (step?.controller) step.controller = null;
currentIndex++;
executeNext();
}
function executeNext() {
if (isCancelled || isPaused) return;
if (jumpTarget) {
const targetIndex = labelMap.get(jumpTarget);
if (typeof targetIndex === 'number') {
currentIndex = targetIndex;
} else {
throw new Error(`Label "${jumpTarget}" not found`);
}
jumpTarget = null; // reset after jump
}
// Handle if conditions
if (conditionStack.length > 0) {
const shouldRun = conditionStack.shift()(); // run & remove
if (!shouldRun) {
skipMode = true;
}
}
// Skip logic
if (skipMode) {
const next = steps[currentIndex];
if (!next || next.type === 'label') {
skipMode = false; // stop skipping at label
} else {
return increment_step(step);
}
}
// End of timeline
const step = steps[currentIndex];
if (!step || currentIndex >= steps.length) {
if (loopEnabled && ++loopCount < loopLimit) {
currentIndex = 0;
executeNext();
} else {
onFinishCallback?.();
}
return;
}
if (step.type === 'after') {
step.controller = after(step.duration, step.fn, () => increment_step(step));
return
}
if (step.type === 'every') {
let count = 0;
let firstRun = true;
const ctrl = every(step.duration, () => {
if (isPaused || isCancelled) return;
// Handle .while (skip when fails before run)
if (step.whileCondition && !step.whileCondition()) {
ctrl.cancel();
return increment_step(step);
}
// Run fn first
step.fn(count++);
firstRun = false;
// Check doWhile after first execution
if (!firstRun && step.doWhileCondition && !step.doWhileCondition()) {
ctrl.cancel();
return increment_step(step);
}
if (count >= step.times) {
ctrl.cancel();
return increment_step(step);
}
}, step.times);
step.controller = ctrl;
}
if (step.type === 'label') {
return increment_step(step);
}
}
return runner;
}