firebase-js-sdk icon indicating copy to clipboard operation
firebase-js-sdk copied to clipboard

Firebase 9 Database: set & update crash when onValue is listening

Open shija-ntula opened this issue 2 years ago • 20 comments

Firebase 9 Database: set & update crash when onValue is listening

  • Operating System version: Ubuntu 20.04.5 LTS
  • Browser version: Google Chrome Version 107.0.5304.87 (Official Build) (64-bit)
  • Firebase SDK version: 9.15.0
  • Firebase Product: database
  • Vue 3

The problem is as follows

Im creating a simple chat app with firebase real time database. My simple app needs to write messages to a chat room and listen to any new message to render in the view. When the App starts, it automatically send the first message saying that the user is in. It the starts a listener with onValue, Everything goes fine. But when sending the next message, the message doesn't reach the database, and in the console I see the following stack

runtime-core.esm-bundler.js:220 Uncaught TypeError: newChildren.insert is not a function
    at IndexMap.ts:147:30
    at map (obj.ts:50:21)
    at Proxy.addToIndexes (IndexMap.ts:115:24)
    at Proxy.updateImmediateChild (ChildrenNode.ts:156:38)
    at IndexedFilter.updateChild (IndexedFilter.ts:94:19)
    at viewProcessorApplyUserOverwrite (ViewProcessor.ts:494:51)
    at viewProcessorApplyOperation (ViewProcessor.ts:103:22)
    at viewApplyOperation (View.ts:213:18)
    at syncPointApplyOperation (SyncPoint.ts:107:9)
    at syncTreeApplyOperationHelper_ (SyncTree.ts:730:9)

If I remove the onValue listener, all sent messages reach my database. although I cant get the updates by then.

Steps to reproduce:

  1. Preparing my database in Firebase.js
import { initializeApp } from "firebase/app";
import {getDatabase, ref, onValue, push, set, update, get, } from "firebase/database";

const config = {
  XXXXXXXXXX
};

const firebase = initializeApp(config);
const database = getDatabase(firebase);

export { database, ref, onValue, push, set, update, get };
  1. Importing and using firebase
import { database, ref, onValue, push, set, update } from "../Firebase";
import router from "../router";

export default {
  name: "Chat",
  props: ["roomid", "roomname", "nickname"],
  data() {
    return {
      message: "",
      chats: [],
      errors: [],
      offStatus: false,
      chatsRef: ref(database, "chatrooms/" + this.roomid + "/chats"),
    };
  },
  created() {
    this.sendMessage(this.nickname + " amejiunga katika redio.", "join");

    onValue(this.chatsRef, (snapshot) => {
      if (snapshot.exists()) {
        this.chats = [];
        snapshot.forEach((doc) => {
          let item = doc.val();
          item.key = doc.key;
          this.chats.push(item);
        });
      }
    });
  },
  unmounted() {
    this.exitChat();
  },
  methods: {
    onSubmit(evt) {
      evt.preventDefault();

      this.sendMessage(this.message, "newmsg");
      this.message = "";
    },
    sendMessage(message, type = "newmsg") {
      const newPostKey = push(this.chatsRef).key;
      // const updates = {};
      // updates[newPostKey] = {
      //   type: type,
      //   user: this.nickname,
      //   message: message,
      //   sendDate: Date(),
      // };

      set(ref(database, "chatrooms/" + this.roomid + "/chats/" + newPostKey), {
        type: type,
        user: this.nickname,
        message: message,
        sendDate: Date(),
      });

      // update(this.chatsRef, updates);
    },
    exitChat() {
      this.sendMessage(this.nickname + " ametoka katika redio.", "exit");
      this.offStatus = true;
      router.go(-1);
    },
  },
};

shija-ntula avatar Jan 10 '23 18:01 shija-ntula

Down grading firebase did'nt help anything. The same error occurred

Any idea/workaround?

Im stuck here.

shija-ntula avatar Jan 11 '23 13:01 shija-ntula

I downgraded to 8.2.3

shija-ntula avatar Jan 11 '23 13:01 shija-ntula

In sendMessage, is this.roomid defined? If it's undefined, then we throw that error.

maneesht avatar Jan 11 '23 22:01 maneesht

In sendMessage, is this.roomid defined? If it's undefined, then we throw that error.

It's defined. Its in props and it comes from another component to which the current component is attached

shija-ntula avatar Jan 12 '23 03:01 shija-ntula

I've resolved the error. And now its working. The problem occurred because of using a single database reference for both listening and writing data.

import { database, ref, onValue, push, set, update } from "../Firebase";
import router from "../router";

export default {
  name: "Chat",
  props: ["roomid", "roomname", "nickname"],
  data() {
    return {
      message: "",
      chats: [],
      errors: [],
      offStatus: false,
      chatsRef: ref(database, "chatrooms/" + this.roomid + "/chats"), // Here assigning the reference
    };
  },
  created() {
    this.sendMessage(this.nickname + " amejiunga katika redio.", "join");

    onValue(this.chatsRef, (snapshot) => {                                  // Here using it to listen
      if (snapshot.exists()) {
        this.chats = [];
        snapshot.forEach((doc) => {
          let item = doc.val();
          item.key = doc.key;
          this.chats.push(item);
        });
      }
    });
  },
  unmounted() {
    this.exitChat();
  },
  methods: {
    onSubmit(evt) {
      evt.preventDefault();

      this.sendMessage(this.message, "newmsg");
      this.message = "";
    },
    sendMessage(message, type = "newmsg") {
      const newPostKey = push(this.chatsRef).key;                     // Here using it to push
      // const updates = {};
      // updates[newPostKey] = {
      //   type: type,
      //   user: this.nickname,
      //   message: message,
      //   sendDate: Date(),
      // };

      set(ref(database, "chatrooms/" + this.roomid + "/chats/" + newPostKey), {
        type: type,
        user: this.nickname,
        message: message,
        sendDate: Date(),
      });

      // update(this.chatsRef, updates);
    },
    exitChat() {
      this.sendMessage(this.nickname + " ametoka katika redio.", "exit");
      this.offStatus = true;
      router.go(-1);
    },
  },
};

I'm not sure why this should be a problem with Firebase internals. I think this is a bug

shija-ntula avatar Jan 12 '23 03:01 shija-ntula

Interesting. The only way I was able to reproduce this was to set this.roomid to undefined, but let me try passing in the values as props.

maneesht avatar Jan 12 '23 17:01 maneesht

Interesting. The only way I was able to reproduce this was to set this.roomid to undefined, but let me try passing in the values as props.

Since the undefined this.roomId is also part of reference (ref(database, "chatrooms/" + this.roomid + "/chats")) I guess that's an issue with db reference

shija-ntula avatar Jan 12 '23 21:01 shija-ntula

I was able to reproduce the issue with create-vue, but downgrading to 8.2.3 didn't seem to work for me.

And after investigating some more today, it seems like the issue has something to do with the interaction between vue's proxying of RTDB and that object interacting with the SDK's, as when I created an object outside of the vue context and used that in vue, everything worked fine.

maneesht avatar Jan 12 '23 23:01 maneesht

Are you using the same CLI tool? And the latest version of it?

maneesht avatar Jan 12 '23 23:01 maneesht

I created my vue project with create-vue and then installed firebase with npm i firebase

shija-ntula avatar Jan 13 '23 04:01 shija-ntula

but downgrading to 8.2.3 didn't seem to work for me.

The same to me

shija-ntula avatar Jan 13 '23 04:01 shija-ntula

In the meantime, I would recommend you take a look at the official vue bindings for Firebase

maneesht avatar Jan 13 '23 16:01 maneesht

This problem seems to be related to the Vue or Pinia/Vuex state. I managed to solve the problem by keeping the reference in an external variable that receives events and updates data by taking it out of state and putting it outside my defineStore

@store
state: () => ({
   messagesRef : {} 
   ...
   this.messagesRef.on('child_added', (snapshot: any) => {} // receiving
   ...
   this.messagesRef.push(payload.message); // pushing error
})

instead:


let messagesRef = {} as any;
export const firebaseStore = defineStore('x', {
   ... do the same ...
}

Pxeba avatar Feb 21 '23 17:02 Pxeba

I initially wrote this comment, and was about to post a short reproduction:

This is a persistent bug even when you use VueFire.

You can set or update a database location, and then use $databaseBind to bind to it, no problem.

But if you set or update a database location that is already $databaseBind'ed, you will get these hard-to-understand errors, e.g.

VM6601 chunk-3G7ZM6PE.js:4031 Uncaught (in promise) TypeError: newChildren.remove is not a function at VM6601 chunk-3G7ZM6PE.js:4031:37 at map (VM6602 chunk-R5DULB7A.js:608:21) at Proxy.addToIndexes (VM6601 chunk-3G7ZM6PE.js:4008:24) at Proxy.updateImmediateChild (VM6601 chunk-3G7ZM6PE.js:4128:38) at Proxy.updateChild (VM6601 chunk-3G7ZM6PE.js:4567:19) at viewProcessorApplyUserOverwrite (VM6601 chunk-3G7ZM6PE.js:6508:51) at viewProcessorApplyOperation (VM6601 chunk-3G7ZM6PE.js:6356:23) at viewApplyOperation (VM6601 chunk-3G7ZM6PE.js:6725:18) at syncPointApplyOperation (VM6601 chunk-3G7ZM6PE.js:6775:30) at syncTreeApplyOperationHelper_ (VM6601 chunk-3G7ZM6PE.js:7128:30)

The database update does occur. The error occurs in the part of the code that is watching for changes.

I am on: "@vue/compat": "^3.2.47", "firebase": "^10.4.0", "vue": "^3.2.47", "vuefire": "^3.1.17"

BUT THEN

I found I could not reproduce it in a short program that does exactly what I described above. So now I know it is something more subtle. I will report back when I find out what subtlety is needed to trigger the error.

darrelfrancis avatar Sep 17 '23 10:09 darrelfrancis

Hi all, as a workaround, are you able to use the function toRaw? In your example @shija-ntula, I was able to call toRaw on this.chatsRef to bypass the Vue Proxy and it seemed to fix the issue:

import { defineComponent, toRaw } from "vue";
...
onValue(toRaw(this.chatsRef), (snapshot) => {
...

maneesht avatar Apr 03 '24 17:04 maneesht

@maneesht this is still happening even if we use toRaw method

suhail23599 avatar Apr 05 '24 09:04 suhail23599

@maneesht this is the log i put in node_module. Here i am getting newChildren as empty. This happens when i use listener like onChildChanged. I have used different DB reference variable for this listener. Firebase version: 10.7.2 Vue: 3.4.19 Product: Realtime database Screenshot 2024-04-05 at 4 01 26 PM

Are there any scenarios wherein newChildren object can be empty?

suhail23599 avatar Apr 05 '24 09:04 suhail23599

we had a workaround to fix this issue. In our application, we had multiple DB listeners which used same firebase DB reference that was causing the issue. Hence, we created different DB references for each listeners like onChildChanged, onchildAdded, onValue etc. Eg: ` this.firebaseChatListListener = db

this.firebaseChatRoomListener = db

onChildAdded(ref(this.firebaseChatListListener, ${path}), callback)

onValue(ref(this.firebaseChatRoomListener, ${path}), callback) `

suhail23599 avatar Apr 11 '24 05:04 suhail23599

Solution

I have solved this problem by writing a small helper function. I replace all calls to firebase set(), with calls to my function setClean()

import {set,ref} from "firebase/database";

export function setClean(firebaseRef, valueMaybeUndefined) {

  //          _                       _   _  _
  //  /|     |_ o      _|_ |_   _    |_) |_ |_
  //   | o   |  | ><    |_ | | (/_   | \ |_ |

  // Because it may be a Vue Proxy, which causes an error if you are also
  // listening at that firebase location, we must reconstitute the 
  // reference from the path.

  const targetPath = firebaseRef._path.pieces_.join("/");
  const targetRef = ref(db, targetPath);

  // _       _                                       _
  //  )     |_ o      _|_ |_   _    \  / /\  |  | | |_
  // /_ o   |  | ><    |_ | | (/_    \/ /--\ |_ |_| |_

  // Because we are not allowed to write undefined to Firebase, 
  // we convert it to null.

  const valueNeverUndefined =
    valueMaybeUndefined === undefined ? null : valueMaybeUndefined;

  console.log("setClean", targetPath, valueNeverUndefined);
  return set(targetRef, valueNeverUndefined);
}

So if my code initially said:

import {set} from "firebase/database";
...
set(x,y)

I now say

import {setClean} from     .... my helper function as shown above ...
...
setClean(x,y)

darrelfrancis avatar Apr 22 '24 04:04 darrelfrancis

Same issue for me. Can't set/remove while having onValue working in my JS react webapp. Workaround with two "db" doesn't help - or what exactly is the way to initialize two different db objects? "firebase": "^10.12.2"

ateryaev avatar Jun 16 '24 06:06 ateryaev