typescriptfirebasegoogle-cloud-firestorefirebase-tools

firestore Emulator database query results are incorrect/unstable


I am using the Google Firestore Emulator locally for development. However, when I query the database data, I find that the results are inconsistent and not correct each time. Here is the detailed information.

1.This is my firestore setup:

/**
 * Firebase configuration for both local development and production
 */

import { initializeApp, getApps, FirebaseApp } from 'firebase/app';
import { getFirestore, connectFirestoreEmulator, Firestore } from 'firebase/firestore';
import { getAuth, connectAuthEmulator, Auth } from 'firebase/auth';
import logger from 'src/logger.ts';

// Firebase configuration
const firebaseConfig = {
    // For local development with emulator, these can be dummy values
    apiKey: process.env.FIREBASE_API_KEY || 'dummy-api-key',
    authDomain: process.env.FIREBASE_AUTH_DOMAIN || 'dummy-project.firebaseapp.com',
    projectId: process.env.FIREBASE_PROJECT_ID || 'dummy-project',
    storageBucket: process.env.FIREBASE_STORAGE_BUCKET || 'dummy-project.appspot.com',
    messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID || '123456789',
    appId: process.env.FIREBASE_APP_ID || 'dummy-app-id',
};

// Initialize Firebase app
let app: FirebaseApp;
if (getApps().length === 0) {
    app = initializeApp(firebaseConfig);
    logger.info('Firebase app initialized');
} else {
    app = getApps()[0];
    logger.info('Using existing Firebase app');
}

// Initialize Firestore
export const db: Firestore = getFirestore(app);

// Initialize Auth
export const auth: Auth = getAuth(app);

// Connect to emulators in development
if (process.env.NODE_ENV === 'development') {
    try {
        // Always connect to emulators in development
        connectFirestoreEmulator(db, 'localhost', 8080);
        logger.info('Connected to Firestore emulator on localhost:8080');

        connectAuthEmulator(auth, 'http://localhost:9099');
        logger.info('Connected to Auth emulator on localhost:9099');
    } catch (error) {
        logger.warn(`Could not connect to Firebase emulators: ${error}`);
        logger.info('Make sure to start Firebase emulators with: firebase emulators:start');
    }
}

export { app };

2.Then I start the Emulator by make firebase emulators:start --only firestore --project dummy-project

3.I do something and there are some items in my database.

item1: {...., expiredAt: {nanoseconds: 123000000, seconds: 1757341144}}
item2: {...., expiredAt: {nanoseconds: 76000000, seconds: 1757341146}}
item3: {...., expiredAt: {nanoseconds: 200000000, seconds: 1757341133}}

4.I try to get these 3 items by executing findUnexpiredThreads() function:

import {
    Firestore,
    collection,
    doc,
    setDoc,
    getDoc,
    getDocs,
    updateDoc,
    query,
    where,
    orderBy,
    limit,
    getCountFromServer,
} from 'firebase/firestore';

    async findUnexpiredThreads(): Promise<IChatThread[]> {
        try {
            const q = query(
                collection(this.db, this.threadsCollection),
                where('expiresAt', '>', toFirestoreSafe(new Date()))
            );

            const snapshot = await getDocs(q);
            return snapshot.docs.map(doc => fromFirestoreSafe(doc.data())) as IChatThread[];
        } catch (error) {
            logger.error(`Failed to find unexpired chat threads: ${error}`);
            return [];
        }
    }

// Convert app data to Firestore-safe values: BigInt -> string, Date -> Timestamp
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function toFirestoreSafe<T = unknown>(value: T): any {
    return deepMap(value as unknown as JsonValue, v => {
        if (typeof v === 'bigint') {
            return v.toString();
        }
        if (v instanceof Date) {
            return Timestamp.fromDate(v);
        }
        return v;
    });
}

// Convert Firestore data to app-friendly values: Timestamp -> Date
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function fromFirestoreSafe<T = unknown>(value: T): any {
    return deepMap(value as unknown as JsonValue, v => {
        if (v instanceof Timestamp) {
            return v.toDate();
        }
        return v;
    });
}

Result: Sometimes it returns 0 items, sometimes it returns 1/2/3 items, the result is unstable. In theory, it should always return 3 items, because the expiredAt of these 3 items is greater than now.

I'm not sure where the problem lies. I checked the contents of the database, and they haven't been modified. I was merely making queries, but I don't know why the results are incorrect, and the results of each query are also inconsistent.


Solution

  • I discovered that when the problem occurred, I stored expiredAt as a Map format (item1: {...., expiredAt: {nanoseconds: 123000000, seconds: 1757341144}}). I fixed it by not converting Date and timestamp types to maps when encountering them in toFirestoreSafe() and fromFirestoreSafe().