Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Promises? #49

Open
ToucheSir opened this issue Jun 22, 2014 · 15 comments
Open

Support for Promises? #49

ToucheSir opened this issue Jun 22, 2014 · 15 comments

Comments

@ToucheSir
Copy link

Having (optional?) support for promises will allow for even more elegant usage with libaries like co. Since fermata is already using es6 features, it wouldn't be too farfetched to either look for a global promise object or expect clients to provide custom promise implementations before using the promise interface.

@natevw
Copy link
Owner

natevw commented Jun 25, 2014

I'm not opposed to this; promises are interesting and it'll be interesting to see what comes of DOM/ES6 support for them. With fermata specifically, the one catch is that we detect the difference between e.g. the URL "http://example.com/offices/post" and the method "POST" to "http://example.com/offices" by the presence of a callback.

The "easy" way is to just provide a signal value fermata.PROMISE instead of a callback, but I'm not sure that's ideal. So it'd be something like response = example('office').post(data, fermata.PROMISE)? Hmm…

@ToucheSir
Copy link
Author

Oh, I hadn't thought about that at all. The only alternative I can think of other than having a promise adapter like fermata.PROMISE is to initialize routes first and use a static method on the fermata object to initiate the request. However, that really ruins the elegance of this library, and thus the signal value appears a better idea. Will dredge the old brain for possible alternatives.

@natevw
Copy link
Owner

natevw commented Aug 7, 2014

So here's a question for you: on the 'upcoming' branch I've got some code to support streams and better control over response types. I guess at this point it's still an experiment, but right now I just tacked on a fourth argument so it's like url.get(responseType, headers, data, cb). If responseType is 'stream' then [in node.js] you get the HTTP response object (which is a ReadableStream) back as data. It's not the most elegant solution in the world, but it was high time to try something…

Anyway, all this to say, if this experiment ships (xref #51) I suppose we could also support 'promise' as a response type (and detect it as a request data type too) like working with streams.

Basically the way I see this getting enabled is sort of similar to how Proxy is handled [and/or needs to start getting handled in the next release]:

  • detect if it's already available via ES6 runtime
  • if not, but do have CommonJS, try to require() a standin via an optionalDependency [right now Proxy is a hard dependency in package.json but I intend to change that, xref Make 'node-proxy' an optionalDependency #40]
  • otherwise just don't support

Unlike with Proxy though, I'm guessing there's several (if not dozens…) of potential promise standins, but hopefully one stands out as the simplest/sanest/correctest to choose as the optional Dependency, and then I'm assuming it should just interop even if people want to use a different Promise polyfill in their own code? [Also paging @calvinmetcalf, would love your feedback on this too!]

@ToucheSir
Copy link
Author

Looking at some other http client libraries that utilize promises, most appear to specify a hard dependency on some choice es6-compliant promise library, if the current platform lacks promise support. The main problem I see with such a method is that it's impossible for commonjs bundlers to predict at compilation time whether each and every host environment will have promise support (eg. older browsers and non-harmony node), and thus what is deemed as an "optional" promise polyfill ends up becoming dead weight when there actually exists built-in support. This only becomes confounded when working with multiple promise-based libraries, as the resulting built files will contain multiple (arguably redundant) promise implementations.

With that in mind, I have no real robust solution for allowing proper promise interop. Seeing as fermata already works without promises, perhaps it could error unless a promise implementation is provided through some hook (ie. fermata.[configurePromiseImplemenation](require('some-promise')))?

@calvinmetcalf
Copy link

Promises are all interoperable so don't worry about that, for what type if it's node only then I'd recommend bluebird which is super fast but super large, for the browser I'd recommend my own project lie.

@natevw
Copy link
Owner

natevw commented Aug 8, 2014

Thanks, that's helpful for that side of things!

On the other side, I'm realizing I need to revisit the API for this and #33 though — the whole point of promises (and streams) is not having to wait for a callback. For streams it makes a little sense, but it'd be 100% silly to have to still give fermata a callback to receive a promise…the way 'request' kind of magically handles streaming is pleasant and that's more like what we'd need for promises (and streams then too).

@NodeGuy
Copy link

NodeGuy commented Dec 11, 2014

I promise to use Fermata if you support promises!

@legomind
Copy link

Any update on this?

@natevw
Copy link
Owner

natevw commented Jan 1, 2016

@legomind Not really, I'm hoping to polish up and release the upcoming branch soon but I'm not sure if this will make the cut. There's still a lot of unknowns from my perspective:

  • what the API syntax for "triggering" a promise to get returned looks like. Do you pass a magical constant in place of the current callback? [this is still the likeliest]
  • how to give access to the request-ish object that is currently returned from an HTTP method call and would now have to give way to a promise instead
  • what the (single) fulfillment value looks like — besides the error parameter, Fermata now returns both a data-or-readable and a metadata parameter to callbacks.
  • how exactly the approach to using native/library Promise looks like (similarities/differences to similar situation with the Proxy object)

I do think promises are kind of nifty, but to be honest I'm not sold on them either. They appear only a few places in new browser API, a few libraries like jQuery sort of use them a little, while node.js itself doesn't at all… — so it seems like using them instead of the "normal" mechanisms [events/callbacks/etc.] would always be an uphill battle and honestly doesn't seem worth it to me personally.

But clearly there are many people who are using promises and would like a better solution for this library. I'll try take another look soon, and would appreciate others to chime in on the questions above, and maybe this can happen despite my lack of a personal "itch" for it.

@natevw
Copy link
Owner

natevw commented Mar 18, 2016

Wanted to write up some recent (design) progress I've made on this recently, especially a potential breakthrough I had the other day on this.

First off, I think at least in first implementation the whole "how do you handle promise library" thing might be solvable by simply having a fermata.Promise = null property on the global/module. If you want to use promises, assign your favorite promise class to that property. I think that resolves that worry I've had.

The bigger idea follows below.

Background: I've not been excited about Promise support because one key "trick" Fermata uses is the presence of a callback function as signal of intent! That is to say url.path({thing:1}) means "add /path?thing=1" to the URL whereas url.path({thing:1}, function (e,d,m) { /* … */}) means "send a PATH request with that data in the body". Now PATH is not an HTTP method I've heard of, but the underlying principle is to support any possible path component ("folder") name within a URL while also supporting any possible HTTP method name. So the simple rule is if you pass a callback, it fires off a request, if you don't it simply extends the URL. So for promises I've been thinking you'd need to still pass in some stub/signal function like url.get(fermata.FAKE_PROMISE_TRIGGERING_CALLBACK) which just seemed ugly and not worth it.

However my realization is that the Promise API still requires the user to (eventually) provide a callback! So what if, after a user set fermata.Promise = MyPromiseClass, we start returning an overridden instance of a Promise object from every URL modification/extension? (So the fermata URL "type" essentially becomes a subclass of Promise.) Or at least from the empty () call that currently returns a string representation of the URL. Anyway, that Promise subclass/override would intercept the .then method (and catch assuming that's necessary) with some similar sort of logic as we have in the previous paragraph. If you pass a callback to .then it starts an actual HTTP request in motion, otherwise it just extends the path with /then.

The example would be something like this:

var fermata = require('fermata');
fermata.Promise = global.Promise || require('some-promise-lib');

var baseUrl = fermata.json("http://example.com/api/v1"),
    wordObject = baseUrl.word_list.english.verbs.get;

wordObject.etymology.get().then(function (args) { var data = args[0], metadata = args[1]; /* … */ }, function (err) { /* … * / });

Or something like that. (To reiterate, I haven't voluntarily used Promises myself much so maybe there's a better example or I'm missing something that will prevent this from working.)

The basic gist would be combining all three of whatever "normally" gets returned by the access mechanism, the Promise instance needed, and the usual request thing you can use to cancel it. Seems messy but might just work?

@natevw
Copy link
Owner

natevw commented Mar 18, 2016

Oh, I didn't call this out, but worth expounding on here:

Another thing that made me squeamish about promises is that fermata now returns both data and metadata as two parameters (in addition to the error parameter which of course has its own mechanism with Promises). But promises can only be resolved with a single value, not two+.

But I think the answer there is to just assume if Promises and the rest of ES6 take off, then you have destructuring anyway to clean it up a bit var [data, metadata] = args;. Not ideal but not horrible either?

@ricxsar
Copy link

ricxsar commented Jun 9, 2017

Any update of this?

@natevw
Copy link
Owner

natevw commented Jun 9, 2017

@ricxsar No, I haven't gotten much feedback lately and don't use Promises enough myself to judge how useful this would be.

@natevw
Copy link
Owner

natevw commented Jan 18, 2018

Update: we need Promise support for async/await. See also #65.

@natevw
Copy link
Owner

natevw commented Jan 18, 2018

await does work with other objects, since ECMAScript 2017 section 25.5.5.3 defines AsyncFunctionAwait ± in terms of Promise.resolve on await's value, which in turn works on any then-able object:

function FakePromise() {}
FakePromise.prototype.then = function (success) {
  setTimeout(function () { success("it worked"); }, 1e3);
};

async function main() {
  console.log(await new FakePromise())
}
main().catch(console.error)

So even without #65 it might be possible to have every URL instance include a .then method which I think could use the same "did we get passed a callback?" [instead of string/array/object] as the existing HTTP method .get/.put/.del/etc. does.

let baseUrl = fermata.json("http://example.com/api/v1"),
    wordPage = baseUrl.word_list.english.adverbs.then
assert(wordPage() === "http://example.com/api/v1/word_list/english/adverbs/then")

try {
  let data = await wordPage.etymology.post({sampleUsage:"Then and there."})
  let meta = data[fermata.METADATA_SYMBOL]    // not sure where else to put it…
  // let {[M]:meta, ...data} = await url.get()
  console.log(data, meta)
} catch (err) {
  console.error(err)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants