morrys / wora

Write Once, Render Anywhere. typescript libraries: cache-persist, apollo-offline, relay-offline, offline-first, apollo-cache, relay-store, netinfo, detect-network
https://morrys.github.io/wora/docs/introduction
MIT License
174 stars 4 forks source link

Relay Offline with MMKV #137

Closed matt-dalton closed 11 months ago

matt-dalton commented 1 year ago

Have you considered creating an MMKV layer for React Native apps that we use for your libraries? Async storage works well, but it involves a lot of data going over the bridge (particularly with the volume of data Relay uses), so in theory MMKV should be much more performant.

I realise you'd probably need to customise this at the cache-persist layer. If this isn't on your roadmap, is there a guide for how we might switch out the storage layer? I've seen simple files like this, but not sure how we'd plug that in at the relay offline layer.

morrys commented 1 year ago

you have this option to configure the storage you prefer :) https://github.com/morrys/wora/blob/master/packages/cache-persist/src/CacheTypes.ts#L23

Once you have created the storage, can you share it here?

matt-dalton commented 1 year ago

Ahh nice...looks super straightforward. We should be able to have a go at this shortly.

redbar0n commented 11 months ago

@matt-dalton how did it go? care to share the storage?

matt-dalton commented 11 months ago

Good reminder! We eventually moved away from the idea of MMKV as a relay store because of the discussions in this thread. We ended up using react-native-quick-sqlite to store the data in a single table. The implementation here was a bit more complex, because we had to create a couple of layers to store/fetch data in the right format for the table. but it was something like:

// This can be passed into relay offline
export const sqlLiteStorage: ICacheStorage = {
    multiRemove: (keys: string[]): Promise<void> => SQLiteAPI.deleteJSON(keys),
    multiGet: (keys: string[]): Promise<string[][]> =>
        SQLiteAPI.getJSON(keys).then((result) => {
            return result || [[]]
        }),
    getAllKeys: (): Promise<string[]> => SQLiteAPI.getAllJSONKeys().then((result) => result || []),
    multiSet: (items: KeyValuePair[]): Promise<void> => SQLiteAPI.setJSON(items),
    setItem: (key: string, value: string): Promise<void> => SQLiteAPI.setJSON([key, value]),
    removeItem: (key: string): Promise<void> => SQLiteAPI.deleteJSON(key),
    getItem: (key: string): Promise<string> =>
        SQLiteAPI.getJSON(key).then((result) => {
            return result || ''
        }),
}

Then here's an API layer. Note we only use quick SQLite for this specific use-case, so this is very much optimised for relay and hence not suitable for broader applications:

class SQLiteAPI {
    private hasInitialized: boolean = false

    async init(): Promise<void> {
        // Seems to take about 200ms the first time this happens to create the DB
        if (!this.hasInitialized) {
            await SQLite.createTables()
            this.hasInitialized = true
        }
    }

    public async getJSON(keyOrKeys: string | string[]): Promise<string | string[][] | null> {
        await this.init()

        if (Array.isArray(keyOrKeys)) {
            return SQLite.selectMultipleKeysFromJSON(keyOrKeys).then((result) => result || null)
        }

        return SQLite.selectKeyFromJSON(keyOrKeys).then((result) => (!!result ? result : null))
    }

    public async setJSON(keyValuePairOrArray: KeyValuePair | KeyValuePair[]): Promise<void> {
        await this.init()

        if (isKeyValuePairArray(keyValuePairOrArray)) {
            return SQLite.upsertMultipleKeysFromJSON(keyValuePairOrArray)
        }

        const [key, value] = keyValuePairOrArray
        return SQLite.upsertKeyFromJSON(key, value)
    }

    public async deleteJSON(keyOrKeys: string | string[]): Promise<void> {
        await this.init()

        if (Array.isArray(keyOrKeys)) {
            return SQLite.deleteMultipleKeysFromJSON(keyOrKeys)
        }

        return SQLite.deleteKeyFromJSON(keyOrKeys)
    }

    public async getAllJSONKeys(): Promise<string[] | null> {
        await this.init()

        return SQLite.selectAllJSONKeys().then((result) => result || null)
    }
}

Then you need to implement a controller/wrapper with functions that grab the JSON data (e.g. SQLite.selectAllJSONKeys()). We built all this fairly quickly so can almost certainly be improved.

If you do want to create an MMKV version, it's much easier. You can just mimic the first file above, and use it to call MMKV fairly directly, since the API is similar.