Skip to content

Instantly share code, notes, and snippets.

@creationix
Last active March 7, 2017 18:36
Show Gist options
  • Save creationix/5762837 to your computer and use it in GitHub Desktop.
Save creationix/5762837 to your computer and use it in GitHub Desktop.
universal callback/continuable/thunk generator runner
function run(generator) {
// Pass in resume for no-wrap function calls
var iterator = generator(resume);
var data = null, yielded = false;
next();
check();
function next(item) {
var cont = iterator.next(item).value;
// Pass in resume to continuables if one was yielded.
if (typeof cont === "function") cont(resume());
yielded = true;
}
function resume() {
var done = false;
return function () {
if (done) return;
done = true;
data = arguments;
check();
};
}
function check() {
while (data && yielded) {
var err = data[0];
var item = data[1];
data = null;
yielded = false;
if (err) return iterator.throw(err);
next(item);
yielded = true;
}
}
}
@creationix
Copy link
Author

Here is an example that has sync functions, async functions, and delegate yields all mixed together!!!

This version is using thunk style.

// A sample function that calls the callback synchronously
function decr(i) {
  return function (callback) {
    callback(null, i - 1);
  };
}

// A sample function that calls the callback asynchronously
function sleep(ms) {
  return function (callback) {
    setTimeout(callback, ms);
  };
}

// A subroutine for testing delegate yield
function* sub(message) {
  process.stdout.write(message);
  for (var i = 0; i < 10; i++) {
    process.stdout.write(".");
    yield sleep(10);
  }
  process.stdout.write("\n");    
}

// Our main generator body
function* main() {
  var i = 1000; // Nice large number to test stack overflow with sync callbacks
  while (i) {
    i = yield decr(i);
    // Test delegate yields
    yield* sub("item " + i);
  }
}

// Run it! Thunk style.
run(main);

@creationix
Copy link
Author

Here is the same example, but using explicit resume. In this version we didn't have to wrap setTimeout, but we do have to pass resume around everywhere.

// A sample function that calls the callback synchronously
function decr(i, callback) {
  callback(null, i - 1);
}

// A subroutine for testing delegate yield
function* sub(message, resume) {
  process.stdout.write(message);
  for (var i = 0; i < 10; i++) {
    process.stdout.write(".");
    yield setTimeout(resume(), 10);
  }
  process.stdout.write("\n");    
}

// Our main generator body
function* main(resume) {
  var i = 1000; // Nice large number to test stack overflow with sync callbacks
  while (i) {
    i = yield decr(i, resume());
    // Test delegate yields
    yield* sub("item " + i, resume);
  }
}

// Run it! Thunk style.
run(main);

@creationix
Copy link
Author

EDIT: this is no longer valid thanks to resume's new syntax.

One real difference between the two versions is protection from callbacks being called multiple times. In the resume style code, there is a global resume function and it must be shared among all async functions. This means if a function calls the resume callback a second time, we have no way of knowing it wasn't from some newer async function. But with thunk style, we can create a new resume function for every thunk and ignore second calls to it.

@creationix
Copy link
Author

NOTE: This is using an old version of run. See next comment for new example.

I updated the code to implement callback double call checks for thunk mode.

Suppose you have an evil function like this:

function evil(callback) {
  // I'm evil, I call the callback multiple times.
  callback(null, 1);
  setTimeout(function () {
    // Try to stop me!
    callback(null, 2);  
  }, 10);
}

Then you can call it using thunk mode:

run(function* () {
  console.log("Hello");
  yield evil;
  yield sleep(1000);
  console.log("World");
});

And everything is fine because of the done flag in each thunk's unique callback.

But if you use resume mode with the shared callback, things get messy:

run(function* (resume) {
  console.log("Hello");
  yield evil(resume);
  yield setTimeout(resume, 1000);
  console.log("World");
});

Which crashes with the following output:

Hello
World

/home/tim/Downloads/files-node/files.js:201
    var cont = iterator.next(item).value;
                        ^
Error: Generator has already finished
    at GeneratorFunctionPrototype.next (native)
    at next (/home/tim/Downloads/files-node/files.js:201:25)
    at check (/home/tim/Downloads/files-node/files.js:220:7)
    at null._onTimeout (/home/tim/Downloads/files-node/files.js:193:5)
    at Timer.listOnTimeout [as ontimeout] (timers.js:105:15)

Because the second callback in evil causes sleep's yield to resume early and when sleep is finally done, the generator is already finished.

@creationix
Copy link
Author

Using the same evil function as above with the updated version of run, we get the following thunk version:

function sleep(ms) {
  return function (callback) {
    setTimeout(callback, ms);
  };
}


run(function* () {
  console.log("Hello");
  yield evil;
  yield sleep(1000);
  console.log("World");
});

Which runs with no problem, same as before.

Or if we prefer to wrap setTimeout inline, it's written as:

run(function* () {
  console.log("Hello");
  yield evil;
  yield function (callback) {
    setTimeout(callback, 1000);
  };
  console.log("World");
});

But we can also work in resume mode without errors.

run(function* (resume) {
  console.log("Hello");
  yield evil(resume());
  yield setTimeout(resume(), 1000);
  console.log("World");
});

@jmar777
Copy link

jmar777 commented Jun 12, 2013

But if you use resume mode with the shared callback, things get messy

I briefly responded on twitter, but to elaborate a bit more... isn't a nasty exception a good thing if you have async code invoking a callback multiple times? If that's happening, there's clearly a bug and I imagine it would be best to "die early and die often" in that case, rather than simply ignore it. Granted, the thunk-style approach could choose to raise a more descriptive error in that case (+1 on that from me, anyway).

@craftgear
Copy link

Hi, creationix.
May I ask you a question?

In the following code, you pass an argument to iterator.next() method.

10    var cont = iterator.next(item).value;

But whenever I pass any value to iterator.next(), there's nothing to happen.

var x = function()* {
    yield true;
}
var y = x();
y.next('foo'); //return true

What's the meaning of passing an argument to iterator.next() ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment