All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.apollographql.apollo.cache.normalized.OptimisticNormalizedCache Maven / Gradle / Ivy

/**
 * Copyright 2018-2019 Amazon.com,
 * Inc. or its affiliates. All Rights Reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

package com.apollographql.apollo.cache.normalized;

import com.apollographql.apollo.api.internal.Action;
import com.apollographql.apollo.api.internal.Function;
import com.apollographql.apollo.api.internal.Optional;
import com.apollographql.apollo.cache.CacheHeaders;
import com.nytimes.android.external.cache.Cache;
import com.nytimes.android.external.cache.CacheBuilder;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import static com.apollographql.apollo.api.internal.Utils.checkNotNull;

public final class OptimisticNormalizedCache extends NormalizedCache {
  private final Cache lruCache = CacheBuilder.newBuilder().build();

  @Nullable @Override public Record loadRecord(@Nonnull final String key, @Nonnull final CacheHeaders cacheHeaders) {
    checkNotNull(key, "key == null");
    checkNotNull(cacheHeaders, "cacheHeaders == null");

    try {
      final Optional nonOptimisticRecord = nextCache()
          .flatMap(new Function>() {
            @Nonnull @Override public Optional apply(@Nonnull NormalizedCache cache) {
              return Optional.fromNullable(cache.loadRecord(key, cacheHeaders));
            }
          });
      final RecordJournal journal = lruCache.getIfPresent(key);
      if (journal != null) {
        return nonOptimisticRecord.map(new Function() {
          @Nonnull @Override public Record apply(@Nonnull Record record) {
            Record result = record.clone();
            result.mergeWith(journal.snapshot);
            return result;
          }
        }).or(journal.snapshot.clone());
      } else {
        return nonOptimisticRecord.orNull();
      }
    } catch (Exception ignore) {
      return null;
    }
  }

  @Nonnull @Override public Set merge(@Nonnull final Record record, @Nonnull final CacheHeaders cacheHeaders) {
    checkNotNull(record, "record == null");
    checkNotNull(cacheHeaders, "cacheHeaders == null");

    return nextCache().map(new Function>() {
      @Nonnull @Override public Set apply(@Nonnull NormalizedCache cache) {
        return cache.merge(record, cacheHeaders);
      }
    }).or(Collections.emptySet());
  }

  @Override public void clearAll() {
    lruCache.invalidateAll();
    //noinspection ResultOfMethodCallIgnored
    nextCache().apply(new Action() {
      @Override public void apply(@Nonnull NormalizedCache cache) {
        cache.clearAll();
      }
    });
  }

  @Override public boolean remove(@Nonnull final CacheKey cacheKey) {
    checkNotNull(cacheKey, "cacheKey == null");

    boolean result = nextCache().map(new Function() {
      @Nonnull @Override public Boolean apply(@Nonnull NormalizedCache cache) {
        return cache.remove(cacheKey);
      }
    }).or(Boolean.FALSE);

    if (lruCache.getIfPresent(cacheKey.key()) != null) {
      lruCache.invalidate(cacheKey.key());
      result = true;
    }

    return result;
  }

  @Nonnull public Set mergeOptimisticUpdates(@Nonnull Collection recordSet) {
    Set aggregatedDependentKeys = new LinkedHashSet<>();
    for (Record record : recordSet) {
      aggregatedDependentKeys.addAll(mergeOptimisticUpdate(record));
    }
    return aggregatedDependentKeys;
  }

  @Nonnull public Set mergeOptimisticUpdate(@Nonnull final Record record) {
    checkNotNull(record, "record == null");

    final RecordJournal journal = lruCache.getIfPresent(record.key());
    if (journal == null) {
      lruCache.put(record.key(), new RecordJournal(record));
      return Collections.singleton(record.key());
    } else {
      return journal.commit(record);
    }
  }

  @Nonnull public Set removeOptimisticUpdates(@Nonnull final UUID mutationId) {
    checkNotNull(mutationId, "mutationId == null");

    Set changedCacheKeys = new HashSet<>();
    Set removedKeys = new HashSet<>();
    Map recordJournals = lruCache.asMap();
    for (Map.Entry entry : recordJournals.entrySet()) {
      String cacheKey = entry.getKey();
      RecordJournal journal = entry.getValue();
      changedCacheKeys.addAll(journal.revert(mutationId));
      if (journal.history.isEmpty()) {
        removedKeys.add(cacheKey);
      }
    }
    lruCache.invalidateAll(removedKeys);
    return changedCacheKeys;
  }

  private final class RecordJournal {
    Record snapshot;
    final LinkedList history = new LinkedList<>();

    RecordJournal(Record mutationRecord) {
      this.snapshot = mutationRecord.clone();
      this.history.add(mutationRecord.clone());
    }

    /**
     * Commits new version of record to the history and invalidate snapshot version.
     */
    Set commit(Record record) {
      history.addLast(record.clone());
      return snapshot.mergeWith(record);
    }

    /**
     * Lookups record by mutation id, if it's found removes it from the history and invalidates snapshot record.
     * Snapshot record is superposition of all record versions in the history.
     */
    Set revert(UUID mutationId) {
      int recordIndex = -1;
      for (int i = 0; i < history.size(); i++) {
        if (mutationId.equals(history.get(i).mutationId())) {
          recordIndex = i;
          break;
        }
      }

      if (recordIndex == -1) {
        return Collections.emptySet();
      }

      Set changedKeys = new HashSet<>();
      changedKeys.add(history.remove(recordIndex).key());
      for (int i = Math.max(0, recordIndex - 1); i < history.size(); i++) {
        Record record = history.get(i);
        if (i == Math.max(0, recordIndex - 1)) {
          snapshot = record.clone();
        } else {
          changedKeys.addAll(snapshot.mergeWith(record));
        }
      }
      return changedKeys;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy