about:benjie

Random learnings and other thoughts from an unashamed geek

Multiple Asynchronous Callbacks

| Comments

When programming in JavaScript (or CoffeeScript) you sometimes face a situation where you need to complete multiple independant asynchronous methods before continuing. This situation crops up in web browsers but its much more common when writing server-side JavaScript, e.g. with Node.js - for example you might need to fetch from a database, fetch from a KVS, read a file and perform a remote HTTP request before outputting the compiled information to the end user.

One method of solving this issue is to chain the asynchronous calls, however this means that they’re run one after the other (serially) - and thus it will take longer to complete them. A better way would be to run them in parallel and have something track their completion. This is exactly what my very simple AsyncBatch class does:

.coffee.jsAsyncBatch class, triggers event once all wrapped callbacks complete
1
2
3
4
5
6
7
8
9
10
11
class AsyncBatch extends EventEmitter
  constructor: ->
    @_complete = {}
    @_scheduled = {}

  wrap: (name,cb) ->
    @_scheduled[name] = true
    return =>
      @_complete[name] = cb.apply @, arguments
      if Object.keys(@_complete).length == Object.keys(@_scheduled).length
        @emit 'done', @_complete

To use AsyncBatch, just do the following

.coffee.jsHow to use AsyncBatch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
###
Create a new AsyncBatch instance
###
batch = new AsyncBatch

###
Wrap your callbacks with batch.wrap and a name
The name is used to store the result returned by your callback
(Don't forget to return the result you're interested in!)
###
delay 50, batch.wrap 'timer', ->
  return "Timer complete"

###
Add a completion handler that accepts `results` as a parameter.
  `results` is a JS object where the keys are the callback names 
  from above and the values are the return values of the
  callbacks.
###
batch.on 'done', (results) ->
  console.log "Batch complete, timer result: #{results.timer}"

NOTE: If your asynchronous method accepts both a success and failure callback then simply wrap both individually but ensure you use the same name for both.

NOTE: Other than the case in the previous NOTE, all callbacks should have different names.

A full example (including a stub EventEmitter implementation) follows:

An example of how to use AsyncBatch to handle parallel callbacks in Node.JS (async-batch.coffee) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#EventEmitter = require('events').EventEmitter

if !EventEmitter?
  class EventEmitter
    on: (eventName, cb) ->
      @_eventCallbacks = {} unless @_eventCallbacks
      @_eventCallbacks[eventName] = [] unless @_eventCallbacks[eventName]
      @_eventCallbacks[eventName].push cb
    emit: (eventName, args...) ->
      if @_eventCallbacks && @_eventCallbacks[eventName]
        cb.apply @, args for cb in @_eventCallbacks[eventName]

class AsyncBatch extends EventEmitter
  constructor: ->
    @_complete = {}
    @_scheduled = {}

  wrap: (name,cb) ->
    @_scheduled[name] = true
    return =>
      @_complete[name] = cb.apply @, arguments
      if Object.keys(@_complete).length == Object.keys(@_scheduled).length
        @emit 'done', @_complete


batch = new AsyncBatch

##########################
# Task 1
http = require 'http'

options =
  host: 'www.example.com'
  port: 80
  path: '/'
  method: 'GET'

req = http.request options, batch.wrap 'status', (res) ->
  console.log 'STATUS: ' + res.statusCode
  console.log 'HEADERS: ' + JSON.stringify(res.headers)
  return res.statusCode

req.on 'error', batch.wrap 'status', (e) ->
  console.log 'problem with request: ' + e.message
  return null

req.end()

#########################
# Task 2
fs = require 'fs'

fs.readFile '/etc/hosts', 'utf8', batch.wrap 'hosts', (err, data) ->
  if err
    return null
  console.log data
  tmp = data.split "\n"
  return tmp[Math.floor(Math.random()*tmp.length)]

########################
# Task 3
delay = (ms, cb) -> setTimeout cb, ms

delay Math.random()*100, batch.wrap 'timer', ->
  console.log "Timer done"
  return true

#########################
# Completion:
batch.on 'done', (results) ->
  console.log "\n\n\nThe fetch, read and timer have completed."
  if results.status != null
    console.log "Fetch succeeded, status code: #{results.status}"
  else
    console.error "Fetch failed!"
  if results.hosts != null
    console.log "Random entry from /etc/hosts: "+results.hosts
  else
    console.error "Couldn't read /etc/hosts!"

batch.on 'done', (results) ->
  console.log "\nSecond completion callback, raw results:"
  console.log results

Comments