Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is my iterator being advanced again?

I have the following program - I use genny.js to handle the asynchronous flow control - I have tried the same with suspend.js - similar error.

I am using the Stripe nodejs API.

My iterator function seems to be called twice - which is causing an error - and I don't understand why it gets called twice. It must be a simple mind trick that I am not seeing.

var genny = require('genny')
genny.longStackSupport = true

var stripe = require("stripe")("sk_live_....")

fetchCharges = genny.fn(function* (d) {
  console.log("Before fetchCharges")
  var charges = yield fetchList(d())
  console.log("After fetchCharges - found ", charges.length)
  return true
})

fetchList = genny.fn(function* (done) {
  console.log("before fetchList")
  var results = yield stripe.charges.list({}, done())
  console.log("after fetchList")
  return results.data
})

genny.run(function* (resume) {
  console.log('before run')
  yield fetchCharges(resume())
  console.log('after run')
})

The console output is:

> node --harmony genny.js

before run
Before fetchCharges
before fetchList
after fetchList
After fetchCharges - found  10
after run
/Volumes/dev/ingest/node_modules/genny/index.js:50
        else throw e; 
                   ^
Error: callback already called
    at resume (/Volumes/dev/ingest/node_modules/genny/index.js:154:39)
    at throwAt (/Volumes/dev/ingest/node_modules/genny/index.js:49:30)
    at resume (/Volumes/dev/ingest/node_modules/genny/index.js:153:28)
    at tryProcessPending (/Volumes/dev/ingest/node_modules/genny/index.js:41:28)
    at resume (/Volumes/dev/ingest/node_modules/genny/index.js:164:17)
    at null._onTimeout (/Volumes/dev/ingest/node_modules/stripe/lib/StripeResource.js:87:34)
    at Timer.listOnTimeout (timers.js:110:15)
From generator:
    at /Volumes/dev/ingest/genny.js:22:26

Now, if I replace fetchList with the following function it works fine:

fetchList = genny.fn(function* (done) {
  console.log('before doTimeout')
  console.log('1sec break ...')
  yield setTimeout(done(), 1000);
  console.log('after doTimeout')
  return []
})

The console output is:

> node --harmony genny.js

before run
Before fetchCharges
before doTimeout
1sec break ...
after doTimeout
After fetchCharges - found  0
after run

To further illustrate the fact that the itertor's next() method gets called twice - I have another (non-working) version of the program.

var genny = require('genny')
genny.longStackSupport = true

var stripe = require("stripe")("sk_live_...")

fetchCharges = genny.fn(function* (d) {
  console.log("Before fetchCharges")
  var charges = yield fetchList(function(err, cb) {
    console.log("callback")
  })
  console.log("After fetchCharges - found ", charges.length)
  return true
})

fetchList = genny.fn(function* (done) {
  console.log("before fetchList")
  var results = yield stripe.charges.list({}, done())
  console.log("after fetchList")
  return results.data
})

genny.run(function* (resume) {
  console.log('before run')
  yield fetchCharges(resume())
  console.log('after run')
})

And it's console output is here:

> node --harmony genny.js

before run
Before fetchCharges
before fetchList
after fetchList
callback
callback

It's weird - and I don't get it. Can somebody cleverer than me please explain.

UPDATE

I have changed the code to call the stripe methods without a callback or the iterator resume function. And now it works. BUT - curiously - look at the console at the "results". I don't get why. So now it doesn't call the fetchList iterator's next() function "a second time" - but I don't see where it even gets called once!?

var results = yield stripe.charges.list()

Here's the updated complete program.

var genny = require('genny')
genny.longStackSupport = true

var stripe = require("stripe")("sk_live_i6TrEk5lSRM1CmbSZZPsQzKc")

fetchCharges = genny.fn(function* (d) {
  console.log("  fetchCharges {")
  var charges = yield fetchList(d())
  console.log("  } fetchCharges - found ", charges.length)
  return true
})

fetchList = genny.fn(function* (done) {
  console.log("    fetchList {")
  var results = yield stripe.charges.list({}, function(err, results) {
    console.log("results ")
  })
  console.log("    } fetchList")
  return results.data
})

genny.run(function* (resume) {
  console.log('Before run {')
  yield fetchCharges(resume())
  console.log('} after run')
})

This returns

> node --harmony genny.js

Before run {
  fetchCharges {
    fetchList {
    } fetchList
  } fetchCharges - found  10
} after run
results 
like image 546
Joerg Avatar asked May 03 '15 07:05

Joerg


1 Answers

The problem you are experiencing stems from a mix of two approaches for asynchrony.

The stripe API docs mentions

Every resource method accepts an optional callback as the last argument.
Additionally, every resource method returns a promise.

However, both genny and suspend do

work seamlessly with Node callback conventions and promises

And here lies your mistake: in the line

var results = yield stripe.charges.list({}, done())

you do implicitly use both at the same time. The done() does create a callback which is passed to stripe, but that call also produces a promise which is yielded, and genny/suspend register another callback on it. This leads to the Error: callback already called you are observing.

You may choose how you want to fix the problem:

  • don't yield the promise

    var results = yield void stripe.charges.list({}, done())
    //                  ^^^^
    
  • don't pass a callback

    var results = yield stripe.charges.list({})
    

(I'd recommend the latter)

like image 173
Bergi Avatar answered Sep 22 '22 17:09

Bergi