@radekmie

On Asynchronicity in Blaze

By Radosław Miernik · Published on · Comment on Reddit

Table of contents

Intro

If I had to pinpoint a single biggest change in the way I write frontend code in recent years, I wouldn’t yell “React!” nor any other view library of framework in particular. Instead, I’d say it’s the way we handle asynchronous operations today. Whether it’s <Suspense> in React or Vue, #await in Svelte or async pipe in Angular, every single one is great in its own way.

I’m not saying we’re done with them either – React throws Promises around, Svelte (in SSR mode) won’t wait for them anyway, and Vue calls it experimental. A lot of effort went already in, but there’s a lot more to come – both from the maintainers and the users. And beta testers, of course.

But not all view layers have caught up yet. If you worked with Meteor, you’ve most likely heard of Blaze. If you didn’t, it’s basically a templating system, just like Mustache, Handlebars, or Jinja. Its two main points are its reactivity model based on Meteor’s Tracker and a direct DOM manipulation – just like Angular, React, Vue, or Svelte. (In contrary to generating static HTML.)

It’s about to change, and here’s how we did it.

Some history

Blaze is old. At first, Meteor’s templating didn’t get a name and was a set of functionalities built on top of Handlerbars1. A couple months after Meteor’s first public release, it got a name: Spark. Two years (and one total rewrite later), Blaze was born. Two and a half years later, it got a dedicated repository.

While a lot of people moved to other view layers (including me), there are still some who work with Blaze whether they want it or not (including me). Some of them are actively working on making Blaze better, and they raised a valid point: Blaze was not ready for async operations.

You could think it’s not a big deal – there are a lot of libraries out there with solely sync APIs, and it’s the user who has to take care of that. And while that’s true, Meteor 3.0 is going all-out on async, putting Blaze in a bad position.

It’s even worse, as the React integration is already sorted out (meteor/react-packages#385, meteor/react-packages#387). Ha, it even got a two-part blog post from Gabriel Grubba, who I worked with, describing how it works and why it was needed! Definitely a worthwhile read: part 1, part 2.

Drafting a plan

Back in March, when Gabriel and I were about to be finished with React integration, we agreed to look into Blaze next. At first, I proposed to look into the AST to see whether it’d be possible to inject Promise-handling code before the actual code generation starts. The idea was to make this:

{{await foo.bar.baz}}

Desugar – on the AST level – into this:

{{#letAwait temp1=foo}}
  {{#letAwait temp2=temp1.bar}}
    {{#letAwait temp3=temp2.baz}}
      {{temp3}}
    {{/letAwait}}
  {{/letAwait}}
{{/letAwait}}

Where #letAwait is a new built-in block. It works just like #let but unwraps the Promises while creating bindings. Of course, we also need a way to handle pending and rejected states too, so we decided to use {{else}}-like blocks:

{{#letAwait greeting=getGreetingAsync name=getNameAsync}}
  {{greeting}}, {{name}}!
{{pending}}
  Either `greeting` or `name` is pending.
{{rejected}}
  Either `greeting` or `name` is rejected.
{{/letAwait}}

That’s basically the same as what Svelte is doing with :then and :catch, with one major difference: Svelte’s #await unwraps one value at once, not many.

A different approach

As soon as we let others know that we’re working on it, Daniel Dornhardt reached out to us, saying that they have a working-yet-hacky version in the works. After a short meeting, we’ve decided to publish it (meteor/blaze#408) and see whether we can make it less hacky.

As it turned out, they took a completely different route and decided to unwrap values while rendering and in expressions2. The overall change was not that big (roughly 300 lines) but touched a lot of the internals. Such changes are difficult on their own, but here we have a non-TypeScript codebase.

However, the idea was tempting, as it required zero new constructs – no new built-ins and no new syntax. Of course, we’d have to add some to handle pending and rejected states, but it was still less invasive for the users.

We decided to continue investigating our AST-based approach anyway.

The implementation

A couple days later, Gabriel filed meteor/blaze#409 with a first #letAwait implementation. It was basically a copy of #let with an additional await. We saw it was not enough to handle all states, so bindings got wrapped in objects3.

Encouraged by the overwhelmingly positive feedback, I decided to take a shot at handling the other two states. The newly added __async_state__ variable available in each #letAwait block seemed to be enough:

{{#letAwait greeting=getGreetingAsync name=getNameAsync}}
  {{#if __async_state__.error}}
    Either `greeting` or `name` is rejected.
  {{else}}
    {{#if __async_state__.pending}}
      Either `greeting` or `name` is pending.
    {{else}}
      {{greeting}}, {{name}}!
    {{/if}}
  {{/if}}
{{/letAwait}}

The benefit of using a variable instead of an {{else}}-like block was that it required no changes to the AST nor parsing4. In the end, #letAwait was meant to be a rather low-level building block, and our ultimate goal is still to make it as simple as {{await expression}}.

Literally two hours later, I thought that both __async_state__ fields I added could actually be helpers instead. This could boost the performance as well because helpers are instantiated only once, while variables are defined in each template instance. I also thought that these could get a better name, and then I remembered the @index variable used in the #each block.

Final touches

At about this point, we’ve decided that I’ll be taking over this topic, so Gabriel could focus on Meteor 3.0. I spent the next couple of days working on the final API and minimizing the impact of this change on the codebase.

The first thing I decided to change was to extend #let instead of having both. I didn’t expect anyone to bind Promises with it anyway, so it’s – arguably – not a breaking change5. Similarly, new helpers were using the @ prefix, reducing the chance of name conflict significantly:

{{#let greeting=getGreetingAsync name=getNameAsync}}
  {{#if @rejected}}
    Either `greeting` or `name` is rejected.
  {{/if}}
    {{#if @pending}}
      Either `greeting` or `name` is pending.
    {{else}}
      {{greeting}}, {{name}}!
    {{/if}}
  {{/if}}
{{/let}}

As a further usability improvement, there’s also @resolved, indicating that any of the bindings has resolved. Additionally, all three accept an optional list of binding names, allowing you to check only a subset of them at once:

{{#let name=getName}}
  {{#let greeting=getGreeting}}
    {{#if @pending}}
      We are fetching your greeting...
    {{/if}}
    {{#if @rejected 'name'}}
      Sorry, an error occurred while fetching your name!
    {{/if}}
    {{#if @resolved 'greeting' 'name'}}
      {{greeting}}, {{name}}!
    {{/if}}
  {{/let}}
{{/let}}

The second thing was to separate the unwrapping and the expression chaining. I think it’s better to have simple but orthogonal (independent) features that synergize well instead of fewer but more complex ones. That’s why #let was implemented in meteor/blaze#412 and required the expression to evaluate to a Promise. That means, bounding foo.bar won’t work if foo is a Promise or a function returning one.

Nested expressions got implemented in meteor/blaze#413. It turned out to be as small as 18 lines of code – the rest are tests and comments. The only two changed functions were the one that calls helpers and the one that accesses objects. In short, foo.bar becomes foo.then(foo => foo.bar), and foo x y becomes Promise.all([x, y]).then(args => foo(...args)) (when needed).

Lastly, there’s still no support for {{await expression}}. Daniel kept nagging me about it (in a good way!), so I decided to come up with something. Creating a helper would have to duplicate #let logic somehow, so I looked elsewhere. I experimented with a template instead and… That’s it!

<template name="await">{{#let value=.}}{{value}}{{/let}}</template>

Such a template allows you to use {{> await expression}} in all contexts. If you ask me, that’s an absolute win – no new constructs are needed and yet it’s trivial. Additionally, you could build more await-like blocks, including ones with error handling.

Open topics

At the time of writing, version 2.7 of Blaze is not released yet, but you can test it out in a pre-release. It has been ready for a while now, including docs and support for thenables, but we’re still testing it in real-life projects. Having said that, there are a few things we consider experimental but not as in “expect it break” but as in “expect it to change”.

The first thing is whether the @ helpers should check for any or all bindings. It seems like an arbitrary decision, and we shouldn’t care about it much, but we’d like to promote good patterns and focus on usability. We also thought about using all for @resolved and any for the other two, but the inconsistency may lead to problems on its own.

The second thing is whether @pending should be true again, when an already resolved or rejected value is being recalculated. We’ve decided not to do it now, but it’s still being considered, as it’d enable more granular loading states. Technically, it’d be as simple as representation change3.

The third thing is what level of synchronization is desired for bindings. There’s none currently, so if a helper triggers while pending, there’s a data race. The resulting value is the one that resolves last, not starts resolving last. If the latter is desired, it has to be handled in the user space.

Lastly, we still consider unwrapping Promises in all others built-in blocks, i.e., #if, #unless, #each, and #with. Currently, you’d have to wrap them in a #let, which is not handy. At the same time, it forces you to think about the pending and rejected states, leading to better UX. (Or at least, we hope so.)

Closing thoughts

I spent the last couple of months working with deep internals of Blaze and React integrations, making them Meteor 3.0 ready. While I work mostly with React nowadays, I had a lot of fun getting back to Blaze. In the end, that was the frontend library when I started with Meteor almost 8 years ago.

At the same time, I truly adore Blaze’s simplicity. Sure, it’s not as performant as React or others6, but it took me only a couple hours to understand literally all of it. If you’d ever want to get to know a full-blown view library, this one’s good.

Finally, I’d like to thank everyone involved in the process. There are definitely a few things to cover in the nearest future, but we definitely did something great here. Most of the discussions took place on Meteor’s Slack in the #blaze-async-workgroup channel, but I won’t link them here, as they will disappear in about a month anyway. But hey, it’s free!

1

Starting off from an existing library is an excellent example of starting small. It bought Meteor developers time to focus on other things first at the cost of API limitations and performance. Luckily, this time it paid off well.

2

Blaze has a very permissive path evaluation logic, making {{foo.bar}} work both when foo is an object and a function returning one. Similarly, if foo.bar is a function, it gets called as well. With async expressions, we want it to work when foo is a Promise too.

3

Representing an async state is not trivial. On the one hand, we can represent it as a union of all three (pending, resolved, rejected). The result allows us to implement loading and error screens, which cover most of the user’s needs. On the other hand, we can extend the last two with a “is pending again” flag to indicate that a resolved value has a new one pending (stale-while-revalidate).

4

Blaze supports {{else}} blocks in custom templates, so we could use that somehow. However, there’s no way to rename it, nor have more than one in a template. Coincidentally, there’s a feature request to support that was reported just a few months earlier (meteor/blaze#404).

5

While making #let unwrap Promises is technically a breaking change, so is every fix, right? I mean, if some operation throws an error and now it doesn’t, the observable behavior has changed. Similarly, we thought that this was not a breaking change since it was not documented how it should work. Did you know that Semver allows that if “you’ll think through the impact of your changes, and evaluate the cost/benefit ratio involved”?

6

Actually, it’d be interesting to see a Blaze benchmark. It’d be much easier to do once it’s decoupled from Meteor (meteor/blaze#315), but it would be nice to compare it anyway (meteor/blaze#383).