Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation doesn't emphasize Promises don't resolve for offline writes #8862

Open
bennbollay opened this issue Mar 24, 2025 · 3 comments
Open

Comments

@bennbollay
Copy link

Operating System

macOS Sonoma

Environment (if applicable)

Node v22.5.0

Firebase SDK Version

11.5.0

Firebase SDK Product(s)

Firestore

Project Tooling

Simple mjs files running via the command line.

Detailed Problem Description

I'm attempting to use the offline cache in nodejs to test some behaviors and expectations around conflict resolution.

Calling setDoc and updateDoc while in offline mode causes the process to immediately exit with an exit code of 0.

I'd expect the console.log present after the setDoc to be present.

Steps and code to reproduce issue

offline_test.mjs:

import { initializeApp } from 'firebase/app';
import {
  disableNetwork,
  doc,
  enableNetwork,
  getDoc,
  getFirestore,
  initializeFirestore,
  memoryLocalCache,
  onSnapshot,
  setDoc,
  setLogLevel,
} from 'firebase/firestore';

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: 'A...o',
  authDomain: 'p...s.firebaseapp.com',
  projectId: 'p...s',
  storageBucket: 'p...s.firebasestorage.app',
  messagingSenderId: '163...02',
  appId: '1:163...02:web:d...b',
};

const app = initializeApp(firebaseConfig);
initializeFirestore(app, { localCache: memoryLocalCache() });
const db = getFirestore();
const state = `CA${Math.round(Math.random() * 10000)}`;

const laRef = doc(db, 'cities', 'LA');
onSnapshot(laRef, (doc) => {
    try {
      console.log('Snapshot', doc.data());
    } catch (err) {
      console.log(err);
    }
  },
  (error) => console.log(`Error: `, error),
  () => console.log(`Complete`)
);

const update = async () => {
  try {
    setLogLevel('debug');
    await getDoc(laRef);

    // Successfully retrieves doc
    await disableNetwork(db);

    console.log(`Updating...`);
    await setDoc(laRef, { state });

    // This log message is not seen
    console.log(`After Update...`);
  } catch (err) {
    console.log(err);
  }
};

(async () => {
  await update();

  // This log message is not seen.
  console.log(`Post Update`);
})();

Output:

[2025-03-24T20:45:45.319Z]  @firebase/firestore: Firestore (11.5.0): FirebaseAuthCredentialsProvider Auth not yet detected
[2025-03-24T20:45:45.320Z]  @firebase/firestore: Firestore (11.5.0): FirestoreClient Using user provided OnlineComponentProvider
[2025-03-24T20:45:45.320Z]  @firebase/firestore: Firestore (11.5.0): FirestoreClient Using user provided OfflineComponentProvider
[2025-03-24T20:45:45.320Z]  @firebase/firestore: Firestore (11.5.0): FirestoreClient Initializing OfflineComponentProvider
[2025-03-24T20:45:45.320Z]  @firebase/firestore: Firestore (11.5.0): FirestoreClient Initializing OnlineComponentProvider
[2025-03-24T20:45:45.338Z]  @firebase/firestore: Firestore (11.5.0): MemoryPersistence Starting transaction: Allocate target
[2025-03-24T20:45:45.338Z]  @firebase/firestore: Firestore (11.5.0): MemoryPersistence Starting transaction: Execute query
[2025-03-24T20:45:45.339Z]  @firebase/firestore: Firestore (11.5.0): QueryEngine Using full collection scan to execute query: Query(target=Target(cities/LA, orderBy: [__name__ (asc)]); limitType=F)
[2025-03-24T20:45:45.339Z]  @firebase/firestore: Firestore (11.5.0): GrpcConnection Creating Firestore stub.
[2025-03-24T20:45:45.390Z]  @firebase/firestore: Firestore (11.5.0): GrpcConnection Opening RPC 'Listen' stream 0x48a9dc9d to firestore.googleapis.com
[2025-03-24T20:45:45.390Z]  @firebase/firestore: Firestore (11.5.0): FirebaseAppCheckTokenProvider AppCheck not yet detected
[2025-03-24T20:45:45.392Z]  @firebase/firestore: Firestore (11.5.0): GrpcConnection RPC 'Listen' stream 0x48a9dc9d sending: {
  database: 'projects/p...s/databases/(default)',
  addTarget: {
    documents: {
      documents: [
        'projects/p...s/databases/(default)/documents/cities/LA'
      ]
    },
    targetId: 2
  }
}
[2025-03-24T20:45:45.613Z]  @firebase/firestore: Firestore (11.5.0): GrpcConnection RPC 'Listen' stream 0x48a9dc9d received: {
  targetChange: {
    targetIds: [ 2 ],
    targetChangeType: 'ADD',
    cause: null,
    resumeToken: <Buffer >,
    readTime: null
  }
}
[2025-03-24T20:45:45.662Z]  @firebase/firestore: Firestore (11.5.0): GrpcConnection RPC 'Listen' stream 0x48a9dc9d received: {
  documentChange: {
    targetIds: [ 2 ],
    removedTargetIds: [],
    document: {
      fields: {
        name: { stringValue: 'Los Angeles' },
        country: { stringValue: 'USA' },
        state: { stringValue: 'CA5064' }
      },
      name: 'projects/p...s/databases/(default)/documents/cities/LA',
      createTime: { seconds: '1742844352', nanos: 680402000 },
      updateTime: { seconds: '1742847128', nanos: 429430000 }
    }
  }
}
[2025-03-24T20:45:45.663Z]  @firebase/firestore: Firestore (11.5.0): GrpcConnection RPC 'Listen' stream 0x48a9dc9d received: {
  targetChange: {
    targetIds: [ 2 ],
    targetChangeType: 'CURRENT',
    cause: null,
    resumeToken: <Buffer 0a 09 08 f9 a3 e4 81 cb a3 8c 03>,
    readTime: { seconds: '1742849145', nanos: 639417000 }
  }
}
[2025-03-24T20:45:45.664Z]  @firebase/firestore: Firestore (11.5.0): GrpcConnection RPC 'Listen' stream 0x48a9dc9d received: {
  targetChange: {
    targetIds: [],
    targetChangeType: 'NO_CHANGE',
    cause: null,
    resumeToken: <Buffer 0a 09 08 f9 a3 e4 81 cb a3 8c 03>,
    readTime: { seconds: '1742849145', nanos: 639417000 }
  }
}
[2025-03-24T20:45:45.664Z]  @firebase/firestore: Firestore (11.5.0): MemoryPersistence Starting transaction: Get last remote snapshot version
[2025-03-24T20:45:45.665Z]  @firebase/firestore: Firestore (11.5.0): MemoryPersistence Starting transaction: Apply remote event
[2025-03-24T20:45:45.668Z]  @firebase/firestore: Firestore (11.5.0): MemoryPersistence Starting transaction: notifyLocalViewChanges
Snapshot { name: 'Los Angeles', country: 'USA', state: 'CA5064' }
[2025-03-24T20:45:45.670Z]  @firebase/firestore: Firestore (11.5.0): GrpcConnection RPC 'Listen' stream 0x48a9dc9d closed locally via close().
[2025-03-24T20:45:45.671Z]  @firebase/firestore: Firestore (11.5.0): PersistentStream stream callback skipped by getCloseGuardedDispatcher.
Updating...
[2025-03-24T20:45:45.671Z]  @firebase/firestore: Firestore (11.5.0): MemoryPersistence Starting transaction: Locally write mutations
[2025-03-24T20:45:45.673Z]  @firebase/firestore: Firestore (11.5.0): MemoryPersistence Starting transaction: notifyLocalViewChanges
Snapshot { state: 'CA7548' }
[2025-03-24T20:45:45.784Z]  @firebase/firestore: Firestore (11.5.0): GrpcConnection RPC 'Listen' stream 0x48a9dc9d ended.
@bennbollay bennbollay added new A new issue that hasn't be categoirzed as question, bug or feature request question labels Mar 24, 2025
@google-oss-bot
Copy link
Contributor

I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.

@bennbollay
Copy link
Author

Some further analysis on this: it appears that while in offline mode the setDoc (and updateDoc) functions never resolve their promise, and there is no outstanding network IO events (or other events) to trigger it. As a result, as soon as the process does an await on the Promise returned from setDoc, the nodejs internal eventloop sees that there's no work to be done and exits.

This seems to be a bug in the implementation of setDoc and updateDoc when running in offline mode. I can only presume that this is either unsupported in node or I'm doing something else wrong, as surely others would have run into this by now.

@bennbollay
Copy link
Author

Was able to use this further research to find a comment on #8657 (as well as #6515):

This is expected behavior. Promises from updates are resolved when the backend applied the changes, when the network is turned off, the backend never sees these updates. The updates are still applied in SDK's cache, and will be sent to backend when the SDK is online again.

If your intend is to use the SDK as a mostly offline tool, you are not supposed to await for these promises.

get() is different, as there is a mechanism where results from sdk cache are returned if backend cannot be reached.

There is no mention of this behavior in the docs, however there is an implied statement here:

Note: If you just want to know when your write has completed, you can listen to the completion callback rather than using hasPendingWrites. In JavaScript, use the Promise returned from your write operation by attaching a .then() callback. In Swift, pass a completion callback to your write function.

This has multiple tickets attached to it and that indicates there is a desperately needed documentation update.

Leaving this issue open to drive attention for a documentation change. It's been a few years, people are still running into it, and the issue would be dramatically improved with a section in the Realtime Updates part of the doc emphasizing this idiosyncratic behavior.

@bennbollay bennbollay changed the title setDoc exits process in node in offline mode Documentation doesn't emphasize Promises don't resolve for offline writes Mar 24, 2025
@jbalidiong jbalidiong added Repro Needed api: firestore needs-attention and removed needs-triage new A new issue that hasn't be categoirzed as question, bug or feature request labels Mar 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants