diff --git a/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/NoOpResolveDelegate.kt b/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/NoOpResolveDelegate.kt new file mode 100644 index 00000000000..50c366f5c41 --- /dev/null +++ b/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/NoOpResolveDelegate.kt @@ -0,0 +1,36 @@ +package com.apollographql.apollo.api.internal + +import com.apollographql.apollo.api.Operation +import com.apollographql.apollo.api.ResponseField + +class NoOpResolveDelegate: ResolveDelegate { + override fun willResolveRootQuery(operation: Operation<*, *, *>) { + } + + override fun willResolve(field: ResponseField, variables: Operation.Variables, value: Any?) { + } + + override fun didResolve(field: ResponseField, variables: Operation.Variables) { + } + + override fun didResolveScalar(value: Any?) { + } + + override fun willResolveObject(objectField: ResponseField, objectSource: T?) { + } + + override fun didResolveObject(objectField: ResponseField, objectSource: T?) { + } + + override fun didResolveList(array: List<*>) { + } + + override fun willResolveElement(atIndex: Int) { + } + + override fun didResolveElement(atIndex: Int) { + } + + override fun didResolveNull() { + } +} \ No newline at end of file diff --git a/apollo-runtime/src/main/java/com/apollographql/apollo/internal/response/RealResponseReader.kt b/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/RealResponseReader.kt similarity index 98% rename from apollo-runtime/src/main/java/com/apollographql/apollo/internal/response/RealResponseReader.kt rename to apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/RealResponseReader.kt index f493314f7fb..d342926511f 100644 --- a/apollo-runtime/src/main/java/com/apollographql/apollo/internal/response/RealResponseReader.kt +++ b/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/RealResponseReader.kt @@ -10,7 +10,6 @@ import com.apollographql.apollo.api.ScalarTypeAdapters import com.apollographql.apollo.api.internal.FieldValueResolver import com.apollographql.apollo.api.internal.ResolveDelegate import com.apollographql.apollo.api.internal.ResponseReader -import java.util.Collections class RealResponseReader( val operationVariables: Operation.Variables, @@ -144,7 +143,7 @@ class RealResponseReader( }.also { resolveDelegate.didResolveList(values) } } didResolve(field) - return if (result != null) Collections.unmodifiableList(result) else null + return result } override fun readCustomType(field: ResponseField.CustomTypeField): T? { @@ -288,7 +287,7 @@ class RealResponseReader( }.also { resolveDelegate.didResolveElement(index) } } resolveDelegate.didResolveList(values) - return Collections.unmodifiableList(result) + return result } } } diff --git a/apollo-runtime/src/main/java/com/apollographql/apollo/internal/response/RealResponseWriter.kt b/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/RealResponseWriter.kt similarity index 91% rename from apollo-runtime/src/main/java/com/apollographql/apollo/internal/response/RealResponseWriter.kt rename to apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/RealResponseWriter.kt index 09c08c7adef..c02e3bf6281 100644 --- a/apollo-runtime/src/main/java/com/apollographql/apollo/internal/response/RealResponseWriter.kt +++ b/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/RealResponseWriter.kt @@ -1,6 +1,6 @@ package com.apollographql.apollo.internal.response -import com.apollographql.apollo.api.CustomTypeAdapter +import com.apollographql.apollo.api.BigDecimal import com.apollographql.apollo.api.Operation import com.apollographql.apollo.api.ResponseField import com.apollographql.apollo.api.ScalarType @@ -8,9 +8,6 @@ import com.apollographql.apollo.api.ScalarTypeAdapters import com.apollographql.apollo.api.internal.ResolveDelegate import com.apollographql.apollo.api.internal.ResponseFieldMarshaller import com.apollographql.apollo.api.internal.ResponseWriter -import java.math.BigDecimal -import java.util.ArrayList -import java.util.LinkedHashMap class RealResponseWriter(private val operationVariables: Operation.Variables, private val scalarTypeAdapters: ScalarTypeAdapters) : ResponseWriter { val buffer: MutableMap = LinkedHashMap() @@ -19,15 +16,15 @@ class RealResponseWriter(private val operationVariables: Operation.Variables, pr } override fun writeInt(field: ResponseField, value: Int?) { - writeScalarFieldValue(field, if (value != null) BigDecimal.valueOf(value.toLong()) else null) + writeScalarFieldValue(field, if (value != null) BigDecimal(value.toLong()) else null) } override fun writeLong(field: ResponseField, value: Long?) { - writeScalarFieldValue(field, if (value != null) BigDecimal.valueOf(value) else null) + writeScalarFieldValue(field, if (value != null) BigDecimal(value) else null) } override fun writeDouble(field: ResponseField, value: Double?) { - writeScalarFieldValue(field, if (value != null) BigDecimal.valueOf(value) else null) + writeScalarFieldValue(field, if (value != null) BigDecimal(value) else null) } override fun writeBoolean(field: ResponseField, value: Boolean?) { @@ -177,15 +174,15 @@ class RealResponseWriter(private val operationVariables: Operation.Variables, pr } override fun writeInt(value: Int?) { - accumulator.add(if (value != null) BigDecimal.valueOf(value.toLong()) else null) + accumulator.add(if (value != null) BigDecimal(value.toLong()) else null) } override fun writeLong(value: Long?) { - accumulator.add(if (value != null) BigDecimal.valueOf(value) else null) + accumulator.add(if (value != null) BigDecimal(value) else null) } override fun writeDouble(value: Double?) { - accumulator.add(if (value != null) BigDecimal.valueOf(value) else null) + accumulator.add(if (value != null) BigDecimal(value) else null) } override fun writeBoolean(value: Boolean?) { @@ -220,10 +217,8 @@ class RealResponseWriter(private val operationVariables: Operation.Variables, pr companion object { private fun checkFieldValue(field: ResponseField, value: Any?) { if (!field.optional && value == null) { - throw NullPointerException(String.format("Mandatory response field `%s` resolved with null value", - field.responseName)) + throw NullPointerException("Mandatory response field `${field.responseName}` resolved with null value") } } } - } \ No newline at end of file diff --git a/apollo-normalized-cache/build.gradle.kts b/apollo-normalized-cache/build.gradle.kts index b00ffaf5bb2..a1f00851f35 100644 --- a/apollo-normalized-cache/build.gradle.kts +++ b/apollo-normalized-cache/build.gradle.kts @@ -1,16 +1,55 @@ plugins { `java-library` - kotlin("jvm") + kotlin("multiplatform") } -dependencies { - api(project(":apollo-api")) - api(project(":apollo-normalized-cache-api")) - implementation(groovy.util.Eval.x(project, "x.dep.cache")) - implementation(groovy.util.Eval.x(project, "x.dep.kotlin.stdLib")) +kotlin { + @Suppress("ClassName") + data class iOSTarget(val name: String, val preset: String, val id: String) - testImplementation(groovy.util.Eval.x(project, "x.dep.junit")) - testImplementation(groovy.util.Eval.x(project, "x.dep.truth")) + val iosTargets = listOf( + iOSTarget("ios", "iosArm64", "ios-arm64"), + iOSTarget("iosSim", "iosX64", "ios-x64") + ) + + for ((targetName, presetName, id) in iosTargets) { + targetFromPreset(presets.getByName(presetName), targetName) { + mavenPublication { + artifactId = "${project.name}-$id" + } + } + } + + jvm { + withJava() + } + + sourceSets { + val commonMain by getting { + dependencies { + api(project(":apollo-api")) + api(project(":apollo-normalized-cache-api")) + implementation(kotlin("stdlib-common")) + } + } + + val jvmMain by getting { + dependsOn(commonMain) + dependencies { + implementation(kotlin("stdlib")) + implementation(groovy.util.Eval.x(project, "x.dep.cache")) + } + } + + val jvmTest by getting { + dependsOn(jvmMain) + dependencies { + implementation(groovy.util.Eval.x(project, "x.dep.junit")) + implementation(groovy.util.Eval.x(project, "x.dep.truth")) + } + } + + } } tasks.withType { diff --git a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/CacheFieldValueResolver.kt b/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/CacheFieldValueResolver.kt similarity index 100% rename from apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/CacheFieldValueResolver.kt rename to apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/CacheFieldValueResolver.kt diff --git a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/RealCacheKeyBuilder.kt b/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/RealCacheKeyBuilder.kt similarity index 90% rename from apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/RealCacheKeyBuilder.kt rename to apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/RealCacheKeyBuilder.kt index b99aa209621..a80a82a18af 100644 --- a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/RealCacheKeyBuilder.kt +++ b/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/RealCacheKeyBuilder.kt @@ -7,7 +7,7 @@ import com.apollographql.apollo.api.ResponseField.Companion.isArgumentValueVaria import com.apollographql.apollo.api.internal.json.JsonWriter import com.apollographql.apollo.api.internal.json.Utils import okio.Buffer -import java.io.IOException +import okio.IOException class RealCacheKeyBuilder : CacheKeyBuilder { @@ -22,7 +22,7 @@ class RealCacheKeyBuilder : CacheKeyBuilder { jsonWriter.serializeNulls = true Utils.writeToJson(resolvedArguments, jsonWriter) jsonWriter.close() - String.format("%s(%s)", field.fieldName, buffer.readUtf8()) + "${field.fieldName}(${buffer.readUtf8()})" } catch (e: IOException) { throw RuntimeException(e) } @@ -41,7 +41,9 @@ class RealCacheKeyBuilder : CacheKeyBuilder { } else { value } - }.toSortedMap() + }.toList() + .sortedBy { it.first } + .toMap() } @Suppress("UNCHECKED_CAST") @@ -52,7 +54,7 @@ class RealCacheKeyBuilder : CacheKeyBuilder { null -> null is Map<*, *> -> resolveArguments(resolvedVariable as Map, variables) is InputType -> { - val inputFieldMapWriter = SortedInputFieldMapWriter(Comparator { o1, o2 -> o1.compareTo(o2) }) + val inputFieldMapWriter = SortedInputFieldMapWriter() resolvedVariable.marshaller().marshal(inputFieldMapWriter) resolveArguments(inputFieldMapWriter.map(), variables) } diff --git a/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/ResponseNormalizer.kt b/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/ResponseNormalizer.kt new file mode 100644 index 00000000000..839b4ea5b36 --- /dev/null +++ b/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/ResponseNormalizer.kt @@ -0,0 +1,165 @@ +package com.apollographql.apollo.cache.normalized.internal + +import com.apollographql.apollo.api.Operation +import com.apollographql.apollo.api.ResponseField +import com.apollographql.apollo.api.internal.ResolveDelegate +import com.apollographql.apollo.cache.normalized.CacheKey +import com.apollographql.apollo.cache.normalized.CacheKeyResolver.Companion.rootKeyForOperation +import com.apollographql.apollo.cache.normalized.CacheReference +import com.apollographql.apollo.cache.normalized.Record +import com.apollographql.apollo.cache.normalized.Record.Companion.builder +import com.apollographql.apollo.cache.normalized.RecordSet +import kotlin.jvm.JvmField + +abstract class ResponseNormalizer : ResolveDelegate { + private lateinit var pathStack: SimpleStack> + private lateinit var recordStack: SimpleStack + private lateinit var valueStack: SimpleStack + private lateinit var path: MutableList + private lateinit var currentRecordBuilder: Record.Builder + private var recordSet = RecordSet() + private var dependentKeys = mutableSetOf() + + open fun records(): Collection? { + return recordSet.allRecords() + } + + open fun dependentKeys(): Set { + return dependentKeys + } + + override fun willResolveRootQuery(operation: Operation<*, *, *>) { + willResolveRecord(rootKeyForOperation(operation)) + } + + override fun willResolve(field: ResponseField, variables: Operation.Variables, value: Any?) { + val key = cacheKeyBuilder().build(field, variables) + path.add(key) + } + + override fun didResolve(field: ResponseField, variables: Operation.Variables) { + path.removeAt(path.size - 1) + val value = valueStack.pop() + val cacheKey = cacheKeyBuilder().build(field, variables) + val dependentKey = currentRecordBuilder.key + "." + cacheKey + dependentKeys.add(dependentKey) + currentRecordBuilder.addField(cacheKey, value) + if (recordStack.isEmpty) { + recordSet.merge(currentRecordBuilder.build()) + } + } + + override fun didResolveScalar(value: Any?) { + valueStack.push(value) + } + + override fun willResolveObject(objectField: ResponseField, objectSource: R?) { + pathStack.push(path) + val cacheKey = objectSource?.let { resolveCacheKey(objectField, it) } ?: CacheKey.NO_KEY + var cacheKeyValue = cacheKey.key() + if (cacheKey.equals(CacheKey.NO_KEY)) { + cacheKeyValue = pathToString() + } else { + path = ArrayList() + path.add(cacheKeyValue) + } + recordStack.push(currentRecordBuilder.build()) + currentRecordBuilder = builder(cacheKeyValue) + } + + override fun didResolveObject(objectField: ResponseField, objectSource: R?) { + path = pathStack.pop() + if (objectSource != null) { + val completedRecord = currentRecordBuilder.build() + valueStack.push(CacheReference(completedRecord.key())) + dependentKeys.add(completedRecord.key()) + recordSet.merge(completedRecord) + } + currentRecordBuilder = recordStack.pop().toBuilder() + } + + override fun didResolveList(array: List<*>) { + val parsedArray = ArrayList(array.size) + var i = 0 + val size = array.size + while (i < size) { + parsedArray.add(0, valueStack.pop()) + i++ + } + valueStack.push(parsedArray) + } + + override fun willResolveElement(atIndex: Int) { + path.add(atIndex.toString()) + } + + override fun didResolveElement(atIndex: Int) { + path.removeAt(path.size - 1) + } + + override fun didResolveNull() { + valueStack.push(null) + } + + abstract fun resolveCacheKey(field: ResponseField, record: R): CacheKey + abstract fun cacheKeyBuilder(): CacheKeyBuilder + fun willResolveRecord(cacheKey: CacheKey) { + pathStack = SimpleStack() + recordStack = SimpleStack() + valueStack = SimpleStack() + dependentKeys = HashSet() + path = ArrayList() + currentRecordBuilder = builder(cacheKey.key()) + recordSet = RecordSet() + } + + private fun pathToString(): String { + val stringBuilder = StringBuilder() + var i = 0 + val size = path.size + while (i < size) { + val pathPiece = path[i] + stringBuilder.append(pathPiece) + if (i < size - 1) { + stringBuilder.append(".") + } + i++ + } + return stringBuilder.toString() + } + + companion object { + @JvmField + val NO_OP_NORMALIZER: ResponseNormalizer<*> = object : ResponseNormalizer() { + override fun willResolveRootQuery(operation: Operation<*, *, *>) {} + override fun willResolve(field: ResponseField, variables: Operation.Variables, value: Any?) {} + override fun didResolve(field: ResponseField, variables: Operation.Variables) {} + override fun didResolveScalar(value: Any?) {} + override fun willResolveObject(objectField: ResponseField, objectSource: Any?) {} + override fun didResolveObject(objectField: ResponseField, objectSource: Any?) {} + override fun didResolveList(array: List<*>) {} + override fun willResolveElement(atIndex: Int) {} + override fun didResolveElement(atIndex: Int) {} + override fun didResolveNull() {} + override fun records(): Collection? { + return emptyList() + } + + override fun dependentKeys(): Set { + return emptySet() + } + + override fun resolveCacheKey(field: ResponseField, record: Any?): CacheKey { + return CacheKey.NO_KEY + } + + override fun cacheKeyBuilder(): CacheKeyBuilder { + return object : CacheKeyBuilder { + override fun build(field: ResponseField, variables: Operation.Variables): String { + return CacheKey.NO_KEY.key() + } + } + } + } + } +} \ No newline at end of file diff --git a/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/SimpleStack.kt b/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/SimpleStack.kt new file mode 100644 index 00000000000..2337bb9570f --- /dev/null +++ b/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/SimpleStack.kt @@ -0,0 +1,29 @@ +package com.apollographql.apollo.cache.normalized.internal + +/** + * Simple stack data structure which accepts null elements. Backed by list. + * @param + */ +class SimpleStack { + private var backing: MutableList + + constructor() { + backing = ArrayList() + } + + constructor(initialSize: Int) { + backing = ArrayList(initialSize) + } + + fun push(element: E) { + backing.add(element) + } + + fun pop(): E { + check(!isEmpty) { "Stack is empty." } + return backing.removeAt(backing.size - 1) + } + + val isEmpty: Boolean + get() = backing.isEmpty() +} \ No newline at end of file diff --git a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/SortedInputFieldMapWriter.kt b/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/SortedInputFieldMapWriter.kt similarity index 75% rename from apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/SortedInputFieldMapWriter.kt rename to apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/SortedInputFieldMapWriter.kt index 13eee6b6232..bbd5a1cdd07 100644 --- a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/SortedInputFieldMapWriter.kt +++ b/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/SortedInputFieldMapWriter.kt @@ -3,18 +3,14 @@ package com.apollographql.apollo.cache.normalized.internal import com.apollographql.apollo.api.ScalarType import com.apollographql.apollo.api.internal.InputFieldMarshaller import com.apollographql.apollo.api.internal.InputFieldWriter -import com.apollographql.apollo.api.internal.Utils.__checkNotNull -import java.io.IOException -import java.util.ArrayList -import java.util.Collections -import java.util.Comparator -import java.util.TreeMap +import com.apollographql.apollo.api.internal.Throws +import okio.IOException -class SortedInputFieldMapWriter(private val fieldNameComparator: Comparator) : InputFieldWriter { - private val buffer = TreeMap(fieldNameComparator) +class SortedInputFieldMapWriter() : InputFieldWriter { + private val buffer = mutableMapOf() fun map(): Map { - return Collections.unmodifiableMap(buffer) + return buffer.toList().sortedBy { it.first }.toMap() } override fun writeString(fieldName: String, value: String?) { @@ -50,9 +46,9 @@ class SortedInputFieldMapWriter(private val fieldNameComparator: Comparator) : InputFieldWriter.ListItemWriter { - val list: MutableList = ArrayList() + private open class ListItemWriter internal constructor() : InputFieldWriter.ListItemWriter { + val list = ArrayList() override fun writeString(value: String?) { if (value != null) { list.add(value) @@ -118,16 +114,16 @@ class SortedInputFieldMapWriter(private val fieldNameComparator: Comparator() + + override fun loadRecord(key: String, cacheHeaders: CacheHeaders): Record? { + val record = nextCache?.loadRecord(key, cacheHeaders) + if (record != null) { + return record + } + + return map.get(key) + } + + override fun performMerge(apolloRecord: Record, oldRecord: Record?, cacheHeaders: CacheHeaders): Set { + return map.getOrPut(apolloRecord.key, {apolloRecord}) + .mergeWith(apolloRecord) + } + + override fun clearAll() { + nextCache?.clearAll() + map.clear() + } + + override fun remove(cacheKey: CacheKey, cascade: Boolean): Boolean { + var result: Boolean = nextCache?.remove(cacheKey, cascade) ?: false + + val record = map.get(cacheKey.key) + if (record != null) { + map.remove(cacheKey.key) + result = true + if (cascade) { + for (cacheReference in record.referencedFields()) { + result = result && remove(CacheKey(cacheReference.key), true) + } + } + } + return result + } +} \ No newline at end of file diff --git a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/ApolloStore.kt b/apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/ApolloStore.kt similarity index 100% rename from apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/ApolloStore.kt rename to apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/ApolloStore.kt diff --git a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/ApolloStoreOperation.kt b/apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/ApolloStoreOperation.kt similarity index 100% rename from apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/ApolloStoreOperation.kt rename to apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/ApolloStoreOperation.kt diff --git a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/OptimisticNormalizedCache.kt b/apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/OptimisticNormalizedCache.kt similarity index 100% rename from apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/OptimisticNormalizedCache.kt rename to apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/OptimisticNormalizedCache.kt diff --git a/apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/internal/NoOpApolloStore.kt b/apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/internal/NoOpApolloStore.kt new file mode 100644 index 00000000000..56c8c20ecce --- /dev/null +++ b/apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/internal/NoOpApolloStore.kt @@ -0,0 +1,143 @@ +package com.apollographql.apollo.cache.normalized.internal + +import com.apollographql.apollo.api.GraphqlFragment +import com.apollographql.apollo.api.Operation +import com.apollographql.apollo.api.Response +import com.apollographql.apollo.api.Response.Companion.builder +import com.apollographql.apollo.api.internal.ResponseFieldMapper +import com.apollographql.apollo.cache.CacheHeaders +import com.apollographql.apollo.cache.normalized.ApolloStore +import com.apollographql.apollo.cache.normalized.ApolloStoreOperation +import com.apollographql.apollo.cache.normalized.ApolloStoreOperation.Companion.emptyOperation +import com.apollographql.apollo.cache.normalized.CacheKey +import com.apollographql.apollo.cache.normalized.CacheKeyResolver +import com.apollographql.apollo.cache.normalized.NormalizedCache +import com.apollographql.apollo.cache.normalized.Record +import java.util.UUID + +/** + * An alternative to RealApolloStore for when a no-operation cache is needed. + */ +class NoOpApolloStore : ApolloStore, ReadableStore, WriteableStore { + override fun merge(recordCollection: Collection, cacheHeaders: CacheHeaders): Set { + return emptySet() + } + + override fun merge(record: Record, cacheHeaders: CacheHeaders): Set { + return emptySet() + } + + override fun read(key: String, cacheHeaders: CacheHeaders): Record? { + return null + } + + override fun read(keys: Collection, cacheHeaders: CacheHeaders): Collection { + return emptySet() + } + + override fun subscribe(subscriber: ApolloStore.RecordChangeSubscriber) {} + override fun unsubscribe(subscriber: ApolloStore.RecordChangeSubscriber) {} + override fun publish(keys: Set) {} + override fun clearAll(): ApolloStoreOperation { + return emptyOperation(java.lang.Boolean.FALSE) + } + + override fun remove(cacheKey: CacheKey, cascade: Boolean): ApolloStoreOperation { + return emptyOperation(java.lang.Boolean.FALSE) + } + + override fun remove(cacheKey: CacheKey): ApolloStoreOperation { + return emptyOperation(java.lang.Boolean.FALSE) + } + + override fun remove(cacheKeys: List): ApolloStoreOperation { + return emptyOperation(0) + } + + override fun networkResponseNormalizer(): ResponseNormalizer> { + return ResponseNormalizer.NO_OP_NORMALIZER as ResponseNormalizer> + } + + override fun cacheResponseNormalizer(): ResponseNormalizer { + return ResponseNormalizer.NO_OP_NORMALIZER as ResponseNormalizer + } + + override fun readTransaction(transaction: Transaction): R { + return transaction.execute(this)!! + } + + override fun writeTransaction(transaction: Transaction): R { + return transaction.execute(this)!! + } + + override fun normalizedCache(): NormalizedCache { + error("Cannot get normalizedCache: no cache configured") + } + + override fun cacheKeyResolver(): CacheKeyResolver { + error("Cannot get cacheKeyResolver: no cache configured") + } + + override fun read( + operation: Operation): ApolloStoreOperation { + error("Cannot read operation: no cache configured") + } + + override fun read( + operation: Operation, responseFieldMapper: ResponseFieldMapper, + responseNormalizer: ResponseNormalizer, cacheHeaders: CacheHeaders): ApolloStoreOperation> { + // This is called in the default path when no cache is configured, do not trigger an error + // Instead return an empty response. This will be seen as a cache MISS and the request will go to the network. + return emptyOperation(Response.builder(operation).build()) + } + + override fun read(fieldMapper: ResponseFieldMapper, + cacheKey: CacheKey, variables: Operation.Variables): ApolloStoreOperation { + error("Cannot read fragment: no cache configured") + } + + override fun write( + operation: Operation, operationData: D): ApolloStoreOperation> { + // Should we throw here instead? + return emptyOperation(emptySet()) + } + + override fun writeAndPublish( + operation: Operation, operationData: D): ApolloStoreOperation { + // Should we throw here instead? + return emptyOperation(false) + } + + override fun write(fragment: GraphqlFragment, cacheKey: CacheKey, + variables: Operation.Variables): ApolloStoreOperation> { + // Should we throw here instead? + return emptyOperation(emptySet()) + } + + override fun writeAndPublish(fragment: GraphqlFragment, cacheKey: CacheKey, + variables: Operation.Variables): ApolloStoreOperation { + // Should we throw here instead? + return emptyOperation(false) + } + + override fun writeOptimisticUpdates(operation: Operation, operationData: D, mutationId: UUID): ApolloStoreOperation> { + // Should we throw here instead? + return emptyOperation(emptySet()) + } + + override fun writeOptimisticUpdatesAndPublish(operation: Operation, operationData: D, + mutationId: UUID): ApolloStoreOperation { + // Should we throw here instead? + return emptyOperation(java.lang.Boolean.FALSE) + } + + override fun rollbackOptimisticUpdatesAndPublish(mutationId: UUID): ApolloStoreOperation { + // Should we throw here instead? + return emptyOperation(java.lang.Boolean.FALSE) + } + + override fun rollbackOptimisticUpdates(mutationId: UUID): ApolloStoreOperation> { + // Should we throw here instead? + return emptyOperation(emptySet()) + } +} \ No newline at end of file diff --git a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/lru/EvictionPolicy.kt b/apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/lru/EvictionPolicy.kt similarity index 100% rename from apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/lru/EvictionPolicy.kt rename to apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/lru/EvictionPolicy.kt diff --git a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/lru/LruNormalizedCache.kt b/apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/lru/LruNormalizedCache.kt similarity index 100% rename from apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/lru/LruNormalizedCache.kt rename to apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/lru/LruNormalizedCache.kt diff --git a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/lru/LruNormalizedCacheFactory.kt b/apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/lru/LruNormalizedCacheFactory.kt similarity index 100% rename from apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/lru/LruNormalizedCacheFactory.kt rename to apollo-normalized-cache/src/jvmMain/kotlin/com/apollographql/apollo/cache/normalized/lru/LruNormalizedCacheFactory.kt diff --git a/apollo-normalized-cache/src/test/java/com/apollographql/apollo/cache/normalized/internal/CacheKeyBuilderTest.kt b/apollo-normalized-cache/src/jvmTest/java/com/apollographql/apollo/cache/normalized/internal/CacheKeyBuilderTest.kt similarity index 100% rename from apollo-normalized-cache/src/test/java/com/apollographql/apollo/cache/normalized/internal/CacheKeyBuilderTest.kt rename to apollo-normalized-cache/src/jvmTest/java/com/apollographql/apollo/cache/normalized/internal/CacheKeyBuilderTest.kt diff --git a/apollo-normalized-cache/src/test/java/com/apollographql/apollo/cache/normalized/lru/LruNormalizedCacheTest.kt b/apollo-normalized-cache/src/jvmTest/java/com/apollographql/apollo/cache/normalized/lru/LruNormalizedCacheTest.kt similarity index 100% rename from apollo-normalized-cache/src/test/java/com/apollographql/apollo/cache/normalized/lru/LruNormalizedCacheTest.kt rename to apollo-normalized-cache/src/jvmTest/java/com/apollographql/apollo/cache/normalized/lru/LruNormalizedCacheTest.kt diff --git a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/NoOpApolloStore.java b/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/NoOpApolloStore.java deleted file mode 100644 index 9bc1aa5f14b..00000000000 --- a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/NoOpApolloStore.java +++ /dev/null @@ -1,160 +0,0 @@ -package com.apollographql.apollo.cache.normalized.internal; - -import com.apollographql.apollo.api.GraphqlFragment; -import com.apollographql.apollo.api.Operation; -import com.apollographql.apollo.api.Response; -import com.apollographql.apollo.api.internal.ResponseFieldMapper; -import com.apollographql.apollo.cache.CacheHeaders; -import com.apollographql.apollo.cache.normalized.ApolloStore; -import com.apollographql.apollo.cache.normalized.ApolloStoreOperation; -import com.apollographql.apollo.cache.normalized.CacheKey; -import com.apollographql.apollo.cache.normalized.CacheKeyResolver; -import com.apollographql.apollo.cache.normalized.NormalizedCache; -import com.apollographql.apollo.cache.normalized.Record; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -/** - * An alternative to RealApolloStore for when a no-operation cache is needed. - */ -public final class NoOpApolloStore implements ApolloStore, ReadableStore, WriteableStore { - - @Override public Set merge(@NotNull Collection recordCollection, @NotNull CacheHeaders cacheHeaders) { - return Collections.emptySet(); - } - - @Override public Set merge(Record record, @NotNull CacheHeaders cacheHeaders) { - return Collections.emptySet(); - } - - @Nullable @Override public Record read(@NotNull String key, @NotNull CacheHeaders cacheHeaders) { - return null; - } - - @Override public Collection read(@NotNull Collection keys, @NotNull CacheHeaders cacheHeaders) { - return Collections.emptySet(); - } - - @Override public void subscribe(RecordChangeSubscriber subscriber) { - } - - @Override public void unsubscribe(RecordChangeSubscriber subscriber) { - } - - @Override public void publish(Set keys) { - } - - @NotNull @Override public ApolloStoreOperation clearAll() { - return ApolloStoreOperation.emptyOperation(Boolean.FALSE); - } - - @NotNull @Override public ApolloStoreOperation remove(@NotNull CacheKey cacheKey, boolean cascade) { - return ApolloStoreOperation.emptyOperation(Boolean.FALSE); - } - - @NotNull @Override public ApolloStoreOperation remove(@NotNull CacheKey cacheKey) { - return ApolloStoreOperation.emptyOperation(Boolean.FALSE); - } - - @NotNull @Override public ApolloStoreOperation remove(@NotNull List cacheKeys) { - return ApolloStoreOperation.emptyOperation(0); - } - - @Override public ResponseNormalizer> networkResponseNormalizer() { - //noinspection unchecked - return ResponseNormalizer.NO_OP_NORMALIZER; - } - - @Override public ResponseNormalizer cacheResponseNormalizer() { - //noinspection unchecked - return ResponseNormalizer.NO_OP_NORMALIZER; - } - - @Override public R readTransaction(Transaction transaction) { - return transaction.execute(this); - } - - @Override public R writeTransaction(Transaction transaction) { - return transaction.execute(this); - } - - @Override public NormalizedCache normalizedCache() { - return null; - } - - @Override public CacheKeyResolver cacheKeyResolver() { - return null; - } - - @NotNull @Override - public ApolloStoreOperation read( - @NotNull Operation operation) { - return ApolloStoreOperation.emptyOperation(null); - } - - @NotNull @Override - public ApolloStoreOperation> read( - @NotNull Operation operation, @NotNull ResponseFieldMapper responseFieldMapper, - @NotNull ResponseNormalizer responseNormalizer, @NotNull CacheHeaders cacheHeaders) { - return ApolloStoreOperation.emptyOperation(Response.builder(operation).build()); - } - - @NotNull @Override - public ApolloStoreOperation read(@NotNull ResponseFieldMapper fieldMapper, - @NotNull CacheKey cacheKey, @NotNull Operation.Variables variables) { - return ApolloStoreOperation.emptyOperation(null); - } - - @NotNull @Override - public ApolloStoreOperation> write( - @NotNull Operation operation, @NotNull D operationData) { - return ApolloStoreOperation.emptyOperation(Collections.emptySet()); - } - - @NotNull @Override - public ApolloStoreOperation writeAndPublish( - @NotNull Operation operation, @NotNull D operationData) { - return ApolloStoreOperation.emptyOperation(Boolean.FALSE); - } - - @NotNull @Override - public ApolloStoreOperation> write(@NotNull GraphqlFragment fragment, @NotNull CacheKey cacheKey, - @NotNull Operation.Variables variables) { - return ApolloStoreOperation.emptyOperation(Collections.emptySet()); - } - - @NotNull @Override - public ApolloStoreOperation writeAndPublish(@NotNull GraphqlFragment fragment, @NotNull CacheKey cacheKey, - @NotNull Operation.Variables variables) { - return ApolloStoreOperation.emptyOperation(Boolean.FALSE); - } - - @NotNull @Override - public ApolloStoreOperation> - writeOptimisticUpdates(@NotNull Operation operation, @NotNull D operationData, @NotNull UUID mutationId) { - return ApolloStoreOperation.emptyOperation(Collections.emptySet()); - } - - @NotNull @Override - public ApolloStoreOperation - writeOptimisticUpdatesAndPublish(@NotNull Operation operation, @NotNull D operationData, - @NotNull UUID mutationId) { - return ApolloStoreOperation.emptyOperation(Boolean.FALSE); - } - - @NotNull @Override - public ApolloStoreOperation rollbackOptimisticUpdatesAndPublish(@NotNull UUID mutationId) { - return ApolloStoreOperation.emptyOperation(Boolean.FALSE); - } - - @NotNull @Override public ApolloStoreOperation> rollbackOptimisticUpdates(@NotNull UUID mutationId) { - return ApolloStoreOperation.emptyOperation(Collections.emptySet()); - } -} diff --git a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/ResponseNormalizer.java b/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/ResponseNormalizer.java deleted file mode 100644 index eccca13678b..00000000000 --- a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/ResponseNormalizer.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.apollographql.apollo.cache.normalized.internal; - -import com.apollographql.apollo.api.Operation; -import com.apollographql.apollo.api.ResponseField; -import com.apollographql.apollo.api.internal.ResolveDelegate; -import com.apollographql.apollo.cache.normalized.CacheKey; -import com.apollographql.apollo.cache.normalized.CacheKeyResolver; -import com.apollographql.apollo.cache.normalized.CacheReference; -import com.apollographql.apollo.cache.normalized.Record; -import com.apollographql.apollo.cache.normalized.RecordSet; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -public abstract class ResponseNormalizer implements ResolveDelegate { - private SimpleStack> pathStack; - private SimpleStack recordStack; - private SimpleStack valueStack; - private List path; - private Record.Builder currentRecordBuilder; - - private RecordSet recordSet = new RecordSet(); - private Set dependentKeys = Collections.emptySet(); - - public Collection records() { - return recordSet.allRecords(); - } - - public Set dependentKeys() { - return dependentKeys; - } - - @Override public void willResolveRootQuery(Operation operation) { - willResolveRecord(CacheKeyResolver.rootKeyForOperation(operation)); - } - - @Override public void willResolve(ResponseField field, Operation.Variables variables, @Nullable Object value) { - String key = cacheKeyBuilder().build(field, variables); - path.add(key); - } - - @Override public void didResolve(ResponseField field, Operation.Variables variables) { - path.remove(path.size() - 1); - Object value = valueStack.pop(); - String cacheKey = cacheKeyBuilder().build(field, variables); - String dependentKey = currentRecordBuilder.getKey() + "." + cacheKey; - dependentKeys.add(dependentKey); - currentRecordBuilder.addField(cacheKey, value); - - if (recordStack.isEmpty()) { - recordSet.merge(currentRecordBuilder.build()); - } - } - - @Override public void didResolveScalar(@Nullable Object value) { - valueStack.push(value); - } - - @Override public void willResolveObject(ResponseField field, @Nullable R objectSource) { - pathStack.push(path); - - CacheKey cacheKey = objectSource != null ? resolveCacheKey(field, objectSource) : CacheKey.NO_KEY; - String cacheKeyValue = cacheKey.key(); - if (cacheKey.equals(CacheKey.NO_KEY)) { - cacheKeyValue = pathToString(); - } else { - path = new ArrayList<>(); - path.add(cacheKeyValue); - } - recordStack.push(currentRecordBuilder.build()); - currentRecordBuilder = Record.builder(cacheKeyValue); - } - - @Override public void didResolveObject(ResponseField field, @Nullable R objectSource) { - path = pathStack.pop(); - if (objectSource != null) { - Record completedRecord = currentRecordBuilder.build(); - valueStack.push(new CacheReference(completedRecord.key())); - dependentKeys.add(completedRecord.key()); - recordSet.merge(completedRecord); - } - currentRecordBuilder = recordStack.pop().toBuilder(); - } - - @Override public void didResolveList(List array) { - List parsedArray = new ArrayList<>(array.size()); - for (int i = 0, size = array.size(); i < size; i++) { - parsedArray.add(0, valueStack.pop()); - } - valueStack.push(parsedArray); - } - - @Override public void willResolveElement(int atIndex) { - path.add(Integer.toString(atIndex)); - } - - @Override public void didResolveElement(int atIndex) { - path.remove(path.size() - 1); - } - - @Override public void didResolveNull() { - valueStack.push(null); - } - - @NotNull public abstract CacheKey resolveCacheKey(@NotNull ResponseField field, @NotNull R record); - - @NotNull public abstract CacheKeyBuilder cacheKeyBuilder(); - - public void willResolveRecord(CacheKey cacheKey) { - pathStack = new SimpleStack<>(); - recordStack = new SimpleStack<>(); - valueStack = new SimpleStack<>(); - dependentKeys = new HashSet<>(); - - path = new ArrayList<>(); - currentRecordBuilder = Record.builder(cacheKey.key()); - recordSet = new RecordSet(); - } - - private String pathToString() { - StringBuilder stringBuilder = new StringBuilder(); - for (int i = 0, size = path.size(); i < size; i++) { - String pathPiece = path.get(i); - stringBuilder.append(pathPiece); - if (i < size - 1) { - stringBuilder.append("."); - } - } - return stringBuilder.toString(); - } - - @SuppressWarnings("unchecked") public static final ResponseNormalizer NO_OP_NORMALIZER = new ResponseNormalizer() { - @Override public void willResolveRootQuery(Operation operation) { - } - - @Override public void willResolve(ResponseField field, Operation.Variables variables, @Nullable Object value) { - } - - @Override public void didResolve(ResponseField field, Operation.Variables variables) { - } - - @Override public void didResolveScalar(Object value) { - } - - @Override public void willResolveObject(ResponseField field, @Nullable Object objectSource) { - } - - @Override public void didResolveObject(ResponseField field, @Nullable Object objectSource) { - } - - @Override public void didResolveList(List array) { - } - - @Override public void willResolveElement(int atIndex) { - } - - @Override public void didResolveElement(int atIndex) { - } - - @Override public void didResolveNull() { - } - - @Override public Collection records() { - return Collections.emptyList(); - } - - @Override public Set dependentKeys() { - return Collections.emptySet(); - } - - @NotNull @Override public CacheKey resolveCacheKey(@NotNull ResponseField field, @NotNull Object record) { - return CacheKey.NO_KEY; - } - - @NotNull @Override public CacheKeyBuilder cacheKeyBuilder() { - return new CacheKeyBuilder() { - @NotNull @Override public String build(@NotNull ResponseField field, @NotNull Operation.Variables variables) { - return CacheKey.NO_KEY.key(); - } - }; - } - }; -} diff --git a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/SimpleStack.java b/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/SimpleStack.java deleted file mode 100644 index 639de00e6f4..00000000000 --- a/apollo-normalized-cache/src/main/java/com/apollographql/apollo/cache/normalized/internal/SimpleStack.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.apollographql.apollo.cache.normalized.internal; - -import java.util.ArrayList; -import java.util.List; - -/** - * Simple stack data structure which accepts null elements. Backed by list. - * @param - */ -public class SimpleStack { - - private List backing; - - public SimpleStack() { - backing = new ArrayList<>(); - } - - public SimpleStack(int initialSize) { - backing = new ArrayList<>(initialSize); - } - - public void push(E element) { - backing.add(element); - } - - public E pop() { - if (isEmpty()) { - throw new IllegalStateException("Stack is empty."); - } - return backing.remove(backing.size() - 1); - } - - public boolean isEmpty() { - return backing.isEmpty(); - } -} diff --git a/apollo-runtime-kotlin/build.gradle.kts b/apollo-runtime-kotlin/build.gradle.kts index 1d39f9f2f1d..122bdfef40d 100644 --- a/apollo-runtime-kotlin/build.gradle.kts +++ b/apollo-runtime-kotlin/build.gradle.kts @@ -28,6 +28,7 @@ kotlin { val commonMain by getting { dependencies { api(project(":apollo-api")) + api(project(":apollo-normalized-cache")) api(groovy.util.Eval.x(project, "x.dep.okio.okioMultiplatform")) api(groovy.util.Eval.x(project, "x.dep.uuid")) implementation(kotlin("stdlib-common")) diff --git a/apollo-runtime-kotlin/src/commonMain/kotlin/com/apollographql/apollo/interceptor/ApolloResponse.kt b/apollo-runtime-kotlin/src/commonMain/kotlin/com/apollographql/apollo/interceptor/ApolloResponse.kt index 5df1c169157..8061d6cedd4 100644 --- a/apollo-runtime-kotlin/src/commonMain/kotlin/com/apollographql/apollo/interceptor/ApolloResponse.kt +++ b/apollo-runtime-kotlin/src/commonMain/kotlin/com/apollographql/apollo/interceptor/ApolloResponse.kt @@ -7,7 +7,7 @@ import com.apollographql.apollo.api.Response import com.benasher44.uuid.Uuid @ApolloExperimental -class ApolloResponse( +data class ApolloResponse( val requestUuid: Uuid, val response: Response, val executionContext: ExecutionContext diff --git a/apollo-runtime-kotlin/src/commonMain/kotlin/com/apollographql/apollo/interceptor/cache/ApolloCacheInterceptor.kt b/apollo-runtime-kotlin/src/commonMain/kotlin/com/apollographql/apollo/interceptor/cache/ApolloCacheInterceptor.kt new file mode 100644 index 00000000000..66f08b9ed8a --- /dev/null +++ b/apollo-runtime-kotlin/src/commonMain/kotlin/com/apollographql/apollo/interceptor/cache/ApolloCacheInterceptor.kt @@ -0,0 +1,96 @@ +package com.apollographql.apollo.interceptor.cache + +import com.apollographql.apollo.api.ApolloExperimental +import com.apollographql.apollo.api.Operation +import com.apollographql.apollo.api.Response +import com.apollographql.apollo.api.Response.Companion.builder +import com.apollographql.apollo.api.ResponseField +import com.apollographql.apollo.api.internal.NoOpResolveDelegate +import com.apollographql.apollo.cache.CacheExecutionContext +import com.apollographql.apollo.cache.CacheHeaders +import com.apollographql.apollo.cache.normalized.CacheKey +import com.apollographql.apollo.cache.normalized.CacheKeyResolver +import com.apollographql.apollo.cache.normalized.Record +import com.apollographql.apollo.cache.normalized.internal.CacheFieldValueResolver +import com.apollographql.apollo.cache.normalized.internal.CacheKeyBuilder +import com.apollographql.apollo.cache.normalized.internal.ReadableStore +import com.apollographql.apollo.cache.normalized.internal.RealCacheKeyBuilder +import com.apollographql.apollo.cache.normalized.internal.ResponseNormalizer +import com.apollographql.apollo.cache.normalized.simple.SimpleNormalizedCache +import com.apollographql.apollo.interceptor.ApolloInterceptorChain +import com.apollographql.apollo.interceptor.ApolloRequest +import com.apollographql.apollo.interceptor.ApolloRequestInterceptor +import com.apollographql.apollo.interceptor.ApolloResponse +import com.apollographql.apollo.internal.response.RealResponseReader +import com.apollographql.apollo.internal.response.RealResponseWriter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow + +@ApolloExperimental +class ApolloCacheInterceptor : ApolloRequestInterceptor { + val normalizedCache = SimpleNormalizedCache() + val readableStore = object : ReadableStore { + override fun read(key: String, cacheHeaders: CacheHeaders): Record? { + return normalizedCache.loadRecord(key, cacheHeaders) + } + + override fun read(keys: Collection, cacheHeaders: CacheHeaders): Collection { + return keys.mapNotNull { normalizedCache.loadRecord(it, cacheHeaders) } + } + } + + override fun intercept(request: ApolloRequest, chain: ApolloInterceptorChain): Flow> { + return flow { + val response = readFromCache(request) + if (response != null) { + emit(ApolloResponse(requestUuid = request.requestUuid, response = response, executionContext = request.executionContext + CacheExecutionContext(true))) + } else { + chain.proceed(request).collect { + if (it.response.data != null) { + writeToCache(request, it.response.data!!) + } + emit(it.copy(executionContext = it.executionContext + CacheExecutionContext(false))) + } + } + } + } + + private fun writeToCache(request: ApolloRequest, data: D) { + val operation = request.operation + val writer = RealResponseWriter(operation.variables(), request.scalarTypeAdapters) + data.marshaller().marshal(writer) + + val responseNormalizer = object : ResponseNormalizer?>() { + override fun resolveCacheKey(field: ResponseField, + record: Map?): CacheKey { + return CacheKeyResolver.DEFAULT.fromFieldRecordSet(field, record!!) + } + + override fun cacheKeyBuilder(): CacheKeyBuilder { + return RealCacheKeyBuilder() + } + } + + responseNormalizer.willResolveRootQuery(operation); + writer.resolveFields(responseNormalizer) + normalizedCache.merge(responseNormalizer.records()?.filterNotNull() ?: emptySet(), CacheHeaders.NONE) + } + + private fun readFromCache(request: ApolloRequest): Response? { + val operation = request.operation + val rootRecord = normalizedCache.loadRecord(CacheKeyResolver.rootKeyForOperation(operation).key, CacheHeaders.NONE) ?: return null + + val fieldValueResolver = CacheFieldValueResolver(readableStore, + operation.variables(), + CacheKeyResolver.DEFAULT, + CacheHeaders.NONE, + RealCacheKeyBuilder() + ) + val responseReader = RealResponseReader(operation.variables(), rootRecord, fieldValueResolver, request.scalarTypeAdapters, NoOpResolveDelegate()) + val data = operation.wrapData(operation.responseFieldMapper().map(responseReader)) + return builder(operation) + .data(data) + .build() + } +} \ No newline at end of file diff --git a/apollo-runtime-kotlin/src/commonMain/kotlin/com/apollographql/apollo/interceptor/cache/CacheExecutionContext.kt b/apollo-runtime-kotlin/src/commonMain/kotlin/com/apollographql/apollo/interceptor/cache/CacheExecutionContext.kt new file mode 100644 index 00000000000..e491a40f497 --- /dev/null +++ b/apollo-runtime-kotlin/src/commonMain/kotlin/com/apollographql/apollo/interceptor/cache/CacheExecutionContext.kt @@ -0,0 +1,14 @@ +package com.apollographql.apollo.cache + +import com.apollographql.apollo.api.ApolloExperimental +import com.apollographql.apollo.api.ExecutionContext + +@ApolloExperimental +data class CacheExecutionContext( + val fromCache: Boolean +) : ExecutionContext.Element { + override val key: ExecutionContext.Key<*> = Key + + companion object Key : ExecutionContext.Key +} + diff --git a/apollo-runtime-kotlin/src/commonMain/kotlin/com/apollographql/apollo/internal/RealApolloCall.kt b/apollo-runtime-kotlin/src/commonMain/kotlin/com/apollographql/apollo/internal/RealApolloCall.kt index 90332e537a9..36bcbf98524 100644 --- a/apollo-runtime-kotlin/src/commonMain/kotlin/com/apollographql/apollo/internal/RealApolloCall.kt +++ b/apollo-runtime-kotlin/src/commonMain/kotlin/com/apollographql/apollo/internal/RealApolloCall.kt @@ -38,7 +38,9 @@ class RealApolloCall constructor( }.flatMapLatest { interceptorChain -> interceptorChain.proceed(request) }.map { apolloResponse -> - apolloResponse.response + apolloResponse.response.copy( + executionContext = apolloResponse.executionContext + ) } } } diff --git a/apollo-runtime-kotlin/src/commonTest/kotlin/com/apollographql/apollo/cache/CacheTest.kt b/apollo-runtime-kotlin/src/commonTest/kotlin/com/apollographql/apollo/cache/CacheTest.kt new file mode 100644 index 00000000000..7d4db4664e8 --- /dev/null +++ b/apollo-runtime-kotlin/src/commonTest/kotlin/com/apollographql/apollo/cache/CacheTest.kt @@ -0,0 +1,61 @@ +package com.apollographql.apollo.cache + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.interceptor.cache.ApolloCacheInterceptor +import com.apollographql.apollo.mock.MockNetworkTransport +import com.apollographql.apollo.mock.MockQuery +import com.apollographql.apollo.mock.TestLoggerExecutor +import com.apollographql.apollo.runBlocking +import kotlinx.coroutines.flow.single +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@Suppress("EXPERIMENTAL_API_USAGE") +class CacheTest { + private lateinit var networkTransport: MockNetworkTransport + private lateinit var apolloClient: ApolloClient + + @BeforeTest + fun setUp() { + networkTransport = MockNetworkTransport() + apolloClient = ApolloClient( + networkTransport = networkTransport, + interceptors = listOf(TestLoggerExecutor, ApolloCacheInterceptor()) + ) + } + + @Test + fun `second request doesn't hit network`() { + networkTransport.offer(""" + { + "data": { + "hero": { + "__typename": "Hero", + "name": "Ian Solo" + } + } + } + """.trimIndent()) + + runBlocking { + var response = apolloClient + .query(TestQuery()) + .execute() + .single() + + assertNotNull(response.data) + assertFalse(response.executionContext[CacheExecutionContext]!!.fromCache) + + response = apolloClient + .query(TestQuery()) + .execute() + .single() + + assertNotNull(response.data) + assertTrue(response.executionContext[CacheExecutionContext]!!.fromCache) + } + } +} diff --git a/apollo-runtime-kotlin/src/commonTest/kotlin/com/apollographql/apollo/cache/TestQuery.kt b/apollo-runtime-kotlin/src/commonTest/kotlin/com/apollographql/apollo/cache/TestQuery.kt new file mode 100644 index 00000000000..ecdc4c1cc87 --- /dev/null +++ b/apollo-runtime-kotlin/src/commonTest/kotlin/com/apollographql/apollo/cache/TestQuery.kt @@ -0,0 +1,172 @@ +package com.apollographql.apollo.cache + +// AUTO-GENERATED FILE. DO NOT MODIFY. +// +// This class was automatically generated by Apollo GraphQL plugin from the GraphQL queries it found. +// It should not be modified by hand. +// + +import com.apollographql.apollo.api.Operation +import com.apollographql.apollo.api.OperationName +import com.apollographql.apollo.api.Query +import com.apollographql.apollo.api.Response +import com.apollographql.apollo.api.ResponseField +import com.apollographql.apollo.api.ScalarTypeAdapters +import com.apollographql.apollo.api.ScalarTypeAdapters.Companion.DEFAULT +import com.apollographql.apollo.api.internal.OperationRequestBodyComposer +import com.apollographql.apollo.api.internal.QueryDocumentMinifier +import com.apollographql.apollo.api.internal.ResponseFieldMapper +import com.apollographql.apollo.api.internal.ResponseFieldMarshaller +import com.apollographql.apollo.api.internal.ResponseReader +import com.apollographql.apollo.api.internal.SimpleOperationResponseParser +import com.apollographql.apollo.api.internal.Throws +import kotlin.Array +import kotlin.Boolean +import kotlin.String +import kotlin.Suppress +import okio.Buffer +import okio.BufferedSource +import okio.ByteString +import okio.IOException + +/** + * This is a sample query to fetch hero name + * that demonstrates Java / Kotlin docs generations + * for query data model + */ +@Suppress("NAME_SHADOWING", "UNUSED_ANONYMOUS_PARAMETER", "LocalVariableName", + "RemoveExplicitTypeArguments", "NestedLambdaShadowedImplicitParameter") +class TestQuery : Query { + override fun operationId(): String = OPERATION_ID + override fun queryDocument(): String = QUERY_DOCUMENT + override fun wrapData(data: Data?): Data? = data + override fun variables(): Operation.Variables = Operation.EMPTY_VARIABLES + override fun name(): OperationName = OPERATION_NAME + override fun responseFieldMapper(): ResponseFieldMapper = ResponseFieldMapper.invoke { + Data(it) + } + + @Throws(IOException::class) + override fun parse(source: BufferedSource, scalarTypeAdapters: ScalarTypeAdapters): Response + = SimpleOperationResponseParser.parse(source, this, scalarTypeAdapters) + + @Throws(IOException::class) + override fun parse(byteString: ByteString, scalarTypeAdapters: ScalarTypeAdapters): Response + = parse(Buffer().write(byteString), scalarTypeAdapters) + + @Throws(IOException::class) + override fun parse(source: BufferedSource): Response = parse(source, DEFAULT) + + @Throws(IOException::class) + override fun parse(byteString: ByteString): Response = parse(byteString, DEFAULT) + + override fun composeRequestBody(scalarTypeAdapters: ScalarTypeAdapters): ByteString = + OperationRequestBodyComposer.compose( + operation = this, + autoPersistQueries = false, + withQueryDocument = true, + scalarTypeAdapters = scalarTypeAdapters + ) + + override fun composeRequestBody(): ByteString = OperationRequestBodyComposer.compose( + operation = this, + autoPersistQueries = false, + withQueryDocument = true, + scalarTypeAdapters = DEFAULT + ) + + override fun composeRequestBody( + autoPersistQueries: Boolean, + withQueryDocument: Boolean, + scalarTypeAdapters: ScalarTypeAdapters + ): ByteString = OperationRequestBodyComposer.compose( + operation = this, + autoPersistQueries = autoPersistQueries, + withQueryDocument = withQueryDocument, + scalarTypeAdapters = scalarTypeAdapters + ) + + /** + * A character from the Star Wars universe + */ + data class Hero( + val __typename: String = "Character", + /** + * The name of the character + */ + val name: String + ) { + fun marshaller(): ResponseFieldMarshaller = ResponseFieldMarshaller.invoke { writer -> + writer.writeString(RESPONSE_FIELDS[0], this@Hero.__typename) + writer.writeString(RESPONSE_FIELDS[1], this@Hero.name) + } + + companion object { + private val RESPONSE_FIELDS: Array = arrayOf( + ResponseField.forString("__typename", "__typename", null, false, null), + ResponseField.forString("name", "name", null, false, null) + ) + + operator fun invoke(reader: ResponseReader): Hero = reader.run { + val __typename = readString(RESPONSE_FIELDS[0])!! + val name = readString(RESPONSE_FIELDS[1])!! + Hero( + __typename = __typename, + name = name + ) + } + + @Suppress("FunctionName") + fun Mapper(): ResponseFieldMapper = ResponseFieldMapper { invoke(it) } + } + } + + /** + * Data from the response after executing this GraphQL operation + */ + data class Data( + val hero: Hero? + ) : Operation.Data { + override fun marshaller(): ResponseFieldMarshaller = ResponseFieldMarshaller.invoke { writer -> + writer.writeObject(RESPONSE_FIELDS[0], this@Data.hero?.marshaller()) + } + + companion object { + private val RESPONSE_FIELDS: Array = arrayOf( + ResponseField.forObject("hero", "hero", null, true, null) + ) + + operator fun invoke(reader: ResponseReader): Data = reader.run { + val hero = readObject(RESPONSE_FIELDS[0]) { reader -> + Hero(reader) + } + Data( + hero = hero + ) + } + + @Suppress("FunctionName") + fun Mapper(): ResponseFieldMapper = ResponseFieldMapper { invoke(it) } + } + } + + companion object { + const val OPERATION_ID: String = + "c10c6dfe569b0fbb60c67e42c973f7ffef2314b43004c527a03bdd790ef0f5dc" + + val QUERY_DOCUMENT: String = QueryDocumentMinifier.minify( + """ + |query TestQuery { + | hero { + | __typename + | name + | } + |} + """.trimMargin() + ) + + val OPERATION_NAME: OperationName = object : OperationName { + override fun name(): String = "TestQuery" + } + } +} diff --git a/apollo-runtime/src/main/java/com/apollographql/apollo/internal/RealApolloStore.java b/apollo-runtime/src/main/java/com/apollographql/apollo/internal/RealApolloStore.java index 5963ad7dfcc..687e3acdb71 100644 --- a/apollo-runtime/src/main/java/com/apollographql/apollo/internal/RealApolloStore.java +++ b/apollo-runtime/src/main/java/com/apollographql/apollo/internal/RealApolloStore.java @@ -6,6 +6,7 @@ import com.apollographql.apollo.api.ResponseField; import com.apollographql.apollo.api.ScalarTypeAdapters; import com.apollographql.apollo.api.internal.ApolloLogger; +import com.apollographql.apollo.api.internal.ResolveDelegate; import com.apollographql.apollo.api.internal.ResponseFieldMapper; import com.apollographql.apollo.cache.CacheHeaders; import com.apollographql.apollo.cache.normalized.ApolloStore; @@ -366,8 +367,8 @@ T doRead(final Oper CacheFieldValueResolver fieldValueResolver = new CacheFieldValueResolver(cache, operation.variables(), cacheKeyResolver(), CacheHeaders.NONE, cacheKeyBuilder); //noinspection unchecked - RealResponseReader responseReader = new RealResponseReader<>(operation.variables(), rootRecord, - fieldValueResolver, scalarTypeAdapters, ResponseNormalizer.NO_OP_NORMALIZER); + RealResponseReader responseReader = new RealResponseReader(operation.variables(), rootRecord, + fieldValueResolver, scalarTypeAdapters, (ResolveDelegate) ResponseNormalizer.NO_OP_NORMALIZER); return operation.wrapData(responseFieldMapper.map(responseReader)); } }); @@ -415,8 +416,8 @@ F doRead(final ResponseFieldMapper responseFieldM CacheFieldValueResolver fieldValueResolver = new CacheFieldValueResolver(cache, variables, cacheKeyResolver(), CacheHeaders.NONE, cacheKeyBuilder); //noinspection unchecked - RealResponseReader responseReader = new RealResponseReader<>(variables, rootRecord, - fieldValueResolver, scalarTypeAdapters, ResponseNormalizer.NO_OP_NORMALIZER); + RealResponseReader responseReader = new RealResponseReader(variables, rootRecord, + fieldValueResolver, scalarTypeAdapters, (ResolveDelegate) ResponseNormalizer.NO_OP_NORMALIZER); return responseFieldMapper.map(responseReader); } }); diff --git a/apollo-runtime/src/main/java/com/apollographql/apollo/response/OperationResponseParser.java b/apollo-runtime/src/main/java/com/apollographql/apollo/response/OperationResponseParser.java index f604850c777..bedb9a40579 100644 --- a/apollo-runtime/src/main/java/com/apollographql/apollo/response/OperationResponseParser.java +++ b/apollo-runtime/src/main/java/com/apollographql/apollo/response/OperationResponseParser.java @@ -30,7 +30,7 @@ public final class OperationResponseParser { @SuppressWarnings("unchecked") public OperationResponseParser(Operation operation, ResponseFieldMapper responseFieldMapper, ScalarTypeAdapters scalarTypeAdapters) { - this(operation, responseFieldMapper, scalarTypeAdapters, ResponseNormalizer.NO_OP_NORMALIZER); + this(operation, responseFieldMapper, scalarTypeAdapters, (ResponseNormalizer>) ResponseNormalizer.NO_OP_NORMALIZER); } public OperationResponseParser(Operation operation, ResponseFieldMapper responseFieldMapper,