loading...
Cover image for Returns, callbacks and the whole zoo

Returns, callbacks and the whole zoo

hoffmann profile image Peter Hoffmann Updated on ・3 min read

I'm currently co-developing a standardized communication between two entities A and B. To release my mind from all the thoughts about how an why and balancing benefits and drawbacks of different methods I'd like to share them with you. And maybe your 2¢ will help me optimizing our strategy. I'd like to add that my context is browser based JavaScript but some ideas might be generalizable.

➡ calling

If A wants to call B I find the following ways:

  1. Calling a predefined function/method in the scope of A: [B.]act(param)
  2. Using a common communication transport:
    1. by message: transport.postMessage({action: act, parameter: param}) used in inter-frame and main-thread/worker-thread communication but also (miss)usable within one document context (see Appendix A)
    2. by event: transport.dispatchEvent(new actEvent(param)).

The second point might seem overelaborate but is quite useful for decoupling and even necessary if both entities are not in the same context. One advantage of the second way is that A will just continue to work even if B is (temporarily) not available. But on the other hand B needs to actively listen to the specified events (a message is received just like any other event).

⬅ answering

There are more ways for B to react and submit a success state or return data from acting.

  1. directly return a result in case of ➡1: function act(param) { …; return success }
  2. alike ➡1: call a predefined function/method in the scope of B: [A.]doneActing(success)
  3. alike ➡2: use a common transport e.g. transport.dispatchEvent(new doneActingEvent(success)
  4. use a callback contained in param: param.callWhenDone(success)
  5. return a promise, fulfilled or rejected depending on success return new Promise(function (f, r) { (success ? f : r)(successData) })

The first is the standard way for all non-asynchronous contexts and again the second might be necessary in some cases. Asynchronous decoupling is achieved by callbacks resp. promises while promises seem to be the new "right" way to do it.

conclusion?

What are your thoughts, when should one use either one? Is interchangeability shown in Appendix B leading to one way for entity A and another for B? What about a hierarchy between both entities, would your recommendation change depending on weather A or B are more important?

Appendix

A: inter-window communication using postMessage

class B {
  constructor (targetWindow) {
    targetWindow.addEventListener('message', message => console.log(`B is reading: '${message.data}'`))
  }
}

class A {
  constructor (targetWindowOfB) {
    this.targetOfB = targetWindowOfB
  }
  letBAct (message) {
    this.targetOfB.postMessage(message, '*')
  }
}

let entityA = new A(window)
let entityB = new B(window)
entityA.letBAct('Hy, here is A, are you listening?')

B is reading: 'Hy, here is A, are you listening?'

B: transformation

Finally the trivial, most methods are interchangeable (1 is left out as a target). Here interchangeable nm is defined as the answering entity using method n and the receiving entity using method m.

1 ➝ 2:

doneActing(act(param))

1 ➝ 3:

transport.dispatchEvent(new doneActingEvent(act(param)))

1 ➝ 4:

param.callWhenDone(act(param))

1 ➝ 5:

var returnPromise = new Promise(function (f, r) {
  let success = act(param)
  (success ? f : r)(success)
  /* or */
  f(act(param))
})

2 ➝ 3:

function doneActing (success) {
  transport.dispatchEvent(new doneActingEvent(success))
}

2 ➝ 4:

function doneActing(success) {
    param.callWhenDone(success)
}

2 ➝ 5:

let returnPromise = new Promise(function (f, r) {
  function doneActing(success) {
    (success ? f : r)(success)
  }
})

3 ➝ 2:

transport.addEventListener('doneActingEvent', event => doneActing(event.data))

3 ➝ 4:

transport.addEventListener('doneActingEvent', event => param.callWhenDone(event.data))

3 ➝ 5:

let returnPromise = new Promise(function (f, r) {
  transport.addEventListener('doneActingEvent', event => (event.data ? f : r)(event.data))
})

4 ➝ 2:

param.callWhenDone = doneActing

4 ➝ 3:

param.callWhenDone = success => transport.dispatchEvent(new doneActingEvent(success))

4 ➝ 5:

let returnPromise = new Promise(function (f, r) {
  param.callWhenDone = success => (success ? f : r)(success)
})

5 ➝ 2:

promiseResponse.finally(doneActing)

5 ➝ 3:

promiseResponse.finally(param.callWhenDone)

5 ➝ 4:

promiseResponse.finally(success => transport.dispatchEvent(new doneActingEvent(success))

Posted on by:

hoffmann profile

Peter Hoffmann

@hoffmann

Father, Full Stack Developer, World Savior

Discussion

pic
Editor guide