Skip to content

Instantly share code, notes, and snippets.

@lawnsea
Last active August 29, 2015 14:05
Show Gist options
  • Save lawnsea/d77b287323f52fab04db to your computer and use it in GitHub Desktop.
Save lawnsea/d77b287323f52fab04db to your computer and use it in GitHub Desktop.

XXX: this will not work

the callback can go cause trouble, calling other functions, including awaitCallback. this is a non-starter.

instead, something like this might be the place to start:

var awaiter = new Awaiter;
setTimeout(awaiter, 100);
// any args that would have been passed to the callback are returned
var args = awaiter.wait(200);

the key is that no JS is executed while waiting. everything happens inside the host.

the issue here is the semantics of the event loop in browsers and the run-to-completion semantics of JS. something will have to give here.

we have to deal with the fact that suspending a running callback (or script) and returning to the event loop is not a thing that the spec knows how to do.

a couple ideas:

  • spawn a new event loop and copy across the awaited tasks. this breaks b/c tasks do not necessarily get scheduled until something happens. iow, the task may not be extant at the time of the call to wait. so, we will have to run the other event loop too, but schedule in our new one instead when stuff happens
  • or, stick in the same event loop, but freeze all the queues and make new ones for the awaited task sources. as tasks are added, if they correspond to awaited on stuff, then instead enqueue them on the new queues

Motivation

One of the original motivating use cases for Web Workers was doing synchronous IO off the main thread (1). With the exception of synchronous XHR, no synchronous IO APIs have been implemented by more than one major browser. Discussion of the merits and difficulties of implementing sync APIs has been repeated and heated (2, 3, 4, 5, 6, 7, 8).

  • discussion has thus far focused on sync versions of existing apis
    • it seems like a bummer to have two apis to support
  • nevertheless, there are serious usecases: emscripten, jsil, etc.

Proposal

What if code running in a Web Worker could instead simply block until an async operation completes? This would add a minimal, low-level capability to workers and make it possible to implement synchronous APIs in user code.

  • how can we do that?
  • all async ops are implemented by host objects
  • all async ops accept callbacks
  • so, blocking on a callback is equivalent to blocking on the operation

awaitCallback

The awaitCallback method accepts one or more functions and a timeout in milliseconds. Each function must be a currently pending callback registered with a host object. awaitCallback blocks until one of the passed functions is called or the specified timeout expires.

If a callback function is called, awaitCallback returns an array containing the return value of the callback function at the same index as the callback function was passed to awaitCallback and undefined in all other indices. If the timeout expires, awaitCallback throws an exception.

Usage

When a callback is called, awaitCallback returns an array containing the callback return value.

function onOneTimeout() { 
  return 'marco';
}

function onTheOtherTimeout() {
  return 'polo'
}

setTimeout(onOneTimeout, 100);
setTimeout(onTheOtherTimeout, 200);

awaitCallback(onOneTimeout, onTheOtherTimeout, 500); // -> ['marco', undefined]

If a function is passed that is not a pending callback, awaitCallback throws.

function onOneTimeout() { 
  return 'marco';
}

function onTheOtherTimeout() {
  return 'polo'
}

setTimeout(onOneTimeout, 100);

awaitCallback(onOneTimeout, onTheOtherTimeout, 500); // throws

Examples

The following examples assume the existence of a console.log polyfill in the worker.

sleep

Sleep for a number of milliseconds.

function sleep(ms) {
  var now = Date.now();

  function onTimeout() {
    return Date.now() - now;
  }

  setTimeout(onTimeout, ms);
  return awaitCallback(onTimeout, 2 * ms);
}

console.log('slept for', sleep(1234), 'ms');

waitForMessage

Wait for a postMessage message.

function waitForMessage(timeout) {
  function onMessage(e) {
    removeEventListener('message', onMessage);
    return e.data;
  }

  addEventListener('message', onMessage);
  return awaitCallback(onMessage, timeout);
}

try {
  console.log('waiting for a message');
  console.log('got one:', waitForMessage(1000));
} catch (e) {
  console.log('timed out waiting for a message');
}

Questions and Concerns

Callbacks fire out of order

Consider the following script:

setTimeout(function () {
  console.log('one');
}, 100);

function two() {
  console.log('two');
}
setTimeout(two, 200);

setTimeout(function () {
  console.log('three');
}, 300);

awaitCallback(two, 250);
/* in the console:
 * two
 * one
 * three
 */

What if the callback throws?

Should this work?

function whoops() {
  throw new Error('doh!');
}

setTimeout(whoops, 0);

try {
  awaitCallback(whoops, 100);
} catch (e) {
  console.log('saw an error:', e);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment