De-Fibers-ize
your code

...with one weird trick!

(Okay, let’s make it a few steps instead.)

About me

  1. Meteor developer for over 7 years.
  2. Software Architect at Vazco.
  3. Author of uniforms and a few other packages.

THERE IS NO TIME! WE HAVE TO GET RID OF FIBERS!

Everything is at radekmie.dev.

Disclaimer!

For more context and details, please refer to the links on the slides.

This is a rather simplified and shallow overview of the problem.

There's also my blog post.

Meteor magic

// Create the collection.
import { Mongo } from 'meteor/mongo';

const Posts = new Mongo.Collection('posts');









Collection#insert returns the inserted ID immediately both on the client and the server.






// Create a new post.
const result = Posts.insert({
  title: 'Clickbait here!',
  text: 'Have you ever...',
});














// What is in the `result`?
console.log(result);

MongoDB driver

// Create the collection.
import { MongoClient } from 'mongodb';
const client = new MongoClient('mongodb://...');
const Posts = client.db().collection('posts');









Collection#insert returns a promise of the response (it includes the inserted ID).






// Create a new post.
const result = await Posts.insertOne({
  title: 'Clickbait here!',
  text: 'Have you ever...',
});














// What is in the `result`?
console.log(result);

Where's the await?

// EXTREMELY simplified version.
MongoConnection.prototype._insert =
  function (collection_name, document, callback) {
    this.rawCollection(collection_name)
      .insertOne(document)
      .then(({ insertedId }) => { callback(null, insertedId); })
      .catch(error => { callback(error, null); });
  };








Meteor#wrapAsync magically got rid of the promise...? Let's look into it!










// Exact implementation.
_.each(["insert", "update", "remove", "dropCollection", "dropDatabase"], function (method) {
  MongoConnection.prototype[method] = function (/* arguments */) {
    var self = this;
    return Meteor.wrapAsync(self["_" + method]).apply(self, arguments);
  };
});

Down the rabbit hole...

// EXTREMELY simplified version.
if (Meteor.isServer)
  var Future = Npm.require('fibers/future');

Meteor.wrapAsync = function (fn, context) {
  return function (/* ...args, callback */) {
    if (!callback) {
      if (Meteor.isClient) {
        callback = logErr; // Logs the error (if any).
      } else {
        var future = new Future();
        callback = future.resolver();
      }
    }

    callback = Meteor.bindEnvironment(callback);
    var result = fn.apply(context || this, [...args, callback]);
    return future ? future.wait() : result;
  };
};

FIBERS!

*snip*

What can we do today?

  1. Try out the new *Async MongoDB methods (#12028).
    1. If you're not ready for that yet, at least ensure you'll be able to add await to all database operations (i.e., make the surrounding functions async).
    2. It's not always going to be possible, e.g., in some Atmosphere packages. If so, check #11505 whether it's already in progress; if not, do file an issue about that!
  2. Remove all of the Fiber and Future occurrences from your code. In most cases, it'll be as easy as one await.
    1. That includes Promise.await, Promise#await, and other Meteor-specific functions, as they rely on Fibers.

?

await without async

declare module 'meteor/promise' {
  export class Promise extends globalThis.Promise {
    static async any, This, Args extends any[]>(
      fn: Fn,
      allowReuseOfCurrentFiber?: boolean,
    ): (this: This, ...args: Args) => Promise>;
    static asyncApply any, This, Args extends any[]>(
      fn: Fn,
      context: This,
      args: Args,
      allowReuseOfCurrentFiber?: boolean,
    ): Promise>;

    static await(value: PromiseLike): T;
    static awaitAll(values: Iterable>): T[];

    await(): T;
  }
}

All of these are Meteor-specific. The less you use them, the easier it will be to migrate off the Fibers.

Questions?