Quickly finding users by phone number with Firebase backend

Viewed 1166

I’m working on an app with a Firebase backend. During sign up I would like to let new users see which of their contacts are already on the app to add them as friends. So basically, use phone numbers to match users with contacts.

I am having a big performance headache when querying the database to find users.
Since Firestore does not support OR queries, I run two queries per phone number (one to check national format, the other for international format), and if any returns a document, set that document as the found user:

findUserByPhoneNumber = (number, callback) => {

  //utility function to, well, sanitize phone numbers
  sanitizeNumber = (str) => {
    if (str) {
      var num = str.match(/\d/g);
      num = num.join("");
      return num;
    } else {
      return null
    }
  }

  var foundUser = null

  Promise.all([
    usersRef.where('phoneNumbers.nationalFormat', '==', sanitizeNumber(number)).get()
      .then(snapshot => {
        if (snapshot.docs.length > 0 && snapshot.docs[0].data()) {
          // console.log('nationalFormat result: ', snapshot.docs[0]);
          foundUser = snapshot.docs[0].data()
        }
        return foundUser
      }),
    usersRef.where('phoneNumbers.internationalFormat', '==', sanitizeNumber(number)).get()
      .then(snapshot => {
        if (snapshot.docs.length > 0 && snapshot.docs[0].data()) {
          // console.log('internationalFormat result: ', snapshot.docs[0]);
          foundUser = snapshot.docs[0].data()
        }
        return foundUser
      })
  ])
  .then(results => {
    res = results.filter(el => { return el != null })
    if (results.length > 0) {
      callback(res[0])
    }
  })
}

findUserByPhoneNumber runs for each contact in a loop. When testing on my phone with 205 contacts, the whole process takes about 30 seconds, which is about 29 seconds longer than I would like, especially given the test database has only 8 records...

getContacts = () => {

  getCs = () => {
    // Declare arrays
    const contactsWithAccount = []
    const contactsWithNoAccount = []

    // Get contacts from user's phone
    Contacts.getAll((err, contacts) => {
      if (err) throw err

      // For each contact, iterate
      for (var i = 0; i < contacts.length; i++) {
        const item = contacts[i]

        if (item.phoneNumbers && item.phoneNumbers.length > 0) {
          const phone = item.phoneNumbers[0].number

          // If the sanitized phone number is different from the current user's phone number (saved in DB), run the following logic
          if (this.state.user.phoneNumbers.nationalFormat != sanitizeNumber(phone)
            && this.state.user.phoneNumbers.internationalFormat != sanitizeNumber(phone)
          ) {

            findUserByPhoneNumber(phone, (fu) => {
              contactObject = {
                key: item.recordID,
                name: item.givenName,
                normalizedName: item.givenName.toLowerCase(),
                phoneNumber: phone,
                user: this.state.user,
                hasAccount: null,
                friendId: null,
                isFriend: null
              }

              const foundUser = fu

              // if found user, push in contactsWithAccount, otherwise push in contactsWithNoAccount
              if (foundUser && foundUser._id != this.state.user._id) {
                contactObject.hasAccount = true
                contactObject.friendId = foundUser._id
                if (this.state.user.friends && this.state.user.friends.includes(foundUser._id)) {
                  contactObject.isFriend = true
                }
                contactsWithAccount.push(contactObject)
              }
              else {
                contactsWithNoAccount.push(contactObject)
              }

              // if the two arrays are filled up, run the callback
              // NOTE_1: we use the two lengths +1 to account for the current
              //         user's document that we skip and dont add to any of the arrays
              // NOTE_2: this bizare method was the only way to handle the results
              //         coming in asynchronously
              if (contactsWithAccount.length + contactsWithNoAccount.length + 1 == contacts.length) {
                console.log('finished');
                sortCs(contactsWithAccount, contactsWithNoAccount)
              }
            })
          }
        }
      }

    })

  }

  // sorts the two arrays alphabetically
  sortCs = (withAccount, withNoAccount) => {
    compare = (a,b) => {
      if (a.name < b.name)
        return -1;
      if (a.name > b.name)
        return 1;
      return 0;
    }
    withAccount.sort(compare)
    withNoAccount.sort(compare)
    this.setState({ withAccount, withNoAccount })
  }

  // unleash the monster
  getCs(sortCs)
}

I am sure the process could be optimized in various ways. Maybe:


  • different database structure
  • bundling all queries into one
  • better use of async
  • starting the process at an earlier step in the signup flow

Whatsapp, HouseParty and a bunch of other apps have this feature in place and it loads instantly. I’m not trying to reach that level of perfection yet but there must be some better way…
Any help/suggestions would be greatly appreciated.

0 Answers
Related