dollarshaveclub / postmate

📭 A powerful, simple, promise-based postMessage library.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Question about child.call and returned value

paulcookie opened this issue · comments

Is there some limitations/caveats for child.call returned value as promise, like it's done at child.get functionality? Because if I need to pass some arguments to child model functions, then I need to switch from promise based code to event driven style.. but I wanna use just one of it

@paulcookie sorry it took me so long to respond. 😞

Could you provide a code snippet? ...so I can more clearly see what you want/wanted to do. It would also be cool to see what you came up with!

wow, thanks for reply, even if it took a long time..

Postmate could do things like this:
child.get('childMethod').then(returnedValue => {...})

but if we need to pass some arguments to child method and get returned promise we can't do this easily, we should use event emitter:

// parent

child.on('childMethodSuccess', someData => { ... })
// we should subscribe to event before call to get returned data

child.call('childMethod', args) // and after we could call child with args

// child model
{
  childMethod: (args) => {
    // some async logic

    parent.emit('childMethodSuccess', returnedValue)  // instead of return we should emit custom event with our data
  }
}

so basically I wanna do things like this:

// parent

child.call('someChildMethod', args).then(someData => { ... }) // call and get Promise as returned value

// child model

{
  childMethod: (args) => new Promise((resolve, reject) => {
    // some async logic

    resolve(returnedValue) // just resolve promise with some data
  }
}

in this case we don't need to create and name custom events

@yowainwright is there a reason why the 'call' method does not return a Promise in the same way that the 'get' method does? This could be useful if you want to both (1) pass data across and (2) validate or do something with the response you’re given.

Looking at the code it seems like things could be simplified so 'call' and 'get' use the same API, e.g., if it passed any data through and then resolveValue were updated to take a third argument that uses the data iff it’s a function:

/**
 * Takes a model, and searches for a value by the property
 * @param  {Object} model     The dictionary to search against
 * @param  {String} property  A path within a dictionary (i.e. 'window.location.href')
 * @param  {Object} data      Additional information from the get request that is
 *                            passed to functions in the child model
 * @return {Promise}
 */
export const resolveValue = (model, property, data) => {
  const unwrappedContext = typeof model[property] === 'function'
    ? model[property](data) : model[property]
  return Postmate.Promise.resolve(unwrappedContext)
}

(NOTE: strangely data is already listed as a param of this function in the code!)

EDIT: I’m seeing in the test code an example that does a call, immediately followed by a get, which seems like it could work, but it still seems odd to me that we can’t just get a response from 'call'? https://github.com/dollarshaveclub/postmate/blob/master/test/acceptance/test.js#L50

Just stumbled upon this as well. It seems a little bit strange, that call wouldn't return a promise like get. @yowainwright is there any technical limitation that we're not aware of? I think this is a great library, so it would be really nice if it would support this use case.

However, here is my solution to the problem for now (based on @mrcoles observation):

// Save to postmate-async-func.js

/* Creates a function property that takes arguments and returns a promise */
export function makeAsyncFunctionProperty(func) {
  let cachedArgs = null;
  return (args) => {
    if ( args !== undefined ) {
      cachedArgs = args;
    } else {
      return func(cachedArgs).catch(e => new Error(e)); // Does not inclued stack trace
    }
  }
}

/* Enhances ParentAPI with new method "callAsync" */
export function enhanceChildAsyncCall(child) {
  child.callAsync = (fname, data) => {
    child.call(fname, data || null);
    return Promise.resolve(child.get(fname)).then(res => {
      if ( res instanceof Error  ) {
        throw res;
      }
      return res;
    });
  };
  return child;
}

The two functions can then be used like this to create async functions that take arguments:

// Your model
import { makeAsyncFunctionProperty } from './postmate-async-func'

const handshake = new Postmate.Model({
 
  asyncMult: makeAsyncFunctionProperty((value) => {
    let result = value * 5;
    return new Promise((resolve) => {
      setTimeout(() => resolve(result), 1000)
    })
  }),

  asyncAdd: makeAsyncFunctionProperty((value) => {
    let result = value + 5;
    return new Promise((resolve) => {
      setTimeout(() => resolve(result), 1000)
    })
  }),

  asyncFail: makeAsyncFunctionProperty((value) => {
    return Promise.reject()
  }),
}
// Your parent
import { enhanceChildAsyncCall } from './postmate-async-func'

handshake.then(child => {
  enhanceChildAsyncCall(child);

  child.callAsync('asyncMult', 4).then(res => {
    console.log("Got result", res)
  });

  child.callAsync('asyncAdd', 3).then(res => {
    console.log("Got result", res)
  });

  child.callAsync('asyncFail').catch(e => {
    console.log("Got error", e)
  });
})

This also takes care of rejected promises.