firebase / firebase-js-sdk

Firebase Javascript SDK
https://firebase.google.com/docs/web/setup
Other
4.83k stars 891 forks source link

Database tries to call connectDatabaseEmulator when already initialized, leading to FIREBASE FATAL ERROR #6853

Closed Janque closed 1 year ago

Janque commented 1 year ago

[REQUIRED] Describe your environment

[REQUIRED] Describe the problem

Steps to reproduce:

I'm using the nextjs integration with firebase database and when there is a hot reload (ex. saving a file), the following error appears: Error: FIREBASE FATAL ERROR: Cannot call useEmulator() after instance has already been initialized. From the line:

 export const database = getDatabase(firebaseApp);

When there has been no hot reload, everything works fine.

After more investigation I found that the code that connects the emulator differs from the one that connects the firebase emulator

//firebase-js-sdk/packages/firestore/src/api/database.ts
export function getFirestore(
  appOrDatabaseId?: FirebaseApp | string,
  optionalDatabaseId?: string
): Firestore {
  const app: FirebaseApp =
    typeof appOrDatabaseId === 'object' ? appOrDatabaseId : getApp();
  const databaseId =
    typeof appOrDatabaseId === 'string'
      ? appOrDatabaseId
      : optionalDatabaseId || DEFAULT_DATABASE_NAME;
  const db = _getProvider(app, 'firestore').getImmediate({
    identifier: databaseId
  }) as Firestore;
  if (!db._initialized) {
    const emulator = getDefaultEmulatorHostnameAndPort('firestore');
    if (emulator) {
      connectFirestoreEmulator(db, ...emulator);
    }
  }
  return db;
}
//firebase-js-sdk/packages/database/src/api/Database.ts
export function getDatabase(
  app: FirebaseApp = getApp(),
  url?: string
): Database {
  const db = _getProvider(app, 'database').getImmediate({
    identifier: url
  }) as Database;
  //In line 323 should be added if(!db._instanceStarted){
    const emulator = getDefaultEmulatorHostnameAndPort('database');
    if (emulator) {
      connectDatabaseEmulator(db, ...emulator);
    }
  //}
  return db;
}
//...
export function connectDatabaseEmulator(
  db: Database,
  host: string,
  port: number,
  options: {
    mockUserToken?: EmulatorMockTokenOptions | string;
  } = {}
): void {
  db = getModularInstance(db);
  db._checkNotDeleted('useEmulator');
  if (db._instanceStarted) {  //This check becomes redundant
    fatal(
      'Cannot call useEmulator() after instance has already been initialized.'
    );
  }
  //...
}

I believe that those changes would fix the issue.

My package.json dependencies

"dependencies": {
    "@fortawesome/fontawesome-svg-core": "^6.2.0",
    "@fortawesome/free-brands-svg-icons": "^6.2.0",
    "@fortawesome/free-solid-svg-icons": "^6.2.0",
    "@fortawesome/react-fontawesome": "^0.2.0",
    "bootstrap": "4.6.2",
    "firebase": "^9.14.0",
    "firebaseui": "^6.0.1",
    "jquery": "^3.6.1",
    "next": "^13.0.2",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "sass": "^1.55.0"
}
maneesht commented 1 year ago

@Janque - can you provide an example of how you're using RTDB with Next.js?

MarkDuckworth commented 1 year ago

If I understand this correctly, the recommendation from @Janque is to modify the getDatabase(...) function in RTDB to only attempt to connect to to the emulator if the RTDB instance has not started? That is, make RTDB behave the same as Firestore in initialization.

For general housekeeping, I'm going to remove the firestore tag from this issue.

Janque commented 1 year ago

@Janque - can you provide an example of how you're using RTDB with Next.js?

Well, as I said it's not really an issue with a specific use, but rather the initialization. Here is an simplified bit that also triggered the error:

// pages/index.js
import { incrementCount } from '../firebase/database';
export default function Home(props) {
   //...
}
export async function getServerSideProps() {
  await incrementCount();
  return {props:{
    //...
  }}
}

// firebase/firebase.js
//...
export const firebaseApp = initializeApp(firebaseConfig);
export const database = getDatabase(firebaseApp);
//...

// firebase/database.js
import { database } from './firebase';
import { ref, set, increment } from "firebase/database";
export async function incrementCount() {
    await set(ref(database, "count"), increment(1));
    return;
}

After I run emulators:start it works fine even after reloading the page, but if I edit (or just save without editing) any file it does a hot reload and then throws the error.

Janque commented 1 year ago

If I understand this correctly, the recommendation from @Janque is to modify the getDatabase(...) function in RTDB to only attempt to connect to to the emulator if the RTDB instance has not started? That is, make RTDB behave the same as Firestore in initialization.

For general housekeeping, I'm going to remove the firestore tag from this issue.

Yes

maneesht commented 1 year ago

@Janque - thanks for the example. I just wanted to make sure there wasn't a useEffect or something similar that was calling getDatabase multiple times as that's something I see from time-to-time. While I think the change can be made, I'm unable to reproduce this issue. getServerSideProps only seems to get triggered on full reload for me. What version of nextjs are you on?

Janque commented 1 year ago

@maneesht it's 13.0.2, but I just updated to 13.0.6 and it still happens. Start the emulators, all fine; edit and save a file like index.js; if the browser didn't reload by it self, I refresh the page and then the error.

Server Error
Error: FIREBASE FATAL ERROR: Cannot call useEmulator() after instance has already been initialized. 

This error happened while generating the page. Any console logs will be displayed in the terminal window.

Source
  fatal
    file:///.../node_modules/@firebase/database/dist/node-esm/index.node.esm.js (311:11)
  connectDatabaseEmulator
    file:///.../node_modules/@firebase/database/dist/node-esm/index.node.esm.js (13732:9)
  getDatabase
    file:///.../node_modules/@firebase/database/dist/node-esm/index.node.esm.js (13713:9)
Janque commented 1 year ago

I just did the change that I suggested in my local package files and the problem is temporarily solved

// node_modules/@firebase/database/dist/node-esm/index.node.esm.js
function getDatabase(app = getApp(), url) {
    const db = _getProvider(app, 'database').getImmediate({
        identifier: url
    });
    if (!db._instanceStarted) {
        const emulator = getDefaultEmulatorHostnameAndPort('database');
        if (emulator) {
            connectDatabaseEmulator(db, ...emulator);
        }
    }
    return db;
}
maneesht commented 1 year ago

@Janque I was able to reproduce the issue. Though the problem is a little more complicated than I originally had thought. The error comes from the connectDatabaseEmulator that the user calls to connect to the emulator, not the one called by Database.ts. If I add the extra check that you have, it still causes an issue for me. I'm checking with others on this.

Janque commented 1 year ago

@maneesht I never call connectDatabaseEmulator directly in my code. But when I did the change in my packages file the problem disappeared I guess because connectDatabaseEmulator wasn’t being called more than once anymore. After that change I haven’t had any issues with it as long as I don’t update.

maneesht commented 1 year ago

That's very odd, do you have environment variables set up for your emulator port?

Janque commented 1 year ago

Not sure what you mean by "for the emulator port", but I don't have any .env files

maneesht commented 1 year ago

Basically, getDefaultEmulatorHostnameAndPort will look at your process.env.__FIREBASE_DEFAULTS__ to see if you have any emulator ports available, and if and only if you have that environment set up will it call connectDatabaseEmulator. Do you have that env var defined? And is emulator defined for you?

I appreciate your patience. The more information I know about your use-case, the better the subsequent tests will be.

Janque commented 1 year ago

Well I don't have any .env setup. So I guess no.

I only have the emulators config in the firebase.json

"emulators": {
  "functions": {
    "port": 5001
  },
  "firestore": {
    "port": 8080
  },
  "database": {
    "port": 9000
  },
  "hosting": {
    "port": 5002
  },
  "pubsub": {
    "port": 8085
  },
  "ui": {
    "enabled": true
  }
}

I'd maybe mention that the default hosting port was 5000 and I had to change it because some AirPlay feature (I think) uses it.

Also when I run the emulators I use firebase emulators:start --import emulators/emulator-fsdb which imports a copy of my firestore db, but I don't think that has anything to do with the issue.