Repository: apollographql/apollo-kotlin
Pull request #4986 (2023-05-25 13:16:22)

Release 3.8.2
Cherry-picks from `main` up to `d11b6b1273bb64f03d49c2c766516f03c01e8af1`

Few comments:

* #4823 is not pulled in because `SchemaUploader` is using a different API on 3.x
* #4839 because that's a behaviour change (request headers replace client headers on 3.x, they will be additive in 4.x)
* 3.x is still using Kotlin 1.8.10, we might want to bump that at some point but I didn't touch any of that


Patch:
diff --git a/docs/source/_redirects b/docs/source/_redirects
index 0a71b56958..4c1ede579a 100644
--- a/docs/source/_redirects
+++ b/docs/source/_redirects
@@ -17,4 +17,5 @@
 /tutorial /docs/kotlin/tutorial/00-introduction
 /advanced/ui-tests/ /docs/kotlin/testing/ui-tests/
 /advanced/test-builders/ /docs/kotlin/testing/test-builders/
-/advanced/response-based-codegen/ /docs/kotlin/advanced/codegen-methods/
+/advanced/response-based-codegen/ /docs/kotlin/advanced/response-based/
+/advanced/codegen-methods/ /docs/kotlin/advanced/response-based/
diff --git a/docs/source/advanced/operation-variables.mdx b/docs/source/advanced/operation-variables.mdx
index 0a839a6db0..ddb7572a1f 100644
--- a/docs/source/advanced/operation-variables.mdx
+++ b/docs/source/advanced/operation-variables.mdx
@@ -22,7 +22,10 @@ query GetTodos($first: Int, $offset: Int) {
 Apollo Kotlin generates the following Kotlin code for this query:
 
 ```kotlin
-class GetTodosQuery(val first: Optional<Int?>, val offset: Optional<Int?>)
+class GetTodosQuery(
+    val first: Optional<Int?> = Optional.Absent,
+    val offset: Optional<Int?> = Optional.Absent
+)
 ```
 
 You can then selectively provide or omit variable values like so:
diff --git a/docs/source/advanced/codegen-methods.mdx b/docs/source/advanced/response-based.mdx
similarity index 58%
rename from docs/source/advanced/codegen-methods.mdx
rename to docs/source/advanced/response-based.mdx
index 638e11e8bf..595a5916f8 100644
--- a/docs/source/advanced/codegen-methods.mdx
+++ b/docs/source/advanced/response-based.mdx
@@ -1,18 +1,22 @@
 ---
-title: Code generation methods in Apollo Kotlin
+title: Response based codegen
 ---
 
-Apollo Kotlin provides multiple code generation engines (**codegens**) that you can choose from depending on your use case:
+Apollo Kotlin takes your GraphQL operations, generates Kotlin models for them and instantiates them from your JSON responses allowing you to access your data in a type safe way. 
 
+There are effectively 3 different domains at play:
 
-| Codegen | Description |
-|---------|-------------|
-| `operationBased` (default) | Generates models according to the shape of each defined operation. |
-| `responseBased` | <p>Generates models according to the shape of each operation's response.</p><p>Compared to `operationBased`, models generated by the `responseBased` codegen are more performant and expose more type information, at the cost of generating more total code. [See details.](#the-responsebased-codegen)  </p> |
-| `compat` (deprecated) | Similar to `operationBased`, except it generates models that are compatible with Apollo Android v2. This codegen helps projects upgrade from v2 to v3. |
+* The GraphQL domain: operations
+* The Kotlin domain: models
+* The JSON domain: responses
 
-> 💡 For more in-depth information on each codegen, see [this design document](https://github.com/apollographql/apollo-android/blob/main/design-docs/Codegen.md).
+By default, Apollo Kotlin generates models that match 1:1 with your GraphQL operations. Inline and named fragments generate synthetic fields, so you can access GraphQL fragments with Kotlin code like `data.hero.onDroid.primaryFunction`. Fragments are classes that can be reused from different operations. This code generation engine (**codegen**) is named `operationBased` because it matches the GraphQL operation.
 
+The Json response may have a different shape than your GraphQL operation though. This is the case when using merged fields or fragments. If you want to access your Kotlin properties as they are in the JSON response, Apollo Kotlin provides a `responseBased` codegen that match 1:1 with the JSON response. GraphQL fragments are represented as Kotlin interfaces, so you can access their fields with Kotlin code like `(data.hero as Droid).primaryFunction`. Because they map to the JSON responses, the `responseBased` models have the property of allowing JSON streaming and/or mapping to dynamic JS objects. But because GraphQL is a very expressive language, [it's also easy to create a GraphQL query that generate a very large JSON response](https://github.com/apollographql/apollo-kotlin/issues/3144).
+
+**For this reason and other [limitations](#limitations-of-responsebased-codegen), we recommend using `operationBased` codegen by default**.
+
+This page first recaps how [`operationBased` codegen](#the-operationbased-codegen-default) works before explaining [`responseBased` codegen](#the-responsebased-codegen). Finally, it lists the different [limitations](#limitations-of-responsebased-codegen) coming with `responseBased` codegen so you can make an informed decision should you use this codegen. 
 
 To use a particular codegen, configure `codegenModels` in your Gradle scripts:
 
@@ -231,3 +235,11 @@ As a convenience, the `responseBased` codegen generates methods with the name pa
 val primaryFunction = hero1.droidFields().primaryFunction
 val height = hero2.humanFields().height
 ```
+
+## Limitations of `responseBased` codegen
+
+1. Because GraphQL is a very expressive language, [it's easy to create a GraphQL query that generate a very large JSON response](https://github.com/apollographql/apollo-kotlin/issues/3144). If you're using a lot of nested fragments, the generated code size can grow exponentially with the nesting level. We have seen relatively small GraphQL queries breaking the JVM limits like [maximum method size](https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-4.html#jvms-4.7.3).
+2. When using fragments, data classes must be generated for each operation where the fragments are used. To avoid name clashes, the models are nested and this comes with two side effects:
+   * The resulting `.class` file name can be very long, breaking the [256 default maximum file name on MacOS](https://apple.stackexchange.com/questions/86611/does-os-x-enforce-a-maximum-filename-length-or-character-restriction).
+   * Similarly named interfaces might be nested (for fragments). While this is valid in Kotlin, [Java does not allow this](https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.1), and [it will break kapt if you're using it](https://youtrack.jetbrains.com/issue/KT-24272/kapt-generated-code-fails-to-compile-with-symbol-not-found-for-same-named-nested-class). 
+3. `@include`, `@skip` and `@defer` directives are not supported on fragments in `responseBased` codegen. Supporting them would require generating twice the models each time one of these directive would be used.
diff --git a/docs/source/config.json b/docs/source/config.json
index 505a18834b..931562a424 100644
--- a/docs/source/config.json
+++ b/docs/source/config.json
@@ -27,7 +27,6 @@
     "Configuration": {
       "File types": "/essentials/file-types",
       "Client Awareness": "/advanced/client-awareness",
-      "Codegen methods": "/advanced/codegen-methods",
       "Multi Modules": "/advanced/multi-modules",
       "Connecting source sets": "/advanced/source-sets",
       "Gradle plugin configuration": "/advanced/plugin-configuration"
@@ -75,7 +74,8 @@
       "Using with Java": "/advanced/java",
       "RxJava support": "/advanced/rxjava",
       "Kotlin native": "/advanced/kotlin-native",
-      "Apollo AST": "/advanced/apollo-ast"
+      "Apollo AST": "/advanced/apollo-ast",
+      "Response based codegen": "/advanced/response-based"
     }
   }
 }
diff --git a/docs/source/essentials/custom-scalars.mdx b/docs/source/essentials/custom-scalars.mdx
index 8d7f6fbfaa..a2fa83dd69 100644
--- a/docs/source/essentials/custom-scalars.mdx
+++ b/docs/source/essentials/custom-scalars.mdx
@@ -138,6 +138,8 @@ In addition, the `com.apollographql.apollo3:apollo-adapters` artifact provides t
 | `com.apollographql.apollo3.adapter.JavaLocalDateAdapter`        | For `java.time.LocalDate` ISO8601 dates                                                             |
 | `com.apollographql.apollo3.adapter.KotlinxLocalDateTimeAdapter` | For `kotlinx.datetime.LocalDateTime` ISO8601 dates                                                  |
 | `com.apollographql.apollo3.adapter.JavaLocalDateTimeAdapter`    | For `java.time.LocalDateTime` ISO8601 dates                                                         |
+| `com.apollographql.apollo3.adapter.KotlinxLocalTimeAdapter`     | For `kotlinx.datetime.LocalTime` ISO8601 dates                                                      |
+| `com.apollographql.apollo3.adapter.JavaLocalTimeAdapter`        | For `java.time.LocalTime` ISO8601 dates                                                         |
 | `com.apollographql.apollo3.adapter.JavaOffsetDateTimeAdapter`   | For `java.time.OffsetDateTime` ISO8601 dates                                                        |
 | `com.apollographql.apollo3.adapter.DateAdapter`                 | For `java.util.Date` ISO8601 dates                                                                  |
 | `com.apollographql.apollo3.adapter.BigDecimalAdapter`           | For a Multiplatform `com.apollographql.apollo3.adapter.BigDecimal` class holding big decimal values |
diff --git a/libraries/apollo-adapters/README.md b/libraries/apollo-adapters/README.md
index 63ed566a47..251463099e 100644
--- a/libraries/apollo-adapters/README.md
+++ b/libraries/apollo-adapters/README.md
@@ -10,6 +10,8 @@ A few convenient adapters:
 | `com.apollographql.apollo3.adapter.JavaLocalDateAdapter`        | For `java.time.LocalDate` ISO8601 dates                                                             |
 | `com.apollographql.apollo3.adapter.KotlinxLocalDateTimeAdapter` | For `kotlinx.datetime.LocalDateTime` ISO8601 dates                                                  |
 | `com.apollographql.apollo3.adapter.JavaLocalDateTimeAdapter`    | For `java.time.LocalDateTime` ISO8601 dates                                                         |
+| `com.apollographql.apollo3.adapter.KotlinxLocalTimeAdapter`     | For `kotlinx.datetime.LocalTime` ISO8601 dates                                                      |
+| `com.apollographql.apollo3.adapter.JavaLocalTimeAdapter`        | For `java.time.LocalTime` ISO8601 dates                                                             |
 | `com.apollographql.apollo3.adapter.JavaOffsetDateTimeAdapter`   | For `java.time.OffsetDateTime` ISO8601 dates                                                        |
 | `com.apollographql.apollo3.adapter.DateAdapter`                 | For `java.util.Date` ISO8601 dates                                                                  |
 | `com.apollographql.apollo3.adapter.BigDecimalAdapter`           | For a Multiplatform `com.apollographql.apollo3.adapter.BigDecimal` class holding big decimal values |
diff --git a/libraries/apollo-adapters/api/apollo-adapters.api b/libraries/apollo-adapters/api/apollo-adapters.api
index 1326ce7181..897d551f07 100644
--- a/libraries/apollo-adapters/api/apollo-adapters.api
+++ b/libraries/apollo-adapters/api/apollo-adapters.api
@@ -42,6 +42,14 @@ public final class com/apollographql/apollo3/adapter/JavaLocalDateTimeAdapter :
 	public fun toJson (Lcom/apollographql/apollo3/api/json/JsonWriter;Lcom/apollographql/apollo3/api/CustomScalarAdapters;Ljava/time/LocalDateTime;)V
 }
 
+public final class com/apollographql/apollo3/adapter/JavaLocalTimeAdapter : com/apollographql/apollo3/api/Adapter {
+	public static final field INSTANCE Lcom/apollographql/apollo3/adapter/JavaLocalTimeAdapter;
+	public synthetic fun fromJson (Lcom/apollographql/apollo3/api/json/JsonReader;Lcom/apollographql/apollo3/api/CustomScalarAdapters;)Ljava/lang/Object;
+	public fun fromJson (Lcom/apollographql/apollo3/api/json/JsonReader;Lcom/apollographql/apollo3/api/CustomScalarAdapters;)Ljava/time/LocalTime;
+	public synthetic fun toJson (Lcom/apollographql/apollo3/api/json/JsonWriter;Lcom/apollographql/apollo3/api/CustomScalarAdapters;Ljava/lang/Object;)V
+	public fun toJson (Lcom/apollographql/apollo3/api/json/JsonWriter;Lcom/apollographql/apollo3/api/CustomScalarAdapters;Ljava/time/LocalTime;)V
+}
+
 public final class com/apollographql/apollo3/adapter/JavaOffsetDateTimeAdapter : com/apollographql/apollo3/api/Adapter {
 	public static final field INSTANCE Lcom/apollographql/apollo3/adapter/JavaOffsetDateTimeAdapter;
 	public synthetic fun fromJson (Lcom/apollographql/apollo3/api/json/JsonReader;Lcom/apollographql/apollo3/api/CustomScalarAdapters;)Ljava/lang/Object;
@@ -74,3 +82,11 @@ public final class com/apollographql/apollo3/adapter/KotlinxLocalDateTimeAdapter
 	public fun toJson (Lcom/apollographql/apollo3/api/json/JsonWriter;Lcom/apollographql/apollo3/api/CustomScalarAdapters;Lkotlinx/datetime/LocalDateTime;)V
 }
 
+public final class com/apollographql/apollo3/adapter/KotlinxLocalTimeAdapter : com/apollographql/apollo3/api/Adapter {
+	public static final field INSTANCE Lcom/apollographql/apollo3/adapter/KotlinxLocalTimeAdapter;
+	public synthetic fun fromJson (Lcom/apollographql/apollo3/api/json/JsonReader;Lcom/apollographql/apollo3/api/CustomScalarAdapters;)Ljava/lang/Object;
+	public fun fromJson (Lcom/apollographql/apollo3/api/json/JsonReader;Lcom/apollographql/apollo3/api/CustomScalarAdapters;)Lkotlinx/datetime/LocalTime;
+	public synthetic fun toJson (Lcom/apollographql/apollo3/api/json/JsonWriter;Lcom/apollographql/apollo3/api/CustomScalarAdapters;Ljava/lang/Object;)V
+	public fun toJson (Lcom/apollographql/apollo3/api/json/JsonWriter;Lcom/apollographql/apollo3/api/CustomScalarAdapters;Lkotlinx/datetime/LocalTime;)V
+}
+
diff --git a/libraries/apollo-adapters/src/commonMain/kotlin/com/apollographql/apollo3/adapter/KotlinxTimeAdapters.kt b/libraries/apollo-adapters/src/commonMain/kotlin/com/apollographql/apollo3/adapter/KotlinxTimeAdapters.kt
index 731d2dfbc4..41f75c5e45 100644
--- a/libraries/apollo-adapters/src/commonMain/kotlin/com/apollographql/apollo3/adapter/KotlinxTimeAdapters.kt
+++ b/libraries/apollo-adapters/src/commonMain/kotlin/com/apollographql/apollo3/adapter/KotlinxTimeAdapters.kt
@@ -7,6 +7,7 @@ import com.apollographql.apollo3.api.json.JsonWriter
 import kotlinx.datetime.Instant
 import kotlinx.datetime.LocalDate
 import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.LocalTime
 
 /**
  * An [Adapter] that converts an ISO 8601 String like "2010-06-01T22:19:44.475Z" to/from
@@ -55,3 +56,19 @@ object KotlinxLocalDateAdapter : Adapter<LocalDate> {
     writer.value(value.toString())
   }
 }
+
+/**
+ * An [Adapter] that converts an ISO 8601 String like "14:35:00" to/from
+ * a [kotlinx.datetime.LocalDate]
+ *
+ * It requires Android Gradle plugin 4.0 or newer and [core library desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring).
+ */
+object KotlinxLocalTimeAdapter : Adapter<LocalTime> {
+  override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): LocalTime {
+    return LocalTime.parse(reader.nextString()!!)
+  }
+
+  override fun toJson(writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, value: LocalTime) {
+    writer.value(value.toString())
+  }
+}
diff --git a/libraries/apollo-adapters/src/jvmMain/kotlin/com/apollographql/apollo3/adapter/JavaTimeAdapters.kt b/libraries/apollo-adapters/src/jvmMain/kotlin/com/apollographql/apollo3/adapter/JavaTimeAdapters.kt
index 476e317557..94531fdbb9 100644
--- a/libraries/apollo-adapters/src/jvmMain/kotlin/com/apollographql/apollo3/adapter/JavaTimeAdapters.kt
+++ b/libraries/apollo-adapters/src/jvmMain/kotlin/com/apollographql/apollo3/adapter/JavaTimeAdapters.kt
@@ -7,6 +7,7 @@ import com.apollographql.apollo3.api.json.JsonWriter
 import java.time.Instant
 import java.time.LocalDate
 import java.time.LocalDateTime
+import java.time.LocalTime
 import java.time.OffsetDateTime
 import java.time.format.DateTimeFormatter
 
@@ -85,3 +86,21 @@ object JavaOffsetDateTimeAdapter : Adapter<OffsetDateTime> {
     writer.value(value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
   }
 }
+
+/**
+ * An [Adapter] that converts a time to/from [java.time.LocalTime]
+ *
+ * Examples:
+ * - "14:35:00"
+ *
+ * It requires Android Gradle plugin 4.0 or newer and [core library desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring).
+ */
+object JavaLocalTimeAdapter : Adapter<LocalTime> {
+  override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): LocalTime {
+    return LocalTime.parse(reader.nextString())
+  }
+
+  override fun toJson(writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, value: LocalTime) {
+    writer.value(value.toString())
+  }
+}
diff --git a/libraries/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/InputValueValidationScope.kt b/libraries/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/InputValueValidationScope.kt
index c50929b4b4..4d18cedda9 100644
--- a/libraries/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/InputValueValidationScope.kt
+++ b/libraries/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/InputValueValidationScope.kt
@@ -70,6 +70,7 @@ internal fun ValidationScope.validateAndCoerceValue(
     is GQLNonNullType -> {
       return validateAndCoerceValue(value, expectedType.type, hasLocationDefaultValue, registerVariableUsage)
     }
+
     is GQLListType -> {
       val coercedValue = if (value !is GQLListValue) {
         /**
@@ -90,11 +91,13 @@ internal fun ValidationScope.validateAndCoerceValue(
           }
       )
     }
+
     is GQLNamedType -> {
       when (val expectedTypeDefinition = typeDefinitions[expectedType.name]) {
         is GQLInputObjectTypeDefinition -> {
           return validateAndCoerceInputObject(value, expectedTypeDefinition, registerVariableUsage)
         }
+
         is GQLScalarTypeDefinition -> {
           if (!expectedTypeDefinition.isBuiltIn()) {
             // custom scalar types are passed through
@@ -102,9 +105,11 @@ internal fun ValidationScope.validateAndCoerceValue(
           }
           return validateAndCoerceScalar(value, expectedType)
         }
+
         is GQLEnumTypeDefinition -> {
           return validateAndCoerceEnum(value, expectedTypeDefinition)
         }
+
         else -> {
           registerIssue("Value cannot be of non-input type ${expectedType.pretty()}", value.sourceLocation)
           return value
@@ -137,7 +142,10 @@ private fun ValidationScope.validateAndCoerceInputObject(
     ) {
       registerIssue(message = "No value passed for required inputField `${inputValueDefinition.name}`", sourceLocation = value.sourceLocation)
     }
-    if (inputValueDefinition.directives.findDeprecationReason() != null) {
+  }
+  value.fields.forEach { field ->
+    val inputValueDefinition = expectedTypeDefinition.inputFields.firstOrNull { it.name == field.name }
+    if (inputValueDefinition?.directives?.findDeprecationReason() != null) {
       issues.add(
           Issue.DeprecatedUsage(
               message = "Use of deprecated input field `${inputValueDefinition.name}`",
@@ -145,7 +153,6 @@ private fun ValidationScope.validateAndCoerceInputObject(
           )
       )
     }
-
   }
 
   return GQLObjectValue(fields = value.fields.mapNotNull { field ->
@@ -192,6 +199,7 @@ private fun ValidationScope.validateAndCoerceScalar(value: GQLValue, expectedTyp
       }
       value
     }
+
     "Float" -> {
       when (value) {
         is GQLFloatValue -> value
@@ -203,18 +211,21 @@ private fun ValidationScope.validateAndCoerceScalar(value: GQLValue, expectedTyp
         }
       }
     }
+
     "String" -> {
       if (value !is GQLStringValue) {
         registerIssue(value, expectedType)
       }
       value
     }
+
     "Boolean" -> {
       if (value !is GQLBooleanValue) {
         registerIssue(value, expectedType)
       }
       value
     }
+
     "ID" -> {
       // 3.5.5 ID can be either string or int
       if (value !is GQLStringValue && value !is GQLIntValue) {
@@ -222,6 +233,7 @@ private fun ValidationScope.validateAndCoerceScalar(value: GQLValue, expectedTyp
       }
       value
     }
+
     else -> {
       registerIssue(value, expectedType)
       value
diff --git a/libraries/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/TypeExtensionsMergeScope.kt b/libraries/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/TypeExtensionsMergeScope.kt
index 228e57c547..cb5d279f66 100644
--- a/libraries/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/TypeExtensionsMergeScope.kt
+++ b/libraries/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/TypeExtensionsMergeScope.kt
@@ -75,7 +75,7 @@ private fun ValidationScope.mergeEnumValues(
     val existing = result.firstOrNull { it.name == other.name }
     result += if (existing != null) {
       result.remove(existing)
-      other.copy(directives = mergeDirectives(existing.directives, other.directives))
+      existing.copy(directives = mergeDirectives(existing.directives, other.directives))
     } else {
       other
     }
diff --git a/libraries/apollo-compiler/api/apollo-compiler.api b/libraries/apollo-compiler/api/apollo-compiler.api
index 890d8e50cb..1de6d3f0eb 100644
--- a/libraries/apollo-compiler/api/apollo-compiler.api
+++ b/libraries/apollo-compiler/api/apollo-compiler.api
@@ -195,6 +195,9 @@ public final class com/apollographql/apollo3/compiler/OptionsKt {
 	public static final field ADD_TYPENAME_IF_ABSTRACT Ljava/lang/String;
 	public static final field ADD_TYPENAME_IF_FRAGMENTS Ljava/lang/String;
 	public static final field ADD_TYPENAME_IF_POLYMORPHIC Ljava/lang/String;
+	public static final field MANIFEST_NONE Ljava/lang/String;
+	public static final field MANIFEST_OPERATION_OUTPUT Ljava/lang/String;
+	public static final field MANIFEST_PERSISTED_QUERY Ljava/lang/String;
 	public static final field MODELS_COMPAT Ljava/lang/String;
 	public static final field MODELS_OPERATION_BASED Ljava/lang/String;
 	public static final field MODELS_OPERATION_BASED_WITH_INTERFACES Ljava/lang/String;
@@ -911,9 +914,10 @@ public final class com/apollographql/apollo3/compiler/introspection/Schema_to_in
 }
 
 public final class com/apollographql/apollo3/compiler/operationoutput/OperationDescriptor {
-	public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
+	public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
 	public final fun getName ()Ljava/lang/String;
 	public final fun getSource ()Ljava/lang/String;
+	public final fun getType ()Ljava/lang/String;
 }
 
 public final class com/apollographql/apollo3/compiler/operationoutput/OperationDescriptorJsonAdapter : com/squareup/moshi/JsonAdapter {
@@ -930,3 +934,40 @@ public final class com/apollographql/apollo3/compiler/operationoutput/OperationO
 	public static final fun toOperationOutput (Lokio/BufferedSource;)Ljava/util/Map;
 }
 
+public final class com/apollographql/apollo3/compiler/pqm/PersistedQueryManifest {
+	public fun <init> (Ljava/lang/String;ILjava/util/List;)V
+	public final fun getFormat ()Ljava/lang/String;
+	public final fun getOperations ()Ljava/util/List;
+	public final fun getVersion ()I
+}
+
+public final class com/apollographql/apollo3/compiler/pqm/PersistedQueryManifestJsonAdapter : com/squareup/moshi/JsonAdapter {
+	public fun <init> (Lcom/squareup/moshi/Moshi;)V
+	public fun fromJson (Lcom/squareup/moshi/JsonReader;)Lcom/apollographql/apollo3/compiler/pqm/PersistedQueryManifest;
+	public synthetic fun fromJson (Lcom/squareup/moshi/JsonReader;)Ljava/lang/Object;
+	public fun toJson (Lcom/squareup/moshi/JsonWriter;Lcom/apollographql/apollo3/compiler/pqm/PersistedQueryManifest;)V
+	public synthetic fun toJson (Lcom/squareup/moshi/JsonWriter;Ljava/lang/Object;)V
+	public fun toString ()Ljava/lang/String;
+}
+
+public final class com/apollographql/apollo3/compiler/pqm/PersistedQueryManifestKt {
+	public static final fun toPersistedQueryManifest (Ljava/util/Map;)Lcom/apollographql/apollo3/compiler/pqm/PersistedQueryManifest;
+}
+
+public final class com/apollographql/apollo3/compiler/pqm/PqmOperation {
+	public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+	public final fun getBody ()Ljava/lang/String;
+	public final fun getId ()Ljava/lang/String;
+	public final fun getName ()Ljava/lang/String;
+	public final fun getType ()Ljava/lang/String;
+}
+
+public final class com/apollographql/apollo3/compiler/pqm/PqmOperationJsonAdapter : com/squareup/moshi/JsonAdapter {
+	public fun <init> (Lcom/squareup/moshi/Moshi;)V
+	public fun fromJson (Lcom/squareup/moshi/JsonReader;)Lcom/apollographql/apollo3/compiler/pqm/PqmOperation;
+	public synthetic fun fromJson (Lcom/squareup/moshi/JsonReader;)Ljava/lang/Object;
+	public fun toJson (Lcom/squareup/moshi/JsonWriter;Lcom/apollographql/apollo3/compiler/pqm/PqmOperation;)V
+	public synthetic fun toJson (Lcom/squareup/moshi/JsonWriter;Ljava/lang/Object;)V
+	public fun toString ()Ljava/lang/String;
+}
+
diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/ApolloCompiler.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/ApolloCompiler.kt
index 9145cc7146..db19365ef4 100644
--- a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/ApolloCompiler.kt
+++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/ApolloCompiler.kt
@@ -19,6 +19,7 @@ import com.apollographql.apollo3.compiler.codegen.kotlin.KotlinCodeGen
 import com.apollographql.apollo3.compiler.ir.IrBuilder
 import com.apollographql.apollo3.compiler.ir.dumpTo
 import com.apollographql.apollo3.compiler.operationoutput.OperationDescriptor
+import com.apollographql.apollo3.compiler.pqm.toPersistedQueryManifest
 import okio.buffer
 import okio.source
 import java.io.File
@@ -172,7 +173,8 @@ object ApolloCompiler {
     val operationOutput = ir.operations.map {
       OperationDescriptor(
           name = it.name,
-          source = QueryDocumentMinifier.minify(it.sourceWithFragments)
+          source = QueryDocumentMinifier.minify(it.sourceWithFragments),
+          type = it.operationType.name.lowercase()
       )
     }.let {
       options.operationOutputGenerator.generate(it)
@@ -184,8 +186,18 @@ object ApolloCompiler {
       """.trimMargin()
     }
 
-    if (options.operationOutputFile != null) {
-      options.operationOutputFile.writeText(operationOutput.toJson("  "))
+    if (options.operationManifestFormat != MANIFEST_NONE) {
+      check(options.operationManifestFile != null) {
+        "Apollo: ${options.operationManifestFormat} requires a manifest file"
+      }
+    }
+    when (options.operationManifestFormat) {
+      MANIFEST_OPERATION_OUTPUT -> {
+        options.operationManifestFile!!.writeText(operationOutput.toJson("  "))
+      }
+      MANIFEST_PERSISTED_QUERY -> {
+        options.operationManifestFile!!.writeText(operationOutput.toPersistedQueryManifest().toJson("  "))
+      }
     }
 
     /**
diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/Options.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/Options.kt
index 4ac86c252c..5b908d8cba 100644
--- a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/Options.kt
+++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/Options.kt
@@ -31,6 +31,10 @@ const val ADD_TYPENAME_IF_POLYMORPHIC = "ifPolymorphic"
 const val ADD_TYPENAME_IF_ABSTRACT = "ifAbstract"
 const val ADD_TYPENAME_ALWAYS = "always"
 
+const val MANIFEST_OPERATION_OUTPUT = "operationOutput"
+const val MANIFEST_PERSISTED_QUERY = "persistedQueryManifest"
+const val MANIFEST_NONE = "none"
+
 enum class TargetLanguage {
   // The order is important. See [isTargetLanguageVersionAtLeast]
   JAVA,
@@ -129,7 +133,8 @@ class Options(
      * OperationOutput represents the modified operations as they are sent to the server. This is useful for whitelisting/
      * persisted queries
      */
-    val operationOutputFile: File? = null,
+    val operationManifestFile: File? = null,
+    val operationManifestFormat: String = MANIFEST_NONE,
     /**
      * The package name used as a base for input objects, fragments, enums and types
      */
@@ -365,7 +370,8 @@ class Options(
       outputDir: File = this.outputDir,
       testDir: File = this.testDir,
       debugDir: File? = this.debugDir,
-      operationOutputFile: File? = this.operationOutputFile,
+      operationOutputFile: File? = this.operationManifestFile,
+      operationManifestFormat: String = this.operationManifestFormat,
       executableFiles: Set<File> = this.executableFiles,
       schemaPackageName: String = this.schemaPackageName,
       useSchemaPackageNameForFragments: Boolean = this.useSchemaPackageNameForFragments,
@@ -407,7 +413,8 @@ class Options(
       schema = schema,
       outputDir = outputDir,
       debugDir = debugDir,
-      operationOutputFile = operationOutputFile,
+      operationManifestFile = operationOutputFile,
+      operationManifestFormat = operationManifestFormat,
       schemaPackageName = schemaPackageName,
       useSchemaPackageNameForFragments = useSchemaPackageNameForFragments,
       packageNameGenerator = packageNameGenerator,
diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/operationoutput/OperationOutput.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/operationoutput/OperationOutput.kt
index 0da4b847bc..d2d20ff44d 100644
--- a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/operationoutput/OperationOutput.kt
+++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/operationoutput/OperationOutput.kt
@@ -19,7 +19,8 @@ typealias OperationOutput = Map<String, OperationDescriptor>
 @JsonClass(generateAdapter = true)
 class OperationDescriptor(
     val name: String,
-    val source: String
+    val source: String,
+    val type: String
 )
 
 fun OperationOutput.findOperationId(name: String): String {
@@ -38,3 +39,4 @@ private fun operationOutputAdapter(): JsonAdapter<OperationOutput> {
   val type = Types.newParameterizedType(Map::class.java, String::class.java, OperationDescriptor::class.java)
   return MOSHI.adapter<OperationOutput>(type)
 }
+
diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/pqm/PersistedQueryManifest.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/pqm/PersistedQueryManifest.kt
new file mode 100644
index 0000000000..3b25c42db4
--- /dev/null
+++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/pqm/PersistedQueryManifest.kt
@@ -0,0 +1,34 @@
+package com.apollographql.apollo3.compiler.pqm
+
+import com.apollographql.apollo3.compiler.operationoutput.OperationOutput
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+class PersistedQueryManifest(
+    val format: String,
+    val version: Int,
+    val operations: List<PqmOperation>
+)
+
+@JsonClass(generateAdapter = true)
+class PqmOperation(
+    val id: String,
+    val body: String,
+    val name: String,
+    val type: String,
+)
+
+fun OperationOutput.toPersistedQueryManifest(): PersistedQueryManifest {
+  return PersistedQueryManifest(
+      format = "apollo-persisted-query-manifest",
+      version = 1,
+      operations = entries.map {
+        PqmOperation(
+            id = it.key,
+            body = it.value.source,
+            name = it.value.name,
+            type = it.value.type
+        )
+      }
+  )
+}
\ No newline at end of file
diff --git a/libraries/apollo-gradle-plugin-external/api/apollo-gradle-plugin-external.api b/libraries/apollo-gradle-plugin-external/api/apollo-gradle-plugin-external.api
index c0070b55db..9057248b3d 100644
--- a/libraries/apollo-gradle-plugin-external/api/apollo-gradle-plugin-external.api
+++ b/libraries/apollo-gradle-plugin-external/api/apollo-gradle-plugin-external.api
@@ -95,6 +95,8 @@ public abstract interface class com/apollographql/apollo3/gradle/api/Service {
 	public abstract fun getName ()Ljava/lang/String;
 	public abstract fun getNullableFieldStyle ()Lorg/gradle/api/provider/Property;
 	public abstract fun getOperationIdGenerator ()Lorg/gradle/api/provider/Property;
+	public abstract fun getOperationManifest ()Lorg/gradle/api/file/RegularFileProperty;
+	public abstract fun getOperationManifestFormat ()Lorg/gradle/api/provider/Property;
 	public abstract fun getOperationOutputFile ()Lorg/gradle/api/file/RegularFileProperty;
 	public abstract fun getOperationOutputGenerator ()Lorg/gradle/api/provider/Property;
 	public abstract fun getOutputDir ()Lorg/gradle/api/file/DirectoryProperty;
@@ -127,6 +129,7 @@ public abstract interface class com/apollographql/apollo3/gradle/api/Service {
 	public abstract fun mapScalarToKotlinLong (Ljava/lang/String;)V
 	public abstract fun mapScalarToKotlinString (Ljava/lang/String;)V
 	public abstract fun mapScalarToUpload (Ljava/lang/String;)V
+	public abstract fun operationManifestConnection (Lorg/gradle/api/Action;)V
 	public abstract fun operationOutputConnection (Lorg/gradle/api/Action;)V
 	public abstract fun outputDirConnection (Lorg/gradle/api/Action;)V
 	public abstract fun packageNamesFromFilePaths (Ljava/lang/String;)V
@@ -154,6 +157,12 @@ public abstract interface class com/apollographql/apollo3/gradle/api/Service$Dir
 	public abstract fun getTask ()Lorg/gradle/api/tasks/TaskProvider;
 }
 
+public final class com/apollographql/apollo3/gradle/api/Service$OperationManifestConnection {
+	public fun <init> (Lorg/gradle/api/tasks/TaskProvider;Lorg/gradle/api/provider/Provider;)V
+	public final fun getManifest ()Lorg/gradle/api/provider/Provider;
+	public final fun getTask ()Lorg/gradle/api/tasks/TaskProvider;
+}
+
 public final class com/apollographql/apollo3/gradle/api/Service$OperationOutputConnection {
 	public fun <init> (Lorg/gradle/api/tasks/TaskProvider;Lorg/gradle/api/provider/Provider;)V
 	public final fun getOperationOutputFile ()Lorg/gradle/api/provider/Provider;
diff --git a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/api/Service.kt b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/api/Service.kt
index d270eef7ef..e498b17bb5 100644
--- a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/api/Service.kt
+++ b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/api/Service.kt
@@ -541,7 +541,7 @@ interface Service {
    * If you want a [DirectoryProperty] that carries the task dependency, use [outputDirConnection]
    */
   val testDir: DirectoryProperty
-
+  
   /**
    * Whether to generate the operationOutput.json
    *
@@ -549,12 +549,70 @@ interface Service {
    */
   val generateOperationOutput: Property<Boolean>
 
+  /**
+   * The format to output for the operation manifest. Valid values are:
+   *
+   * - "operationOutput": a manifest that matches the format used by [OperationOutputGenerator]
+   * - "persistedQueryManifest": a manifest format for an upcoming GraphOS feature
+   * - nothing (Default): by default no manifest is generated
+   *
+   * "operationOutput" uses a JSON format like so:
+   * ```json
+   * {
+   *   "3f8a446ab7672c1efad3735b6fa86caaeefe7ec47f87fca9b84e71e0d93e6bea": {
+   *     "name": "DroidDetails",
+   *     "source": "query DroidDetails { species(id: \"c3BlY2llczoy\") { id name filmConnection { edges { node { id title } } } } }"
+   *   },
+   *   "e772cb55495ad5becc0c804ca3de7d5a5f31f145552bc33529f025d6cb0a8ce6": {
+   *     "name": "AllFilms",
+   *     "source": "query AllFilms { allFilms(first: 100) { totalCount films { title releaseDate } } }"
+   *   }
+   * }
+   * ```
+   *
+   * "persistedQueryManifest" uses a format compatible with an upcoming GraphQL feature like so:
+   * ```json
+   * {
+   *   "format": "apollo-persisted-query-manifest",
+   *   "version": 1,
+   *   "operations": [
+   *     {
+   *       "id": "dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f",
+   *       "body": "query UniversalQuery { __typename }",
+   *       "name": "UniversalQuery",
+   *       "type": "query"
+   *     },
+   *     {
+   *       "id": "f11e4dcb28788af2e41689bb366472084aa1aa1e1ba633c3d605279cff08ed59",
+   *       "body": "query FragmentedQuery { post { ...PostFragment } }  fragment PostFragment on Post { id title }",
+   *       "name": "FragmentedQuery",
+   *       "type": "query"
+   *     },
+   *     {
+   *       "id": "04649073787db6f24b495d49e5e87526734335a002edbd6e06e7315e302af5ac",
+   *       "body": "mutation SetNameMutation($name: String!) { setName($name) }",
+   *       "name": "SetNameMutation",
+   *       "type": "mutation"
+   *     }
+   *   ]
+   * }
+   * ```
+   *
+   */
+  val operationManifestFormat: Property<String>
+
   /**
    * The file where the operation output will be written. It's called [operationOutputFile] but this an "input" parameter for the compiler
-   * If you want a [RegularFileProperty] that carries the task dependency, use [operationOutputConnection]
+   * If you want a [RegularFileProperty] that carries the task dependency, use [operationManifestConnection]
    */
   val operationOutputFile: RegularFileProperty
 
+  /**
+   * The file where to write the operation manifest.
+   * If you want a [RegularFileProperty] that carries the task dependency, use [operationManifestConnection].
+   */
+  val operationManifest: RegularFileProperty
+
   /**
    * A debug directory where the compiler will output intermediary results
    */
@@ -709,6 +767,7 @@ interface Service {
    *
    */
   fun usedCoordinates(file: File)
+
   fun usedCoordinates(file: String)
 
   /**
@@ -735,6 +794,15 @@ interface Service {
    */
   fun operationOutputConnection(action: Action<in OperationOutputConnection>)
 
+  /**
+   * overrides the way the operation manifest is connected.
+   * Use this if you want to connect the generated operation manifest. For an example
+   * you can use this to send the modified queries to your backend for whitelisting
+   *
+   * By default, operation manifest is not connected
+   */
+  fun operationManifestConnection(action: Action<in OperationManifestConnection>)
+
   class OperationOutputConnection(
       /**
        * The task that produces operationOutput
@@ -750,6 +818,21 @@ interface Service {
       val operationOutputFile: Provider<RegularFile>,
   )
 
+  class OperationManifestConnection(
+      /**
+       * The task that produces operationOutput
+       */
+      val task: TaskProvider<out Task>,
+
+      /**
+       * A json file containing the operation manifest
+       *
+       * This file can be used to upload the queries exact content and their matching operation ID to a server for whitelisting
+       * or persisted queries. The specific format of the file depends on [operationManifestFormat]
+       */
+      val manifest: Provider<RegularFile>,
+  )
+
   /**
    * Overrides the way the generated models are connected.
    * Use this if you want to connect the generated models to another task than the default destination.
@@ -830,6 +913,7 @@ interface Service {
      */
     val outputDir: Provider<Directory>
 
+
     /**
      * The task that produces outputDir. Usually this is not needed as [outputDir] carries
      * task dependency.
diff --git a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloGenerateSourcesTask.kt b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloGenerateSourcesTask.kt
index f8cbae0928..135a60a0f7 100644
--- a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloGenerateSourcesTask.kt
+++ b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/ApolloGenerateSourcesTask.kt
@@ -68,8 +68,10 @@ import javax.inject.Inject
 @CacheableTask
 abstract class ApolloGenerateSourcesTask : DefaultTask() {
   @get:OutputFile
-  @get:Optional
-  abstract val operationOutputFile: RegularFileProperty
+  abstract val operationManifestFile: RegularFileProperty
+
+  @get:Input
+  abstract val operationManifestFormat: Property<String>
 
   @get:OutputFile
   @get:Optional
@@ -368,7 +370,8 @@ abstract class ApolloGenerateSourcesTask : DefaultTask() {
         testDir = testDir.asFile.get(),
         debugDir = debugDir.asFile.orNull,
         alwaysGenerateTypesMatching = alwaysGenerateTypesMatching,
-        operationOutputFile = operationOutputFile.asFile.orNull,
+        operationManifestFile = operationManifestFile.asFile.get(),
+        operationManifestFormat = operationManifestFormat.get(),
         operationOutputGenerator = operationOutputGenerator,
         useSemanticNaming = useSemanticNaming.getOrElse(defaultUseSemanticNaming),
         warnOnDeprecatedUsages = warnOnDeprecatedUsages.getOrElse(defaultWarnOnDeprecatedUsages),
diff --git a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/BuildDirLayout.kt b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/BuildDirLayout.kt
index bf7e7ea31e..cbf7b0f69d 100644
--- a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/BuildDirLayout.kt
+++ b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/BuildDirLayout.kt
@@ -1,16 +1,22 @@
 package com.apollographql.apollo3.gradle.internal
 
+import com.apollographql.apollo3.compiler.MANIFEST_OPERATION_OUTPUT
 import com.apollographql.apollo3.gradle.api.Service
 import org.gradle.api.Project
 import org.gradle.api.file.Directory
 import org.gradle.api.file.RegularFile
 import org.gradle.api.provider.Provider
+import java.io.File
 
 object BuildDirLayout {
-  internal fun operationOutput(project: Project, service: Service): Provider<RegularFile> {
+  internal fun operationManifest(project: Project, service: Service, format: String): File {
+    val dir = when (format) {
+      MANIFEST_OPERATION_OUTPUT -> "operationOutput"
+      else -> "manifest"
+    }
     return project.layout.buildDirectory.file(
-        "generated/operationOutput/apollo/${service.name}/operationOutput.json"
-    )
+        "generated/$dir/apollo/${service.name}/$format.json"
+    ).get().asFile
   }
 
   internal fun metadata(project: Project, service: Service): Provider<RegularFile> {
diff --git a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultApolloExtension.kt b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultApolloExtension.kt
index 3fd03d95a8..9ff8c0a3ff 100644
--- a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultApolloExtension.kt
+++ b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultApolloExtension.kt
@@ -402,13 +402,23 @@ abstract class DefaultApolloExtension(
       it.finalizedBy(checkApolloDuplicates)
     }
 
+    check(service.operationOutputAction == null || service.operationManifestAction == null) {
+      "Apollo: it is an error to set both operationOutputAction and operationManifestAction. Remove operationOutputAction"
+    }
     if (service.operationOutputAction != null) {
       val operationOutputConnection = Service.OperationOutputConnection(
           task = codegenTaskProvider,
-          operationOutputFile = codegenTaskProvider.flatMap { it.operationOutputFile }
+          operationOutputFile = codegenTaskProvider.flatMap { it.operationManifestFile }
       )
       service.operationOutputAction!!.execute(operationOutputConnection)
     }
+    if (service.operationManifestAction != null) {
+      val operationManifestConnection = Service.OperationManifestConnection(
+          task = codegenTaskProvider,
+          manifest = codegenTaskProvider.flatMap { it.operationManifestFile }
+      )
+      service.operationManifestAction!!.execute(operationManifestConnection)
+    }
 
     if (service.outputDirAction == null) {
       service.outputDirAction = defaultOutputDirAction
@@ -542,7 +552,7 @@ abstract class DefaultApolloExtension(
         task.graph.set(registerOperationsConfig.graph)
         task.graphVariant.set(registerOperationsConfig.graphVariant)
         task.key.set(registerOperationsConfig.key)
-        task.operationOutput.set(codegenProvider.flatMap { it.operationOutputFile })
+        task.operationOutput.set(codegenProvider.flatMap { it.operationManifestFile })
       }
     }
   }
@@ -737,12 +747,9 @@ abstract class DefaultApolloExtension(
         set(service.debugDir)
         disallowChanges()
       }
-      if (service.generateOperationOutput.getOrElse(false)) {
-        task.operationOutputFile.apply {
-          set(service.operationOutputFile.orElse(BuildDirLayout.operationOutput(project, service)))
-          disallowChanges()
-        }
-      }
+      task.operationManifestFile.set(service.operationManifestFile())
+      task.operationManifestFormat.set(service.operationManifestFormat())
+
       if (shouldGenerateMetadata(service)) {
         task.metadataOutputFile.apply {
           set(BuildDirLayout.metadata(project, service))
diff --git a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultService.kt b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultService.kt
index 9fa95df83c..2e21f7160b 100644
--- a/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultService.kt
+++ b/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultService.kt
@@ -1,5 +1,8 @@
 package com.apollographql.apollo3.gradle.internal
 
+import com.apollographql.apollo3.compiler.MANIFEST_NONE
+import com.apollographql.apollo3.compiler.MANIFEST_OPERATION_OUTPUT
+import com.apollographql.apollo3.compiler.MANIFEST_PERSISTED_QUERY
 import com.apollographql.apollo3.compiler.MODELS_COMPAT
 import com.apollographql.apollo3.compiler.PackageNameGenerator
 import com.apollographql.apollo3.compiler.Roots
@@ -9,6 +12,8 @@ import com.apollographql.apollo3.gradle.api.Registry
 import com.apollographql.apollo3.gradle.api.Service
 import org.gradle.api.Action
 import org.gradle.api.Project
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Provider
 import org.gradle.util.GradleVersion
 import java.io.File
 import javax.inject.Inject
@@ -91,7 +96,12 @@ abstract class DefaultService @Inject constructor(val project: Project, override
   var registerOperationsConfig: DefaultRegisterOperationsConfig? = null
 
   override fun registerOperations(configure: Action<in RegisterOperationsConfig>) {
-    generateOperationOutput.set(true)
+    val existing = operationManifestFormat.orNull
+    check(existing == null || existing == MANIFEST_OPERATION_OUTPUT) {
+      "Apollo: registerOperation {} requires $MANIFEST_OPERATION_OUTPUT (found $existing)"
+    }
+    operationManifestFormat.set(MANIFEST_OPERATION_OUTPUT)
+    operationManifestFormat.finalizeValue()
 
     val registerOperationsConfig = objects.newInstance(DefaultRegisterOperationsConfig::class.java)
 
@@ -105,11 +115,16 @@ abstract class DefaultService @Inject constructor(val project: Project, override
   }
 
   var operationOutputAction: Action<in Service.OperationOutputConnection>? = null
+  var operationManifestAction: Action<in Service.OperationManifestConnection>? = null
 
   override fun operationOutputConnection(action: Action<in Service.OperationOutputConnection>) {
     this.operationOutputAction = action
   }
 
+  override fun operationManifestConnection(action: Action<in Service.OperationManifestConnection>) {
+    this.operationManifestAction = action
+  }
+
   var outputDirAction: Action<in Service.DirectoryConnection>? = null
   var testDirAction: Action<in Service.DirectoryConnection>? = null
 
@@ -193,4 +208,73 @@ abstract class DefaultService @Inject constructor(val project: Project, override
   override fun mapScalarToJavaObject(graphQLName: String) = mapScalar(graphQLName, "java.lang.Object", "com.apollographql.apollo3.api.Adapters.AnyAdapter")
 
   override fun mapScalarToUpload(graphQLName: String) = mapScalar(graphQLName, "com.apollographql.apollo3.api.Upload", "com.apollographql.apollo3.api.UploadAdapter")
+
+  /**
+   * Resolves the operation manifest and formats.
+   */
+  @Suppress("DEPRECATION")
+  private fun resolveOperationManifest(): Pair<String, File> {
+    generateOperationOutput.disallowChanges()
+    operationOutputFile.disallowChanges()
+    operationManifest.disallowChanges()
+    operationManifestFormat.disallowChanges()
+
+    var format = operationManifestFormat.orNull
+    if (format == null) {
+      if (generateOperationOutput.orElse(false).get()) {
+        format = MANIFEST_OPERATION_OUTPUT
+      }
+    } else {
+      when (format) {
+        MANIFEST_NONE,
+        MANIFEST_OPERATION_OUTPUT,
+        MANIFEST_PERSISTED_QUERY,
+        -> Unit
+
+        else -> {
+          error("Apollo: unknown operation manifest format: $format")
+        }
+      }
+      check(!generateOperationOutput.isPresent) {
+        "Apollo: it is an error to set both `generateOperationOutput` and `operationManifestFormat`. Remove `generateOperationOutput`"
+      }
+    }
+    var userFile = operationManifest.orNull?.asFile
+    if (userFile == null) {
+      userFile = operationOutputFile.orNull?.asFile
+    } else {
+      check(!operationOutputFile.isPresent) {
+        "Apollo: it is an error to set both `operationManifest` and `operationOutputFile`. Remove `operationOutputFile`"
+      }
+    }
+
+    if (userFile != null) {
+      if (format == null) {
+        format = MANIFEST_OPERATION_OUTPUT
+      }
+    } else {
+      userFile = BuildDirLayout.operationManifest(project, this, format ?: MANIFEST_OPERATION_OUTPUT)
+    }
+
+    if (format == null) {
+      format = MANIFEST_NONE
+    }
+    return format to userFile
+  }
+
+  fun operationManifestFile(): RegularFileProperty {
+    return project.provider {
+      resolveOperationManifest().second
+    }.let { fileProvider ->
+      project.objects.fileProperty().value {
+        fileProvider.get()
+      }
+    }
+  }
+
+  fun operationManifestFormat(): Provider<String> {
+    return project.provider {
+      resolveOperationManifest().first
+    }
+  }
 }
diff --git a/libraries/apollo-normalized-cache-sqlite-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCache.kt b/libraries/apollo-normalized-cache-sqlite-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCache.kt
index 23d4651256..31c86fdf9c 100644
--- a/libraries/apollo-normalized-cache-sqlite-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCache.kt
+++ b/libraries/apollo-normalized-cache-sqlite-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCache.kt
@@ -47,7 +47,9 @@ class SqlNormalizedCache internal constructor(
         recordDatabase.delete(record.key)
       }
     }
-    return records
+    val missRecordKeys = keys - records.map { it.key }.toSet()
+    val missRecords = missRecordKeys.ifEmpty { null }?.let { nextCache?.loadRecords(it, cacheHeaders) }.orEmpty()
+    return records + missRecords
   }
 
   override fun clearAll() {
@@ -56,12 +58,14 @@ class SqlNormalizedCache internal constructor(
   }
 
   override fun remove(cacheKey: CacheKey, cascade: Boolean): Boolean {
-    return recordDatabase.transaction {
+    val selfRemoved = recordDatabase.transaction {
       internalDeleteRecord(
           key = cacheKey.key,
           cascade = cascade,
       )
     }
+    val chainRemoved = nextCache?.remove(cacheKey, cascade) ?: false
+    return selfRemoved || chainRemoved
   }
 
   override fun remove(pattern: String): Int {
@@ -93,7 +97,7 @@ class SqlNormalizedCache internal constructor(
       return emptySet()
     }
     return try {
-      internalUpdateRecord(record = record, recordMerger = recordMerger, date = cacheHeaders.date())
+      internalUpdateRecord(record = record, recordMerger = recordMerger, date = cacheHeaders.date()) + nextCache?.merge(record, cacheHeaders).orEmpty()
     } catch (e: Exception) {
       // Unable to merge the record in the database, it is possibly corrupted - treat this as a cache miss
       apolloExceptionHandler(Exception("Unable to merge a record from the database", e))
@@ -107,7 +111,7 @@ class SqlNormalizedCache internal constructor(
       return emptySet()
     }
     return try {
-      internalUpdateRecords(records = records, recordMerger = recordMerger, date = cacheHeaders.date())
+      internalUpdateRecords(records = records, recordMerger = recordMerger, date = cacheHeaders.date()) + nextCache?.merge(records, cacheHeaders).orEmpty()
     } catch (e: Exception) {
       // Unable to merge the records in the database, it is possibly corrupted - treat this as a cache miss
       apolloExceptionHandler(Exception("Unable to merge records from the database", e))
@@ -125,20 +129,18 @@ class SqlNormalizedCache internal constructor(
    * Assume an enclosing transaction
    */
   private fun internalDeleteRecord(key: String, cascade: Boolean): Boolean {
-    return if (cascade) {
+    if (cascade) {
       recordDatabase.select(key)
           ?.referencedFields()
-          ?.all {
+          ?.forEach {
             internalDeleteRecord(
                 key = it.key,
                 cascade = true,
             )
           }
-          ?: false
-    } else {
-      recordDatabase.delete(key)
-      recordDatabase.changes() > 0
     }
+    recordDatabase.delete(key)
+    return recordDatabase.changes() > 0
   }
 
   /**
diff --git a/libraries/apollo-normalized-cache-sqlite/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCache.kt b/libraries/apollo-normalized-cache-sqlite/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCache.kt
index a90d798978..614d2fdfd1 100644
--- a/libraries/apollo-normalized-cache-sqlite/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCache.kt
+++ b/libraries/apollo-normalized-cache-sqlite/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCache.kt
@@ -44,7 +44,9 @@ class SqlNormalizedCache internal constructor(
         recordDatabase.delete(record.key)
       }
     }
-    return records
+    val missRecordKeys = keys - records.map { it.key }.toSet()
+    val missRecords = missRecordKeys.ifEmpty { null }?.let { nextCache?.loadRecords(it, cacheHeaders) }.orEmpty()
+    return records + missRecords
   }
 
   override fun clearAll() {
@@ -53,12 +55,14 @@ class SqlNormalizedCache internal constructor(
   }
 
   override fun remove(cacheKey: CacheKey, cascade: Boolean): Boolean {
-    return recordDatabase.transaction {
+    val selfRemoved = recordDatabase.transaction {
       internalDeleteRecord(
           key = cacheKey.key,
           cascade = cascade,
       )
     }
+    val chainRemoved = nextCache?.remove(cacheKey, cascade) ?: false
+    return selfRemoved || chainRemoved
   }
 
   override fun remove(pattern: String): Int {
@@ -77,7 +81,7 @@ class SqlNormalizedCache internal constructor(
       return emptySet()
     }
     return try {
-      internalUpdateRecords(records = records, cacheHeaders.date())
+      internalUpdateRecords(records = records, cacheHeaders.date()) + nextCache?.merge(records, cacheHeaders).orEmpty()
     } catch (e: Exception) {
       // Unable to merge the records in the database, it is possibly corrupted - treat this as a cache miss
       apolloExceptionHandler(Exception("Unable to merge records from the database", e))
@@ -94,7 +98,7 @@ class SqlNormalizedCache internal constructor(
       return emptySet()
     }
     return try {
-      internalUpdateRecord(record, cacheHeaders.date())
+      internalUpdateRecord(record, cacheHeaders.date()) + nextCache?.merge(record, cacheHeaders).orEmpty()
     } catch (e: Exception) {
       // Unable to merge the record in the database, it is possibly corrupted - treat this as a cache miss
       apolloExceptionHandler(Exception("Unable to merge a record from the database", e))
@@ -112,20 +116,18 @@ class SqlNormalizedCache internal constructor(
    * Assume an enclosing transaction
    */
   private fun internalDeleteRecord(key: String, cascade: Boolean): Boolean {
-    return if (cascade) {
+    if (cascade) {
       recordDatabase.select(key)
           ?.referencedFields()
-          ?.all {
+          ?.forEach {
             internalDeleteRecord(
                 key = it.key,
                 cascade = true,
             )
           }
-          ?: false
-    } else {
-      recordDatabase.delete(key)
-      recordDatabase.changes() > 0
     }
+    recordDatabase.delete(key)
+    return recordDatabase.changes() > 0
   }
 
   /**
@@ -169,6 +171,7 @@ class SqlNormalizedCache internal constructor(
     )
 
   }
+
   /**
    * Update a single [Record], loading the previous one
    */
diff --git a/libraries/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/ClientCacheExtensions.kt b/libraries/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/ClientCacheExtensions.kt
index 21af231ba6..260ddcfcdf 100644
--- a/libraries/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/ClientCacheExtensions.kt
+++ b/libraries/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/ClientCacheExtensions.kt
@@ -125,8 +125,8 @@ fun ApolloClient.Builder.store(store: ApolloStore, writeToCacheAsynchronously: B
 }
 
 /**
- * Gets the result from the network, then observes the cache for any changes.
- * [fetchPolicy] will control how the result is first queried, while [refetchPolicy] will control the subsequent fetches.
+ * Gets initial response(s) then observes the cache for any changes.
+ * [fetchPolicy] controls how the result is first queried, while [refetchPolicy] will control the subsequent fetches.
  * Network and cache exceptions are ignored by default, this can be changed by setting [fetchThrows] for the first fetch and [refetchThrows]
  * for subsequent fetches (non Apollo exceptions like `OutOfMemoryError` are always propagated).
  *
diff --git a/libraries/apollo-runtime/api/apollo-runtime.api b/libraries/apollo-runtime/api/apollo-runtime.api
index 44c09434f0..59fec7bdb5 100644
--- a/libraries/apollo-runtime/api/apollo-runtime.api
+++ b/libraries/apollo-runtime/api/apollo-runtime.api
@@ -130,6 +130,7 @@ public final class com/apollographql/apollo3/ApolloClient$Builder : com/apollogr
 	public final fun webSocketReconnectWhen (Lkotlin/jvm/functions/Function1;)Lcom/apollographql/apollo3/ApolloClient$Builder;
 	public final fun webSocketReopenWhen (Lkotlin/jvm/functions/Function3;)Lcom/apollographql/apollo3/ApolloClient$Builder;
 	public final fun webSocketServerUrl (Ljava/lang/String;)Lcom/apollographql/apollo3/ApolloClient$Builder;
+	public final fun webSocketServerUrl (Lkotlin/jvm/functions/Function1;)Lcom/apollographql/apollo3/ApolloClient$Builder;
 	public final fun wsProtocol (Lcom/apollographql/apollo3/network/ws/WsProtocol$Factory;)Lcom/apollographql/apollo3/ApolloClient$Builder;
 }
 
@@ -365,6 +366,8 @@ public abstract interface class com/apollographql/apollo3/network/http/TokenProv
 
 public final class com/apollographql/apollo3/network/ws/AppSyncWsProtocol : com/apollographql/apollo3/network/ws/WsProtocol {
 	public static final field Companion Lcom/apollographql/apollo3/network/ws/AppSyncWsProtocol$Companion;
+	public fun <init> (JLkotlin/jvm/functions/Function1;Lcom/apollographql/apollo3/network/ws/WebSocketConnection;Lcom/apollographql/apollo3/network/ws/WsProtocol$Listener;)V
+	public synthetic fun <init> (JLkotlin/jvm/functions/Function1;Lcom/apollographql/apollo3/network/ws/WebSocketConnection;Lcom/apollographql/apollo3/network/ws/WsProtocol$Listener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public fun <init> (Ljava/util/Map;JLcom/apollographql/apollo3/network/ws/WebSocketConnection;Lcom/apollographql/apollo3/network/ws/WsProtocol$Listener;)V
 	public fun connectionInit (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public fun handleServerMessage (Ljava/util/Map;)V
@@ -378,6 +381,9 @@ public final class com/apollographql/apollo3/network/ws/AppSyncWsProtocol$Compan
 }
 
 public final class com/apollographql/apollo3/network/ws/AppSyncWsProtocol$Factory : com/apollographql/apollo3/network/ws/WsProtocol$Factory {
+	public fun <init> ()V
+	public fun <init> (JLkotlin/jvm/functions/Function1;)V
+	public synthetic fun <init> (JLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public fun <init> (Ljava/util/Map;J)V
 	public synthetic fun <init> (Ljava/util/Map;JILkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public fun create (Lcom/apollographql/apollo3/network/ws/WebSocketConnection;Lcom/apollographql/apollo3/network/ws/WsProtocol$Listener;Lkotlinx/coroutines/CoroutineScope;)Lcom/apollographql/apollo3/network/ws/WsProtocol;
@@ -466,7 +472,7 @@ public final class com/apollographql/apollo3/network/ws/WebSocketEngineKt {
 }
 
 public final class com/apollographql/apollo3/network/ws/WebSocketNetworkTransport : com/apollographql/apollo3/network/NetworkTransport {
-	public synthetic fun <init> (Ljava/lang/String;Ljava/util/List;Lcom/apollographql/apollo3/network/ws/WebSocketEngine;JLcom/apollographql/apollo3/network/ws/WsProtocol$Factory;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public synthetic fun <init> (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lcom/apollographql/apollo3/network/ws/WebSocketEngine;JLcom/apollographql/apollo3/network/ws/WsProtocol$Factory;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public final fun closeConnection (Ljava/lang/Throwable;)V
 	public fun dispose ()V
 	public fun execute (Lcom/apollographql/apollo3/api/ApolloRequest;)Lkotlinx/coroutines/flow/Flow;
@@ -484,6 +490,7 @@ public final class com/apollographql/apollo3/network/ws/WebSocketNetworkTranspor
 	public final fun reconnectWhen (Lkotlin/jvm/functions/Function1;)Lcom/apollographql/apollo3/network/ws/WebSocketNetworkTransport$Builder;
 	public final fun reopenWhen (Lkotlin/jvm/functions/Function3;)Lcom/apollographql/apollo3/network/ws/WebSocketNetworkTransport$Builder;
 	public final fun serverUrl (Ljava/lang/String;)Lcom/apollographql/apollo3/network/ws/WebSocketNetworkTransport$Builder;
+	public final fun serverUrl (Lkotlin/jvm/functions/Function1;)Lcom/apollographql/apollo3/network/ws/WebSocketNetworkTransport$Builder;
 	public final fun webSocketEngine (Lcom/apollographql/apollo3/network/ws/WebSocketEngine;)Lcom/apollographql/apollo3/network/ws/WebSocketNetworkTransport$Builder;
 }
 
diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/ApolloClient.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/ApolloClient.kt
index cbe0f94f84..0a110c4175 100644
--- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/ApolloClient.kt
+++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/ApolloClient.kt
@@ -213,6 +213,7 @@ private constructor(
     private var httpExposeErrorBody: Boolean? = null
     private var webSocketEngine: WebSocketEngine? = null
     private var webSocketReopenWhen: (suspend (Throwable, attempt: Long) -> Boolean)? = null
+    private var webSocketReopenServerUrl: (suspend () -> String)? = null
 
     init {
       failOnNativeIfLegacyMemoryManager()
@@ -310,6 +311,7 @@ private constructor(
 
     /**
      * The url of the GraphQL server used for WebSockets
+     * Use this function or webSocketServerUrl((suspend () -> String)) but not both.
      *
      * See also [subscriptionNetworkTransport] for more customization
      */
@@ -317,6 +319,20 @@ private constructor(
       this.webSocketServerUrl = webSocketServerUrl
     }
 
+    /**
+     * Configure dynamically the url of the GraphQL server used for WebSockets.
+     * Use this function or webSocketServerUrl(String) but not both.
+     *
+     * @param webSocketServerUrl a function returning the new server URL.
+     * This function will be called every time a WebSocket is opened. For example, you can use it to update your
+     * auth credentials in case of an unauthorized error.
+     *
+     * It is a suspending function, so it can be used to introduce delay before setting the new server URL.
+     */
+    fun webSocketServerUrl(webSocketServerUrl: (suspend () -> String)) = apply {
+      this.webSocketReopenServerUrl = webSocketServerUrl
+    }
+
     /**
      * The timeout after which an inactive WebSocket will be closed
      *
@@ -557,6 +573,9 @@ private constructor(
         check(webSocketReopenWhen == null) {
           "Apollo: 'webSocketReopenWhen' has no effect if 'subscriptionNetworkTransport' is set"
         }
+        check(webSocketReopenServerUrl == null) {
+          "Apollo: 'webSocketReopenServerUrl' has no effect if 'subscriptionNetworkTransport' is set"
+        }
         subscriptionNetworkTransport!!
       } else {
         val url = webSocketServerUrl ?: httpServerUrl
@@ -580,6 +599,9 @@ private constructor(
                 if (webSocketReopenWhen != null) {
                   reopenWhen(webSocketReopenWhen)
                 }
+                if (webSocketReopenServerUrl != null) {
+                  serverUrl(webSocketReopenServerUrl)
+                }
               }
               .build()
         }
@@ -626,6 +648,7 @@ private constructor(
       }
       subscriptionNetworkTransport?.let { builder.subscriptionNetworkTransport(it) }
       webSocketServerUrl?.let { builder.webSocketServerUrl(it) }
+      webSocketReopenServerUrl?.let { builder.webSocketServerUrl(it) }
       webSocketEngine?.let { builder.webSocketEngine(it) }
       webSocketReopenWhen?.let { builder.webSocketReopenWhen(it) }
       webSocketIdleTimeoutMillis?.let { builder.webSocketIdleTimeoutMillis(it) }
diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/AppSyncWsProtocol.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/AppSyncWsProtocol.kt
index ab502f858e..9940f6b619 100644
--- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/AppSyncWsProtocol.kt
+++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/AppSyncWsProtocol.kt
@@ -17,11 +17,19 @@ import kotlinx.coroutines.withTimeout
  * A [WsProtocol] for https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html
  */
 class AppSyncWsProtocol(
-    private val authorization: Map<String, Any?>,
     private val connectionAcknowledgeTimeoutMs: Long,
+    private val connectionPayload: suspend () -> Map<String, Any?>? = { null },
     webSocketConnection: WebSocketConnection,
     listener: Listener,
 ) : WsProtocol(webSocketConnection, listener) {
+  constructor(
+      authorization: Map<String, Any?>,
+      connectionAcknowledgeTimeoutMs: Long,
+      webSocketConnection: WebSocketConnection,
+      listener: Listener
+  ) : this(connectionAcknowledgeTimeoutMs, { authorization }, webSocketConnection, listener)
+    
+  private var authorization: Map<String, Any?>? = null
 
   override suspend fun connectionInit() {
     val message = mutableMapOf<String, Any?>(
@@ -29,6 +37,8 @@ class AppSyncWsProtocol(
     )
 
     sendMessageMapText(message)
+    
+    authorization = connectionPayload()
 
     withTimeout(connectionAcknowledgeTimeoutMs) {
       val map = receiveMessageMap()
@@ -86,7 +96,7 @@ class AppSyncWsProtocol(
   }
 
   /**
-   * @param authorization: a map containing the authorization information. For an example:
+   * @param connectionPayload: a function generating a map containing the authorization information. For an example:
    * ```
    *  mapOf(
    *     "host" to "example1234567890000.appsync-api.us-east-1.amazonaws.com",
@@ -96,9 +106,14 @@ class AppSyncWsProtocol(
    *
    */
   class Factory(
-      private val authorization: Map<String, Any?>,
       private val connectionAcknowledgeTimeoutMs: Long = 10_000,
+      private val connectionPayload: suspend () -> Map<String, Any?>? = { null },
   ) : WsProtocol.Factory {
+    constructor(
+        authorization: Map<String, Any?>,
+        connectionAcknowledgeTimeoutMs: Long = 10_000,
+    ) : this(connectionAcknowledgeTimeoutMs, { authorization })
+        
     override val name: String
       get() = "graphql-ws"
 
@@ -108,7 +123,7 @@ class AppSyncWsProtocol(
         scope: CoroutineScope
     ): WsProtocol {
       return AppSyncWsProtocol(
-          authorization = authorization,
+          connectionPayload = connectionPayload,
           webSocketConnection = webSocketConnection,
           connectionAcknowledgeTimeoutMs = connectionAcknowledgeTimeoutMs,
           listener = listener
diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/WebSocketNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/WebSocketNetworkTransport.kt
index 683d8b2105..6a61e1d6ba 100644
--- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/WebSocketNetworkTransport.kt
+++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/WebSocketNetworkTransport.kt
@@ -18,6 +18,7 @@ import com.apollographql.apollo3.internal.isDeferred
 import com.apollographql.apollo3.internal.transformWhile
 import com.apollographql.apollo3.network.NetworkTransport
 import com.apollographql.apollo3.network.ws.internal.Command
+import com.apollographql.apollo3.network.ws.internal.ConnectionReEstablished
 import com.apollographql.apollo3.network.ws.internal.Dispose
 import com.apollographql.apollo3.network.ws.internal.Event
 import com.apollographql.apollo3.network.ws.internal.GeneralError
@@ -26,6 +27,7 @@ import com.apollographql.apollo3.network.ws.internal.NetworkError
 import com.apollographql.apollo3.network.ws.internal.OperationComplete
 import com.apollographql.apollo3.network.ws.internal.OperationError
 import com.apollographql.apollo3.network.ws.internal.OperationResponse
+import com.apollographql.apollo3.network.ws.internal.RestartConnection
 import com.apollographql.apollo3.network.ws.internal.StartOperation
 import com.apollographql.apollo3.network.ws.internal.StopOperation
 import com.benasher44.uuid.Uuid
@@ -58,12 +60,12 @@ import okio.use
  */
 class WebSocketNetworkTransport
 private constructor(
-    private val serverUrl: String,
+    private val serverUrl: (suspend () -> String),
     private val headers: List<HttpHeader>,
     private val webSocketEngine: WebSocketEngine = DefaultWebSocketEngine(),
     private val idleTimeoutMillis: Long = 60_000,
     private val protocolFactory: WsProtocol.Factory = SubscriptionWsProtocol.Factory(),
-    private val reopenWhen: (suspend (Throwable, attempt: Long) -> Boolean)?,
+    private val reopenWhen: (suspend (Throwable, attempt: Long) -> Boolean)?
 ) : NetworkTransport {
 
   /**
@@ -156,16 +158,19 @@ private constructor(
 
             if (reopenWhen?.invoke(message.cause, reopenAttemptCount) == true) {
               reopenAttemptCount++
-              activeMessages.values.forEach {
-                // Re-queue all start messages
-                // This will restart the websocket
-                messages.trySend(it)
-              }
+              messages.send(RestartConnection)
             } else {
               reopenAttemptCount = 0L
               // forward the NetworkError downstream. Active flows will throw
               mutableEvents.tryEmit(message)
             }
+          } else if (message is ConnectionReEstablished) {
+            reopenAttemptCount = 0L
+            activeMessages.values.forEach {
+              // Re-queue all start messages
+              // This will restart the websocket
+              messages.trySend(it)
+            }
           } else {
             reopenAttemptCount = 0L
             mutableEvents.tryEmit(message)
@@ -187,7 +192,7 @@ private constructor(
 
             val webSocketConnection = try {
               webSocketEngine.open(
-                  url = serverUrl,
+                  url = serverUrl(),
                   headers = if (headers.any { it.name == "Sec-WebSocket-Protocol" }) {
                     headers
                   } else {
@@ -234,6 +239,11 @@ private constructor(
               activeMessages.remove(message.request.requestUuid)
               protocol!!.stopOperation(message.request)
             }
+
+            is RestartConnection -> {
+              messages.send(ConnectionReEstablished())
+            }
+
             else -> {
               // Other cases have been handled above
             }
@@ -267,6 +277,12 @@ private constructor(
         is OperationComplete -> {
           false
         }
+
+        is ConnectionReEstablished -> {
+          // means we are in the process of restarting the connection
+          false
+        }
+
         is NetworkError -> {
           emit(it)
           false
@@ -309,7 +325,7 @@ private constructor(
         is NetworkError -> throw ApolloNetworkException("Network error while executing ${request.operation.name()}", response.cause)
 
         // Cannot happen as these events are filtered out upstream
-        is OperationComplete, is GeneralError -> error("Unexpected event $response")
+        is ConnectionReEstablished, is OperationComplete, is GeneralError -> error("Unexpected event $response")
       }
     }.filterNot {
       deferredJsonMerger.isEmptyPayload
@@ -335,7 +351,7 @@ private constructor(
   }
 
   class Builder {
-    private var serverUrl: String? = null
+    private var serverUrl: (suspend () -> String)? = null
     private var headers: MutableList<HttpHeader> = mutableListOf()
     private var webSocketEngine: WebSocketEngine? = null
     private var idleTimeoutMillis: Long? = null
@@ -343,6 +359,19 @@ private constructor(
     private var reopenWhen: (suspend (Throwable, attempt: Long) -> Boolean)? = null
 
     fun serverUrl(serverUrl: String) = apply {
+      this.serverUrl = { serverUrl }
+    }
+
+    /**
+     * Configure the server URL dynamically.
+     *
+     * @param serverUrl a function returning the new server URL.
+     * This function will be called every time a WebSocket is opened. For example, you can use it to update your
+     * auth credentials in case of an unauthorized error.
+     *
+     * It is a suspending function, so it can be used to introduce delay before setting the new serverUrl.
+     */
+    fun serverUrl(serverUrl: (suspend () -> String)?) = apply {
       this.serverUrl = serverUrl
     }
 
@@ -396,14 +425,13 @@ private constructor(
     }
 
     fun build(): WebSocketNetworkTransport {
-      @Suppress("DEPRECATION")
       return WebSocketNetworkTransport(
-          serverUrl ?: error("No serverUrl specified"),
-          headers,
-          webSocketEngine ?: DefaultWebSocketEngine(),
-          idleTimeoutMillis ?: 60_000,
-          protocolFactory ?: SubscriptionWsProtocol.Factory(),
-          reopenWhen
+          serverUrl = serverUrl ?: error("No serverUrl specified"),
+          headers = headers,
+          webSocketEngine = webSocketEngine ?: DefaultWebSocketEngine(),
+          idleTimeoutMillis = idleTimeoutMillis ?: 60_000,
+          protocolFactory = protocolFactory ?: SubscriptionWsProtocol.Factory(),
+          reopenWhen = reopenWhen
       )
     }
   }
diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/internal/WsMessage.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/internal/WsMessage.kt
index c41026e6e0..ab3cd44653 100644
--- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/internal/WsMessage.kt
+++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/ws/internal/WsMessage.kt
@@ -7,6 +7,7 @@ internal sealed interface Message
 internal sealed interface Command : Message
 internal class StartOperation<D : Operation.Data>(val request: ApolloRequest<D>) : Command
 internal class StopOperation<D : Operation.Data>(val request: ApolloRequest<D>) : Command
+internal object RestartConnection: Command
 internal object Dispose: Command
 
 internal sealed interface Event : Message {
@@ -20,6 +21,9 @@ internal sealed interface Event : Message {
 internal class OperationResponse(override val id: String?, val payload: Map<String, Any?>) : Event
 internal class OperationError(override val id: String?, val payload: Map<String, Any?>?) : Event
 internal class OperationComplete(override val id: String?) : Event
+internal class ConnectionReEstablished : Event {
+  override val id: String? = null
+}
 internal class GeneralError(val payload: Map<String, Any?>?) : Event {
   override val id: String? = null
 }


Test patch:
diff --git a/libraries/apollo-adapters/src/jvmTest/kotlin/JavaTimeAdaptersTest.kt b/libraries/apollo-adapters/src/jvmTest/kotlin/JavaTimeAdaptersTest.kt
index c062f13c69..6cde061186 100644
--- a/libraries/apollo-adapters/src/jvmTest/kotlin/JavaTimeAdaptersTest.kt
+++ b/libraries/apollo-adapters/src/jvmTest/kotlin/JavaTimeAdaptersTest.kt
@@ -2,10 +2,12 @@ import com.apollographql.apollo3.adapter.DateAdapter
 import com.apollographql.apollo3.adapter.JavaInstantAdapter
 import com.apollographql.apollo3.adapter.JavaLocalDateAdapter
 import com.apollographql.apollo3.adapter.JavaLocalDateTimeAdapter
+import com.apollographql.apollo3.adapter.JavaLocalTimeAdapter
 import com.apollographql.apollo3.adapter.JavaOffsetDateTimeAdapter
 import com.apollographql.apollo3.adapter.KotlinxInstantAdapter
 import com.apollographql.apollo3.adapter.KotlinxLocalDateAdapter
 import com.apollographql.apollo3.adapter.KotlinxLocalDateTimeAdapter
+import com.apollographql.apollo3.adapter.KotlinxLocalTimeAdapter
 import com.apollographql.apollo3.api.Adapter
 import com.apollographql.apollo3.api.CustomScalarAdapters
 import com.apollographql.apollo3.api.json.BufferedSourceJsonReader
@@ -83,6 +85,15 @@ class JavaTimeAdaptersTest {
     assertEquals("2010-06-01", JavaLocalDateAdapter.toJson(localDate))
   }
 
+  @Test
+  fun localTime() {
+    val localTime = JavaLocalTimeAdapter.fromJson("14:35:20")
+    assertEquals(14, localTime.hour)
+    assertEquals(35, localTime.minute)
+    assertEquals(20, localTime.second)
+    assertEquals("14:35:20", JavaLocalTimeAdapter.toJson(localTime))
+  }
+
   @Test
   fun kotlinxInstant() {
     var instant = KotlinxInstantAdapter.fromJson("2010-06-01T22:19:44.475Z")
@@ -109,4 +120,13 @@ class JavaTimeAdaptersTest {
     assertEquals("2010-06-01", KotlinxLocalDateAdapter.toJson(localDate))
   }
 
+  @Test
+  fun kotlinxLocalTime() {
+    val localTime = KotlinxLocalTimeAdapter.fromJson("14:35:20")
+    assertEquals(14, localTime.hour)
+    assertEquals(35, localTime.minute)
+    assertEquals(20, localTime.second)
+    assertEquals("14:35:20", KotlinxLocalTimeAdapter.toJson(localTime))
+  }
+
 }
diff --git a/libraries/apollo-ast/src/test/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/OperationValidationTest.kt b/libraries/apollo-ast/src/test/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/OperationValidationTest.kt
new file mode 100644
index 0000000000..dcbcce63a5
--- /dev/null
+++ b/libraries/apollo-ast/src/test/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/OperationValidationTest.kt
@@ -0,0 +1,27 @@
+package com.apollographql.apollo3.graphql.ast.test.validation
+
+import com.apollographql.apollo3.ast.SourceLocation
+import com.apollographql.apollo3.ast.introspection.toSchema
+import com.apollographql.apollo3.ast.parseAsGQLDocument
+import com.apollographql.apollo3.ast.validateAsExecutable
+import okio.buffer
+import okio.source
+import org.junit.Test
+import java.io.File
+import kotlin.test.assertEquals
+
+class OperationValidationTest {
+  @Test
+  fun testAddRequiredFields() {
+    val schema = File("src/test/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/inputTypeDeprecatedField.graphqls").toSchema()
+    val operations = File("src/test/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/inputTypeDeprecatedField.graphql")
+        .source()
+        .buffer()
+        .parseAsGQLDocument()
+        .valueAssertNoErrors()
+    val operationIssues = operations.validateAsExecutable(schema).issues
+    assertEquals(1, operationIssues.size)
+    assertEquals("Use of deprecated input field `deprecatedParameter`", operationIssues[0].message)
+    assertEquals(SourceLocation(12, 17, null).pretty(), operationIssues[0].sourceLocation.pretty())
+  }
+}
diff --git a/libraries/apollo-ast/src/test/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/inputTypeDeprecatedField.graphql b/libraries/apollo-ast/src/test/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/inputTypeDeprecatedField.graphql
new file mode 100644
index 0000000000..4289ac8b28
--- /dev/null
+++ b/libraries/apollo-ast/src/test/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/inputTypeDeprecatedField.graphql
@@ -0,0 +1,17 @@
+query DontUseDeprecatedInputField($authorId: String!) {
+  viewer {
+    books(input: { authorId: $authorId }) {
+      id
+      name
+    }
+  }
+}
+
+query UseDeprecatedInputField($authorId: String!) {
+  viewer {
+    books(input: { authorId: $authorId, deprecatedParameter: 4 }) {
+      id
+      name
+    }
+  }
+}
diff --git a/libraries/apollo-ast/src/test/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/inputTypeDeprecatedField.graphqls b/libraries/apollo-ast/src/test/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/inputTypeDeprecatedField.graphqls
new file mode 100644
index 0000000000..597014250a
--- /dev/null
+++ b/libraries/apollo-ast/src/test/kotlin/com/apollographql/apollo3/graphql/ast/test/validation/inputTypeDeprecatedField.graphqls
@@ -0,0 +1,22 @@
+type Query {
+  viewer: Viewer
+}
+
+type Viewer {
+  books(input: BookInput!): [Book]!
+}
+
+input BookInput {
+  authorId: String
+  deprecatedParameter: Int @deprecated(reason: "Not needed anymore")
+  plus: String
+  couple: String
+  of: String
+  other: String
+  filters: String
+}
+
+type Book {
+  id: ID!
+  name: String!
+}
diff --git a/libraries/apollo-normalized-cache-sqlite-incubating/src/commonTest/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheTest.kt b/libraries/apollo-normalized-cache-sqlite-incubating/src/commonTest/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheTest.kt
index d939cacf02..4f4c29535b 100644
--- a/libraries/apollo-normalized-cache-sqlite-incubating/src/commonTest/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheTest.kt
+++ b/libraries/apollo-normalized-cache-sqlite-incubating/src/commonTest/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheTest.kt
@@ -3,14 +3,14 @@ package com.apollographql.apollo3.cache.normalized.sql
 import com.apollographql.apollo3.cache.normalized.api.ApolloCacheHeaders
 import com.apollographql.apollo3.cache.normalized.api.CacheHeaders
 import com.apollographql.apollo3.cache.normalized.api.CacheKey
+import com.apollographql.apollo3.cache.normalized.api.MemoryCacheFactory
 import com.apollographql.apollo3.cache.normalized.api.NormalizedCache
 import com.apollographql.apollo3.cache.normalized.api.Record
-import com.apollographql.apollo3.cache.normalized.sql.internal.json.JsonDatabase
+import com.apollographql.apollo3.cache.normalized.sql.internal.JsonRecordDatabase
 import com.apollographql.apollo3.cache.normalized.sql.internal.json.JsonQueries
 import com.apollographql.apollo3.cache.normalized.sql.internal.json.RecordForKey
 import com.apollographql.apollo3.cache.normalized.sql.internal.json.Records
 import com.apollographql.apollo3.cache.normalized.sql.internal.json.RecordsForKeys
-import com.apollographql.apollo3.cache.normalized.sql.internal.JsonRecordDatabase
 import com.apollographql.apollo3.exception.apolloExceptionHandler
 import com.squareup.sqldelight.Query
 import com.squareup.sqldelight.TransactionWithReturn
@@ -111,6 +111,62 @@ class SqlNormalizedCacheTest {
     assertNull(record)
   }
 
+  @Test
+  fun testSqlThenMemoryMergeAndDeleteCascade() {
+    val chainedCache = cache.chain(MemoryCacheFactory().create())
+    testChainedMergeAndDeleteCascade(chainedCache)
+  }
+
+  @Test
+  fun testMemoryThenSqlMergeAndDeleteCascade() {
+    val chainedCache = MemoryCacheFactory().create().chain(cache)
+    testChainedMergeAndDeleteCascade(chainedCache)
+  }
+
+  private fun testChainedMergeAndDeleteCascade(chainedCache: NormalizedCache) {
+    chainedCache.merge(
+        record = Record(
+            key = "referencedKey",
+            fields = mapOf(
+                "field1" to true,
+            ),
+        ),
+        cacheHeaders = CacheHeaders.NONE,
+    )
+    chainedCache.merge(
+        record = Record(
+            key = STANDARD_KEY,
+            fields = mapOf(
+                "field1" to "value1",
+                "field2" to CacheKey("referencedKey"),
+            ),
+        ),
+        cacheHeaders = CacheHeaders.NONE,
+    )
+    chainedCache.onEach {
+      val record1 = it.loadRecord(STANDARD_KEY, CacheHeaders.NONE)
+      assertNotNull(record1)
+      val record2 = it.loadRecord("referencedKey", CacheHeaders.NONE)
+      assertNotNull(record2)
+    }
+    chainedCache.remove(cacheKey = CacheKey(STANDARD_KEY), cascade = true)
+    chainedCache.onEach {
+      val record1 = it.loadRecord(STANDARD_KEY, CacheHeaders.NONE)
+      assertNull(record1)
+      val record2 = it.loadRecord("referencedKey", CacheHeaders.NONE)
+      assertNull(record2)
+    }
+  }
+
+  private fun NormalizedCache.onEach(block: (NormalizedCache) -> Unit): NormalizedCache? {
+    var c: NormalizedCache? = this
+    while (c != null) {
+      block(c)
+      c = c.nextCache
+    }
+    return c
+  }
+
   @Test
   fun testClearAll() {
     createRecord(QUERY_ROOT_KEY)
diff --git a/libraries/apollo-normalized-cache-sqlite/src/commonTest/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheTest.kt b/libraries/apollo-normalized-cache-sqlite/src/commonTest/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheTest.kt
index fe219ad90e..dfb12dd0f3 100644
--- a/libraries/apollo-normalized-cache-sqlite/src/commonTest/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheTest.kt
+++ b/libraries/apollo-normalized-cache-sqlite/src/commonTest/kotlin/com/apollographql/apollo3/cache/normalized/sql/SqlNormalizedCacheTest.kt
@@ -1,15 +1,16 @@
 package com.apollographql.apollo3.cache.normalized.sql
 
-import com.apollographql.apollo3.cache.normalized.sql.internal.json.JsonQueries
-import com.apollographql.apollo3.cache.normalized.sql.internal.json.RecordForKey
-import com.apollographql.apollo3.cache.normalized.sql.internal.json.Records
-import com.apollographql.apollo3.cache.normalized.sql.internal.json.RecordsForKeys
 import com.apollographql.apollo3.cache.normalized.api.ApolloCacheHeaders
 import com.apollographql.apollo3.cache.normalized.api.CacheHeaders
 import com.apollographql.apollo3.cache.normalized.api.CacheKey
+import com.apollographql.apollo3.cache.normalized.api.MemoryCacheFactory
 import com.apollographql.apollo3.cache.normalized.api.NormalizedCache
 import com.apollographql.apollo3.cache.normalized.api.Record
 import com.apollographql.apollo3.cache.normalized.sql.internal.JsonRecordDatabase
+import com.apollographql.apollo3.cache.normalized.sql.internal.json.JsonQueries
+import com.apollographql.apollo3.cache.normalized.sql.internal.json.RecordForKey
+import com.apollographql.apollo3.cache.normalized.sql.internal.json.Records
+import com.apollographql.apollo3.cache.normalized.sql.internal.json.RecordsForKeys
 import com.apollographql.apollo3.exception.apolloExceptionHandler
 import com.squareup.sqldelight.Query
 import com.squareup.sqldelight.TransactionWithReturn
@@ -110,6 +111,62 @@ class SqlNormalizedCacheTest {
     assertNull(record)
   }
 
+  @Test
+  fun testSqlThenMemoryMergeAndDeleteCascade() {
+    val chainedCache = cache.chain(MemoryCacheFactory().create())
+    testChainedMergeAndDeleteCascade(chainedCache)
+  }
+
+  @Test
+  fun testMemoryThenSqlMergeAndDeleteCascade() {
+    val chainedCache = MemoryCacheFactory().create().chain(cache)
+    testChainedMergeAndDeleteCascade(chainedCache)
+  }
+
+  private fun testChainedMergeAndDeleteCascade(chainedCache: NormalizedCache) {
+    chainedCache.merge(
+        record = Record(
+            key = "referencedKey",
+            fields = mapOf(
+                "field1" to true,
+            ),
+        ),
+        cacheHeaders = CacheHeaders.NONE,
+    )
+    chainedCache.merge(
+        record = Record(
+            key = STANDARD_KEY,
+            fields = mapOf(
+                "field1" to "value1",
+                "field2" to CacheKey("referencedKey"),
+            ),
+        ),
+        cacheHeaders = CacheHeaders.NONE,
+    )
+    chainedCache.onEach {
+      val record1 = it.loadRecord(STANDARD_KEY, CacheHeaders.NONE)
+      assertNotNull(record1)
+      val record2 = it.loadRecord("referencedKey", CacheHeaders.NONE)
+      assertNotNull(record2)
+    }
+    chainedCache.remove(cacheKey = CacheKey(STANDARD_KEY), cascade = true)
+    chainedCache.onEach {
+      val record1 = it.loadRecord(STANDARD_KEY, CacheHeaders.NONE)
+      assertNull(record1)
+      val record2 = it.loadRecord("referencedKey", CacheHeaders.NONE)
+      assertNull(record2)
+    }
+  }
+
+  private fun NormalizedCache.onEach(block: (NormalizedCache) -> Unit): NormalizedCache? {
+    var c: NormalizedCache? = this
+    while (c != null) {
+      block(c)
+      c = c.nextCache
+    }
+    return c
+  }
+
   @Test
   fun testClearAll() {
     createRecord(QUERY_ROOT_KEY)
@@ -255,7 +312,7 @@ class SqlNormalizedCacheTest {
     assertEquals("bad cache", throwable!!.cause!!.message)
   }
 
-  private class BadCacheQueries : JsonQueries  {
+  private class BadCacheQueries : JsonQueries {
     override fun <T : Any> recordForKey(key: String, mapper: (key: String, record: String) -> T): Query<T> {
       TODO("Not yet implemented")
     }
diff --git a/libraries/apollo-testing-support/src/commonMain/kotlin/com/apollographql/apollo3/testing/TestNetworkTransport.kt b/libraries/apollo-testing-support/src/commonMain/kotlin/com/apollographql/apollo3/testing/TestNetworkTransport.kt
index 9eeeba9fba..4c55038b86 100644
--- a/libraries/apollo-testing-support/src/commonMain/kotlin/com/apollographql/apollo3/testing/TestNetworkTransport.kt
+++ b/libraries/apollo-testing-support/src/commonMain/kotlin/com/apollographql/apollo3/testing/TestNetworkTransport.kt
@@ -13,7 +13,6 @@ import kotlinx.atomicfu.locks.reentrantLock
 import kotlinx.atomicfu.locks.withLock
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.yield
 
 private sealed interface TestResponse {
@@ -62,12 +61,18 @@ class MapTestNetworkTransport : NetworkTransport {
   private val operationsToResponses = mutableMapOf<Operation<out Operation.Data>, TestResponse>()
 
   override fun <D : Operation.Data> execute(request: ApolloRequest<D>): Flow<ApolloResponse<D>> {
-    val response = lock.withLock { operationsToResponses[request.operation] }
-        ?: error("No response registered for operation ${request.operation}")
-    if (response is TestResponse.NetworkError) throw ApolloNetworkException("Network error queued in QueueTestNetworkTransport")
-    @Suppress("UNCHECKED_CAST")
-    val apolloResponse = (response as TestResponse.Response).response as ApolloResponse<D>
-    return flowOf(apolloResponse.newBuilder().isLast(true).build())
+    return flow {
+      // "Emulate" a network call
+      yield()
+
+      val response = lock.withLock { operationsToResponses[request.operation] }
+          ?: error("No response registered for operation ${request.operation}")
+      if (response is TestResponse.NetworkError) throw ApolloNetworkException("Network error queued in QueueTestNetworkTransport")
+      @Suppress("UNCHECKED_CAST")
+      val apolloResponse = (response as TestResponse.Response).response as ApolloResponse<D>
+
+      emit(apolloResponse.newBuilder().isLast(true).build())
+    }
   }
 
   fun <D : Operation.Data> register(operation: Operation<D>, response: ApolloResponse<D>) {
diff --git a/tests/integration-tests/build.gradle.kts b/tests/integration-tests/build.gradle.kts
index 369fa03260..198aecb805 100644
--- a/tests/integration-tests/build.gradle.kts
+++ b/tests/integration-tests/build.gradle.kts
@@ -74,6 +74,7 @@ fun configureApollo(generateKotlinModels: Boolean) {
                 customScalarsMapping.set(mapOf(
                     "Upload" to "com.apollographql.apollo3.api.Upload"
                 ))
+                operationManifestFormat.set("persistedQueryManifest")
               }
 
               "normalizer" -> {
@@ -145,3 +146,17 @@ fun com.apollographql.apollo3.gradle.api.Service.configureConnection(generateKot
 }
 configureApollo(true)
 configureApollo(false)
+
+val checkPersistedQueryManifest = tasks.register("checkPersistedQueryManifest") {
+  dependsOn("generateApolloSources")
+  doLast {
+    check(
+        file("build/generated/manifest/apollo/upload-kotlin/persistedQueryManifest.json").readText() == file("testFixtures/manifest.json").readText()
+    ) {
+      "Persisted Query Manifest has changed"
+    }
+  }
+}
+tasks.named("jvmTest").configure {
+  dependsOn(checkPersistedQueryManifest)
+}
\ No newline at end of file
diff --git a/tests/integration-tests/src/jvmTest/kotlin/test/PersistedQueryManifestTest.kt b/tests/integration-tests/src/jvmTest/kotlin/test/PersistedQueryManifestTest.kt
new file mode 100644
index 0000000000..537246195b
--- /dev/null
+++ b/tests/integration-tests/src/jvmTest/kotlin/test/PersistedQueryManifestTest.kt
@@ -0,0 +1,41 @@
+package test
+
+import com.apollographql.apollo3.integration.httpcache.AllFilmsQuery
+import com.apollographql.apollo3.integration.upload.MultipleUploadMutation
+import com.apollographql.apollo3.testing.HostFileSystem
+import com.apollographql.apollo3.testing.pathToUtf8
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import okio.Buffer
+import okio.Path.Companion.toPath
+import okio.buffer
+import org.junit.Test
+import kotlin.test.assertEquals
+
+/**
+ * These tests read the operationOutput.json generated during compilation and compare it to the generated models
+ *
+ * This makes sure minification is the same between operationOutput.json and the models
+ *
+ * This is a JVM only test because we need to assume "http-kotlin" for the service name
+ * where the file will be generated. Apple code shouldn't be much different in all cases
+ */
+class PersistedQueryManifestTest {
+  @Test
+  fun persistedQueryManifestTheModels() {
+    val manifest = pathToUtf8("integration-tests/build/generated/manifest/apollo/upload-kotlin/persistedQueryManifest.json")
+
+    val source = Json.parseToJsonElement(manifest).jsonObject.get("operations")!!.jsonArray.mapNotNull {
+      val descriptor = it.jsonObject
+      if (descriptor.getValue("name").jsonPrimitive.content == "MultipleUpload") {
+        descriptor.getValue("body").jsonPrimitive.content
+      } else {
+        null
+      }
+    }.single()
+
+    assertEquals(MultipleUploadMutation.OPERATION_DOCUMENT, source)
+  }
+}
\ No newline at end of file
diff --git a/tests/integration-tests/testFixtures/manifest.json b/tests/integration-tests/testFixtures/manifest.json
new file mode 100644
index 0000000000..de10e960fd
--- /dev/null
+++ b/tests/integration-tests/testFixtures/manifest.json
@@ -0,0 +1,30 @@
+{
+  "format": "apollo-persisted-query-manifest",
+  "version": 1,
+  "operations": [
+    {
+      "id": "debf75d1c1580dc06d3deb37daaa1439cbc0aeef776e2cb836d2720ca9b2761e",
+      "body": "mutation NestedUpload($topFile: Upload, $topFileList: [Upload], $nested: NestedObject) { nestedUpload(topFile: $topFile, topFileList: $topFileList, nested: $nested) }",
+      "name": "NestedUpload",
+      "type": "mutation"
+    },
+    {
+      "id": "bd822d287b6428f2d9135349edbd3a2997ca9ec8ed433c1b9a078d3cf2f70eae",
+      "body": "mutation SingleUploadTwice($file1: Upload!, $file2: Upload!) { file1: singleUpload(file: $file1) { id path filename mimetype } file2: singleUpload(file: $file2) { id path filename mimetype } }",
+      "name": "SingleUploadTwice",
+      "type": "mutation"
+    },
+    {
+      "id": "7b597dfc22b044880da8ddea282fd3cdc414972fecc42673e01870f2c2a83873",
+      "body": "mutation MultipleUpload($files: [Upload!]!) { multipleUpload(files: $files) { id path filename mimetype } }",
+      "name": "MultipleUpload",
+      "type": "mutation"
+    },
+    {
+      "id": "99c2bd8836d69bb361489392d1bcf2897780c9d5f653b7ab87d6a5c88a3b14fc",
+      "body": "mutation SingleUpload($file: Upload!) { singleUpload(file: $file) { id path filename mimetype } }",
+      "name": "SingleUpload",
+      "type": "mutation"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/sample-server/src/main/kotlin/com/apollographql/apollo/sample/server/SampleServer.kt b/tests/sample-server/src/main/kotlin/com/apollographql/apollo/sample/server/SampleServer.kt
new file mode 100644
index 0000000000..0d62f293f9
--- /dev/null
+++ b/tests/sample-server/src/main/kotlin/com/apollographql/apollo/sample/server/SampleServer.kt
@@ -0,0 +1,22 @@
+package com.apollographql.apollo.sample.server
+
+import org.springframework.boot.runApplication
+import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext
+import java.io.Closeable
+
+class SampleServer(val port: Int = -1) : Closeable {
+  val args = if (port > 0) arrayOf("--server.port=$port") else emptyArray()
+  private var context = runApplication<DefaultApplication>(*args) as AnnotationConfigReactiveWebServerApplicationContext
+
+  fun graphqlUrl(): String {
+    return "http://localhost:${context.webServer.port}/graphql"
+  }
+
+  fun subscriptionsUrl(): String {
+    return "http://localhost:${context.webServer.port}/subscriptions"
+  }
+
+  override fun close() {
+    context.close()
+  }
+}
\ No newline at end of file
diff --git a/tests/test-network-transport/src/test/kotlin/testnetworktransport/MapTestNetworkTransportHandlerTest.kt b/tests/test-network-transport/src/test/kotlin/testnetworktransport/MapTestNetworkTransportHandlerTest.kt
index 7aebb616b8..7ee3429eb5 100644
--- a/tests/test-network-transport/src/test/kotlin/testnetworktransport/MapTestNetworkTransportHandlerTest.kt
+++ b/tests/test-network-transport/src/test/kotlin/testnetworktransport/MapTestNetworkTransportHandlerTest.kt
@@ -8,6 +8,8 @@ import com.apollographql.apollo3.testing.internal.runTest
 import com.apollographql.apollo3.testing.registerTestResponse
 import com.benasher44.uuid.uuid4
 import testnetworktransport.test.GetHeroQuery_TestBuilder.Data
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.toList
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
@@ -110,4 +112,12 @@ class MapTestNetworkTransportHandlerTest {
     val response: ApolloResponse<GetHeroNameOnlyQuery.Data> = apolloClient.query(GetHeroNameOnlyQuery()).execute()
     assertEquals("Darth Vader", response.data!!.hero.name)
   }
+
+  @Test
+  fun errorWhenNoResponseFoundInMap() = runTest(before = { setUp() }, after = { tearDown() }) {
+    apolloClient.query(GetHeroNameOnlyQuery()).toFlow().catch { e ->
+      assertTrue(e is IllegalStateException)
+      assertTrue(e.message!!.startsWith("No response registered for operation"))
+    }.toList()
+  }
 }
diff --git a/tests/test-network-transport/src/test/kotlin/testnetworktransport/QueueTestNetworkTransportHandlerTest.kt b/tests/test-network-transport/src/test/kotlin/testnetworktransport/QueueTestNetworkTransportHandlerTest.kt
index 26756467bc..f7ed6abf18 100644
--- a/tests/test-network-transport/src/test/kotlin/testnetworktransport/QueueTestNetworkTransportHandlerTest.kt
+++ b/tests/test-network-transport/src/test/kotlin/testnetworktransport/QueueTestNetworkTransportHandlerTest.kt
@@ -10,6 +10,8 @@ import com.apollographql.apollo3.testing.enqueueTestResponse
 import com.apollographql.apollo3.testing.internal.runTest
 import com.benasher44.uuid.uuid4
 import testnetworktransport.test.GetHeroQuery_TestBuilder.Data
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.toList
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
@@ -116,4 +118,12 @@ class QueueTestNetworkTransportHandlerTest {
     val actual = apolloClient.query(query).execute().data!!
     assertEquals(testData.hero.name, actual.hero.name)
   }
+
+  @Test
+  fun errorWhenNoResponseFoundInQueue() = runTest(before = { setUp() }, after = { tearDown() }) {
+    apolloClient.query(GetHeroNameOnlyQuery()).toFlow().catch { e ->
+      assertTrue(e is IllegalStateException)
+      assertTrue(e.message!!.startsWith("No more responses in queue"))
+    }.toList()
+  }
 }
diff --git a/tests/websockets/src/commonTest/kotlin/AppSyncTest.kt b/tests/websockets/src/commonTest/kotlin/AppSyncTest.kt
index aea4c2bb02..0bf5709eae 100644
--- a/tests/websockets/src/commonTest/kotlin/AppSyncTest.kt
+++ b/tests/websockets/src/commonTest/kotlin/AppSyncTest.kt
@@ -44,20 +44,24 @@ class AppSyncTest {
     val apiKey = TODO("changeMe")
     val host = "6l5lltvi6fgmrpx5abfxrtq6wu.appsync-api.eu-west-3.amazonaws.com"
 
-    val authorization = mapOf(
-        "host" to host,
-        "x-api-key" to apiKey
-    )
+    fun generateAuth(): Map<String, String?> {
+        val authorization = mapOf(
+            "host" to host,
+            "x-api-key" to apiKey
+        )
+        return authorization
+    }
+
     val url = AppSyncWsProtocol.buildUrl(
         baseUrl = "https://6l5lltvi6fgmrpx5abfxrtq6wu.appsync-realtime-api.eu-west-3.amazonaws.com/",
-        authorization = authorization
+        authorization = generateAuth()
     )
     val apolloClient = ApolloClient.Builder().networkTransport(
         networkTransport = WebSocketNetworkTransport.Builder().serverUrl(
             serverUrl = url,
         ).protocol(
             protocolFactory = AppSyncWsProtocol.Factory(
-                authorization = authorization
+                connectionPayload = { generateAuth() }
             )
         ).build()
     ).build()
diff --git a/tests/websockets/src/jvmTest/kotlin/SampleServerCustomTest.kt b/tests/websockets/src/jvmTest/kotlin/SampleServerCustomTest.kt
new file mode 100644
index 0000000000..6ac526d9ee
--- /dev/null
+++ b/tests/websockets/src/jvmTest/kotlin/SampleServerCustomTest.kt
@@ -0,0 +1,75 @@
+import app.cash.turbine.test
+import com.apollographql.apollo.sample.server.SampleServer
+import com.apollographql.apollo3.ApolloClient
+import com.apollographql.apollo3.network.ws.WebSocketNetworkTransport
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.Test
+import sample.server.CountSubscription
+
+class SampleServerCustomTest {
+  @Test
+  fun websocketReopenWhenDoesNotPile() {
+
+    val port = 56678
+    val url = "http://localhost:$port/subscriptions"
+    var sampleServer: SampleServer? = SampleServer(port)
+
+    var reopenCount = 0
+
+    println("serverUrl=${sampleServer?.subscriptionsUrl()}")
+    val apolloClient = ApolloClient.Builder()
+        .subscriptionNetworkTransport(
+            WebSocketNetworkTransport.Builder()
+                .serverUrl(url)
+                .reopenWhen { _, _ ->
+                  println("reopen ${System.currentTimeMillis() / 1000}")
+
+                  delay(1000)
+                  reopenCount++
+                  true
+                }
+                .build()
+        )
+        .serverUrl("https://unused.com/")
+        .build()
+
+    runBlocking {
+      repeat(50) {id ->
+        launch {
+          try {
+            withTimeout(30_000) {
+              apolloClient.subscription(CountSubscription(Int.MAX_VALUE, 3600 * 1000))
+                  .toFlow()
+                  .onEach {
+                    println("#$id ${it.data}")
+                  }
+                  .take(2)
+                  .collect()
+            }
+          } catch (e: TimeoutCancellationException) {
+            error("timeout")
+          }
+        }
+      }
+      launch {
+        delay(1000)
+        println("closing server")
+        sampleServer!!.close()
+        println("server closed")
+        delay(10_000)
+        println("reopening server")
+        sampleServer = SampleServer(port)
+        println("server reopened")
+      }
+    }
+    sampleServer?.close()
+  }
+}
\ No newline at end of file