npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

babel-plugin-meteor-tracker-async-enhance

v1.0.2

Published

This babel plugin tries to alleviate problems with MeteorJS Trackers' autoruns & reactive code when used together with async / await.

Downloads

422

Readme

Meteor Tracker-Async-Enhance Babel Plugin for the Client 🍝

This plugin allows you to keep your client side code reactive even when using multiple - and nested - await statements inside Tracker.autorun contexts.

It allows you to not having to add Tracker.withComputation statements manually for many common use cases.*

  • allows you to skip writing Tracker.withComputation explicitly after each await for code querying reactive data sources to keep the reactivity.
  • works for code in you Tracker.autorun as well as all async functions called from autorun contexts.

Introduction

This babel plugin tries to alleviate problems with Meteor Trackers' autoruns & reactive code when used together with async / await.

This works by adding additional code to the client code in your app which keeps track of the current computation (this is what makes Tracker work behind the scenes) across async await calls / restoring the current computation after a call to await.

We store a copy of the current Tracker.currentComputation inside each async function in your code and then restore it after each await call, but only in the current functions' context... it's complicatqed :) .

It's incomplete, but it seems to be a working first version, with limitations.

Here's a sandbox to play & test with: Play with it for yourself & see what it does for your client code here!

Here is an example for the transformations that will be applied to all async functions & awaits:

Turns this code:

async function test() {
    const a = await this.getA()
    const b = await this.getB()
    const c = await this.getC()
    return [a, b, c]
}

into this:

async function test() {
    const ____secretCurrentComputation____ = Tracker?.currentComputation || null;              // Store Tracker.currentComputation if it exists.

    const a = await this.getA();
    return Tracker.withComputation(____secretCurrentComputation____, async () => {             // The first async function still gets the current computation.
        const ____secretCurrentComputation____ = Tracker?.currentComputation || null;          // But after that it'll be gone.

        const b = await this.getB();
                                                                                               
        return Tracker.withComputation(____secretCurrentComputation____, async () => {         // So we wrap the rest of the functions' body in a Tracker.withComputation
            const ____secretCurrentComputation____ = Tracker?.currentComputation || null;      // *for each await statement* so it'll keep the autorun global around after it will continue.

            const c = await this.getC();                                                       // BUT also so that the autorun will be cleaned up after
            return Tracker.withComputation(____secretCurrentComputation____, async () => {     // the block has been executed... which is very important as to
                                                                                               // not have other, wrong dependencies registered because the Tracker.Autorun context
                const ____secretCurrentComputation____ = Tracker?.currentComputation || null;  // hasn't been removed correctly.
                return [a, b, c];                                                              
            });
        });
    });
}

We know it's not beautiful - but it works.

It helps us keep our complex codebase with lots of helper code reactive for in our blaze project! 🚂

Play with it for yourself & see what it does for your client code here!

Issues & Performace

  • Call Stack gets multiple times deeper for async functions, but
  • Bundle Size- and Performance - Impact seem to be surprisingly small - for what it does.
    • Bundle size: + 10-20% for your handwritten client code (which is probably less than you think). Packages etc aren't impacted out of the box.
    • Performance: Also possibly 10%-ish from our experiments (we ran performance tests using chrome debugger), at most. As this happens mostly on the client, and the client code should be relatively "thin" & limited in scope anyhow, we judged it a fair tradeoff. Most things in the client are more limited by database access, network access / waiting & animations, transitions etc.

Features & Limitations

This works:

const reactiveVar = new ReactiveVar(42)

Tracker.autorun(async () => {
    // basic awaits are supported
    await somethingOrOther()

    // assignments from awaits work
    const a = await Meteor.currentUserAsync()

    // That's all that's guaranteed to work. This always works for the first level of the function body
    // of async functions.

    // normally after the first await you'd have to wrap each reactive call in a Tracker.withComputation() statement
    const b = await Collection1.findOneAsync()
    const c = await Collection2.findOneAsync()

    // in order for your code to stay reactive. But with the bebel plugin you won't need to do this anymore. 
    const myVal = reactiveVar.get()
})

This probably doesn't work:

Tracker.autorun(async () => {
    // will probably not work, as we only support basic `await` statements & assignments from `await` statements
    const z = (await a() / Math.floor(await b()) * await c())

    // this probably also won't work as expected yet:
    for (var z0 = 0; z0 < 5; z0++) {
      // we won't inject our code here. Feel free to use Tracker.withComputation manually if you like:
      await abcAsync()
      const z = myReactiveVar.get()
    }

    // but this will:
    // this probably also won't work as expected yet:
    for (var z0 = 0; z0 < 5; z0++) {
      // we won't inject our code here. Feel free to use Tracker.withComputation manually if you like:
      await abc()
      const z = Tracker.withComputation(Tracker.currentComputation, async () => {
          return myReactiveVar.get()
      })
    }

    const myVal = reactiveVar.get()
})

What is still missing?

  • doesn't work with multiple awaits in a single expression (line) yet. The async function(s) called after the initial await will not have the context. Can be mitigated by not using multiple awaits in a single line / expression.

    If necessary / mitigation strategy:

    • pull out the await results into separate lines & assignments and use the results in the operation.

      so instead of

       const z = await getA() && await getB()

      do

       const a = await getA()
       const b = await getB()
       const z = a && b

      for example.

  • also doesn't work for loops & if/else blocks currently... easiest is to pull out the await returns to outside the block statement or use Tracker.withComputation manually inside. _If the loop uses an async callback function it'll actually work, as that opens a new async function and inside of that, await & await assignments will work again. _

  • doesn't work with regular old Promise - objects yet. Promise-Objects could be monkey-patched by overriding their constructor though I think.

Installation

  1. Add the plugin to your app:

    meteor npm install --save-dev babel-plugin-meteor-tracker-async-enhance

  2. Add the plugin to your babel configs' plugins array.

Eg. we're using a .babelrc file in our meteor project dir looking like this:

{
    "presets": ["@babel/preset-env"],
    "plugins": ["babel-plugin-meteor-tracker-async-enhance"]
}

You can verify that the plugin is transforming your client code by using eg. the chrome inspector and turning off the "Enable JavaScript source maps" in the preferences pane.

If you then visit your apps' bundled .js file, you should be able to find instances of ____secretCurrentComputation____ throughout the code.

Future optimizations and improvement ideas:

  • Only add the additional code to async functions actually containing awaits in their code, otherwise it's not necessary.

  • I think I have some ideas on how to extend this to work for more / deeper code cases:

    • pulling await results out of expression statements could be possible
    • Restoring the context after loops & block statements could also allow their "bodies" to be covered / supported going forward