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!
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>