Search code examples
javascriptfirebasefirebase-realtime-databasees6-modules

Firebase Realtime Database "Service not available" error when using dynamic imports


I am trying to dynamically import Firebase Realtime Database (RTDB) services in my web application to reduce the initial bundle size (the database feature is only available for logged-in users). However, when I attempt to initialize the RTDB service using dynamic imports, I receive the following error:

Uncaught (in promise) Error: Service database is not available
    at Jp.getImmediate (provider.ts:130:15)
    at VE (Database.ts:321:44)

The Firebase Database methods that trace the error are:

  /**
   *
   * @param options.identifier A provider can provide mulitple instances of a service
   * if this.component.multipleInstances is true.
   * @param options.optional If optional is false or not provided, the method throws an error when
   * the service is not immediately available.
   * If optional is true, the method returns null if the service is not immediately available.
   */
  getImmediate(options: {
    identifier?: string;
    optional: true;
  }): NameServiceMapping[T] | null;
  getImmediate(options?: {
    identifier?: string;
    optional?: false;
  }): NameServiceMapping[T];
  getImmediate(options?: {
    identifier?: string;
    optional?: boolean;
  }): NameServiceMapping[T] | null {
    // if multipleInstances is not supported, use the default name
    const normalizedIdentifier = this.normalizeInstanceIdentifier(
      options?.identifier
    );
    const optional = options?.optional ?? false;

    if (
      this.isInitialized(normalizedIdentifier) ||
      this.shouldAutoInitialize()
    ) {
      try {
        return this.getOrInitializeService({
          instanceIdentifier: normalizedIdentifier
        });
      } catch (e) {
        if (optional) {
          return null;
        } else {
          throw e;
        }
      }
    } else {
      // In case a component is not initialized and should/can not be auto-initialized at the moment, return null if the optional flag is set, or throw
      if (optional) {
        return null;
      } else {
        throw Error(`Service ${this.name} is not available`);
      }
    }
  }


/**
 *
 * @param app - FirebaseApp instance
 * @param name - service name
 *
 * @returns the provider for the service with the matching name
 *
 * @internal
 */
export function _getProvider<T extends Name>(
  app: FirebaseApp,
  name: T
): Provider<T> {
  const heartbeatController = (app as FirebaseAppImpl).container
    .getProvider('heartbeat')
    .getImmediate({ optional: true });
  if (heartbeatController) {
    void heartbeatController.triggerHeartbeat();
  }
  return (app as FirebaseAppImpl).container.getProvider(name);
} 

Below is a minimal example that demonstrates the issue:

main.js:

import { initializeApp } from 'firebase/app';
import { getAuth, onAuthStateChanged } from 'firebase/auth';

const firebaseConfig = {
    // ... Your Firebase config
};

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

onAuthStateChanged(auth, async (user) => {
    if (user) {
        const rtdbModule = await import('./dist/js/dynamic.min.js');
        const db = rtdbModule.initDB(app); // Pass the initialized Firebase App here
        // Use the database instance afterward
        ...
    }
});

dynamic.js:

import { getDatabase } from 'firebase/database';

export function initDB(app) {
    console.log(app); // This logs the correct app instance
    const db = getDatabase(app);
    return db;
}

I'd like to understand why the error occurs and if there is a better way to dynamically import RTDB to save bundle size.

Any insights or suggestions would be greatly appreciated!


Solution

  • OK, so the issue was with the bundling of my code. In my bundle script, I outputted the code in two separate bundles:

    main.js
    dynamic.js (which only loaded the RTDB module and returned the result)
    

    When I did that, I got the Service database is not available error, since the generated dynamic module was in a different namespace, and didn't include global variables or other runtime flags that Firebase needs.

    I fixed it by outputting a single module, using ES6 modules and code splitting:

    Bundle setup:

         esbuild(
            {
              bundle: true,
              sourcemap: true,
              minify: true,
              format: "esm",
              splitting: true, // added this
              outdir: 'dist/js', // added this
            },
          )
    

    Now, esbuild recognizes the dynamic import and splits it into a different file (dynamic.js), which is properly namespaced.

    
    async function setUpAuthEvents(auth) {
        onAuthStateChanged(auth, async function (user) {
            if (user) {
                const rtdbModule = await import("./dynamic.js");
                rtdb = rtdbModule.initDB(fbApp);
                // rtdb can be used from now on...
    ...
    

    Outputted files:

    main.js (doesn't include RTDB package)
    dynamic.js 
    chunk.js
    

    Finally, I load main.js as a module:

    <script type="module" src="dist/js/main.min.js"></script>