nuxt-community / firebase-module

🔥 Easily integrate Firebase into your Nuxt project. 🔥
https://firebase.nuxtjs.org
MIT License
641 stars 98 forks source link

Firestore Emulators and Hot Code Reloading Error #390

Closed richardscollin closed 3 years ago

richardscollin commented 3 years ago

If you're able to point me to some documentation I may have missed, that would be greatly appreciated. I'm still very new to vue and nuxt so it could be something very basic that I'm overlooking.

Otherwise, I think there may be an issue with how the emulators are configured when using hot reloading.

Summary of issue:

I'm getting the following error when using the firestore emulator on the second time (first time works fine) when visiting a page component:

(also note this doesn't occur when fulling building and deploying the app - only when running with the nuxt-ts command without args)

Firestore has already been started and its settings can no longer be changed. You can only modify settings before calling any other methods on a Firestore object. 

Stack trace:

 ERROR  Firestore has already been started and its settings can no longer be changed. You can only modify settings before calling any other methods on a Firestore object.

  at new FirestoreError (node_modules/@firebase/firestore/src/util/error.ts:216:5)
  at FirebaseFirestore$1.FirebaseFirestore._setSettings (node_modules/@firebase/firestore/lite/src/api/database.ts:207:13)
  at Firestore.settings (node_modules/@firebase/firestore/src/api/database.ts:240:20)
  at server.js:1192:20
  at Object.fire.firestoreReady (.nuxt/firebase/index.js:76:0)
  at asyncData (pages/tutors/_tutor.vue?0014:20:0)

The referenced code: .nuxt/firebase/index.js:76

  fire.firestore = null
  fire.firestoreReady = async () => {
    if (!fire.firestore) {
      await fire.appReady()
      fire.firestore = await firestoreService(session, firebase, ctx, inject) // line 76
    }

    return fire.firestore
  }

My relevant section of code:

<template>
  <div class="">Hello {{ this.displayName }}</div>
</template>

<script lang="ts">

import Vue from 'vue';
export default Vue.extend({
  props: {},
  data() {
    return {};
  },
  async asyncData(context) {
    const tutorName = context.params.tutor;

    await context.app.$fire.firestoreReady();

    const snapshot = await context.app.$fire.firestore
      .collection('tutors')
      .where('slug', '==', tutorName)
      .limit(1)
      .get();

    if (snapshot.size == 1) {
      const tutorData = snapshot.docs[0].data();
      console.log('tutorData');
      console.log(tutorData);
    } else {
      console.error(`No tutor ${tutorName} found`);
    }

    return { displayName: tutorName };
  },
});
</script>

<style scoped></style>

How I'm running: yarn dev

package.json

...
  "scripts": {
    "dev": "run-p dev:nuxt dev:firebase",
    "dev:firebase": "firebase emulators:start --only=auth,functions,firestore",
    "dev:nuxt": "nuxt-ts",
    "build": "nuxt-ts build",
    "generate": "nuxt-ts generate",
    "start": "nuxt-ts start",
    "lint:js": "eslint --ext .js,.vue --ignore-path .gitignore .",
    "lint": "yarn lint:js"
  },
...

I just tested and when running a static build as below it works fine. When I run with: yarn dev:firebase yarn build && yarn generate && yarn start

Other files for reference:

nuxt.config.js

import colors from 'vuetify/es5/util/colors';
import firebaseConfig from './firebase.config';

const dev = process.env.NODE_ENV === 'development';
const appName = 'Tutor Cafe';

export default {
  // Target (https://go.nuxtjs.dev/config-target)
  target: 'static',

  // Global page headers (https://go.nuxtjs.dev/config-head)
  head: {
    titleTemplate: appName,
    title: appName,
    description: 'A platform for finding in person language tutors',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],

    // this domain is the service worker library
    link: [{ rel: 'preconnect', href: 'https://cdn.jsdelivr.net' }],
  },

  // Auto import components (https://go.nuxtjs.dev/config-components)
  components: true,

  // Modules for dev and build (recommended) (https://go.nuxtjs.dev/config-modules)
  buildModules: [
    '@nuxtjs/firebase',
    // https://go.nuxtjs.dev/typescript
    '@nuxt/typescript-build',
    // https://go.nuxtjs.dev/vuetify
    '@nuxtjs/vuetify',
    // https://github.com/nuxt-community/robots-module
    '@nuxtjs/robots',
    // https://github.com/nuxt-community/svg-module
    '@nuxtjs/svg',
    // https://i18n.nuxtjs.org/setup/
    'nuxt-i18n',
  ],
  // Modules (https://go.nuxtjs.dev/config-modules)
  modules: [
    // https://go.nuxtjs.dev/axios
    '@nuxtjs/axios',
    // https://go.nuxtjs.dev/pwa
    '@nuxtjs/pwa',
    // https://firebase.nuxtjs.org/guide/getting-started
    '@nuxtjs/firebase',
  ],

  i18n: {
    lazy: true,
    seo: true,
    locales: [
      { code: 'en', iso: 'en-US', name: 'English', file: 'en.json' },
      { code: 'zh', iso: 'zh-tw', name: 'Chinese', file: 'zh-tw.json' },
    ],
    langDir: '/locales/',
    strategy: 'prefix_and_default',
    defaultLocale: 'zh',
    detectBrowserLanguage: {
      onlyOnRoot: true,
    },
    vueI18n: {
      fallbackLocale: 'en',
      defaultLocale: 'zh',
    },
    detectBrowserLanguage: false,
  },

  // I saw this render chunk mentioned in a similar issue so I tried adding it and
  // using it with both false and true values
  render: {
    bundleRenderer: {
      runInNewContext: false,  // Note I tride this with both false and true
    },
  },
  firebase: {
    lazy: true,
    config: firebaseConfig,
    services: {
      auth: {
        emulatorPort: dev ? 9099 : false,
      },
      firestore: {
        // emulatorHost: 'localhost',
        // emulatorPort: dev ? 8080 : false,
        settings: {
          host: 'localhost:8080',
          ssl: false,
        },
      },
      functions: {
        emulatorPort: dev ? 5001 : false,
      },
      storage: true,
      database: false,
      messaging: false,
      performance: false,
      analytics: true,
      remoteConfig: false,
    },
  },
};
lansolo99 commented 3 years ago

I experienced the same issue during development using emulators.

After a lot of reverse engineering, I figured out this only happens using asyncData or Fetch (default behavior with fetchOnServer to true). It doesn't occurs if I set fetchOnServer prop to false.

So the temporary workaround would be to conditionally set fetchOnServer based on node env but it's not satisfactory, and I can't use asyncData at all.

Also I have this message popping out in the console:

@firebase/firestore: Firestore (8.1.1): Host has been set in both settings() and useEmulator(), emulator host will be used

There is no firestore settings object in my nuxt config so I don't get the point here..

I'm not skilled enough to get this things sorted out diving into the webpack conf, so if anyone would have an answer ...

richardscollin commented 3 years ago

@lansolo99 Thanks for the work around. I'll make use of it.

From my digging I suspect it's a bug in how nuxt-fire makes the useEmulator calls. It appears the emulator features were added in the library last month so likely haven't gotten extensive usage yet. I'm also not an expert on how nuxt plugins work but I'll post my theory in case anyone else wants to dig a little deeper.

One theory I have is the plugin is re-injected each time so that when this is run the null check inside the async function isn't actually doing anything because the service is being reinitialized to null each time. Or maybe there's something else that's making the null check essentially useless.

In main.js:

  if (process.server) {
  <% for (service of serverServices) { %>
  <% const serviceName = service.id %>
  fire.<%= serviceName %> = null
  fire.<%= serviceName %>Ready = async () => {
    if (!fire.<%= serviceName %>) {
      await fire.appReady()
      fire.<%= serviceName %> = await <%= `${service.id}Service(session, firebase, ctx, inject)` %>
    }

    return fire.<%= serviceName %>
  }

Related to the warning:

It seems a little weird to me to check if the serviceOptions are an object when if they've been set to true earlier then we know they are set to an object. I believe the warning that you mentioned is caused by this flow. There's a call to the firebase settings function followed by a call to the useEmulator function.

plugins/services/firestore.js:

 const firestoreService = session.<%= serviceMapping.id %>()

  <% if (typeof serviceOptions.settings === 'object') { %>
    firestoreService.settings(<%= serialize(serviceOptions.settings) %>)
  <% } %>

lib/module.js:

  // Assign some defaults
  options.services.app = Object.assign({ static: false }, options.services.app)
  for (const service in options.services) {
    if (options.services[service] === true) {
      // If a service is enabled always set an object
      // so we dont have to bother about this in the
      // service templates
      options.services[service] = {}
    }
  }
lupas commented 3 years ago

Hey Guys Sorry for the late response.

Thanks for the issue an also thanks for already digging into the code to analyze the issue!

So we have 3 issues here I wanna get fixed:

1: "Host has been set in both settings() and useEmulator(), emulator host will be used"

Can easily reproduce this one, seems to be an issue also in other projects that are not using this module (e.g. see here).

@lansolo99: here is no firestore settings object in my nuxt config so I don't get the point here..

@lansolo99 I don't understand this part however. If you do not have services.firestore.emulatorPort set, the emulator does not get loaded and the warning also does not appear for me. If the error really appears for you without having this in your nuxt config, that would really confuse me.

It's just an info an not even really a warning, so not critical at all. Nevertheless, will investigate on how this could be avoided...

2: "Firestore has already been started and its settings can no longer be changed. You can only modify settings before calling any other methods on a Firestore object."

Same here, will try to find the culprit of this and let you know.

3. Confusing code

I'll have a closer look in the coming days (can't promise a timeline) and fix it, I'll let you know.

Thanks for your efforts & br, Pascal

lansolo99 commented 3 years ago

@lupas Just to quickly answer about my Firestore settings confusion: I have the Firestore "emulatorPort" property set of course, but not the Firestore "settings" property as it is mentioned in the docs :

image

The Firestore emulator port is also set within the firebase.json file but I guess it's mandatory to have the emulators started up.

Thanks for your investigation, will follow up 👍

richardscollin commented 3 years ago

The null check in if (!fire.<%= serviceName %>) in combination with the fire.<%= serviceName %> = null indeed seems to be redundant, sorry for that.

Is the check redundant? Because it's being used within the function fire.<%= serviceName %>Ready. I believe it's necessary to prevent fire.<%= serviceName %> from being initialized twice. However, given the issue we are encountering, it doesn't seem to be working properly.

I'll have a closer look in the coming days (can't promise a timeline) and fix it, I'll let you know.

Okay. Feel free to post any incomplete notes here. I might do a bit more digging too. Although @lansolo99 's fetchOnServer: false, works well enough for me because I only need these calls to happen on the client anyway.

fshareef commented 3 years ago

After a lot of reverse engineering, I figured out this only happens using asyncData or Fetch (default behavior with fetchOnServer to true). It doesn't occurs if I set fetchOnServer prop to false.

This issue happens for me regardless of whether or not I use asyncData(). It also occurred when using mounted() hook, or a computed property.

Seems like it's just happening everywhere I call $fire.firestore with emulators set up.

lupas commented 3 years ago

Hmm this case is quite interesting.

The Firestore emulator port is also set within the firebase.json file but I guess it's mandatory to have the emulators started up.

Yep, even when no firebase.json is present the warning still appears. So it has nothing to do with the firebase.json. There error is thrown in firebase-js-sdk/packages/firestore/src/api/database.ts#L241 btw.

I tested the whole thing without using nuxt/firebase and could find the following:

import firebase from 'firebase/app/dist/index.cjs.js'
import 'firebase/firestore/dist/index.node.cjs.js'

const config = {
  apiKey: 'AIzaSyDa-YwgWTp2GDyVYEfv-XLb62100_HoEvU',
  authDomain: 'nuxt-fire-demo.firebaseapp.com',
  databaseURL: 'https://nuxt-fire-demo.firebaseio.com',
  projectId: 'nuxt-fire-demo',
  storageBucket: 'nuxt-fire-demo.appspot.com',
  messagingSenderId: '807370470428',
  appId: '1:807370470428:web:26da98c86c3fd352',
  measurementId: 'G-XT6PVC1D4X',
}

if (!firebase.apps.length) {
  firebase.initializeApp(config)
}

const firestore = firebase.firestore()
firestore.useEmulator('localhost', 8080)
firestore.useEmulator('localhost', 8080) // run twice

When I run useEmulator twice, i get the exact same error message:

[2020-12-22T23:31:43.473Z] @firebase/firestore: Firestore (8.2.1): Host has been set in both settings() and useEmulator(), emulator host will be used

Running it once does not throw the error message.

Just typing this out made me think... we probably run useEmulator with every request, let me check if it is only the second+ requests which throws that warning...

lupas commented 3 years ago

Hey y'all I found and fixed the issue with v7.2.2.

This should actually fix both issues (error + warning) mentioned above. Let me know if that did or especially did not do it for, then I would reopen this issue.

Thanks for all your effort, appreciate it!

Have nice holidays and soon a Merry Christmas! Pascal

jaggdl commented 3 years ago

Nice, thanks so much lupas!

If someone is still having the same issue after updating to 7.2.2 just make sure to remove static: true from firestore settings.

lansolo99 commented 3 years ago

@lupas Just tested the upgrade today, and indeed, the error doesn't occurs anymore, thanks!