Apollo/GraphQL mutation - Meteor code must always run within a Fiber

Hey Team,

I am trying to play around with the newest release of Meteor (2.7.1) and Apollo, for the GraphQL richness, but am really struggling with the mutations side of things.

When trying to add a simple database insert I get “Error: GraphQL error: Meteor code must always run within a Fiber. Try wrapping callbacks that you pass to non-Meteor libraries with Meteor.bindEnvironment.

GraphQL file:

type Device {
  _id: ID!
  mac: String!
  name: String
}

type Query {
  getDevice (id: ID!): Device
  getDevices: [Device]
}

type Mutation {
  createDevice( mac: String!, name: String! ): Device
}

Database file:

import { Mongo } from 'meteor/mongo';

const deviceCollection = Object.assign(new Mongo.Collection('devices'), {
  save({ mac, name }) {
    const newDeviceId = this.insert({
      mac,
      name,
      createdAt: new Date(),
    });

    return this.findOne(newDeviceId);
  }
});

export { deviceCollection as DeviceCollection }

Resolvers File:

import { DeviceCollection } from '../../api/Devices/devices';
import deviceSchema from './devices.graphql';

export default {
  Query: {
    getDevice: async (obj, { id }) => DeviceCollection.findOne(id),
    getDevices: async () => DeviceCollection.find({}).fetch()
  },
  Mutation: {
    createDevice( obj, { mac, name }, context ) {
      return DeviceCollection.save({ mac, name });
    }
  }
};

Server File:

import { Meteor } from 'meteor/meteor';
import { startApolloServer } from './apollo';

try {
  startApolloServer().then();
} catch (e) {
  console.error(e.reason);
}

Meteor.startup(() => {

});

Apollo file:

import { ApolloServer } from 'apollo-server-express';
import { WebApp } from 'meteor/webapp';
import { getUser } from 'meteor/apollo';
import merge from 'lodash/merge';
import { DeviceCollection } from '../../api/Devices/devices';
import deviceSchema from '../../api/Devices/devices.graphql';
import deviceResolvers from '../../api/Devices/resolvers';

const typeDefs = [
  deviceSchema
];

const resolvers = merge(
  deviceResolvers
);

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => ({
    user: await getUser(req.headers.authorization)
  })
});

export async function startApolloServer() {
  await server.start();
  const app = WebApp.connectHandlers;

  server.applyMiddleware({
    app,
    cors: true
  });
}

App file:

import React from "react";
import gql from "graphql-tag";
import { graphql } from "react-apollo";

import DeviceForm from "./DeviceForm";

const deviceQuery = gql`
{
  getDevices {
    _id
    mac
    name
  }
}
`;

const App = ({ data }) => {
  if (data.loading) return <h1>Loading</h1>;
  return (
    <div>
      <h1>Hello</h1>
      <DeviceForm refetch={data.refetch} />
      <ul>
        {data.getDevices.map(device => (
          <li key={device._id}>{device.name}</li>
        ))}
      </ul>
    </div>
  )
};

export default graphql(deviceQuery)(App);

Device Form:

import React, { Component } from "react";
import gql from "graphql-tag";
import { graphql } from "react-apollo";

const createDevice = gql`
  mutation createDevice( $mac: String!, $name: String! ) {
    createDevice( mac: $mac, name: $name ) {
      _id
    }
  }
`;

class DeviceForm extends Component {
  submitForm = () => {
    this.props.createDevice({
      variables: {
        mac: this.mac.value,
        name: this.name.value
      },
      refetch: () => ['getDevices']
    }).then(({ data }) => {
      //this.props.refetch();
    }).catch( error => {
      console.log( error );
    });
  };

  render() {
    return (
      <div>
        <input type="text" ref={input => (this.mac = input)}/>
        <input type="text" ref={input => (this.name = input)}/>
        <button onClick={this.submitForm}>Create</button>
      </div>
    )
  }
}

export default graphql(createDevice, {
  name: "createDevice"
})(DeviceForm)

The app compiles and everything looks fine, until you click “create” and it tries to insert a new entry into the devices database.

To make async functions like database calls operate as if they are synchronous, Meteor uses fibers. That is why on the server you don’t need to use await in front of them to assign the result. Each client request to the Meteor server gets wrapped in a fiber. That fiber manages the async behaviour of the operations performed during request handling.

When you grab the client request using meteor/webapp the handler is not automatically called in a fiber. A database function like insert that expects to run in a fiber throws your error. A solution (as described by your error message) is to manually bind the request handling in Meteor.bindEnvironment. You can also wrap your resolvers or the individual Meteor async functions with bindEnvironment.

Another solution is just to call the async Mongo Driver Collection methods returned by rawCollection() which are just regular asynchronous functions that don’t require fibers and return promises. This is what the meteor/apollo package does for getUser. Unlike Meteor, GraphQL expects to be running multiple client requests in parallel so this might be the best option.

So your devices.js file and save function could become:

import { Mongo } from 'meteor/mongo';
import { Random } from 'meteor/random';

const Devices = new Mongo.Collection('devices');  // The `this` got screwy inside .assign so I got rid of it.
const deviceCollection = Object.assign(Devices, {
  async save({ mac, name }) {
    const _id = Random.id();
    await Devices.rawCollection().insertOne({
      _id, // provide the _id so you have a string rather thant a Mongo ObjectID
      mac,
      name,
      createdAt: new Date(),
    });
    return await Devices.rawCollection().findOne({_id});
  }
});

export { deviceCollection as DeviceCollection }

You would need to replace all your database calls such as in the resolvers, device and devices. Also, you can already see that this is losing some handy meteor functionality like automatic ‘string’ _ids and familiar return objects.

If you want to stay within Meteor’s async functions rather than avoid them, there is a lengthy description of fibers and some ways to work with them here. More familiar looking than bindEnvironment might be using the meteor/promise package which would let you execute in a fiber and await the result. Like this:

import { Mongo } from 'meteor/mongo';
import { Promise } from 'meteor/promise';

const deviceCollection = Object.assign(new Mongo.Collection('devices'), {
  async save({ mac, name }) {
    const newDeviceId = Promise.await(this.insert({
      mac,
      name,
      createdAt: new Date(),
    }));
    return Promise.await(this.findOne(newDeviceId));
  }
});

export { deviceCollection as DeviceCollection }

I am not sure what the performance implications of running in a fiber this way would be but I expect each client request gets its own fiber so calls can still be run in parallel.

1 Like

Thanks Bud, much appreciated.

I noticed something in your code that mine did not have, the async.

I simply added that to the front of the “save({ …” and it started working. Face. In Palm…

Thanks again.