@sabl/async-test
TypeScript icon, indicating that this package has built-in type declarations

0.5.0 • Public • Published

codecov

version: 0.5.0 | tag: v0.5.0 | commit: 60f7c80b8

@sabl/async-test

async-test contains several simple utilities for testing concurrent or async programs in JavaScript and TypeScript. It builds on @sabl/async, which contains utilities that are useful in both test and production scenarios.

API

later

later<T>(fn: () => T, ms?: number): Promise<T>
later<T>(fn: () => Promise<T>, ms?: number): Promise<T>
later<T>(value: T, ms?: number): Promise<T>
later<T>(promise: Promise<T>, ms?: number): Promise<T>

later resolves a value or function after a delay. It wraps JavaScript's setTimeout as a promise. It is especially helpful in testing async programs to create artificial delays that work with await. The input can be any of the following:

Note that if the ms timeout is less than or equal to zero, the call will be rejected immediately even if an input promise was already resolved.

Synchronous function

With a synchronous function, later works identically to setTimeout except that it returns a promise.

// Before
setTimeout(() => console.log('hello'), 10);

// After 
later(() => console.log('hello'), 10); 

The promise resolves to whatever value is returned by the callback.

const promise = later(() => {
  console.log('hello');
  return 'world'
}, 10); 
const result = await promise;
console.log(result); // world

Async function

If the value provided to later itself returns a promise, then that promise will be awaited before later resolves. Note that the callback will not be started until the timeout expires, so the total time before the function resolves may be longer than ms. If you simply wish to set a timeout for how long an async function may take, use limit.

const promise = later(() => Promise.resolve('hello'), 10);
const result = await promise;
console.log(result); // hello

Existing promise

An existing promise will be await ed after the timeout to resolve the final value.

const innerPromise = Promise.resolve('hello');
const promise = later(innerPromise, 10);
const result = await promise;
console.log(result); // hello

Literal value

Any value that is not a function or promise will be used as is to resolve the promise after the timeout:

const promise = later('hello', 10);
const result = await promise;
console.log(result); // hello

Timeline

export class Timeline {
  constructor(tickMs?: number);

  get tick(): number;
  get running(): boolean;
  get drained(): boolean;

  setTimeout(cb: () => unknown, ticks: number): number;
  clearTimeout(id: number): boolean;
  wait(ticks: number): Promise<void>;
  
  start(): void;
  reset(): Promise<void>;
  next(): Promise<void>;
  drain(): Promise<void>;
}

Timeline schedules callbacks to be executed in a deterministic order one frame at a time. It designed to set up tests of async programs where the exact order of async events matters. Frame numbers add up intuitively for understandable ordering.

Timeline's setTimeout and clearTimeout methods are replacements for using the otherwise builtin setTimeout and clearTimeout, which do not actually guarantee that a callback will be executed after the exact number of ms specified. `

constructor

new Timeline(tickMs?: number) 

The constructor accepts a single options parameter tickMs, which determines the number of platform ms to wait before starting the next tick. If null, there is no pause between ticks but the timeline will idle when drained.

If 0 or positive, the platform setTimeout(..., tickMs) will be awaited between ticks, and ticking will continue until the timeline is reset even if there are no callbacks scheduled.

tick

The current tick number.

running

Whether the timeline is running.

drained

true if there are no scheduled callbacks

setTimeout(fn, ticks = 0)

Schedule a callback to be executed ticks ticks in the future. Can be called from within a callback. Returns an id which can be used to clear the callback.

If ticks is exactly 0:

  • If timeline has not yet started, callback will be invoked before the first (frame 1) tick
  • If timeline is running, callback will be invoked at the end of the current tick
  • Same-frame scheduling is respected recursively, including for async callbacks

clearTimeout(id)

Clear a previously scheduled callback using its id value as returned from setTimeout.

wait(ticks)

Returns a promise which will be resolved after ticks ticks. Useful for succinctly awaiting in tests to let a certain number of frames complete, regardless of how long that takes in real time.

start()

Start the timeline.

  • If tickMs provided to constructor is >= 0

    Timeline will continue ticking indefinitely until reset() is called, and will wait tickMs milliseconds between ticks using the platform setTimeout.

  • If tickMs is empty (default)

    Timeline will cycle without stopping through all scheduled frames. When all callbacks have been cleared or called, the timeline will pause until setTimeout is called again.

drain()

Returns a promise which will resolve when all scheduled callbacks have been cleared or executed.

reset()

Cancels and clears all pending callbacks, stops ticking, and resets tick number to 0.

next()

Advance a single tick on a timeline that was never started. Can be used to manually tick forward one frame at a time. Returns a promise which resolves when all callbacks from the frame have been executed, and any promises returned from them have resolved or rejected.

Example

A contrived example that demonstrates deterministic ordering and additive frame numbers:

const tl = new Timeline();
const log: string[] = [];
const logTick = (msg: string) => log.push(`tick ${tl.tick} : ` + msg);

tl.setTimeout(() => logTick('E @ 3'), 3); 

tl.setTimeout(() => { 
  tl.setTimeout(() => logTick('G @ 3 + 3 = 6'), 3);
}, 3);

tl.setTimeout(async () => {
  logTick('A @ 1');  
  
  await new Promise(resolve => setTimeout(resolve, 500));

  tl.setTimeout(() => {
    logTick('B @ 1 + 1 = 2'); 

    tl.setTimeout(() => logTick('F @ 1 + 1 + 3 = 5'), 3);
    tl.setTimeout(() => logTick('C @ 1 + 1 + 0 = still 2'), 0);
    tl.setTimeout(() => {
      tl.setTimeout(() => {
        tl.setTimeout(() => {
          logTick('D @ 1 + 1 + 0 + 0 + 0 = *still* 2')
        }, 0);
      }, 0);
    }, 0);
  }, 1);
}, 1);

tl.setTimeout(() => logTick('pre-tick @ 0')); // Can omit 0 timeout

tl.start();

await tl.drain();

console.log(log.join('\n'));

// tick 0: pre-tick @ 0
// tick 1: A @ 1 
// tick 2: B @ 1 + 1 = 2
// tick 2: C @ 1 + 1 + 0 = still 2
// tick 2: D @ 1 + 1 + 0 + 0 + 0 = *still* 2
// tick 3: E @ 3
// tick 5: F @ 1 + 1 + 3 = 5
// tick 6: G @ 3 + 3 = 6

Readme

Keywords

Package Sidebar

Install

npm i @sabl/async-test

Weekly Downloads

1

Version

0.5.0

License

MIT

Unpacked Size

82.4 kB

Total Files

20

Last publish

Collaborators

  • jhonig-malva