@silver-zepp/sequence
Version:
Sequence - a JavaScript utility for managing complex chains of synchronous and asynchronous operations with timing control.
3 lines (2 loc) • 8.74 kB
JavaScript
/** @about Sequence 1.0.0 @min_zeppos 2.0 @author: Silver, Zepp Health. @license: MIT */
export class Sequence{#tick_interval;#autodestroy;#is_running=0;#is_looping=0;#cur_task_index=0;#cur_delay=0;#timer=null;#cur_parallel_tasks=null;#state="idle";#cb_err_handler=null;#cb_fin_handler=null;#task_start_time=0;#expected_end_time=0;#tasks_arr=[];static#all_sequences_arr=[];static#task_delay=1;static#task_log=2;static#task_call=3;static#task_parallel=4;static#task_await=5;static#delta_time=0;static#time_scale=1;static#last_frame_time=Date.now();static#global_logger=console;#last_result=null;constructor(options={}){this.#tick_interval=Math.max(options.tick_interval??50,1);this.#autodestroy=options.autodestroy??true;Sequence.#all_sequences_arr.push(this)}start(){if(this.#is_running)return this;this.#state="running";this.#is_running=1;this.#tick();return this}stop(){if(!this.#is_running)return this;this.#state="completed";this.#is_running=0;this.#cleanupParallelTasks();if(this.#timer){clearTimeout(this.#timer);this.#timer=null}if(this.#cb_fin_handler){this.#cb_fin_handler()}if(this.#autodestroy){this.destroy()}return this}pause(){if(this.#is_running){this.#state="paused";this.#is_running=0;clearTimeout(this.#timer);this.#timer=null}return this}resume(){if(!this.#is_running){this.#state="running";this.#is_running=1;this.#tick()}return this}delay(ms){if(typeof ms!=="number"||ms<0)throw new Error("Delay must be a non-negative number");return this.#add({type:Sequence.#task_delay,duration:ms})}log(...messages){return this.#add({type:Sequence.#task_log,messages:messages})}toJSON(){if(this.isDestroyed()){return{destroyed:true}}return{state:this.#state,cur_task_index:this.#cur_task_index,task_count:this.#tasks_arr?this.#tasks_arr.length:0,is_looping:this.#is_looping===1,autodestroy:this.#autodestroy}}call(fn){if(typeof fn!=="function")throw new Error("Call argument must be a function");return this.#add({type:Sequence.#task_call,fn:fn})}await(fn,options={}){if(typeof fn!=="function")throw new Error("Await argument must be a function");const timeout=options.timeout||5e3;return this.#add({type:Sequence.#task_await,fn:fn,timeout:timeout})}restart(){this.stop();this.#cur_task_index=0;this.#cur_delay=0;return this.start()}clear(){this.stop();this.#tasks_arr.length=0;this.#is_looping=0;return this}repeat(times){if(!Number.isInteger(times)||times<=0){throw new Error("Repeat count must be a positive integer")}const task_count=this.#tasks_arr.length;for(let i=1;i<times;i++){for(let j=0;j<task_count;j++){this.#tasks_arr[this.#tasks_arr.length]=this.#tasks_arr[j]}}return this}loop(){this.#is_looping=1;return this}persist(){this.#autodestroy=false;return this}removeTask(index){if(index>=0&&index<this.#tasks_arr.length){this.#tasks_arr.splice(index,1);if(this.#cur_task_index>=index){this.#cur_task_index--}}return this}parallel(sequences,options={}){const{mode=Sequence.PARALLEL_MODE_ALL,timeout=5e3}=options;return this.#add({type:Sequence.#task_parallel,sequences:sequences,mode:mode,timeout:timeout})}destroy(){this.stopAllTasks();this.clear();const index=Sequence.#all_sequences_arr.indexOf(this);if(index>-1){Sequence.#all_sequences_arr.splice(index,1)}this.#tick_interval=0;this.#autodestroy=false;this.#is_running=0;this.#is_looping=0;this.#cur_task_index=0;this.#cur_delay=0;this.#timer=null;this.#cur_parallel_tasks=null;this.#cb_err_handler=null;this.#cb_fin_handler=null;this.#state=null;this.#task_start_time=0;this.#expected_end_time=0;this.#tasks_arr=null;this.#cleanupParallelTasks()}stopAllTasks(){this.stop();if(this.#cur_parallel_tasks){this.#cur_parallel_tasks.forEach(task=>task.stop())}this.#cur_parallel_tasks=null}catch(handler){this.#cb_err_handler=handler;return this}finally(handler){this.#cb_fin_handler=handler;return this}getCurrentTaskIndex(){return this.#cur_task_index}getState(){return this.#state}isRunning(){return this.#is_running===1}isLooping(){return this.#is_looping===1}isDestroyed(){return this.#state===null}getTaskCount(){return this.#tasks_arr.length}setTickInterval(interval){if(typeof interval!=="number"||interval<=0)throw new Error("Tick interval must be a positive number");this.#tick_interval=interval;return this}static get PARALLEL_MODE_ALL(){return 1}static get PARALLEL_MODE_RACE(){return 2}static get PARALLEL_MODE_ANY(){return 3}static DestroyAllSequences(){for(let i=0;i<Sequence.#all_sequences_arr.length;i++){Sequence.#all_sequences_arr[i].destroy()}Sequence.#all_sequences_arr.length=0}static GetDeltaTime(){return Sequence.#delta_time*Sequence.#time_scale}static GetTimeScale(){return Sequence.#time_scale}static SetTimeScale(scale){if(typeof scale!=="number"||scale<=0){throw new Error("Time scale must be a positive number")}Sequence.#time_scale=scale}static UpdateDeltaTime(){const now=Date.now();const raw_dt=(now-Sequence.#last_frame_time)/1e3;Sequence.#delta_time=Math.max(raw_dt,.001);Sequence.#last_frame_time=now}static SetLogger(custom_logger){if(custom_logger&&typeof custom_logger==="object"&&typeof custom_logger.log==="function"){Sequence.#global_logger=custom_logger}else{throw new Error("Invalid logger. Logger must be an object with a log method.")}}#handleError(error){this.#state="error";if(this.#cb_err_handler){this.#cb_err_handler(error)}else{Sequence.#global_logger.log("SQC ERR:",error)}}#add(task){this.#tasks_arr[this.#tasks_arr.length]=task;return this}#tick(){if(!this.#is_running)return;const tasks=this.#tasks_arr;let task_index=this.#cur_task_index;const cur_time=Date.now();Sequence.UpdateDeltaTime();const dt=Sequence.GetDeltaTime()*1e3;if(task_index>=tasks.length){if(this.#is_looping){this.#cur_task_index=0;this.#cur_delay=0;this.#task_start_time=0;this.#expected_end_time=0;task_index=0}else{this.stop();return}}const task=tasks[task_index];try{switch(task.type){case Sequence.#task_parallel:this.#handleParallelTask(task);return;case Sequence.#task_delay:if(this.#task_start_time===0){this.#task_start_time=cur_time;this.#expected_end_time=cur_time+task.duration;this.#cur_delay=0}this.#cur_delay+=dt;if(this.#cur_delay>=task.duration||cur_time>=this.#expected_end_time){this.#cur_task_index++;this.#task_start_time=0;this.#expected_end_time=0;this.#cur_delay=0}break;case Sequence.#task_log:Sequence.#global_logger.log(task.messages.map(m=>typeof m==="function"?m():m));this.#cur_task_index++;break;case Sequence.#task_call:task.fn(this.#last_result);this.#cur_task_index++;break;case Sequence.#task_await:const promise=new Promise((resolve,reject)=>{const t_id=setTimeout(()=>{reject(new Error("Operation timed out"))},task.timeout);new Promise(resolve=>{const res=task.fn(resolve,this.#last_result);if(res!==undefined){resolve(res)}}).then(result=>{clearTimeout(t_id);resolve(result)})});promise.then(result=>{this.#last_result=result;this.#cur_task_index++;this.#tick()}).catch(error=>{this.#handleError(error);this.stop()});return}}catch(error){this.#handleError(error);this.stop();return}if(this.#is_running){const next_tick_delay=this.#tick_interval/Sequence.#time_scale;const adjusted_delay=next_tick_delay>dt?next_tick_delay-dt:0;this.#timer=setTimeout(()=>this.#tick(),adjusted_delay)}else{this.stop()}}#handleParallelTask(task){const start=Date.now();let done_count=0;let is_done=false;const total_tasks=task.sequences.length;this.#cur_parallel_tasks=new Array(total_tasks);for(let i=0;i<total_tasks;i++){this.#cur_parallel_tasks[i]=new Sequence({autodestroy:false}).#addAll(task.sequences[i].#tasks_arr)}const cb_check_completion=()=>{if(is_done)return;const time_elapsed=Date.now()-start;const is_timed_out=time_elapsed>=task.timeout;const should_complete=is_timed_out||task.mode===Sequence.PARALLEL_MODE_ALL&&done_count===total_tasks||task.mode===Sequence.PARALLEL_MODE_RACE&&done_count>0||task.mode===Sequence.PARALLEL_MODE_ANY&&done_count>0;if(should_complete){is_done=true;if(is_timed_out){Sequence.#global_logger.log(`Parallel execution timed out after ${task.timeout}ms`)}this.#cleanupParallelTasks();this.#cur_task_index++;this.#tick()}else if(this.#is_running){this.#timer=setTimeout(cb_check_completion,this.#tick_interval)}};for(let i=0;i<total_tasks;i++){this.#cur_parallel_tasks[i].finally(()=>{if(is_done)return;done_count++;if(this.#cur_parallel_tasks&&this.#cur_parallel_tasks[i]){this.#cur_parallel_tasks[i].stop();this.#cur_parallel_tasks[i].destroy();this.#cur_parallel_tasks[i]=null}cb_check_completion()}).start()}cb_check_completion()}#addAll(tasks){for(let i=0;i<tasks.length;i++)this.#add(tasks[i]);return this}#cleanupParallelTasks(){if(this.#cur_parallel_tasks){for(let i=0;i<this.#cur_parallel_tasks.length;i++){const task=this.#cur_parallel_tasks[i];if(task){task.stop();task.destroy()}}this.#cur_parallel_tasks=null}if(this.#timer){clearTimeout(this.#timer);this.#timer=null}}}