Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce OptimisticNormalizedCache and API to write/rollback #607

Merged
merged 2 commits into from
Aug 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ public final class SqlNormalizedCache extends NormalizedCache {
private final SQLiteStatement updateStatement;
private final SQLiteStatement deleteStatement;
private final SQLiteStatement deleteAllRecordsStatement;
private final RecordFieldJsonAdapter recordFieldAdapter;

SqlNormalizedCache(RecordFieldJsonAdapter recordFieldAdapter, ApolloSqlHelper dbHelper) {
super(recordFieldAdapter);
this.recordFieldAdapter = recordFieldAdapter;
this.dbHelper = dbHelper;
database = dbHelper.getWritableDatabase();
insertStatement = database.compileStatement(INSERT_STATEMENT);
Expand Down Expand Up @@ -98,13 +99,13 @@ public final class SqlNormalizedCache extends NormalizedCache {
Optional<Record> optionalOldRecord = selectRecordForKey(apolloRecord.key());
Set<String> changedKeys;
if (!optionalOldRecord.isPresent()) {
createRecord(apolloRecord.key(), recordAdapter().toJson(apolloRecord.fields()));
createRecord(apolloRecord.key(), recordFieldAdapter.toJson(apolloRecord.fields()));
changedKeys = Collections.emptySet();
} else {
Record oldRecord = optionalOldRecord.get();
changedKeys = oldRecord.mergeWith(apolloRecord);
if (!changedKeys.isEmpty()) {
updateRecord(oldRecord.key(), recordAdapter().toJson(oldRecord.fields()));
updateRecord(oldRecord.key(), recordFieldAdapter.toJson(oldRecord.fields()));
}
}

Expand Down Expand Up @@ -204,7 +205,7 @@ Optional<Record> selectRecordForKey(String key) {
Record cursorToRecord(Cursor cursor) throws IOException {
String key = cursor.getString(1);
String jsonOfFields = cursor.getString(2);
return Record.builder(key).addFields(recordAdapter().from(jsonOfFields)).build();
return Record.builder(key).addFields(recordFieldAdapter.from(jsonOfFields)).build();
}

void clearCurrentCache() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
query HeroNameWithId {
hero {
id
name
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

import android.support.annotation.NonNull;

import com.apollographql.apollo.cache.normalized.CacheKey;
import com.apollographql.apollo.integration.normalizer.HeroAndFriendsNamesQuery;
import com.apollographql.apollo.integration.normalizer.type.Episode;
import com.apollographql.apollo.cache.ApolloCacheHeaders;
import com.apollographql.apollo.cache.CacheHeaders;
import com.apollographql.apollo.cache.normalized.CacheKey;
import com.apollographql.apollo.cache.normalized.NormalizedCache;
import com.apollographql.apollo.cache.normalized.NormalizedCacheFactory;
import com.apollographql.apollo.cache.normalized.Record;
import com.apollographql.apollo.cache.normalized.RecordFieldJsonAdapter;
import com.apollographql.apollo.exception.ApolloException;
import com.apollographql.apollo.integration.normalizer.HeroAndFriendsNamesQuery;
import com.apollographql.apollo.integration.normalizer.type.Episode;

import org.junit.After;
import org.junit.Before;
Expand Down Expand Up @@ -46,7 +46,7 @@ public void tearDown() throws IOException {

@Test
public void testHeadersReceived() throws ApolloException, IOException {
final NormalizedCache normalizedCache = new NormalizedCache(RecordFieldJsonAdapter.create()) {
final NormalizedCache normalizedCache = new NormalizedCache() {
@Nullable @Override public Record loadRecord(@NonNull String key, @NonNull CacheHeaders cacheHeaders) {
assertThat(cacheHeaders.hasHeader(ApolloCacheHeaders.DO_NOT_STORE)).isTrue();
return null;
Expand Down Expand Up @@ -87,7 +87,7 @@ public void testHeadersReceived() throws ApolloException, IOException {

@Test
public void testDefaultHeadersReceived() throws IOException, ApolloException {
final NormalizedCache normalizedCache = new NormalizedCache(RecordFieldJsonAdapter.create()) {
final NormalizedCache normalizedCache = new NormalizedCache() {
@Nullable @Override public Record loadRecord(@NonNull String key, @NonNull CacheHeaders cacheHeaders) {
assertThat(cacheHeaders.hasHeader(ApolloCacheHeaders.DO_NOT_STORE)).isTrue();
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,8 @@ private MockResponse mockResponse(String fileName) throws IOException {
assertThat(masterQueryResponse.data().hero().friends()).hasSize(3);

CharacterNameByIdQuery detailQuery = new CharacterNameByIdQuery("1002");
Response<CharacterNameByIdQuery.Data> detailQueryResponse = apolloClient.query(detailQuery).responseFetcher
(ApolloResponseFetchers
.CACHE_ONLY).execute();
Response<CharacterNameByIdQuery.Data> detailQueryResponse = apolloClient.query(detailQuery)
.responseFetcher(ApolloResponseFetchers.CACHE_ONLY).execute();
assertThat(detailQueryResponse.fromCache()).isTrue();
assertThat(detailQueryResponse.data().character().asHuman().name()).isEqualTo("Han Solo");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package com.apollographql.apollo;

import com.apollographql.apollo.cache.normalized.lru.EvictionPolicy;
import com.apollographql.apollo.cache.normalized.lru.LruNormalizedCacheFactory;
import com.apollographql.apollo.fetcher.ApolloResponseFetchers;
import com.apollographql.apollo.integration.normalizer.HeroAndFriendsNamesQuery;
import com.apollographql.apollo.integration.normalizer.HeroAndFriendsNamesWithIDsQuery;
import com.apollographql.apollo.integration.normalizer.HeroNameWithIdQuery;
import com.apollographql.apollo.integration.normalizer.type.Episode;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import okhttp3.OkHttpClient;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;

import static com.google.common.truth.Truth.assertThat;

public class OptimisticCacheTestCase {
private ApolloClient apolloClient;
private MockWebServer server;

@Before public void setUp() {
server = new MockWebServer();

OkHttpClient okHttpClient = new OkHttpClient.Builder()
.writeTimeout(2, TimeUnit.SECONDS)
.readTimeout(2, TimeUnit.SECONDS)
.build();

apolloClient = ApolloClient.builder()
.serverUrl(server.url("/"))
.okHttpClient(okHttpClient)
.normalizedCache(new LruNormalizedCacheFactory(EvictionPolicy.NO_EVICTION), new IdFieldCacheKeyResolver())
.dispatcher(Utils.immediateExecutorService())
.build();
}

@After public void tearDown() {
try {
server.shutdown();
} catch (IOException ignored) {
}
}

private MockResponse mockResponse(String fileName) throws IOException {
return new MockResponse().setChunkedBody(Utils.readFileToString(getClass(), "/" + fileName), 32);
}

@Test public void simple_write_rollback() throws Exception {
server.enqueue(mockResponse("HeroAndFriendsNameResponse.json"));
HeroAndFriendsNamesQuery query = new HeroAndFriendsNamesQuery(Episode.JEDI);
apolloClient.query(query).execute();

UUID updateVersion = UUID.randomUUID();
HeroAndFriendsNamesQuery.Data data = new HeroAndFriendsNamesQuery.Data(new HeroAndFriendsNamesQuery.Hero(
"Droid",
"R222-D222",
Arrays.asList(
new HeroAndFriendsNamesQuery.Friend(
"Human",
"SuperMan"
),
new HeroAndFriendsNamesQuery.Friend(
"Human",
"Batman"
)
)
));
apolloClient.apolloStore().writeOptimisticUpdatesAndPublish(query, data, updateVersion).execute();

data = apolloClient.query(query).responseFetcher(ApolloResponseFetchers.CACHE_ONLY).execute().data();
assertThat(data.hero().name()).isEqualTo("R222-D222");
assertThat(data.hero().friends()).hasSize(2);
assertThat(data.hero().friends().get(0).name()).isEqualTo("SuperMan");
assertThat(data.hero().friends().get(1).name()).isEqualTo("Batman");

apolloClient.apolloStore().rollbackOptimisticUpdatesAndPublish(updateVersion).execute();

data = apolloClient.query(query).responseFetcher(ApolloResponseFetchers.CACHE_ONLY).execute().data();
assertThat(data.hero().name()).isEqualTo("R2-D2");
assertThat(data.hero().friends()).hasSize(3);
assertThat(data.hero().friends().get(0).name()).isEqualTo("Luke Skywalker");
assertThat(data.hero().friends().get(1).name()).isEqualTo("Han Solo");
assertThat(data.hero().friends().get(2).name()).isEqualTo("Leia Organa");
}

@Test public void partial_rollback() throws Exception {
server.enqueue(mockResponse("HeroAndFriendsNameWithIdsResponse.json"));
HeroAndFriendsNamesWithIDsQuery query1 = new HeroAndFriendsNamesWithIDsQuery(Episode.JEDI);
apolloClient.query(query1).execute();

UUID updateVersion1 = UUID.randomUUID();
HeroAndFriendsNamesWithIDsQuery.Data data1 = new HeroAndFriendsNamesWithIDsQuery.Data(
new HeroAndFriendsNamesWithIDsQuery.Hero(
"Droid",
"2001",
"R222-D222",
Arrays.asList(
new HeroAndFriendsNamesWithIDsQuery.Friend(
"Human",
"1000",
"SuperMan"
),
new HeroAndFriendsNamesWithIDsQuery.Friend(
"Human",
"1003",
"Batman"
)
)
)
);
apolloClient.apolloStore().writeOptimisticUpdatesAndPublish(query1, data1, updateVersion1).execute();

// check if query1 see optimistic updates
data1 = apolloClient.query(query1).responseFetcher(ApolloResponseFetchers.CACHE_ONLY).execute().data();
assertThat(data1.hero().id()).isEqualTo("2001");
assertThat(data1.hero().name()).isEqualTo("R222-D222");
assertThat(data1.hero().friends()).hasSize(2);
assertThat(data1.hero().friends().get(0).id()).isEqualTo("1000");
assertThat(data1.hero().friends().get(0).name()).isEqualTo("SuperMan");
assertThat(data1.hero().friends().get(1).id()).isEqualTo("1003");
assertThat(data1.hero().friends().get(1).name()).isEqualTo("Batman");

server.enqueue(mockResponse("HeroNameWithIdResponse.json"));
HeroNameWithIdQuery query2 = new HeroNameWithIdQuery();
apolloClient.query(query2).execute();

UUID updateVersion2 = UUID.randomUUID();
HeroNameWithIdQuery.Data data2 = new HeroNameWithIdQuery.Data(new HeroNameWithIdQuery.Hero(
"Human",
"1000",
"Beast"
));
apolloClient.apolloStore().writeOptimisticUpdatesAndPublish(query2, data2, updateVersion2).execute();

// check if query1 see the latest optimistic updates
data1 = apolloClient.query(query1).responseFetcher(ApolloResponseFetchers.CACHE_ONLY).execute().data();
assertThat(data1.hero().id()).isEqualTo("2001");
assertThat(data1.hero().name()).isEqualTo("R222-D222");
assertThat(data1.hero().friends()).hasSize(2);
assertThat(data1.hero().friends().get(0).id()).isEqualTo("1000");
assertThat(data1.hero().friends().get(0).name()).isEqualTo("Beast");
assertThat(data1.hero().friends().get(1).id()).isEqualTo("1003");
assertThat(data1.hero().friends().get(1).name()).isEqualTo("Batman");

// check if query2 see the latest optimistic updates
data2 = apolloClient.query(query2).responseFetcher(ApolloResponseFetchers.CACHE_ONLY).execute().data();
assertThat(data2.hero().id()).isEqualTo("1000");
assertThat(data2.hero().name()).isEqualTo("Beast");

// rollback query1 optimistic updates
apolloClient.apolloStore().rollbackOptimisticUpdatesAndPublish(updateVersion1).execute();

// check if query1 see the latest optimistic updates
data1 = apolloClient.query(query1).responseFetcher(ApolloResponseFetchers.CACHE_ONLY).execute().data();
assertThat(data1.hero().id()).isEqualTo("2001");
assertThat(data1.hero().name()).isEqualTo("R2-D2");
assertThat(data1.hero().friends()).hasSize(3);
assertThat(data1.hero().friends().get(0).id()).isEqualTo("1000");
assertThat(data1.hero().friends().get(0).name()).isEqualTo("Beast");
assertThat(data1.hero().friends().get(1).id()).isEqualTo("1002");
assertThat(data1.hero().friends().get(1).name()).isEqualTo("Han Solo");
assertThat(data1.hero().friends().get(2).id()).isEqualTo("1003");
assertThat(data1.hero().friends().get(2).name()).isEqualTo("Leia Organa");

// check if query2 see the latest optimistic updates
data2 = apolloClient.query(query2).responseFetcher(ApolloResponseFetchers.CACHE_ONLY).execute().data();
assertThat(data2.hero().id()).isEqualTo("1000");
assertThat(data2.hero().name()).isEqualTo("Beast");

// rollback query2 optimistic updates
apolloClient.apolloStore().rollbackOptimisticUpdatesAndPublish(updateVersion2).execute();

// check if query2 see the latest optimistic updates
data2 = apolloClient.query(query2).responseFetcher(ApolloResponseFetchers.CACHE_ONLY).execute().data();
assertThat(data2.hero().id()).isEqualTo("1000");
assertThat(data2.hero().name()).isEqualTo("SuperMan");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import com.apollographql.apollo.cache.normalized.CacheKeyResolver;
import com.apollographql.apollo.cache.normalized.NormalizedCache;
import com.apollographql.apollo.cache.normalized.Record;
import com.apollographql.apollo.cache.normalized.RecordFieldJsonAdapter;
import com.apollographql.apollo.internal.util.ApolloLogger;

import org.junit.Test;
Expand All @@ -25,7 +24,7 @@ public class ApolloStoreTest {

@Test public void storeClearAllCallsNormalizedCacheClearAll() throws Exception {
final NamedCountDownLatch latch = new NamedCountDownLatch("storeClearAllCallsNormalizedCacheClearAll", 1);
final RealApolloStore realApolloStore = new RealApolloStore(new NormalizedCache(RecordFieldJsonAdapter.create()) {
final RealApolloStore realApolloStore = new RealApolloStore(new NormalizedCache() {
@Nullable @Override public Record loadRecord(@Nonnull String key, @Nonnull CacheHeaders cacheHeaders) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"data": {
"hero": {
"__typename": "Human",
"id": "1000",
"name": "SuperMan"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import javax.annotation.Nonnull;

Expand Down Expand Up @@ -163,8 +164,8 @@ interface RecordChangeSubscriber {
@Nonnull Operation<D, T, V> operation, @Nonnull D operationData);

/**
* Write operation to the store and publish changes of {@link Record} which have changed, that will notify any
* {@link com.apollographql.apollo.ApolloQueryWatcher} that depends on these {@link Record} to re-fetch.
* Write operation to the store and publish changes of {@link Record} which have changed, that will notify any {@link
* com.apollographql.apollo.ApolloQueryWatcher} that depends on these {@link Record} to re-fetch.
*
* @param operation {@link Operation} response data of which should be written to the store
* @param operationData {@link Operation.Data} operation response data to be written to the store
Expand All @@ -189,8 +190,8 @@ interface RecordChangeSubscriber {
@Nonnull Operation.Variables variables);

/**
* Write fragment to the store and publish changes of {@link Record} which have changed, that will notify any
* {@link com.apollographql.apollo.ApolloQueryWatcher} that depends on these {@link Record} to re-fetch.
* Write fragment to the store and publish changes of {@link Record} which have changed, that will notify any {@link
* com.apollographql.apollo.ApolloQueryWatcher} that depends on these {@link Record} to re-fetch.
*
* @param fragment data to be written to the store
* @param cacheKey {@link CacheKey} to be used as root record key
Expand All @@ -200,5 +201,48 @@ interface RecordChangeSubscriber {
@Nonnull ApolloStoreOperation<Boolean> writeAndPublish(@Nonnull GraphqlFragment fragment, @Nonnull CacheKey cacheKey,
@Nonnull Operation.Variables variables);

/**
* Write operation data to the optimistic store.
*
* @param operation {@link Operation} response data of which should be written to the store
* @param operationData {@link Operation.Data} operation response data to be written to the store
* @param updateVersion optimistic update version that will be required to rollback changes
* @return {@ApolloStoreOperation} to be performed, that will be resolved with set of keys of {@link Record} which
* have changed
*/
@Nonnull <D extends Operation.Data, T, V extends Operation.Variables> ApolloStoreOperation<Set<String>>
writeOptimisticUpdates(@Nonnull Operation<D, T, V> operation, @Nonnull D operationData, @Nonnull UUID updateVersion);

/**
* Write operation data to the optimistic store and publish changes of {@link Record}s which have changed, that will
* notify any {@linkcom.apollographql.apollo.ApolloQueryWatcher} that depends on these {@link Record} to re-fetch.
*
* @param operation {@link Operation} response data of which should be written to the store
* @param operationData {@link Operation.Data} operation response data to be written to the store
* @param updateVersion optimistic update version that will be required to rollback changes
* @return {@ApolloStoreOperation} to be performed
*/
@Nonnull <D extends Operation.Data, T, V extends Operation.Variables> ApolloStoreOperation<Boolean>
writeOptimisticUpdatesAndPublish(@Nonnull Operation<D, T, V> operation, @Nonnull D operationData,
@Nonnull UUID updateVersion);

/**
* Rollback operation data optimistic updates.
*
* @param updateVersion optimistic update version to rollback
* @return {@ApolloStoreOperation} to be performed
*/
@Nonnull ApolloStoreOperation<Set<String>> rollbackOptimisticUpdates(@Nonnull UUID updateVersion);

/**
* Rollback operation data optimistic updates and publish changes of {@link Record}s which have changed, that will
* notify any {@linkcom.apollographql.apollo.ApolloQueryWatcher} that depends on these {@link Record} to re-fetch.
*
* @param updateVersion optimistic update version to rollback
* @return {@ApolloStoreOperation} to be performed, that will be resolved with set of keys of {@link Record} which
* have changed
*/
@Nonnull ApolloStoreOperation<Boolean> rollbackOptimisticUpdatesAndPublish(@Nonnull UUID updateVersion);

ApolloStore NO_APOLLO_STORE = new NoOpApolloStore();
}
Loading