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

Async support? #18

Open
00dani opened this issue May 2, 2013 · 8 comments
Open

Async support? #18

00dani opened this issue May 2, 2013 · 8 comments

Comments

@00dani
Copy link

00dani commented May 2, 2013

Teacup doesn't have any support for asynchronous templating at present. There are various JS templating engines with async support; here's a few examples:

Could and should asynchronous support be added to Teacup, too? (For what it's worth, I'd personally most prefer promise support, as in QEJS, if any async support is to be added.) I expect adding such will complicate the currently-pretty-simple codebase rather a lot, though, so it's fine if it's left out.

@asalant
Copy link
Contributor

asalant commented May 2, 2013

Interesting. So you could have blocks that run async with a callback and when complete their return value could render to the location in the template where the block is defined. Or the block could be passed a reference to an object it could render to and the rendered content would be inserted where the block is in the template.

Would it look something like?:

div (done) ->
  someAsyncCall (err, result) ->
    done err, ->
      span "Got async result: #{result}"

Need to mull on it a bit to see if there would be a straightforward path.

@00dani
Copy link
Author

00dani commented May 2, 2013

Well, I'd personally prefer promise support over callback support for Teacup, if asynchronous templating is to be made possible.

Specifically, I'd like Teacup to be able to operate asynchronously such that any promises passed as arguments into tag functions are resolved before trying to render that tag. The tag functions would need to detect when they receive promise arguments (identified as objects with a then method) and reserve empty blocks in the template accordingly.

# base case
div somePromiseProducingAsyncCall()
# leaves a block open inside the <div>
# when the promise is fulfilled its value is used to fill in that block
# equivalent to above suggested callback-style:
raw (done) -> # I think raw is needed, since there needs to be *something* providing the done callback?
  someCallbackTakingAsyncCall (err, res) ->
    done err, -> div res

The above base case is definitely possible. I'd also like for tag functions to work when called inside a then; that's something I'm not totally sure can be achieved:

# should allow tag functions inside a then, like this:
div someAsyncCall().then (res) ->
  span "Got async result: #{res}"
# may require that 'then' wraps the tag func call in a function
# since then Teacup may choose when to call the function and what context to use, etc.
# would look like this:
div someAsyncCall().then (res) ->
  -> span "Got async result: #{res}"
# functionally equivalent to first callback sample, shown again below
div (done) ->
  someAsyncCall (err, res) ->
    done err, ->
      span "Got async result: #{res}"

Edit: On further thought, I think div promise.then (res) -> -> span res would work implicitly if we follow the consistent rule that "when a promise is passed as an argument, it is resolved, and then whatever result it produces is used as the argument".

render and renderable would still produce HTML as a simple string when they can, i.e., when no promises have been used. If any promises need to be waited on, then the render function would instead return a promise for the rendered HTML. (Alternatively, they might just always produce a promise for rendered HTML, for consistency?)

# if there are any promises used inside a template
# then calling it returns a promise for the rendered HTML instead of a string
template = renderable ->
  div somePromise
template().then (html) ->
  res.send html

Of course, standard callbacks are rather more ubiquitous than promises, so you might prefer to support the former.

Although if you do add either kind of async support, supporting both isn't actually that complicated, since the basic "reserve space in this block for stuff we're waiting on" code will be identical. You could even do this:

if Q.isPromise block # we have a promise
  promise = block
else if _.isFunction block and block.length is 1 # we have a callback async block
  deferred = Q.defer()
  block deferred.makeNodeResolver()
  promise = deferred.promise
# work with async stuff; we now only have promises and no callbacks

@stanch
Copy link

stanch commented Jun 26, 2013

What about using IcedCoffeeScript instead of the vanilla compiler? It adds CPS transformations (aka dataflow concurrency).
Something like

await somePromiseProducingAsyncCall(), defer result
div result

@00dani
Copy link
Author

00dani commented Jun 27, 2013

@stanch That snippet you've offered isn't syntatically-valid IcedCoffeeScript, nor is it set up to use promises correctly. This is what you'd use in ICS to hook up to a promise:

await somePromiseProducingAsyncCall().then defer result
div result

But, like all IcedCoffeeScript, it compiles to a horrible mess:

var result, __iced_deferrals, __iced_k, __iced_k_noop,
  _this = this;

__iced_k = __iced_k_noop = function() {};

(function(__iced_k) {
  __iced_deferrals = new iced.Deferrals(__iced_k, {});
  somePromiseProducingAsyncCall().then(__iced_deferrals.defer({
    assign_fn: (function() {
      return function() {
        return result = arguments[0];
      };
    })(),
    lineno: 0
  }));
  __iced_deferrals._fulfill();
})(function() {
  return div(result);
});

And if you have more than one promise to handle, you need to do something like this:

await 
   promiseForDiv.then defer divContent
   promiseForSpan.then defer spanContent
div divContent
span spanContent

This is hardly any better than doing it with promises alone, which would look like this:

Q.spread [promiseForDiv, promiseForSpan], (divContent, spanContent) ->
  div divContent
  span spanContent

And the ICS version has the added disadvantage of compiling to CPS-transformed mess instead of JavaScript almost exactly like the source CoffeeScript.

Note that either method still requires that the actual templating process remains completely synchronous: All asynchronous calls are resolved to their results before the templating starts. Async support built into the template engine would allow for asynchronous calls to be resolved during a template's processing. It'd make the above example come out like this:

div promiseForDiv
span promiseForSpan

@hurrymaplelad
Copy link
Contributor

Still interest here?

Unless I'm misunderstanding the solutions proposed above, anyone rendering a template with promises would have to wait until all promises had resolved before they'd get any HTML back, so it shouldn't matter from the caller's perspective whether Teacup renders blocks as promises resolve into space reserved earlier, or resolves all promises before starting rendering.

If that's the case, we could write a small wrapper / mixin (teacup-as-promised?) that would change the render signature to always return a promise, and would resolve all promise arguments before starting rendering. Happy to throw together a proof of concept if there's interest.

@scien
Copy link
Contributor

scien commented Oct 10, 2015

@hurrymaplelad did you ever make progress on this?

@hurrymaplelad
Copy link
Contributor

@scien never heard more interest. Assuming ES6 promises, I'd I'd start with something like:

Teacup::renderAsync = (template, args...) ->
  Promise.all(args)
  .then (resolvedArgs) =>
    @render template, resolvedArgs...

@scien
Copy link
Contributor

scien commented Oct 20, 2015

@hurrymaplelad threw together a proof of concept using what i found to be

  1. the most straightforward syntax (no error handling, just a simple callback function to complete an asynchronous tag)
  2. the simplest implementation (not tearing apart the current codebase or starting from scratch)

there are still some issues to discuss that would likely come up in any implementation due to teacup using a single string buffer to render all templates. with any async implementation, you'd be able to have multiple renders taking place at once, and you may have nested renders, so the htmlOut implementation would likely need to change.

#57

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

No branches or pull requests

5 participants