You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			228 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			228 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			TypeScript
		
	
| import { Snode } from '../../data/data';
 | |
| type SimpleFunction<T> = (arg: T) => void;
 | |
| type Return<T> = Promise<T> | T;
 | |
| 
 | |
| async function toPromise<T>(value: Return<T>): Promise<T> {
 | |
|   return value instanceof Promise ? value : Promise.resolve(value);
 | |
| }
 | |
| 
 | |
| export class TaskTimedOutError extends Error {
 | |
|   constructor() {
 | |
|     super('Task timed out');
 | |
|     // Set the prototype explicitly.
 | |
|     Object.setPrototypeOf(this, TaskTimedOutError.prototype);
 | |
|   }
 | |
| }
 | |
| 
 | |
| // one action resolves all
 | |
| const snodeGlobalLocks: Record<string, Promise<any>> = {};
 | |
| 
 | |
| export async function allowOnlyOneAtATime(
 | |
|   name: string,
 | |
|   process: () => Promise<any>,
 | |
|   timeoutMs?: number
 | |
| ) {
 | |
|   // if currently not in progress
 | |
|   if (snodeGlobalLocks[name] === undefined) {
 | |
|     // set lock
 | |
|     snodeGlobalLocks[name] = new Promise(async (resolve, reject) => {
 | |
|       // set up timeout feature
 | |
|       let timeoutTimer = null;
 | |
|       if (timeoutMs) {
 | |
|         timeoutTimer = setTimeout(() => {
 | |
|           window?.log?.warn(`allowOnlyOneAtATime - TIMEDOUT after ${timeoutMs}s`);
 | |
|           // tslint:disable-next-line: no-dynamic-delete
 | |
|           delete snodeGlobalLocks[name]; // clear lock
 | |
|           reject();
 | |
|         }, timeoutMs);
 | |
|       }
 | |
|       // do actual work
 | |
|       let innerRetVal;
 | |
|       try {
 | |
|         innerRetVal = await process();
 | |
|       } catch (e) {
 | |
|         if (typeof e === 'string') {
 | |
|           window?.log?.error(`allowOnlyOneAtATime - error ${e}`);
 | |
|         } else {
 | |
|           window?.log?.error(`allowOnlyOneAtATime - error ${e.code} ${e.message}`);
 | |
|         }
 | |
| 
 | |
|         // clear timeout timer
 | |
|         if (timeoutMs) {
 | |
|           if (timeoutTimer !== null) {
 | |
|             clearTimeout(timeoutTimer);
 | |
|             timeoutTimer = null;
 | |
|           }
 | |
|         }
 | |
|         // tslint:disable-next-line: no-dynamic-delete
 | |
|         delete snodeGlobalLocks[name]; // clear lock
 | |
|         reject(e);
 | |
|       }
 | |
|       // clear timeout timer
 | |
|       if (timeoutMs) {
 | |
|         if (timeoutTimer !== null) {
 | |
|           clearTimeout(timeoutTimer);
 | |
|           timeoutTimer = null;
 | |
|         }
 | |
|       }
 | |
|       // tslint:disable-next-line: no-dynamic-delete
 | |
|       delete snodeGlobalLocks[name]; // clear lock
 | |
|       // release the kraken
 | |
|       resolve(innerRetVal);
 | |
|     });
 | |
|   }
 | |
|   return snodeGlobalLocks[name];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Create a promise which waits until `done` is called or until `timeout` period is reached.
 | |
|  * If `timeout` is reached then this will throw an Error.
 | |
|  *
 | |
|  * @param task The task to wait for.
 | |
|  * @param timeout The timeout period.
 | |
|  */
 | |
| export async function waitForTask<T>(
 | |
|   task: (done: SimpleFunction<T>) => Return<void>,
 | |
|   timeoutMs: number = 2000
 | |
| ): Promise<T> {
 | |
|   const timeoutPromise = new Promise<T>((_, rej) => {
 | |
|     const wait = setTimeout(() => {
 | |
|       clearTimeout(wait);
 | |
|       rej(new TaskTimedOutError());
 | |
|     }, timeoutMs);
 | |
|   });
 | |
| 
 | |
|   const taskPromise = new Promise(async (res, rej) => {
 | |
|     try {
 | |
|       await toPromise(task(res));
 | |
|     } catch (e) {
 | |
|       rej(e);
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   return Promise.race([timeoutPromise, taskPromise]) as Promise<T>;
 | |
| }
 | |
| 
 | |
| export interface PollOptions {
 | |
|   timeoutMs: number;
 | |
|   interval: number;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Creates a promise which calls the `task` every `interval` until `done` is called or until `timeout` period is reached.
 | |
|  * If `timeout` is reached then this will throw an Error.
 | |
|  *
 | |
|  * @param task The task which runs every `interval` ms.
 | |
|  * @param options The polling options.
 | |
|  */
 | |
| export async function poll(
 | |
|   task: (done: SimpleFunction<void>) => Return<void>,
 | |
|   options: Partial<PollOptions> = {}
 | |
| ): Promise<void> {
 | |
|   const defaults: PollOptions = {
 | |
|     timeoutMs: 2000,
 | |
|     interval: 100,
 | |
|   };
 | |
| 
 | |
|   const { timeoutMs, interval } = {
 | |
|     ...defaults,
 | |
|     ...options,
 | |
|   };
 | |
| 
 | |
|   const endTime = Date.now() + timeoutMs;
 | |
|   let stop = false;
 | |
|   const finish = () => {
 | |
|     stop = true;
 | |
|   };
 | |
| 
 | |
|   const _poll = async (resolve: any, reject: any) => {
 | |
|     if (stop) {
 | |
|       resolve();
 | |
|     } else if (Date.now() >= endTime) {
 | |
|       finish();
 | |
|       reject(new Error('Periodic check timeout'));
 | |
|     } else {
 | |
|       try {
 | |
|         await toPromise(task(finish));
 | |
|       } catch (e) {
 | |
|         finish();
 | |
|         reject(e);
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       setTimeout(() => {
 | |
|         void _poll(resolve, reject);
 | |
|       }, interval);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   return new Promise((resolve, reject) => {
 | |
|     void _poll(resolve, reject);
 | |
|   });
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Creates a promise which waits until `check` returns `true` or rejects if `timeout` preiod is reached.
 | |
|  * If `timeout` is reached then this will throw an Error.
 | |
|  *
 | |
|  * @param check The boolean check.
 | |
|  * @param timeout The time before an error is thrown.
 | |
|  */
 | |
| export async function waitUntil(check: () => Return<boolean>, timeoutMs: number = 2000) {
 | |
|   // This is causing unhandled promise rejection somewhere in MessageQueue tests
 | |
|   return poll(
 | |
|     async done => {
 | |
|       const result = await toPromise(check());
 | |
|       if (result) {
 | |
|         done();
 | |
|       }
 | |
|     },
 | |
|     {
 | |
|       timeoutMs,
 | |
|       interval: timeoutMs / 20,
 | |
|     }
 | |
|   );
 | |
| }
 | |
| 
 | |
| export async function timeout<T>(promise: Promise<T>, timeoutMs: number = 2000): Promise<T> {
 | |
|   const timeoutPromise = new Promise<T>((_, rej) => {
 | |
|     const wait = setTimeout(() => {
 | |
|       clearTimeout(wait);
 | |
|       rej(new TaskTimedOutError());
 | |
|     }, timeoutMs);
 | |
|   });
 | |
| 
 | |
|   return Promise.race([timeoutPromise, promise]);
 | |
| }
 | |
| 
 | |
| export async function delay(timeoutMs: number = 2000): Promise<Boolean> {
 | |
|   return new Promise(resolve => {
 | |
|     setTimeout(() => {
 | |
|       resolve(true);
 | |
|     }, timeoutMs);
 | |
|   });
 | |
| }
 | |
| 
 | |
| // tslint:disable: no-string-based-set-timeout
 | |
| export const sleepFor = async (ms: number) =>
 | |
|   new Promise(resolve => {
 | |
|     setTimeout(resolve, ms);
 | |
|   });
 | |
| 
 | |
| // Taken from https://stackoverflow.com/questions/51160260/clean-way-to-wait-for-first-true-returned-by-promise
 | |
| // The promise returned by this function will resolve true when the first promise
 | |
| // in ps resolves true *or* it will resolve false when all of ps resolve false
 | |
| export const firstTrue = async (ps: Array<Promise<any>>) => {
 | |
|   const newPs = ps.map(
 | |
|     async p =>
 | |
|       new Promise(
 | |
|         // eslint-disable more/no-then
 | |
|         // tslint:disable: no-void-expression
 | |
|         (resolve, reject) => p.then(v => v && resolve(v), reject)
 | |
|       )
 | |
|   );
 | |
|   // eslint-disable-next-line more/no-then
 | |
|   newPs.push(Promise.all(ps).then(() => false));
 | |
|   return Promise.race(newPs) as Promise<Snode>;
 | |
| };
 |