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

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