digitalfabrik/integreat-app

View on GitHub
native/src/utils/DefaultDataContainer.ts

Summary

Maintainability
B
4 hrs
Test Coverage
B
88%
import { difference, flatMap, isEmpty, map, omitBy } from 'lodash'
import { DateTime } from 'luxon'
import BlobUtil from 'react-native-blob-util'

import { CategoriesMapModel, CityModel, EventModel, LocalNewsModel, PoiModel } from 'shared/api'

import Cache from '../models/Cache'
import DatabaseContext from '../models/DatabaseContext'
import {
  CityResourceCacheStateType,
  DataContainer,
  LanguageResourceCacheStateType,
  PageResourceCacheStateType,
} from './DataContainer'
import DatabaseConnector from './DatabaseConnector'
import { log } from './sentry'

type CacheType = {
  pois: Cache<Array<PoiModel>>
  cities: Cache<Array<CityModel>>
  events: Cache<Array<EventModel>>
  categories: Cache<CategoriesMapModel>
  localNews: Cache<Array<LocalNewsModel>>
  resourceCache: Cache<CityResourceCacheStateType>
  lastUpdate: Cache<DateTime | null>
}
type CacheKeyType = keyof CacheType

class DefaultDataContainer implements DataContainer {
  _databaseConnector: DatabaseConnector
  caches: CacheType

  constructor() {
    this._databaseConnector = new DatabaseConnector()
    this.caches = {
      pois: new Cache<Array<PoiModel>>(
        this._databaseConnector,
        (connector: DatabaseConnector, context: DatabaseContext) => connector.loadPois(context),
        (value: Array<PoiModel>, connector: DatabaseConnector, context: DatabaseContext) =>
          connector.storePois(value, context),
      ),
      cities: new Cache<Array<CityModel>>(
        this._databaseConnector,
        (connector: DatabaseConnector) => connector.loadCities(),
        (value: Array<CityModel>, connector: DatabaseConnector) => connector.storeCities(value),
      ),
      events: new Cache<Array<EventModel>>(
        this._databaseConnector,
        (connector: DatabaseConnector, context: DatabaseContext) => connector.loadEvents(context),
        (value: Array<EventModel>, connector: DatabaseConnector, context: DatabaseContext) =>
          connector.storeEvents(value, context),
      ),
      categories: new Cache<CategoriesMapModel>(
        this._databaseConnector,
        (connector: DatabaseConnector, context: DatabaseContext) => connector.loadCategories(context),
        (value: CategoriesMapModel, connector: DatabaseConnector, context: DatabaseContext) =>
          connector.storeCategories(value, context),
      ),
      localNews: new Cache<LocalNewsModel[]>(
        this._databaseConnector,
        (connector: DatabaseConnector, context: DatabaseContext) => connector.loadLocalNews(context),
        (value: LocalNewsModel[], connector: DatabaseConnector, context: DatabaseContext) =>
          connector.storeLocalNews(value, context),
      ),
      resourceCache: new Cache<CityResourceCacheStateType>(
        this._databaseConnector,
        (connector: DatabaseConnector, context: DatabaseContext) => connector.loadResourceCache(context),
        (value: CityResourceCacheStateType, connector: DatabaseConnector, context: DatabaseContext) =>
          connector.storeResourceCache(value, context),
      ),
      lastUpdate: new Cache<DateTime | null>(
        this._databaseConnector,
        (connector: DatabaseConnector, context: DatabaseContext) => connector.loadLastUpdate(context),
        (value: DateTime | null, connector: DatabaseConnector, context: DatabaseContext) =>
          connector.storeLastUpdate(value, context),
      ),
    }
  }

  clearInMemoryCache = (): void => {
    Object.keys(this.caches).forEach(cache => this.caches[cache as keyof CacheType].evict())
  }

  deleteCity = async (city: string): Promise<void> => {
    await this._databaseConnector.deleteCities([city])
  }

  // WARNING: Be careful using this method, it deletes ALL offline content, including meta data which may lead to inconsistent app states and break our offline functionality.
  _clearOfflineCache = async (): Promise<void> => {
    await this._databaseConnector.deleteAllFiles()
  }

  isCached(key: CacheKeyType, context: DatabaseContext): boolean {
    return this.caches[key].isCached(context)
  }

  getCities = async (): Promise<Array<CityModel>> => {
    const cache = this.caches.cities
    return cache.get(new DatabaseContext())
  }

  getCategoriesMap = (city: string, language: string): Promise<CategoriesMapModel> => {
    const context = new DatabaseContext(city, language)
    const cache: Cache<CategoriesMapModel> = this.caches.categories
    return cache.get(context)
  }

  getEvents = (city: string, language: string): Promise<Array<EventModel>> => {
    const context = new DatabaseContext(city, language)
    const cache: Cache<Array<EventModel>> = this.caches.events
    return cache.get(context)
  }

  getPois = (city: string, language: string): Promise<Array<PoiModel>> => {
    const context = new DatabaseContext(city, language)
    const cache: Cache<Array<PoiModel>> = this.caches.pois
    return cache.get(context)
  }

  getLocalNews = (city: string, language: string): Promise<LocalNewsModel[]> => {
    const context = new DatabaseContext(city, language)
    const cache: Cache<LocalNewsModel[]> = this.caches.localNews
    return cache.get(context)
  }

  getResourceCache = async (city: string, language: string): Promise<LanguageResourceCacheStateType> => {
    const context = new DatabaseContext(city)
    const cache: Cache<CityResourceCacheStateType> = this.caches.resourceCache
    const resourceCache = await cache.get(context)

    return resourceCache[language] ?? {}
  }

  getLastUpdate = (city: string, language: string): Promise<DateTime | null> => {
    const context = new DatabaseContext(city, language)
    const cache: Cache<DateTime | null> = this.caches.lastUpdate
    return cache.get(context)
  }

  setCategoriesMap = async (city: string, language: string, categories: CategoriesMapModel): Promise<void> => {
    const context = new DatabaseContext(city, language)
    const cache: Cache<CategoriesMapModel> = this.caches.categories
    await cache.cache(categories, context)
  }

  setPois = async (city: string, language: string, pois: Array<PoiModel>): Promise<void> => {
    const context = new DatabaseContext(city, language)
    const cache: Cache<Array<PoiModel>> = this.caches.pois
    await cache.cache(pois, context)
  }

  setLocalNews = async (city: string, language: string, localNews: LocalNewsModel[]): Promise<void> => {
    const context = new DatabaseContext(city, language)
    const cache: Cache<LocalNewsModel[]> = this.caches.localNews
    await cache.cache(localNews, context)
  }

  setCities = async (cities: Array<CityModel>): Promise<void> => {
    const cache = this.caches.cities
    await cache.cache(cities, new DatabaseContext())
  }

  setEvents = async (city: string, language: string, events: Array<EventModel>): Promise<void> => {
    const context = new DatabaseContext(city, language)
    const cache: Cache<Array<EventModel>> = this.caches.events
    await cache.cache(events, context)
  }

  getFilePathsFromLanguageResourceCache(languageResourceCache: LanguageResourceCacheStateType): Array<string> {
    const pageResourceCaches: Array<PageResourceCacheStateType> = Object.values(languageResourceCache)
    return flatMap(
      pageResourceCaches,
      (file: PageResourceCacheStateType): Array<string> => map(file, ({ filePath }) => filePath),
    )
  }

  setResourceCache = async (
    city: string,
    language: string,
    resourceCache: LanguageResourceCacheStateType,
  ): Promise<void> => {
    const context = new DatabaseContext(city)
    const cache: Cache<CityResourceCacheStateType> = this.caches.resourceCache
    const previousResourceCache = cache.getCached(context)

    if (!previousResourceCache) {
      await cache.cache(
        {
          [language]: resourceCache,
        },
        context,
      )
      return
    }

    const newResourceCache = { ...previousResourceCache, [language]: resourceCache }
    const previousLanguageResourceCache = previousResourceCache[language]

    if (previousLanguageResourceCache) {
      // Cleanup old resources
      const oldPaths = this.getFilePathsFromLanguageResourceCache(previousLanguageResourceCache)
      const newPaths = this.getFilePathsFromLanguageResourceCache(resourceCache)
      const removedPaths = difference(oldPaths, newPaths)

      if (!isEmpty(removedPaths)) {
        const collection: CityResourceCacheStateType = omitBy(
          previousResourceCache,
          (val, key: string) => key === language,
        )
        const pathsOfOtherLanguages = flatMap(collection, (languageCache: LanguageResourceCacheStateType) =>
          this.getFilePathsFromLanguageResourceCache(languageCache),
        )
        const pathsToClean = difference(removedPaths, pathsOfOtherLanguages)
        log('Cleaning up the following resources:')
        log(pathsToClean.join(', '))
        await Promise.all(pathsToClean.map(path => BlobUtil.fs.unlink(path)))
      }
    }

    await cache.cache(newResourceCache, context)
  }

  setLastUpdate = async (city: string, language: string, lastUpdate: DateTime | null): Promise<void> => {
    const context = new DatabaseContext(city, language)
    const cache: Cache<DateTime | null> = this.caches.lastUpdate
    await cache.cache(lastUpdate, context)
  }

  poisAvailable = async (city: string, language: string): Promise<boolean> => {
    const context = new DatabaseContext(city, language)
    return this.isCached('pois', context) || this._databaseConnector.isPoisPersisted(context)
  }

  citiesAvailable = async (): Promise<boolean> => {
    const context = new DatabaseContext()
    return this.isCached('cities', context) || this._databaseConnector.isCitiesPersisted()
  }

  categoriesAvailable = async (city: string, language: string): Promise<boolean> => {
    const context = new DatabaseContext(city, language)
    return this.isCached('categories', context) || this._databaseConnector.isCategoriesPersisted(context)
  }

  eventsAvailable = async (city: string, language: string): Promise<boolean> => {
    const context = new DatabaseContext(city, language)
    return this.isCached('events', context) || this._databaseConnector.isEventsPersisted(context)
  }

  localNewsAvailable = async (city: string, language: string): Promise<boolean> => {
    const context = new DatabaseContext(city, language)
    return this.isCached('localNews', context) || this._databaseConnector.isLocalNewsPersisted(context)
  }

  storeLastUsage = async (city: string): Promise<void> => {
    await this._databaseConnector.storeLastUsage(new DatabaseContext(city))
  }
}

const dataContainer = new DefaultDataContainer()

export default dataContainer