Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A fragile weaving of various modules together to convince the Firestore web SDK to use persistence in an un-ejected Expo app.
/*
expo-firestore-persistence-hack
A fragile weaving of various modules together to convince the Firestore
web SDK to use persistence in an un-ejected Expo app.
To use, first:
```
$ expo install expo-sqlite
$ yarn add indexeddbshim
```
Then add the contents of this file to `expo-firestore-persistence-hack.js`
in your project, and import it very early (likely in App.js). After this is
imported, you can ask firestore to enable persistence:
`firebase.firestore().enablePersistence()`
The bulk of the work is done by indexeddbshim, which provides an IndexedDB
implementation on top of WebSQL. See the comments below above each hunk of
code to understand how hacky this all is.
*/
import { SQLite } from "expo-sqlite";
// Hack 1: The SQLite module from Expo is nominally
// WebSQL compatible. Let's just have the IndexedDB shim
// use it!
window.openDatabase = SQLite.openDatabase;
// Hack 2: indexeddbshim will try to examine navigator.userAgent
// if navigator exists. In React Native, it does, but has no
// userAgent set. Set one here to avoid a crash.
navigator.userAgent = "React-Native";
// Hack 3: Initialize indexeddbshim with origin checks disabled,
// cause they'll fail on our platform (and don't quite make sense.)
// (Do not change this to an import, cause that will get hoisted above
// our userAgent hack above.)
const setGlobalVars = require("./node_modules/indexeddbshim/dist/indexeddbshim-noninvasive");
setGlobalVars(window, { checkOrigin: false });
// Hack 4: Firestore persistence really wants to use localStorage
// to communicate between tabs. We don't really care about
// communicating between tabs - everything will be in the same
// process. However, Firestore needs something. So we'll give it
// a really weak, fake, in-memory localStorage. (Persisted storage
// will go through IndexedDB and into SQLite, on disk.)
window.__localStorageStore = {};
window.localStorage = {
getItem: function(key) {
return window.__localStorageStore[key];
},
setItem: function(key, value) {
window.__localStorageStore[key] = value;
},
removeItem: function(key) {
delete window.__localStorageStore[key];
},
clear: function() {
window.__localStorageStore = {};
},
key: function(i) {
// Ever since ES6, the order of keys returned here is
// stable 🤞
Object.keys(window.__localStorageStore)[i];
}
};
// You should now be able to initialize Firebase, and call
// firebase.firestore().enablePersistence()
//
// YMMV! YOLO!
@smontlouis
Copy link

smontlouis commented Sep 11, 2019

Thanks for the hack. Unfortunately this is what I got on Android apk build using Expo :

[2019-09-11T03:05:34.359Z]  @firebase/firestore: Firestore (6.3.5): INTERNAL UNHANDLED ERROR:  Could not evaluate a key from keyPath and there is no key generator

[2019-09-11T03:05:34.367Z]  @firebase/firestore: Firestore (6.3.5): FIRESTORE (6.3.5) INTERNAL ASSERTION FAILED: AsyncQueue is already failed: Could not evaluate a key from keyPath and there is no key generator


Error: FIRESTORE (6.3.5) INTERNAL ASSERTION FAILED: AsyncQueue is already failed: Could not evaluate a key from keyPath and there is no key generator
--

image

And another log

image

Code is here : https://github.com/bulby97/bible-strong/blob/feature/expo-firestore-persistence/src/helpers/expo-firestore-persistence-hack.js

@zwily
Copy link
Author

zwily commented Feb 19, 2020

I'm not using this (I ended up ejecting and going to the native Firebase libs for other reasons), but there is more discussion at indexeddbshim/IndexedDBShim#313.

@brettz9
Copy link

brettz9 commented Feb 20, 2020

Thanks for this write-up which seems it has helped other users.

Letting you know that lines 31-34 setting userAgent should now no longer be necessary as of version 6.1.0 of IndexedDBShim.

(Btw, for anyone reading this, we'd welcome docs for IndexedDBShim which explain usage on React Native usage and/or Firebase/Firestore; I'm not familiar with these environments myself even to know whether they'd use Node or browser APIs.)

@nandorojo
Copy link

nandorojo commented Mar 13, 2020

Should we only include this code if we're not on web? Asking for cases with Expo web.

For instance, should App.js look like this?

import { Platform } from 'react-native'
import firebase from 'firebase'

if (Platform.OS !== 'web') {
  require('expo-firestore-persistence-hack.js')
}

firebase.firestore().enablePersistence()

//... other code

@nandorojo
Copy link

nandorojo commented Mar 26, 2020

Thanks for this write-up which seems it has helped other users.

Letting you know that lines 31-34 setting userAgent should now no longer be necessary as of version 6.1.0 of IndexedDBShim.

(Btw, for anyone reading this, we'd welcome docs for IndexedDBShim which explain usage on React Native usage and/or Firebase/Firestore; I'm not familiar with these environments myself even to know whether they'd use Node or browser APIs.)

The need for this comes from Firestore's lack of offline support for React Native.

Screen Shot 2020-03-26 at 2 23 13 AM

That screenshot comes from this feature request for Firestore to enable offline persistence, which requires IndexedDB.

I'm still really hoping to make this happen, as it would be amazing to have in React Native apps.

@brettz9
Copy link

brettz9 commented Mar 26, 2020

I expect you should be able to use my new fork at https://github.com/indexeddbshim/indexeddbshim (I have been the main contributor of IndexedDBShim for the past few years, and have forked because the maintainer is out of touch, and I wanted to alter the repo settings).

The new fork removes some of the require statements from the non-Node builds that were problematic in those environments (as well as including the above-mentioned userAgent fix).

Feel free to file an issue at https://github.com/indexeddbshim/indexeddbshim/issues , including to report if it works for you (and if any special config was needed).

Good luck!

@nandorojo
Copy link

nandorojo commented Mar 26, 2020

@niklasnatter
Copy link

niklasnatter commented Jun 26, 2020

Just wanted to thank you for your work on this!

It looks like indexeddbshim does not examine navigator.userAgent anymore, so the following lines seem to be sufficient for me on Android:

import * as SQLite from 'expo-sqlite';
import setGlobalVars from 'indexeddbshim/dist/indexeddbshim-noninvasive';

window.localStorage = {
    _data: {},

    getItem: function (key) {
        return this._data[key];
    },
    setItem: function (key, value) {
        this._data[key] = value;
    },
    removeItem: function (key) {
        delete this._data[key];
    },
    clear: function () {
        this._data = {};
    },
    key: function (i) {
        return Object.keys(this._data)[i];
    },
};

setGlobalVars(window, { checkOrigin: false, win: SQLite });

@brettz9
Copy link

brettz9 commented Jun 27, 2020

Excellent, thanks for reporting back!

Note that my comment about making a fork of indexeddbshim (and a @indexeddbshim/indexeddbshim npm package) is no longer relevant, as the maintainer got back in touch, and we've redirected the old axemclion/indexeddbshim repo on Github to indexeddbshim/indexeddbshim while keeping indexeddbshim as the npm package name (and I just backported my @indexeddbshim/indexeddbshim changes to the indexeddbshim release, so everyone can resume (or keep) using indexeddbshim).

@nandorojo
Copy link

nandorojo commented Jun 27, 2020

Just curious, since I haven’t used this in the meantime — is this hack successful at adding persistence on RN now?

@brettz9
Copy link

brettz9 commented Jun 27, 2020

I think it should work since we've removed the conditional Node require statements (into a file not bundled into dist/indexeddbshim-noninvasive). If someone can confirm, please report to indexeddbshim/IndexedDBShim#313 so we can close the issue. Thanks!

@nandorojo
Copy link

nandorojo commented Jun 27, 2020

I can test it on iOS tomorrow.

@nandorojo
Copy link

nandorojo commented Jun 27, 2020

Btw, which version of indexeddbshim should I be using?

@brettz9
Copy link

brettz9 commented Jun 27, 2020

6.6.0, the latest.

@nandorojo
Copy link

nandorojo commented Jun 30, 2020

@nnatter would you mind posting the full example with your change? I notice you changed it to an import, but the original "Hack #3" says not do hoist it before the userAgent is set.

@niklasnatter
Copy link

niklasnatter commented Jun 30, 2020

The code posted above should be a full example - is there anything missing for you?

@nandorojo
Copy link

nandorojo commented Jun 30, 2020

Oh, sorry about that, got it! Trying it out now.

@nandorojo
Copy link

nandorojo commented Jun 30, 2020

I haven't tested on collection queries yet, but I just did tests on Android and iOS for Firestore documents, and it seems to be working!

@jm1200
Copy link

jm1200 commented Dec 8, 2020

Hello, I have been using expo-firestore-offline-persistence for about a month and it has been great. I ran into an issue after I accidentally installed the latest version of firebase with npm install firebase instead of expo install firebase have been getting the following error:

Error enabling offline persistence. Falling back to persistence disabled: FirebaseError: [code=failed-precondition]: A newer version of the Firestore SDK was previously used and so the persisted data is not compatible with the version of the SDK you are now using. The SDK will operate with persistence disabled. If you need persistence, please re-upgrade to a newer version of the SDK or else clear the persisted IndexedDB data for your app to start fresh.

I tried deleting my node_modules and reinstalling everything the correct way but am still getting the error. I don't know enough about SQLite or indexeddbshim and was hoping someone might be able to point me in the right direction for how to: or else clear the persisted IndexedDB data for your app to start fresh.

@olliecwd
Copy link

olliecwd commented Feb 22, 2021

Hey there. This is really cool! Thanks for the great work on this :D

I'm currently using it to build out a prototype of an app, and I was wondering how far I could take it. I personally haven't found any issues with it so far, but my testing hasn't been too substantial. Do we know roughly what the guarantees are on this? Has anyone else been using it extensively?

I really like it; am I mad for thinking about using this in production/release?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment