Coroutines

Coroutines

Nemo vel ad consectetur namut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod.

Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius!

Usage

import Coroutine from 'coroutine.ts';

// Coroutines support generator functions
function* myLongRunningAsyncFunction() {
  let i = 0;
  while (i < 500){ i++; yield null; }
  return ["list", "of", "results"]
}

// Here's an example of defining, running, and waiting for a coroutine.
function runAsyncThing() {
  // 1. Define the coroutine
  const co = new Coroutine<null|string[]>(() => myLongRunningAsyncFunction());

  // 2. Await the promise to detect when the coroutine is done
  try { await co.promise; }
  catch {
    // There was an error running the coroutine!
    return null;
  }

  // 3. Check the value returned by the coroutine object for the result
  if (co.value) {
    // co.value === ["list", "of", "results"]
  }
}

Implementation

export enum CoroutineType {
  RequestIdleCallback,
  RequestAnimationFrame,
  SetTimeout,
}

export default class Coroutine<T> {
  private generator: IterableIterator<T>;
  private lastValue: T;
  public stopped: boolean = false;
  public complete: boolean = false;
  private resolve: (coroutine: this) => void;
  private reject: (result: { error: Error; coroutine: Coroutine<T> }) => void;
  private promise_: Promise<Coroutine<T>>;

  public timeRunning: number = 0;

  public get promise() {
    return this.promise_;
  }
  public get value() {
    return this.lastValue;
  }

  constructor(private fn: () => IterableIterator<any>) {
    this.generator = fn();
    this.stopped = false;
    this.promise_ = new Promise<this>((resolve, reject) => {
      this.resolve = resolve;
      this.reject = reject;
    });

    CoroutineManager.Instance.add(this);
  }

  public run = () => {
    if (this.stopped || this.complete) {
      return;
    }
    const start = Date.now();

    let result;
    try {
      result = this.generator.next();
    } catch (err) {
      this.reject({ error: err, coroutine: this });
    }

    this.lastValue = result?.value === undefined ? this.lastValue : result.value;

    if (result?.value === undefined) {
      this.complete = true;
      this.reject({ error: new Error("No return value found!"), coroutine: this });
      return;
    }

    if (result.done) {
      this.complete = true;
      this.resolve(this);
    }

    this.timeRunning += Date.now() - start;
  };

  public reset = () => {
    this.generator = this.fn();
    this.lastValue = undefined;
    this.stopped = false;
    this.complete = false;
    this.promise_ = new Promise<this>((resolve, reject) => {
      this.resolve = resolve;
      this.reject = reject;
    });

    return this;
  };

  public stop = () => {
    this.stopped = true;
    return this;
  };
}

export class CoroutineManager {
  public static Instance = new CoroutineManager();
  private readonly rICOptionsObj = { timeout: 1000 / 24 };

  private coroutines: Coroutine<any>[] = [];
  private maxRunning: number = 5;

  public get count(): number {
    return this.coroutines.length;
  }

  public type: CoroutineType = CoroutineType.RequestIdleCallback;
  private running = true;

  // this should be at MOST 1000 / 60.
  // anything > 60 will look smooth, but will take long.
  // 90 here is arbitrary and found after some testing
  public frameBudgetMs = 1000 / 90;

  private constructor() {
    this.start();
  }

  public start = () => {
    this.running = true;
    this.update();
  };

  public stop = () => {
    this.running = false;
  };

  public add = (coroutine: Coroutine<any>) => {
    this.coroutines.push(coroutine);
    return this;
  };

  public remove = (co: Coroutine<any>) => {
    this.coroutines = this.coroutines.filter((x) => x !== co);
    return this;
  };

  public update = () => {
    if (!this.running) {
      return;
    }

    const start = Date.now();
    let runningCount = 0;
    for (const coroutine of this.coroutines) {
      if (!coroutine.complete && !coroutine.stopped) {
        runningCount++;
        if (runningCount > this.maxRunning) {
          break;
        }
        coroutine.run();

        const cost = Date.now() - start;
        if (cost > this.frameBudgetMs) {
          console.warn(`Coroutines exceeded frame budget! (${cost} / ${this.frameBudgetMs})`);
          // we have exceeded our time budget; stop updating coroutines
          break;
        }
      }
    }

    // Prune complete coroutines
    this.coroutines = this.coroutines.filter((x) => !x.complete);

    // Queue next update based on the type of coroutine
    switch (this.type) {
      case CoroutineType.RequestIdleCallback:
        requestIdleCallback(this.update, this.rICOptionsObj);
        break;
      case CoroutineType.RequestAnimationFrame:
        requestAnimationFrame(this.update);
        break;
      case CoroutineType.SetTimeout:
        setTimeout(this.update, 1);
        break;
      default:
        throw new Error(`Invalid coroutine type provided: "${this.type}"`);
    }
  };
}

Related Posts

TypeScript + Lua

TypeScript + Lua

Nemo vel ad consectetur namut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat.

Read More
Z-Order Curve

Z-Order Curve

Nemo vel ad consectetur namut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat.

Read More
Bitmasker

Bitmasker

Small helper class to track ‘flagged’ state. Works together with an enum and provides easy toggled/not toggled checks.

Read More