From 5792cef0a74fe6988cb05c7f73542e3e793e1935 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 13 Dec 2024 18:30:09 +0100 Subject: [PATCH 01/21] Update `DeferredJsonMerger` to take `pending` and `completed` into account. --- .../apollo/internal/DeferredJsonMerger.kt | 101 +- .../test/defer/DeferredJsonMergerTest.kt | 1375 ++++++++++++----- 2 files changed, 1093 insertions(+), 383 deletions(-) diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt index b034caecc28..f2eb734ad82 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt @@ -15,19 +15,25 @@ private typealias MutableJsonMap = MutableMap * Each call to [merge] will merge the given chunk into the [merged] Map, and will also update the [mergedFragmentIds] Set with the * value of its `path` and `label` field. * - * The fields in `data` are merged into the node found in [merged] at `path` (for the first call to [merge], the payload is - * copied to [merged] as-is). + * The fields in `data` are merged into the node found in [merged] at the path known by looking at the `id` field (for the first call to + * [merge], the payload is copied to [merged] as-is). * - * `errors` in incremental items (if present) are merged together in an array and then set to the `errors` field of the [merged] Map, - * at each call to [merge]. - * `extensions` in incremental items (if present) are merged together in an array and then set to the `extensions/incremental` field of the + * `errors` in incremental and completed items (if present) are merged together in an array and then set to the `errors` field of the * [merged] Map, at each call to [merge]. + * `extensions` in incremental items (if present) are merged together in an array and then set to the `extensions` field of the [merged] + * Map, at each call to [merge]. */ @ApolloInternal +@Suppress("UNCHECKED_CAST") class DeferredJsonMerger { private val _merged: MutableJsonMap = mutableMapOf() val merged: JsonMap = _merged + /** + * Map of identifiers to their corresponding DeferredFragmentIdentifier, found in `pending`. + */ + private val idsToDeferredFragmentIdentifiers = mutableMapOf() + private val _mergedFragmentIds = mutableSetOf() val mergedFragmentIds: Set = _mergedFragmentIds @@ -47,11 +53,12 @@ class DeferredJsonMerger { return merge(payloadMap) } - @Suppress("UNCHECKED_CAST") fun merge(payload: JsonMap): JsonMap { if (merged.isEmpty()) { - // Initial payload, no merging needed - _merged += payload + // Initial payload, no merging needed (strip some fields that should not appear in the final result) + _merged += payload - "hasNext" - "pending" + handlePending(payload) + handleCompleted(payload) return merged } @@ -60,48 +67,68 @@ class DeferredJsonMerger { isEmptyPayload = true } else { isEmptyPayload = false - val mergedErrors = mutableListOf() - val mergedExtensions = mutableListOf() for (incrementalItem in incrementalList) { - mergeData(incrementalItem) - // Merge errors and extensions (if any) of the incremental list - (incrementalItem["errors"] as? List)?.let { mergedErrors += it } - (incrementalItem["extensions"] as? JsonMap)?.let { mergedExtensions += it } - } - // Keep only this payload's errors and extensions, if any - if (mergedErrors.isNotEmpty()) { - _merged["errors"] = mergedErrors - } else { - _merged.remove("errors") - } - if (mergedExtensions.isNotEmpty()) { - _merged["extensions"] = mapOf("incremental" to mergedExtensions) - } else { - _merged.remove("extensions") + mergeIncrementalData(incrementalItem) + // Merge errors (if any) of the incremental item + (incrementalItem["errors"] as? List)?.let { getOrPutMergedErrors() += it } } } hasNext = payload["hasNext"] as Boolean? ?: false + handlePending(payload) + handleCompleted(payload) + + (payload["extensions"] as? JsonMap)?.let { getOrPutExtensions() += it } + return merged } - @Suppress("UNCHECKED_CAST") - private fun mergeData(incrementalItem: JsonMap) { - val data = incrementalItem["data"] as JsonMap? - val path = incrementalItem["path"] as List - val mergedData = merged["data"] as JsonMap + private fun getOrPutMergedErrors() = _merged.getOrPut("errors") { mutableListOf() } as MutableList - // payloadData can be null if there are errors - if (data != null) { - val nodeToMergeInto = nodeAtPath(mergedData, path) as MutableJsonMap - deepMerge(nodeToMergeInto, data) + private fun getOrPutExtensions() = _merged.getOrPut("extensions") { mutableMapOf() } as MutableJsonMap - _mergedFragmentIds += DeferredFragmentIdentifier(path = path, label = incrementalItem["label"] as String?) + private fun handlePending(payload: JsonMap) { + val pending = payload["pending"] as? List + if (pending != null) { + for (pendingItem in pending) { + val id = pendingItem["id"] as String + val path = pendingItem["path"] as List + val label = pendingItem["label"] as String? + idsToDeferredFragmentIdentifiers[id] = DeferredFragmentIdentifier(path = path, label = label) + } } } - @Suppress("UNCHECKED_CAST") + private fun handleCompleted(payload: JsonMap) { + val completed = payload["completed"] as? List + if (completed != null) { + for (completedItem in completed) { + val errors = completedItem["errors"] as? List + if (errors != null) { + // Merge errors (if any) of the completed item + getOrPutMergedErrors() += errors + } else { + // No errors: we have merged all the fields of the fragment so it can be parsed + val id = completedItem["id"] as String + val deferredFragmentIdentifier = idsToDeferredFragmentIdentifiers.remove(id) + ?: error("Id '$id' not found in pending results") + _mergedFragmentIds += deferredFragmentIdentifier + } + } + } + } + + private fun mergeIncrementalData(incrementalItem: JsonMap) { + val id = incrementalItem["id"] as String? ?: error("No id found in incremental item") + val data = incrementalItem["data"] as JsonMap? ?: error("No data found in incremental item") + val subPath = incrementalItem["subPath"] as List? ?: emptyList() + val path = (idsToDeferredFragmentIdentifiers[id]?.path ?: error("Id '$id' not found in pending results")) + subPath + val mergedData = merged["data"] as JsonMap + val nodeToMergeInto = nodeAtPath(mergedData, path) as MutableJsonMap + deepMerge(nodeToMergeInto, data) + } + private fun deepMerge(destination: MutableJsonMap, map: JsonMap) { for ((key, value) in map) { if (destination.containsKey(key) && destination[key] is MutableMap<*, *>) { @@ -116,7 +143,6 @@ class DeferredJsonMerger { } } - @Suppress("UNCHECKED_CAST") private fun jsonToMap(json: BufferedSource): JsonMap = BufferedSourceJsonReader(json).readAny() as JsonMap @@ -130,7 +156,6 @@ class DeferredJsonMerger { node = if (node is List<*>) { node[key as Int] } else { - @Suppress("UNCHECKED_CAST") node as JsonMap node[key] } diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt index 40918461ae3..ee2a1ff3f89 100644 --- a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt +++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt @@ -16,11 +16,12 @@ private fun String.buffer() = Buffer().writeUtf8(this) private fun jsonToMap(json: String): Map = BufferedSourceJsonReader(json.buffer()).readAny() as Map class DeferredJsonMergerTest { - @Test - fun mergeJsonSingleIncrementalItem() { - val deferredJsonMerger = DeferredJsonMerger() + @Test + fun mergeJsonSingleIncrementalItem() { + val deferredJsonMerger = DeferredJsonMerger() - val payload1 = """ + //language=JSON + val payload1 = """ { "data": { "computers": [ @@ -38,14 +39,45 @@ class DeferredJsonMergerTest { } ] }, + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" + } + ], "hasNext": true } """ + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true + } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] + } + } + """ deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(payload1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) - + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + //language=JSON val payload2 = """ { "incremental": [ @@ -57,22 +89,34 @@ class DeferredJsonMergerTest { "resolution": "640x480" } }, + "id": "0" + } + ], + "completed": [ + { + "id": "0" + } + ], + "pending": [ + { + "id": "1", "path": [ "computers", - 0 + 1 ], - "label": "query:Query1:0", - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" - } - } + "label": "query:Query1:0" } ], + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + }, "hasNext": true } """ + //language=JSON val mergedPayloads_1_2 = """ { "data": { @@ -94,54 +138,64 @@ class DeferredJsonMergerTest { } ] }, - "hasNext": true, "extensions": { - "incremental": [ - { - "duration": { - "amount": 100, - "unit": "ms" - } - } - ] + "duration": { + "amount": 100, + "unit": "ms" + } } } """ deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) - assertEquals(setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - ), deferredJsonMerger.mergedFragmentIds - ) - + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0") + ), + deferredJsonMerger.mergedFragmentIds + ) + //language=JSON val payload3 = """ - { - "incremental": [ - { - "data": { - "cpu": "486", - "year": 1996, - "screen": { - "resolution": "640x480" - } - }, - "path": [ - "computers", - 1 - ], - "label": "query:Query1:0", - "extensions": { - "duration": { - "amount": 25, - "unit": "ms" - } + { + "incremental": [ + { + "data": { + "cpu": "486", + "year": 1996, + "screen": { + "resolution": "640x480" } - } - ], - "hasNext": true - } + }, + "id": "1" + } + ], + "completed": [ + { + "id": "1" + } + ], + "pending": [ + { + "id": "2", + "path": [ + "computers", + 0, + "screen" + ], + "label": "fragment:ComputerFields:0" + } + ], + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" + } + }, + "hasNext": true + } """ + //language=JSON val mergedPayloads_1_2_3 = """ { "data": { @@ -166,61 +220,64 @@ class DeferredJsonMergerTest { } ] }, - "hasNext": true, "extensions": { - "incremental": [ - { - "duration": { - "amount": 25, - "unit": "ms" - } - } - ] + "duration": { + "amount": 25, + "unit": "ms" + } } } """ deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) - assertEquals(setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), - ), deferredJsonMerger.mergedFragmentIds - ) - + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + ), + deferredJsonMerger.mergedFragmentIds + ) + //language=JSON val payload4 = """ - { - "incremental": [ - { - "data": null, - "path": [ - "computers", - 0, - "screen" - ], - "errors": [ - { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - } - ], - "label": "fragment:ComputerFields:0" - } - ], - "hasNext": true - } + { + "completed": [ + { + "id": "2", + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + } + ] + } + ], + "pending": [ + { + "id": "3", + "path": [ + "computers", + 1, + "screen" + ], + "label": "fragment:ComputerFields:0" + } + ], + "hasNext": true + } """ + //language=JSON val mergedPayloads_1_2_3_4 = """ { "data": { @@ -245,7 +302,6 @@ class DeferredJsonMergerTest { } ] }, - "hasNext": true, "errors": [ { "message": "Cannot resolve isColor", @@ -262,54 +318,63 @@ class DeferredJsonMergerTest { "isColor" ] } - ] + ], + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" + } + } } """ deferredJsonMerger.merge(payload4.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4), deferredJsonMerger.merged) - assertEquals(setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), - ), deferredJsonMerger.mergedFragmentIds - ) - + assertEquals(jsonToMap(mergedPayloads_1_2_3_4), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + ), + deferredJsonMerger.mergedFragmentIds + ) + //language=JSON val payload5 = """ - { - "incremental": [ - { - "data": { - "isColor": false - }, - "path": [ - "computers", - 1, - "screen" - ], - "errors": [ - { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] - } - ], - "label": "fragment:ComputerFields:0", - "extensions": { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" - } + { + "incremental": [ + { + "data": { + "isColor": false + }, + "id": "3", + "errors": [ + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] } - } - ], - "hasNext": false - } + ] + } + ], + "completed": [ + { + "id": "3" + } + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + }, + "hasNext": false + } """ + //language=JSON val mergedPayloads_1_2_3_4_5 = """ { "data": { @@ -335,19 +400,22 @@ class DeferredJsonMergerTest { } ] }, - "hasNext": true, - "extensions": { - "incremental": [ - { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" - } - } - ] - }, "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + }, { "message": "Another error", "locations": [ @@ -357,24 +425,34 @@ class DeferredJsonMergerTest { } ] } - ] + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + } } """ - deferredJsonMerger.merge(payload5.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) - assertEquals(setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), - ), deferredJsonMerger.mergedFragmentIds - ) - } + deferredJsonMerger.merge(payload5.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), + ), + deferredJsonMerger.mergedFragmentIds + ) + } - @Test - fun mergeJsonMultipleIncrementalItems() { - val deferredJsonMerger = DeferredJsonMerger() + @Test + fun mergeJsonMultipleIncrementalItems() { + val deferredJsonMerger = DeferredJsonMerger() - val payload1 = """ + //language=JSON + val payload1 = """ { "data": { "computers": [ @@ -392,151 +470,166 @@ class DeferredJsonMergerTest { } ] }, - "hasNext": true - } - """ - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(payload1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) - - - val payload2_3 = """ - { - "incremental": [ - { - "data": { - "cpu": "386", - "year": 1993, - "screen": { - "resolution": "640x480" - } - }, - "path": [ - "computers", - 0 - ], - "label": "query:Query1:0", - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" - } - } - }, - { - "data": { - "cpu": "486", - "year": 1996, - "screen": { - "resolution": "640x480" - } + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" }, - "path": [ - "computers", - 1 - ], - "label": "query:Query1:0", - "extensions": { - "duration": { - "amount": 25, - "unit": "ms" - } + { + "id": "1", + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0" } - } - ], - "hasNext": true - } + ], + "hasNext": true + } """ - val mergedPayloads_1_2_3 = """ + //language=JSON + val mergedPayloads_1 = """ { "data": { "computers": [ { "id": "Computer1", - "cpu": "386", - "year": 1993, "screen": { - "isTouch": true, - "resolution": "640x480" + "isTouch": true } }, { "id": "Computer2", - "cpu": "486", - "year": 1996, "screen": { - "isTouch": false, - "resolution": "640x480" - } - } - ] - }, - "hasNext": true, - "extensions": { - "incremental": [ - { - "duration": { - "amount": 100, - "unit": "ms" - } - }, - { - "duration": { - "amount": 25, - "unit": "ms" + "isTouch": false } } ] } } """ - deferredJsonMerger.merge(payload2_3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) - assertEquals(setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), - ), deferredJsonMerger.mergedFragmentIds - ) - + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) - val payload4_5 = """ + //language=JSON + val payload2_3 = """ { "incremental": [ { - "data": null, + "data": { + "cpu": "386", + "year": 1993, + "screen": { + "resolution": "640x480" + } + }, + "id": "0" + }, + { + "data": { + "cpu": "486", + "year": 1996, + "screen": { + "resolution": "640x480" + } + }, + "id": "1" + } + ], + "completed": [ + { + "id": "0" + }, + { + "id": "1" + } + ], + "pending": [ + { + "id": "2", "path": [ "computers", 0, "screen" ], - "errors": [ - { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - } - ], "label": "fragment:ComputerFields:0" }, { - "data": { - "isColor": false - }, + "id": "3", "path": [ "computers", 1, "screen" ], + "label": "fragment:ComputerFields:0" + } + ], + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + }, + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480" + } + } + ] + }, + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + } + } + """ + deferredJsonMerger.merge(payload2_3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + ), + deferredJsonMerger.mergedFragmentIds + ) + + //language=JSON + val payload4_5 = """ + { + "incremental": [ + { + "data": { + "isColor": false + }, + "id": "3", "errors": [ { "message": "Another error", @@ -547,21 +640,46 @@ class DeferredJsonMergerTest { } ] } - ], - "label": "fragment:ComputerFields:0", - "extensions": { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" + ] + } + ], + "completed": [ + { + "id": "2", + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] } - } + ] + }, + { + "id": "3" } ], - "hasNext": true + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + }, + "hasNext": false } """ - val mergedPayloads_1_2_3_4_5 = """ + //language=JSON + val mergedPayloads_1_2_3_4_5 = """ { "data": { "computers": [ @@ -586,19 +704,16 @@ class DeferredJsonMergerTest { } ] }, - "hasNext": true, - "extensions": { - "incremental": [ - { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" - } - } - ] - }, "errors": [ + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + }, { "message": "Cannot resolve isColor", "locations": [ @@ -613,34 +728,35 @@ class DeferredJsonMergerTest { "screen", "isColor" ] - }, - { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] } - ] + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + } } """ - deferredJsonMerger.merge(payload4_5.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) - assertEquals(setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), - ), deferredJsonMerger.mergedFragmentIds - ) - } + deferredJsonMerger.merge(payload4_5.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), + ), + deferredJsonMerger.mergedFragmentIds + ) + } - @Test - fun emptyPayloads() { - val deferredJsonMerger = DeferredJsonMerger() + @Test + fun emptyPayloads() { + val deferredJsonMerger = DeferredJsonMerger() - val payload1 = """ + //language=JSON + val payload1 = """ { "data": { "computers": [ @@ -658,21 +774,40 @@ class DeferredJsonMergerTest { } ] }, + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" + }, + { + "id": "1", + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0" + } + ], "hasNext": true } """ - deferredJsonMerger.merge(payload1.buffer()) - assertFalse(deferredJsonMerger.isEmptyPayload) + deferredJsonMerger.merge(payload1.buffer()) + assertFalse(deferredJsonMerger.isEmptyPayload) - val payload2 = """ + //language=JSON + val payload2 = """ { "hasNext": true } """ - deferredJsonMerger.merge(payload2.buffer()) - assertTrue(deferredJsonMerger.isEmptyPayload) - - val payload3 = """ + deferredJsonMerger.merge(payload2.buffer()) + assertTrue(deferredJsonMerger.isEmptyPayload) + //language=JSON + val payload3 = """ { "incremental": [ { @@ -683,31 +818,581 @@ class DeferredJsonMergerTest { "resolution": "640x480" } }, - "path": [ - "computers", - 0 - ], - "label": "query:Query1:0", - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" + "id": "0" + } + ], + "hasNext": true + } + """ + deferredJsonMerger.merge(payload3.buffer()) + assertFalse(deferredJsonMerger.isEmptyPayload) + + //language=JSON + val payload4 = """ + { + "hasNext": false + } + """ + deferredJsonMerger.merge(payload4.buffer()) + assertTrue(deferredJsonMerger.isEmptyPayload) + } + + /** + * Example A from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + */ + @Test + fun june2023ExampleA() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": { + "f2": { + "a": "a", + "b": "b", + "c": { + "d": "d", + "e": "e", + "f": { "h": "h", "i": "i" } + } + } + }, + "pending": [{ "path": [], "id": "0" }], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "f2": { + "a": "a", + "b": "b", + "c": { + "d": "d", + "e": "e", + "f": { "h": "h", "i": "i" } + } + } + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { "id": "0", "data": { "MyFragment": "Query" } }, + { "id": "0", "subPath": ["f2", "c", "f"], "data": { "j": "j" } } + ], + "completed": [{ "id": "0" }], + "hasNext": false + } + """ + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "f2": { + "a": "a", + "b": "b", + "c": { + "d": "d", + "e": "e", + "f": { "h": "h", "i": "i", "j": "j" } + } + }, + "MyFragment": "Query" + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = null), + ), + deferredJsonMerger.mergedFragmentIds + ) + } + + /** + * Example A2 from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + */ + @Test + fun june2023ExampleA2() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": {"f2": {"a": "A", "b": "B", "c": { + "d": "D", "e": "E", "f": { + "h": "H", "i": "I" + } + }}}, + "pending": [{"id": "0", "path": [], "label": "D1"}], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I" } } } + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + + //language=JSON + val payload2 = """ + { + "incremental": [ + {"id": "0", "subPath": ["f2", "c", "f"], "data": {"j": "J", "k": "K"}} + ], + "pending": [{"id": "1", "path": ["f2", "c", "f"], "label": "D2"}], + "completed": [ + {"id": "0"} ], "hasNext": true } """ - deferredJsonMerger.merge(payload3.buffer()) - assertFalse(deferredJsonMerger.isEmptyPayload) + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I", + "j": "J", + "k": "K" + } + } + } + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "D1"), + ), + deferredJsonMerger.mergedFragmentIds + ) - val payload4 = """ + //language=JSON + val payload3 = """ { + "incremental": [ + {"id": "1", "data": {"l": "L", "m": "M"}} + ], + "completed": [ + {"id": "1"} + ], "hasNext": false } """ - deferredJsonMerger.merge(payload4.buffer()) - assertTrue(deferredJsonMerger.isEmptyPayload) - } -} \ No newline at end of file + + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I", + "j": "J", + "k": "K", + "l": "L", + "m": "M" + } + } + } + } + } + """ + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "D1"), + DeferredFragmentIdentifier(path = listOf("f2", "c", "f"), label = "D2"), + ), + deferredJsonMerger.mergedFragmentIds + ) + } + + /** + * Example B1 from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + */ + @Test + fun june2023ExampleB1() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": { + "a": { "b": { "c": { "d": "d" } } } + }, + "pending": [ + { "path": [], "id": "0", "label": "Blue" }, + { "path": ["a", "b"], "id": "1", "label": "Red" } + ], + "hasNext": true + } + """ + + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + } + } + } + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { "id": "1", "data": { "potentiallySlowFieldA": "potentiallySlowFieldA" } }, + { "id": "1", "data": { "e": { "f": "f" } } } + ], + "completed": [{ "id": "1" }], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + }, + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + } + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + ), + deferredJsonMerger.mergedFragmentIds + ) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { "id": "0", "data": { "g": { "h": "h" }, "potentiallySlowFieldB": "potentiallySlowFieldB" } } + ], + "completed": [{ "id": "0" }], + "hasNext": false + } + """ + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + }, + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + }, + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + } + """ + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + ), + deferredJsonMerger.mergedFragmentIds + ) + } + + /** + * Example B2 from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + */ + @Test + fun june2023ExampleB2() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": { + "a": { "b": { "c": { "d": "d" } } } + }, + "pending": [ + { "path": [], "id": "0", "label": "Blue" }, + { "path": ["a", "b"], "id": "1", "label": "Red" } + ], + "hasNext": true + } + """ + + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + } + } + } + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { "id": "0", "data": { "g": { "h": "h" }, "potentiallySlowFieldB": "potentiallySlowFieldB" } }, + { "id": "1", "data": { "e": { "f": "f" } } } + ], + "completed": [{ "id": "0" }], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + } + } + }, + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + ), + deferredJsonMerger.mergedFragmentIds + ) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { "id": "1", "data": { "potentiallySlowFieldA": "potentiallySlowFieldA" } } + ], + "completed": [{ "id": "1" }], + "hasNext": false + } + """ + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + }, + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + }, + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + } + """ + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + ), + deferredJsonMerger.mergedFragmentIds + ) + } + + /** + * Example D from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + */ + @Test + fun june2023ExampleD() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": { "me": {} }, + "pending": [ + { "path": [], "id": "0" }, + { "path": ["me"], "id": "1" } + ], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "me": {} + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "1", + "data": { "list": [{ "item": {} }, { "item": {} }, { "item": {} }] } + }, + { "id": "1", "subPath": ["list", 0, "item"], "data": { "id": "1" } }, + { "id": "1", "subPath": ["list", 1, "item"], "data": { "id": "2" } }, + { "id": "1", "subPath": ["list", 2, "item"], "data": { "id": "3" } } + ], + "completed": [{ "id": "1" }], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "list": [ + { "item": { "id": "1" } }, + { "item": { "id": "2" } }, + { "item": { "id": "3" } } + ] + } + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("me"), label = null), + ), + deferredJsonMerger.mergedFragmentIds + ) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { "id": "0", "subPath": ["me", "list", 0, "item"], "data": { "value": "Foo" } }, + { "id": "0", "subPath": ["me", "list", 1, "item"], "data": { "value": "Bar" } }, + { "id": "0", "subPath": ["me", "list", 2, "item"], "data": { "value": "Baz" } } + ], + "completed": [{ "id": "0" }], + "hasNext": false + } + """ + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "me": { + "list": [ + { "item": { "id": "1", "value": "Foo" } }, + { "item": { "id": "2", "value": "Bar" } }, + { "item": { "id": "3", "value": "Baz" } } + ] + } + } + } + """ + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("me"), label = null), + DeferredFragmentIdentifier(path = listOf(), label = null), + ), + deferredJsonMerger.mergedFragmentIds + ) + } +} From 25916b693c0ed065de44972be35227cef610e82a Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 16 Dec 2024 14:44:46 +0100 Subject: [PATCH 02/21] Track pending fragment ids rather than completed ones. --- .../apollo/api/BooleanExpression.kt | 10 +- .../api/android/apollo-runtime.api | 2 +- .../api/apollo-runtime.klib.api | 4 +- .../apollo-runtime/api/jvm/apollo-runtime.api | 2 +- .../apollo/internal/DeferredJsonMerger.kt | 22 +- .../network/http/HttpNetworkTransport.kt | 2 +- .../websocket/WebSocketNetworkTransport.kt | 2 +- .../network/ws/WebSocketNetworkTransport.kt | 3 +- .../test/defer/DeferredJsonMergerTest.kt | 473 +++++++++++++++--- 9 files changed, 435 insertions(+), 85 deletions(-) diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt index 464b4833559..bddc85a8729 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt @@ -74,16 +74,20 @@ fun BooleanExpression.evaluate( return evaluate { when (it) { is BVariable -> !(variables?.contains(it.name) ?: false) - is BLabel -> hasDeferredFragment(deferredFragmentIdentifiers, croppedPath!!, it.label) + is BLabel -> !isDeferredFragmentPending(deferredFragmentIdentifiers, croppedPath!!, it.label) is BPossibleTypes -> it.possibleTypes.contains(typename) } } } -private fun hasDeferredFragment(deferredFragmentIdentifiers: Set?, path: List, label: String?): Boolean { +private fun isDeferredFragmentPending( + deferredFragmentIdentifiers: Set?, + path: List, + label: String?, +): Boolean { if (deferredFragmentIdentifiers == null) { // By default, parse all deferred fragments - this is the case when parsing from the normalized cache. - return true + return false } return deferredFragmentIdentifiers.contains(DeferredFragmentIdentifier(path, label)) } diff --git a/libraries/apollo-runtime/api/android/apollo-runtime.api b/libraries/apollo-runtime/api/android/apollo-runtime.api index 1eb9c3330bb..42df03b77cf 100644 --- a/libraries/apollo-runtime/api/android/apollo-runtime.api +++ b/libraries/apollo-runtime/api/android/apollo-runtime.api @@ -219,7 +219,7 @@ public final class com/apollographql/apollo/internal/DeferredJsonMerger { public fun ()V public final fun getHasNext ()Z public final fun getMerged ()Ljava/util/Map; - public final fun getMergedFragmentIds ()Ljava/util/Set; + public final fun getPendingFragmentIds ()Ljava/util/Set; public final fun isEmptyPayload ()Z public final fun merge (Ljava/util/Map;)Ljava/util/Map; public final fun merge (Lokio/BufferedSource;)Ljava/util/Map; diff --git a/libraries/apollo-runtime/api/apollo-runtime.klib.api b/libraries/apollo-runtime/api/apollo-runtime.klib.api index 29a86706313..315cd896802 100644 --- a/libraries/apollo-runtime/api/apollo-runtime.klib.api +++ b/libraries/apollo-runtime/api/apollo-runtime.klib.api @@ -205,8 +205,8 @@ final class com.apollographql.apollo.internal/DeferredJsonMerger { // com.apollo final val merged // com.apollographql.apollo.internal/DeferredJsonMerger.merged|{}merged[0] final fun (): kotlin.collections/Map // com.apollographql.apollo.internal/DeferredJsonMerger.merged.|(){}[0] - final val mergedFragmentIds // com.apollographql.apollo.internal/DeferredJsonMerger.mergedFragmentIds|{}mergedFragmentIds[0] - final fun (): kotlin.collections/Set // com.apollographql.apollo.internal/DeferredJsonMerger.mergedFragmentIds.|(){}[0] + final val pendingFragmentIds // com.apollographql.apollo.internal/DeferredJsonMerger.pendingFragmentIds|{}pendingFragmentIds[0] + final fun (): kotlin.collections/Set // com.apollographql.apollo.internal/DeferredJsonMerger.pendingFragmentIds.|(){}[0] final var hasNext // com.apollographql.apollo.internal/DeferredJsonMerger.hasNext|{}hasNext[0] final fun (): kotlin/Boolean // com.apollographql.apollo.internal/DeferredJsonMerger.hasNext.|(){}[0] diff --git a/libraries/apollo-runtime/api/jvm/apollo-runtime.api b/libraries/apollo-runtime/api/jvm/apollo-runtime.api index ae89653f943..b0f372bd9de 100644 --- a/libraries/apollo-runtime/api/jvm/apollo-runtime.api +++ b/libraries/apollo-runtime/api/jvm/apollo-runtime.api @@ -219,7 +219,7 @@ public final class com/apollographql/apollo/internal/DeferredJsonMerger { public fun ()V public final fun getHasNext ()Z public final fun getMerged ()Ljava/util/Map; - public final fun getMergedFragmentIds ()Ljava/util/Set; + public final fun getPendingFragmentIds ()Ljava/util/Set; public final fun isEmptyPayload ()Z public final fun merge (Ljava/util/Map;)Ljava/util/Map; public final fun merge (Lokio/BufferedSource;)Ljava/util/Map; diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt index f2eb734ad82..77595a2b40d 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt @@ -12,7 +12,7 @@ private typealias MutableJsonMap = MutableMap /** * Utility class for merging GraphQL JSON payloads received in multiple chunks when using the `@defer` directive. * - * Each call to [merge] will merge the given chunk into the [merged] Map, and will also update the [mergedFragmentIds] Set with the + * Each call to [merge] will merge the given chunk into the [merged] Map, and will also update the [pendingFragmentIds] Set with the * value of its `path` and `label` field. * * The fields in `data` are merged into the node found in [merged] at the path known by looking at the `id` field (for the first call to @@ -32,10 +32,8 @@ class DeferredJsonMerger { /** * Map of identifiers to their corresponding DeferredFragmentIdentifier, found in `pending`. */ - private val idsToDeferredFragmentIdentifiers = mutableMapOf() - - private val _mergedFragmentIds = mutableSetOf() - val mergedFragmentIds: Set = _mergedFragmentIds + private val _pendingFragmentIds = mutableMapOf() + val pendingFragmentIds: Set get() = _pendingFragmentIds.values.toSet() var hasNext: Boolean = true private set @@ -95,7 +93,7 @@ class DeferredJsonMerger { val id = pendingItem["id"] as String val path = pendingItem["path"] as List val label = pendingItem["label"] as String? - idsToDeferredFragmentIdentifiers[id] = DeferredFragmentIdentifier(path = path, label = label) + _pendingFragmentIds[id] = DeferredFragmentIdentifier(path = path, label = label) } } } @@ -104,16 +102,14 @@ class DeferredJsonMerger { val completed = payload["completed"] as? List if (completed != null) { for (completedItem in completed) { + // Merge errors (if any) of the completed item val errors = completedItem["errors"] as? List if (errors != null) { - // Merge errors (if any) of the completed item getOrPutMergedErrors() += errors } else { - // No errors: we have merged all the fields of the fragment so it can be parsed + // Fragment is no longer pending - only if there were no errors val id = completedItem["id"] as String - val deferredFragmentIdentifier = idsToDeferredFragmentIdentifiers.remove(id) - ?: error("Id '$id' not found in pending results") - _mergedFragmentIds += deferredFragmentIdentifier + _pendingFragmentIds.remove(id) ?: error("Id '$id' not found in pending results") } } } @@ -123,7 +119,7 @@ class DeferredJsonMerger { val id = incrementalItem["id"] as String? ?: error("No id found in incremental item") val data = incrementalItem["data"] as JsonMap? ?: error("No data found in incremental item") val subPath = incrementalItem["subPath"] as List? ?: emptyList() - val path = (idsToDeferredFragmentIdentifiers[id]?.path ?: error("Id '$id' not found in pending results")) + subPath + val path = (_pendingFragmentIds[id]?.path ?: error("Id '$id' not found in pending results")) + subPath val mergedData = merged["data"] as JsonMap val nodeToMergeInto = nodeAtPath(mergedData, path) as MutableJsonMap deepMerge(nodeToMergeInto, data) @@ -165,7 +161,7 @@ class DeferredJsonMerger { fun reset() { _merged.clear() - _mergedFragmentIds.clear() + _pendingFragmentIds.clear() hasNext = true isEmptyPayload = false } diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt index b40b74a53c8..d96f14d39e5 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt @@ -222,7 +222,7 @@ private constructor( jsonMerger = DeferredJsonMerger() } val merged = jsonMerger.merge(part) - val deferredFragmentIds = jsonMerger.mergedFragmentIds + val deferredFragmentIds = jsonMerger.pendingFragmentIds val isLast = !jsonMerger.hasNext if (jsonMerger.isEmptyPayload) { diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt index 7dec7222547..1bfe1f0b98a 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt @@ -215,7 +215,7 @@ private class DefaultSubscriptionParser(private val request: } val (payload, mergedFragmentIds) = if (responseMap.isDeferred()) { - deferredJsonMerger.merge(responseMap) to deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.merge(responseMap) to deferredJsonMerger.pendingFragmentIds } else { responseMap to null } diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt index 80383932fcc..8ffda6b9285 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt @@ -45,7 +45,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.launch -import okio.use /** * A [NetworkTransport] that manages a single instance of a [WebSocketConnection]. @@ -304,7 +303,7 @@ private constructor( val responsePayload = response.payload val requestCustomScalarAdapters = request.executionContext[CustomScalarAdapters]!! val (payload, mergedFragmentIds) = if (responsePayload.isDeferred()) { - deferredJsonMerger.merge(responsePayload) to deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.merge(responsePayload) to deferredJsonMerger.pendingFragmentIds } else { responsePayload to null } diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt index ee2a1ff3f89..c0a9c4cf480 100644 --- a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt +++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt @@ -75,7 +75,12 @@ class DeferredJsonMergerTest { """ deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0") + ), + deferredJsonMerger.pendingFragmentIds + ) //language=JSON val payload2 = """ @@ -150,9 +155,9 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0") + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0") ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -232,10 +237,9 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -331,10 +335,10 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3_4), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -439,11 +443,9 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) } @@ -514,7 +516,13 @@ class DeferredJsonMergerTest { """ deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + ), + deferredJsonMerger.pendingFragmentIds + ) //language=JSON val payload2_3 = """ @@ -615,10 +623,10 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -743,11 +751,9 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) } @@ -838,7 +844,7 @@ class DeferredJsonMergerTest { } /** - * Example A from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + * Example A from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) */ @Test fun june2023ExampleA() { @@ -879,7 +885,12 @@ class DeferredJsonMergerTest { """ deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = null), + ), + deferredJsonMerger.pendingFragmentIds + ) //language=JSON val payload2 = """ @@ -912,15 +923,13 @@ class DeferredJsonMergerTest { deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( - setOf( - DeferredFragmentIdentifier(path = listOf(), label = null), - ), - deferredJsonMerger.mergedFragmentIds + setOf(), + deferredJsonMerger.pendingFragmentIds ) } /** - * Example A2 from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + * Example A2 from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) */ @Test fun june2023ExampleA2() { @@ -958,7 +967,12 @@ class DeferredJsonMergerTest { """ deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "D1"), + ), + deferredJsonMerger.pendingFragmentIds + ) //language=JSON val payload2 = """ @@ -998,9 +1012,9 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = "D1"), + DeferredFragmentIdentifier(path = listOf("f2", "c", "f"), label = "D2"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -1042,16 +1056,13 @@ class DeferredJsonMergerTest { deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( - setOf( - DeferredFragmentIdentifier(path = listOf(), label = "D1"), - DeferredFragmentIdentifier(path = listOf("f2", "c", "f"), label = "D2"), - ), - deferredJsonMerger.mergedFragmentIds + setOf(), + deferredJsonMerger.pendingFragmentIds ) } /** - * Example B1 from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + * Example B1 from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) */ @Test fun june2023ExampleB1() { @@ -1086,7 +1097,13 @@ class DeferredJsonMergerTest { """ deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + ), + deferredJsonMerger.pendingFragmentIds + ) //language=JSON val payload2 = """ @@ -1121,9 +1138,9 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -1161,16 +1178,13 @@ class DeferredJsonMergerTest { deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( - setOf( - DeferredFragmentIdentifier(path = listOf(), label = "Blue"), - DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), - ), - deferredJsonMerger.mergedFragmentIds + setOf(), + deferredJsonMerger.pendingFragmentIds ) } /** - * Example B2 from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + * Example B2 from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) */ @Test fun june2023ExampleB2() { @@ -1205,7 +1219,13 @@ class DeferredJsonMergerTest { """ deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + ), + deferredJsonMerger.pendingFragmentIds + ) //language=JSON val payload2 = """ @@ -1243,9 +1263,9 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -1283,16 +1303,13 @@ class DeferredJsonMergerTest { deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( - setOf( - DeferredFragmentIdentifier(path = listOf(), label = "Blue"), - DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), - ), - deferredJsonMerger.mergedFragmentIds + setOf(), + deferredJsonMerger.pendingFragmentIds ) } /** - * Example D from https://github.com/graphql/defer-stream-wg/discussions/69 (Nov 1 2024 version) + * Example D from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) */ @Test fun june2023ExampleD() { @@ -1318,7 +1335,13 @@ class DeferredJsonMergerTest { """ deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) - assertEquals(setOf(), deferredJsonMerger.mergedFragmentIds) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = null), + DeferredFragmentIdentifier(path = listOf("me"), label = null), + ), + deferredJsonMerger.pendingFragmentIds + ) //language=JSON val payload2 = """ @@ -1354,9 +1377,9 @@ class DeferredJsonMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = null), + DeferredFragmentIdentifier(path = listOf(), label = null), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds ) //language=JSON @@ -1388,11 +1411,339 @@ class DeferredJsonMergerTest { deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( + setOf(), + deferredJsonMerger.pendingFragmentIds + ) + } + + /** + * Example F from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleF() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": { + "me": {} + }, + "pending": [ + {"id": "0", "path": ["me"], "label": "B"} + ], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "me": {} + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = null), - DeferredFragmentIdentifier(path = listOf(), label = null), + DeferredFragmentIdentifier(path = listOf("me"), label = "B"), ), - deferredJsonMerger.mergedFragmentIds + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + {"id":"0" , "data": {"a": "A", "b": "B"}} + ], + "completed": [ + {"id": "0"} + ], + "hasNext": false + } + """ + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "a": "A", + "b": "B" + } + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf(), + deferredJsonMerger.pendingFragmentIds ) } + + /** + * Example G from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleG() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [{ "name": "My Project" }] + } + }, + "pending": [ + { "id": "0", "path": ["me"], "label": "Billing" }, + { "id": "1", "path": ["me"], "label": "Prev" } + ], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [{ "name": "My Project" }] + } + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("me"), label = "Billing"), + DeferredFragmentIdentifier(path = listOf("me"), label = "Prev"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "0", + "data": { + "tier": "BRONZE", + "renewalDate": "2023-03-20", + "latestInvoiceTotal": "${'$'}12.34" + } + } + ], + "completed": [{ "id": "0" }], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [{ "name": "My Project" }], + "tier": "BRONZE", + "renewalDate": "2023-03-20", + "latestInvoiceTotal": "${'$'}12.34" + } + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("me"), label = "Prev"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { + "id": "1", + "data": { "previousInvoices": [{ "name": "My Invoice" }] } + } + ], + "completed": [{ "id": "1" }], + "hasNext": false + } + """ + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [{ "name": "My Project" }], + "tier": "BRONZE", + "renewalDate": "2023-03-20", + "latestInvoiceTotal": "${'$'}12.34", + "previousInvoices": [{ "name": "My Invoice" }] + } + } + } + """ + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf(), + deferredJsonMerger.pendingFragmentIds + ) + } + + /** + * Example H from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleH() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": { + "me": {} + }, + "pending": [ + {"id": "0", "path": [], "label": "A"}, + {"id": "1", "path": ["me"], "label": "B"} + ], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "me": {} + } + } + """ + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf(), label = "A"), + DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "0", + "subPath": ["me"], + "data": { "foo": { "bar": {} } } + }, + { + "id": "0", + "subPath": ["me", "foo", "bar"], + "data": { + "baz": "BAZ" + } + } + ], + "completed": [ + {"id": "0"} + ], + "hasNext": true + } + """ + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "foo": { + "bar": { + "baz": "BAZ" + } + } + } + } + } + """ + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload3 = """ + { + "completed": [ + { + "id": "1", + "errors": [ + { + "message": "Cannot return null for non-nullable field Bar.qux.", + "locations": [ + { + "line": 1, + "column": 1 + } + ], + "path": ["foo", "bar", "qux"] + } + ] + } + ], + "hasNext": false + } + """ + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "me": { + "foo": { + "bar": { + "baz": "BAZ" + } + } + } + }, + "errors": [ + { + "message": "Cannot return null for non-nullable field Bar.qux.", + "locations": [ + { + "line": 1, + "column": 1 + } + ], + "path": ["foo", "bar", "qux"] + } + ] + } + """ + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + ), + deferredJsonMerger.pendingFragmentIds + ) + } } From 824cc85ba0a61de473369921cd16f662f08492f3 Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 16 Dec 2024 17:46:57 +0100 Subject: [PATCH 03/21] Update more tests --- .../apollo/internal/DeferredJsonMerger.kt | 2 +- .../kotlin/test/DeferNormalizedCacheTest.kt | 239 +++++++++++------- .../kotlin/test/DeferSubscriptionsTest.kt | 80 ------ .../src/commonTest/kotlin/test/DeferTest.kt | 217 ++++++---------- 4 files changed, 218 insertions(+), 320 deletions(-) delete mode 100644 tests/defer/src/commonTest/kotlin/test/DeferSubscriptionsTest.kt diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt index 77595a2b40d..747ac84990b 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt @@ -59,6 +59,7 @@ class DeferredJsonMerger { handleCompleted(payload) return merged } + handlePending(payload) val incrementalList = payload["incremental"] as? List if (incrementalList == null) { @@ -74,7 +75,6 @@ class DeferredJsonMerger { hasNext = payload["hasNext"] as Boolean? ?: false - handlePending(payload) handleCompleted(payload) (payload["extensions"] as? JsonMap)?.let { getOrPutExtensions() += it } diff --git a/tests/defer/src/commonTest/kotlin/test/DeferNormalizedCacheTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferNormalizedCacheTest.kt index 91bea1f6f93..e6bced95233 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferNormalizedCacheTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferNormalizedCacheTest.kt @@ -4,6 +4,7 @@ import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.api.ApolloRequest import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.Error +import com.apollographql.apollo.api.Error.Builder import com.apollographql.apollo.api.Operation import com.apollographql.apollo.cache.normalized.ApolloStore import com.apollographql.apollo.cache.normalized.FetchPolicy @@ -72,9 +73,8 @@ class DeferNormalizedCacheTest { // Fill the cache by doing a network only request val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) apolloClient.query(WithFragmentSpreadsQuery()).fetchPolicy(FetchPolicy.NetworkOnly).toFlow().collect() @@ -86,9 +86,20 @@ class DeferNormalizedCacheTest { // We get the last/fully formed data val cacheExpected = WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false))))) + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) ) assertEquals(cacheExpected, cacheActual) } @@ -99,9 +110,8 @@ class DeferNormalizedCacheTest { // Fill the cache by doing a first request val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) apolloClient.query(WithFragmentSpreadsQuery()).fetchPolicy(FetchPolicy.NetworkOnly).toFlow().collect() @@ -114,16 +124,26 @@ class DeferNormalizedCacheTest { val networkExpected = listOf( WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) - ), - WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) ), WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false))))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) ), ) assertEquals(networkExpected, networkActual) @@ -134,9 +154,8 @@ class DeferNormalizedCacheTest { apolloClient = apolloClient.newBuilder().fetchPolicy(FetchPolicy.CacheFirst).build() val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) @@ -148,16 +167,26 @@ class DeferNormalizedCacheTest { val networkExpected = listOf( WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) - ), - WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) ), WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false))))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) ), ) assertEquals(networkExpected, networkActual) @@ -176,9 +205,8 @@ class DeferNormalizedCacheTest { apolloClient = apolloClient.newBuilder().fetchPolicy(FetchPolicy.NetworkFirst).build() val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) @@ -188,16 +216,26 @@ class DeferNormalizedCacheTest { val networkExpected = listOf( WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) - ), - WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) ), WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false))))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) ), ) assertEquals(networkExpected, networkActual) @@ -216,9 +254,8 @@ class DeferNormalizedCacheTest { apolloClient = apolloClient.newBuilder().fetchPolicy(FetchPolicy.CacheAndNetwork).build() val jsonList1 = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"pending":[{"id":"0","path":["computers",0]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"isColor":false},"id":"2"}],"completed":[{"id":"0"},{"id":"2"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList1) @@ -232,10 +269,6 @@ class DeferNormalizedCacheTest { WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) ), - WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) - ), WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", @@ -245,9 +278,8 @@ class DeferNormalizedCacheTest { assertEquals(networkExpected, networkActual) val jsonList2 = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":true},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"0"},{"data":{"isColor":true},"id":"2"}],"completed":[{"id":"0"},{"id":"2"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList2) @@ -262,10 +294,6 @@ class DeferNormalizedCacheTest { WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null)) ), - WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", null)))) - ), WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", @@ -281,9 +309,8 @@ class DeferNormalizedCacheTest { apolloClient = apolloClient.newBuilder().fetchPolicy(FetchPolicy.CacheFirst).build() val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":null,"path":["computers",0,"screen"],"label":"b","errors":[{"message":"Cannot resolve isColor","locations":[{"line":1,"column":119}],"path":["computers",0,"screen","isColor"]}]}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2","errors":[{"message":"Error field","locations":[{"line":3,"column":35}],"path":["computers",0,"screen","isColor"]}]},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) @@ -299,36 +326,40 @@ class DeferNormalizedCacheTest { query, uuid, ).data(WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) - )).build(), + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) + ) + ).build(), - ApolloResponse.Builder( - query, - uuid, - ).data(WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) - )).build(), ApolloResponse.Builder( query, uuid, - ) - .data( - WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) - ) - ) - .errors( + ).data( + WithFragmentSpreadsQuery.Data( listOf( - Error.Builder(message = "Cannot resolve isColor") - .locations(listOf(Error.Location(1, 119))) - .path(listOf("computers", 0, "screen", "isColor")) - .build() + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), ) ) - .build(), + ).errors( + listOf( + Builder("Error field") + .locations(listOf(Error.Location(3, 35))) + .path(listOf("computers", 0, "screen", "isColor")) + .build() + ) + ).build() ) assertResponseListEquals(networkExpected, networkActual) @@ -337,7 +368,7 @@ class DeferNormalizedCacheTest { val exception = apolloClient.query(WithFragmentSpreadsQuery()).execute().exception check(exception is CacheMissException) assertIs(exception.suppressedExceptions.first()) - assertEquals("Object 'computers.0.screen' has no field named 'isColor'", exception.message) + assertEquals("Object 'computers.0' has no field named 'cpu'", exception.message) mockServer.awaitRequest() } @@ -404,9 +435,8 @@ class DeferNormalizedCacheTest { @Test fun mutation() = runTest(before = { setUp() }, after = { tearDown() }) { val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0],"label":"c"}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0],"label":"c"},{"id":"1","path":["computers",1],"label":"c"}],"hasNext":true}""", + """{"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) val networkActual = apolloClient.mutation(WithFragmentSpreadsMutation()).toFlow().toList().map { it.dataOrThrow() } @@ -414,16 +444,25 @@ class DeferNormalizedCacheTest { val networkExpected = listOf( WithFragmentSpreadsMutation.Data( - listOf(WithFragmentSpreadsMutation.Computer("Computer", "Computer1", null)) - ), - WithFragmentSpreadsMutation.Data( - listOf(WithFragmentSpreadsMutation.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) + listOf( + WithFragmentSpreadsMutation.Computer("Computer", "Computer1", null), + WithFragmentSpreadsMutation.Computer("Computer", "Computer2", null), + ) ), WithFragmentSpreadsMutation.Data( listOf(WithFragmentSpreadsMutation.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false))))) + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsMutation.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ) + ) ), ) assertEquals(networkExpected, networkActual) @@ -433,9 +472,20 @@ class DeferNormalizedCacheTest { // We get the last/fully formed data val cacheExpected = WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false))))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) ) assertEquals(cacheExpected, cacheActual) } @@ -443,9 +493,8 @@ class DeferNormalizedCacheTest { @Test fun mutationWithOptimisticDataFails() = runTest(before = { setUp() }, after = { tearDown() }) { val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0],"label":"c"}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0],"label":"c"},{"id":"1","path":["computers",1],"label":"c"}],"hasNext":true}""", + """{"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) val responses = apolloClient.mutation(WithFragmentSpreadsMutation()).optimisticUpdates( @@ -468,8 +517,8 @@ class DeferNormalizedCacheTest { return@runTest } val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386"},"path":["computers",0]}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":false,"incremental":[{"data":{"cpu":"386"},"id":"0"},{"data":{"cpu":"486"},"id":"1"}],"completed":[{"id":"0"},{"id":"1"}]}""", ) val multipartBody = mockServer.enqueueMultipart("application/json") multipartBody.enqueuePart(jsonList[0].encodeUtf8(), false) diff --git a/tests/defer/src/commonTest/kotlin/test/DeferSubscriptionsTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferSubscriptionsTest.kt deleted file mode 100644 index 2942cac0b13..00000000000 --- a/tests/defer/src/commonTest/kotlin/test/DeferSubscriptionsTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -package test - -import com.apollographql.apollo.ApolloClient -import com.apollographql.apollo.network.websocket.WebSocketNetworkTransport -import com.apollographql.apollo.testing.internal.runTest -import defer.WithFragmentSpreadsSubscription -import defer.WithInlineFragmentsSubscription -import defer.fragment.CounterFields -import kotlinx.coroutines.flow.toList -import kotlin.test.Ignore -import kotlin.test.Test -import kotlin.test.assertEquals - -/** - * This test is ignored on the CI because it requires a specific server to run. - * - * It can be manually tested by running the server from https://github.com/BoD/DeferDemo/tree/master/helix - */ -@Ignore -class DeferSubscriptionsTest { - private lateinit var apolloClient: ApolloClient - - private fun setUp() { - apolloClient = ApolloClient.Builder() - .serverUrl("http://localhost:4000/graphql") - .subscriptionNetworkTransport( - WebSocketNetworkTransport.Builder() - .serverUrl("ws://localhost:4000/graphql") - .build() - ) - .build() - } - - private fun tearDown() { - apolloClient.close() - } - - @Test - fun subscriptionWithInlineFragment() = runTest(before = { setUp() }, after = { tearDown() }) { - val expectedDataList = listOf( - // Emission 0, deferred payload 0 - WithInlineFragmentsSubscription.Data(WithInlineFragmentsSubscription.Count("Counter", 1, null)), - // Emission 0, deferred payload 1 - WithInlineFragmentsSubscription.Data(WithInlineFragmentsSubscription.Count("Counter", 1, WithInlineFragmentsSubscription.OnCounter(2))), - // Emission 1, deferred payload 0 - WithInlineFragmentsSubscription.Data(WithInlineFragmentsSubscription.Count("Counter", 2, null)), - // Emission 1, deferred payload 1 - WithInlineFragmentsSubscription.Data(WithInlineFragmentsSubscription.Count("Counter", 2, WithInlineFragmentsSubscription.OnCounter(4))), - // Emission 2, deferred payload 0 - WithInlineFragmentsSubscription.Data(WithInlineFragmentsSubscription.Count("Counter", 3, null)), - // Emission 2, deferred payload 1 - WithInlineFragmentsSubscription.Data(WithInlineFragmentsSubscription.Count("Counter", 3, WithInlineFragmentsSubscription.OnCounter(6))), - ) - - val actualDataList = apolloClient.subscription(WithInlineFragmentsSubscription()).toFlow().toList().map { it.dataOrThrow() } - assertEquals(expectedDataList, actualDataList) - } - - @Test - fun subscriptionWithFragmentSpreads() = runTest(before = { setUp() }, after = { tearDown() }) { - val expectedDataList = listOf( - // Emission 0, deferred payload 0 - WithFragmentSpreadsSubscription.Data(WithFragmentSpreadsSubscription.Count("Counter", 1, null)), - // Emission 0, deferred payload 1 - WithFragmentSpreadsSubscription.Data(WithFragmentSpreadsSubscription.Count("Counter", 1, CounterFields(2))), - // Emission 1, deferred payload 0 - WithFragmentSpreadsSubscription.Data(WithFragmentSpreadsSubscription.Count("Counter", 2, null)), - // Emission 1, deferred payload 1 - WithFragmentSpreadsSubscription.Data(WithFragmentSpreadsSubscription.Count("Counter", 2, CounterFields(4))), - // Emission 2, deferred payload 0 - WithFragmentSpreadsSubscription.Data(WithFragmentSpreadsSubscription.Count("Counter", 3, null)), - // Emission 2, deferred payload 1 - WithFragmentSpreadsSubscription.Data(WithFragmentSpreadsSubscription.Count("Counter", 3, CounterFields(6))), - ) - - val actualDataList = apolloClient.subscription(WithFragmentSpreadsSubscription()).toFlow().toList().map { it.dataOrThrow() } - assertEquals(expectedDataList, actualDataList) - } - -} diff --git a/tests/defer/src/commonTest/kotlin/test/DeferTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferTest.kt index d04b2dc3b79..9d7a80a4d00 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferTest.kt @@ -3,6 +3,7 @@ package test import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.Error +import com.apollographql.apollo.api.Error.Builder import com.apollographql.apollo.autoPersistedQueryInfo import com.apollographql.apollo.mpp.currentTimeMillis import com.apollographql.apollo.testing.internal.runTest @@ -43,11 +44,8 @@ class DeferTest { @Test fun deferWithFragmentSpreads() = runTest(before = { setUp() }, after = { tearDown() }) { val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) val expectedDataList = listOf( @@ -57,38 +55,20 @@ class DeferTest { WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), ) ), - WithFragmentSpreadsQuery.Data( - listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null))), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), - ) - ), - WithFragmentSpreadsQuery.Data( - listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null))), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", null))), - ) - ), WithFragmentSpreadsQuery.Data( listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false)))), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", null))), - ) - ), - WithFragmentSpreadsQuery.Data( - listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false)))), + ScreenFields(false) + ) + ) + ), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", - ScreenFields(true)))), + ScreenFields(true) + ) + ) + ), ) ), ) @@ -101,11 +81,8 @@ class DeferTest { @Test fun deferWithInlineFragments() = runTest(before = { setUp() }, after = { tearDown() }) { val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"b"}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"b"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"b"},{"id":"3","path":["computers",1,"screen"],"label":"b"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) val expectedDataList = listOf( @@ -115,38 +92,20 @@ class DeferTest { WithInlineFragmentsQuery.Computer("Computer", "Computer2", null), ) ), - WithInlineFragmentsQuery.Data( - listOf( - WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, - WithInlineFragmentsQuery.Screen("Screen", "640x480", null))), - WithInlineFragmentsQuery.Computer("Computer", "Computer2", null), - ) - ), - WithInlineFragmentsQuery.Data( - listOf( - WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, - WithInlineFragmentsQuery.Screen("Screen", "640x480", null))), - WithInlineFragmentsQuery.Computer("Computer", "Computer2", WithInlineFragmentsQuery.OnComputer("486", 1996, - WithInlineFragmentsQuery.Screen("Screen", "800x600", null))), - ) - ), - WithInlineFragmentsQuery.Data( - listOf( - WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, - WithInlineFragmentsQuery.Screen("Screen", "640x480", - WithInlineFragmentsQuery.OnScreen(false)))), - WithInlineFragmentsQuery.Computer("Computer", "Computer2", WithInlineFragmentsQuery.OnComputer("486", 1996, - WithInlineFragmentsQuery.Screen("Screen", "800x600", null))), - ) - ), WithInlineFragmentsQuery.Data( listOf( WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, WithInlineFragmentsQuery.Screen("Screen", "640x480", - WithInlineFragmentsQuery.OnScreen(false)))), + WithInlineFragmentsQuery.OnScreen(false) + ) + ) + ), WithInlineFragmentsQuery.Computer("Computer", "Computer2", WithInlineFragmentsQuery.OnComputer("486", 1996, WithInlineFragmentsQuery.Screen("Screen", "800x600", - WithInlineFragmentsQuery.OnScreen(true)))), + WithInlineFragmentsQuery.OnScreen(true) + ) + ) + ), ) ), ) @@ -159,11 +118,8 @@ class DeferTest { @Test fun deferWithFragmentSpreadsAndError() = runTest(before = { setUp() }, after = { tearDown() }) { val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":null,"path":["computers",0,"screen"],"label":"b","errors":[{"message":"Cannot resolve isColor","locations":[{"line":1,"column":119}],"path":["computers",0,"screen","isColor"]}]}],"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2","errors":[{"message":"Error field","locations":[{"line":3,"column":35}],"path":["computers",0,"screen","isColor"]}]},{"id":"3"}]}""", ) val query = WithFragmentSpreadsQuery() @@ -178,58 +134,10 @@ class DeferTest { WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), ) - )).build(), - - ApolloResponse.Builder( - query, - uuid, - ).data( - WithFragmentSpreadsQuery.Data( - listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null))), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), - ) - ) - ).build(), - - ApolloResponse.Builder( - query, - uuid, ) - .data( - WithFragmentSpreadsQuery.Data( - listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null))), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), - ) - ) - ) - .errors( - listOf( - Error.Builder(message = "Cannot resolve isColor") - .locations(listOf(Error.Location(1, 119))) - .path(listOf("computers", 0, "screen", "isColor")) - .build() - ) - ) - .build(), - - ApolloResponse.Builder( - query, - uuid, - ).data( - WithFragmentSpreadsQuery.Data( - listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null))), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", null))), - ) - ) ).build(), + ApolloResponse.Builder( query, uuid, @@ -237,13 +145,25 @@ class DeferTest { WithFragmentSpreadsQuery.Data( listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null))), + ComputerFields.Screen("Screen", "640x480", null) + ) + ), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", - ScreenFields(true)))), + ScreenFields(true) + ) + ) + ), ) ) - ).build(), + ).errors( + listOf( + Builder("Error field") + .locations(listOf(Error.Location(3, 35))) + .path(listOf("computers", 0, "screen", "isColor")) + .build() + ) + ).build() ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) @@ -270,11 +190,8 @@ class DeferTest { } val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) jsonList.withIndex().forEach { (index, value) -> @@ -292,21 +209,27 @@ class DeferTest { @Test fun emptyPayloadsAreIgnored() = runTest(before = { setUp() }, after = { tearDown() }) { val jsonWithEmptyPayload = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386"},"path":["computers",0]}],"hasNext":true}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"incremental":[{"data":{"cpu":"386"},"id":"0"},{"data":{"cpu":"486"},"id":"1"}],"completed":[{"id":"0"},{"id":"1"}]}""", """{"hasNext":false}""", ) val jsonWithoutEmptyPayload = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"computer1"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386"},"path":["computers",0]}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":false,"incremental":[{"data":{"cpu":"386"},"id":"0"},{"data":{"cpu":"486"},"id":"1"}],"completed":[{"id":"0"},{"id":"1"}]}""", ) val expectedDataList = listOf( SimpleDeferQuery.Data( - listOf(SimpleDeferQuery.Computer("Computer", "computer1", null)) + listOf( + SimpleDeferQuery.Computer("Computer", "Computer1", null), + SimpleDeferQuery.Computer("Computer", "Computer2", null), + ) ), SimpleDeferQuery.Data( - listOf(SimpleDeferQuery.Computer("Computer", "computer1", SimpleDeferQuery.OnComputer("386"))) + listOf( + SimpleDeferQuery.Computer("Computer", "Computer1", SimpleDeferQuery.OnComputer("386")), + SimpleDeferQuery.Computer("Computer", "Computer2", SimpleDeferQuery.OnComputer("486")), + ) ), ) @@ -327,11 +250,8 @@ class DeferTest { .build() val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) val finalResponse = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().last() @@ -341,10 +261,16 @@ class DeferTest { listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false)))), + ScreenFields(false) + ) + ) + ), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", - ScreenFields(true)))), + ScreenFields(true) + ) + ) + ), ) ), finalResponse.dataOrThrow() @@ -360,11 +286,8 @@ class DeferTest { mockServer.enqueueString("""{"errors":[{"message":"PersistedQueryNotFound"}]}""") val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", - """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) val finalResponse = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().last() @@ -374,10 +297,16 @@ class DeferTest { listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false)))), + ScreenFields(false) + ) + ) + ), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", - ScreenFields(true)))), + ScreenFields(true) + ) + ) + ), ) ), finalResponse.dataOrThrow() From 8840a506baa139a9a3018ebe5af22b04dcf7944d Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 16 Dec 2024 19:41:00 +0100 Subject: [PATCH 04/21] Add Apollo Server end-to-end tests --- .../defer-with-apollo-server-tests.yml | 37 ++ .../apollo/internal/DeferredJsonMerger.kt | 14 +- tests/defer/README.md | 13 + tests/defer/apollo-server/README.md | 4 + tests/defer/apollo-server/computers.graphqls | 27 ++ tests/defer/apollo-server/computers.js | 40 ++ tests/defer/apollo-server/package.json | 18 + .../patches/@apollo+server+4.11.2.patch | 28 ++ tests/defer/build.gradle.kts | 9 +- .../commonMain/graphql/base/operation.graphql | 18 + .../kotlin/test/DeferWithApolloServerTest.kt | 360 ++++++++++++++++++ 11 files changed, 559 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/defer-with-apollo-server-tests.yml create mode 100644 tests/defer/apollo-server/README.md create mode 100644 tests/defer/apollo-server/computers.graphqls create mode 100644 tests/defer/apollo-server/computers.js create mode 100644 tests/defer/apollo-server/package.json create mode 100644 tests/defer/apollo-server/patches/@apollo+server+4.11.2.patch create mode 100644 tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt diff --git a/.github/workflows/defer-with-apollo-server-tests.yml b/.github/workflows/defer-with-apollo-server-tests.yml new file mode 100644 index 00000000000..8397a845919 --- /dev/null +++ b/.github/workflows/defer-with-apollo-server-tests.yml @@ -0,0 +1,37 @@ +name: defer-router-tests + +on: + schedule: + - cron: '0 3 * * *' +env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + +jobs: + defer-with-router-tests: + runs-on: ubuntu-latest + if: github.repository == 'apollographql/apollo-kotlin' + steps: + - name: Checkout project + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 + + - name: Install and run graph + working-directory: tests/defer/apollo-server/ + run: | + npm install --legacy-peer-deps + npx patch-package + APOLLO_PORT=4000 npm start & + + - name: Setup Java + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 #v4.2.1 + with: + distribution: 'temurin' + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda #v3.4.2 + + - name: Run Apollo Kotlin @defer tests + env: + DEFER_WITH_APOLLO_SERVER_TESTS: true + run: | + ./gradlew --no-daemon --console plain -p tests :defer:allTests diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt index 747ac84990b..c0d0e0d1d09 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt @@ -52,30 +52,29 @@ class DeferredJsonMerger { } fun merge(payload: JsonMap): JsonMap { + val completed = payload["completed"] as? List if (merged.isEmpty()) { // Initial payload, no merging needed (strip some fields that should not appear in the final result) _merged += payload - "hasNext" - "pending" handlePending(payload) - handleCompleted(payload) + handleCompleted(completed) return merged } handlePending(payload) val incrementalList = payload["incremental"] as? List - if (incrementalList == null) { - isEmptyPayload = true - } else { - isEmptyPayload = false + if (incrementalList != null) { for (incrementalItem in incrementalList) { mergeIncrementalData(incrementalItem) // Merge errors (if any) of the incremental item (incrementalItem["errors"] as? List)?.let { getOrPutMergedErrors() += it } } } + isEmptyPayload = completed == null && incrementalList == null hasNext = payload["hasNext"] as Boolean? ?: false - handleCompleted(payload) + handleCompleted(completed) (payload["extensions"] as? JsonMap)?.let { getOrPutExtensions() += it } @@ -98,8 +97,7 @@ class DeferredJsonMerger { } } - private fun handleCompleted(payload: JsonMap) { - val completed = payload["completed"] as? List + private fun handleCompleted(completed: List?) { if (completed != null) { for (completedItem in completed) { // Merge errors (if any) of the completed item diff --git a/tests/defer/README.md b/tests/defer/README.md index 9e4ad207f3d..4f4ac085620 100644 --- a/tests/defer/README.md +++ b/tests/defer/README.md @@ -16,3 +16,16 @@ To run them locally: subgraph: `(cd tests/defer/router/subgraphs/computers && npm install && APOLLO_PORT=4001 npm start)&` 2. Run the router: `path/to/router --supergraph tests/defer/router/simple-supergraph.graphqls &` 3. Run the tests: `DEFER_WITH_ROUTER_TESTS=true ./gradlew -p tests :defer:allTests` + +## End-to-end tests with Apollo Server + +The tests in `DeferWithApolloServerTest` are not run by default (they are excluded in the gradle conf) because they +expect an instance of [Apollo Server](https://www.apollographql.com/docs/apollo-server) running locally. + +They are enabled only when running from the specific `defer-with-apollo-server-tests` CI workflow. + +To run them locally: + +1. Install and run the + subgraph: `(cd tests/defer/apollo-server && npm install --legacy-peer-deps && npx patch-package && APOLLO_PORT=4000 npm start)&` +2. Run the tests: `DEFER_WITH_APOLLO_SERVER_TESTS=true ./gradlew -p tests :defer:allTests` diff --git a/tests/defer/apollo-server/README.md b/tests/defer/apollo-server/README.md new file mode 100644 index 00000000000..ef149563b19 --- /dev/null +++ b/tests/defer/apollo-server/README.md @@ -0,0 +1,4 @@ +# Test server using Apollo Server, for `@defer` tests + +- This uses graphql-js `17.0.0-alpha.7`, which implements the latest draft of the `@defer` incremental format (as of 2024-12-16). +- Apollo Server `4.11.2` needs a patch (in `patches`) to surface this format in the responses. diff --git a/tests/defer/apollo-server/computers.graphqls b/tests/defer/apollo-server/computers.graphqls new file mode 100644 index 00000000000..4c410b7fe7e --- /dev/null +++ b/tests/defer/apollo-server/computers.graphqls @@ -0,0 +1,27 @@ +type Query { + computers: [Computer!]! + computer(id: ID!): Computer +} + +type Mutation { + computers: [Computer!]! +} + +type Computer { + id: ID! + cpu: String! + year: Int! + screen: Screen! + errorField: String + nonNullErrorField: String! +} + +type Screen { + resolution: String! + isColor: Boolean! +} + +directive @defer( + if: Boolean! = true + label: String +) on FRAGMENT_SPREAD | INLINE_FRAGMENT diff --git a/tests/defer/apollo-server/computers.js b/tests/defer/apollo-server/computers.js new file mode 100644 index 00000000000..d5cedcd16b0 --- /dev/null +++ b/tests/defer/apollo-server/computers.js @@ -0,0 +1,40 @@ +import {ApolloServer} from '@apollo/server'; +import {startStandaloneServer} from '@apollo/server/standalone'; +import {readFileSync} from 'fs'; + +const port = process.env.APOLLO_PORT || 4000; + +const computers = [ + {id: 'Computer1', cpu: "386", year: 1993, screen: {resolution: "640x480", isColor: false}}, + {id: 'Computer2', cpu: "486", year: 1996, screen: {resolution: "800x600", isColor: true}}, +] + +const typeDefs = readFileSync('./computers.graphqls', {encoding: 'utf-8'}); +const resolvers = { + Query: { + computers: (_, args, context) => { + return computers; + }, + computer: (_, args, context) => { + return computers.find(p => p.id === args.id); + } + }, + Mutation: { + computers: (_, args, context) => { + return computers; + } + }, + Computer: { + errorField: (_, args, context) => { + throw new Error("Error field"); + }, + nonNullErrorField: (_, args, context) => { + return null; + } + } +} +const server = new ApolloServer({typeDefs, resolvers}); +const {url} = await startStandaloneServer(server, { + listen: {port: port}, +}); +console.log(`🚀 Computers subgraph ready at ${url}`); diff --git a/tests/defer/apollo-server/package.json b/tests/defer/apollo-server/package.json new file mode 100644 index 00000000000..1b469e77c4e --- /dev/null +++ b/tests/defer/apollo-server/package.json @@ -0,0 +1,18 @@ +{ + "type": "module", + "name": "subgraph-computers", + "version": "1.1.0", + "description": "", + "main": "computers.js", + "scripts": { + "start": "node computers.js" + }, + "dependencies": { + "@apollo/server": "4.11.2", + "graphql": "17.0.0-alpha.7", + "patch-package": "^8.0.0" + }, + "keywords": [], + "author": "", + "license": "MIT" +} diff --git a/tests/defer/apollo-server/patches/@apollo+server+4.11.2.patch b/tests/defer/apollo-server/patches/@apollo+server+4.11.2.patch new file mode 100644 index 00000000000..d6a742855b7 --- /dev/null +++ b/tests/defer/apollo-server/patches/@apollo+server+4.11.2.patch @@ -0,0 +1,28 @@ +diff --git a/node_modules/@apollo/server/dist/esm/runHttpQuery.js b/node_modules/@apollo/server/dist/esm/runHttpQuery.js +index 96ef0ab..0d341fa 100644 +--- a/node_modules/@apollo/server/dist/esm/runHttpQuery.js ++++ b/node_modules/@apollo/server/dist/esm/runHttpQuery.js +@@ -187,6 +187,7 @@ function orderExecutionResultFields(result) { + } + function orderInitialIncrementalExecutionResultFields(result) { + return { ++ ...result, + hasNext: result.hasNext, + errors: result.errors, + data: result.data, +@@ -196,6 +197,7 @@ function orderInitialIncrementalExecutionResultFields(result) { + } + function orderSubsequentIncrementalExecutionResultFields(result) { + return { ++ ...result, + hasNext: result.hasNext, + incremental: orderIncrementalResultFields(result.incremental), + extensions: result.extensions, +@@ -203,6 +205,7 @@ function orderSubsequentIncrementalExecutionResultFields(result) { + } + function orderIncrementalResultFields(incremental) { + return incremental?.map((i) => ({ ++ ...i, + hasNext: i.hasNext, + errors: i.errors, + path: i.path, diff --git a/tests/defer/build.gradle.kts b/tests/defer/build.gradle.kts index 6448f9c36d0..df6652fc118 100644 --- a/tests/defer/build.gradle.kts +++ b/tests/defer/build.gradle.kts @@ -74,5 +74,12 @@ tasks.withType(AbstractTestTask::class.java) { } else { filter.setExcludePatterns("test.DeferWithRouterTest") } -} + // Run the defer with Apollo Server tests only from a specific CI job + val runDeferWithApolloServerTests = System.getenv("DEFER_WITH_APOLLO_SERVER_TESTS").toBoolean() + if (runDeferWithApolloServerTests) { + filter.setIncludePatterns("test.DeferWithApolloServerTest") + } else { + filter.setExcludePatterns("test.DeferWithApolloServerTest") + } +} diff --git a/tests/defer/src/commonMain/graphql/base/operation.graphql b/tests/defer/src/commonMain/graphql/base/operation.graphql index fad24d09441..3f6c144b4a2 100644 --- a/tests/defer/src/commonMain/graphql/base/operation.graphql +++ b/tests/defer/src/commonMain/graphql/base/operation.graphql @@ -109,6 +109,15 @@ query CanDeferAFragmentThatIsAlsoNotDeferredDeferredFragmentIsFirstQuery { } } +query DeferFragmentThatIsAlsoNotDeferredIsSkipped1Query { + computer(id: "Computer1") { + screen { + ...ScreenFields @defer + ...ScreenFields + } + } +} + query CanDeferAFragmentThatIsAlsoNotDeferredNotDeferredFragmentIsFirstQuery { computer(id: "Computer1") { screen { @@ -118,6 +127,15 @@ query CanDeferAFragmentThatIsAlsoNotDeferredNotDeferredFragmentIsFirstQuery { } } +query DeferFragmentThatIsAlsoNotDeferredIsSkipped2Query { + computer(id: "Computer1") { + screen { + ...ScreenFields + ...ScreenFields @defer + } + } +} + query HandlesErrorsThrownInDeferredFragmentsQuery { computer(id: "Computer1") { id diff --git a/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt new file mode 100644 index 00000000000..b04a788138b --- /dev/null +++ b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt @@ -0,0 +1,360 @@ +package test + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.ApolloResponse +import com.apollographql.apollo.api.Error +import com.apollographql.apollo.api.Optional +import com.apollographql.apollo.testing.internal.runTest +import com.benasher44.uuid.uuid4 +import defer.CanDeferFragmentsOnTheTopLevelQueryFieldQuery +import defer.CanDisableDeferUsingIfArgumentQuery +import defer.DeferFragmentThatIsAlsoNotDeferredIsSkipped1Query +import defer.DeferFragmentThatIsAlsoNotDeferredIsSkipped2Query +import defer.DoesNotDisableDeferWithNullIfArgumentQuery +import defer.HandlesErrorsThrownInDeferredFragmentsQuery +import defer.HandlesNonNullableErrorsThrownInDeferredFragmentsQuery +import defer.HandlesNonNullableErrorsThrownOutsideDeferredFragmentsQuery +import defer.WithFragmentSpreadsMutation +import defer.WithFragmentSpreadsQuery +import defer.WithInlineFragmentsQuery +import defer.fragment.ComputerErrorField +import defer.fragment.ComputerFields +import defer.fragment.FragmentOnQuery +import defer.fragment.ScreenFields +import kotlinx.coroutines.flow.toList +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * End-to-end tests for `@defer`. + * + * These tests are not run by default (they are excluded in the gradle conf) because they expect an instance of + * [Apollo Server](https://www.apollographql.com/docs/apollo-server) running locally. + * + * They are enabled only when running from the specific `defer-with-apollo-server-tests` CI workflow. + */ +class DeferWithApolloServerTest { + private lateinit var apolloClient: ApolloClient + + private fun setUp() { + apolloClient = ApolloClient.Builder() + .serverUrl("http://127.0.0.1:4000/") + .build() + } + + private fun tearDown() { + apolloClient.close() + } + + @Test + fun deferWithFragmentSpreads() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true} + // {"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]} + val expectedDataList = listOf( + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) + ), + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) + ), + ) + + val actualDataList = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun deferWithInlineFragments() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true} + // {"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"b"},{"id":"3","path":["computers",1,"screen"],"label":"b"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]} + val expectedDataList = listOf( + WithInlineFragmentsQuery.Data( + listOf( + WithInlineFragmentsQuery.Computer("Computer", "Computer1", null), + WithInlineFragmentsQuery.Computer("Computer", "Computer2", null), + ) + ), + WithInlineFragmentsQuery.Data( + listOf( + WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, + WithInlineFragmentsQuery.Screen("Screen", "640x480", + WithInlineFragmentsQuery.OnScreen(false) + ) + ) + ), + WithInlineFragmentsQuery.Computer("Computer", "Computer2", WithInlineFragmentsQuery.OnComputer("486", 1996, + WithInlineFragmentsQuery.Screen("Screen", "800x600", + WithInlineFragmentsQuery.OnScreen(true) + ) + ) + ), + ) + ), + ) + val actualDataList = apolloClient.query(WithInlineFragmentsQuery()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun deferWithFragmentSpreadsMutation() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0],"label":"c"},{"id":"1","path":["computers",1],"label":"c"}],"hasNext":true} + // {"hasNext":false,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]} + val expectedDataList = listOf( + WithFragmentSpreadsMutation.Data( + listOf( + WithFragmentSpreadsMutation.Computer("Computer", "Computer1", null), + WithFragmentSpreadsMutation.Computer("Computer", "Computer2", null), + ) + ), + WithFragmentSpreadsMutation.Data( + listOf( + WithFragmentSpreadsMutation.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsMutation.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) + ), + ) + + val actualDataList = apolloClient.mutation(WithFragmentSpreadsMutation()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun canDisableDeferUsingIfArgument() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"__typename":"Computer","id":"Computer1","cpu":"386"},{"__typename":"Computer","id":"Computer2","cpu":"486"}]} + val expectedDataList = listOf( + CanDisableDeferUsingIfArgumentQuery.Data( + listOf( + CanDisableDeferUsingIfArgumentQuery.Computer("Computer", "Computer1", CanDisableDeferUsingIfArgumentQuery.OnComputer("386")), + CanDisableDeferUsingIfArgumentQuery.Computer("Computer", "Computer2", CanDisableDeferUsingIfArgumentQuery.OnComputer("486")), + ) + ), + ) + val actualDataList = apolloClient.query(CanDisableDeferUsingIfArgumentQuery()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun doesNotDisableDeferWithNullIfArgument() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true} + // {"hasNext":false,"incremental":[{"data":{"cpu":"386"},"id":"0"},{"data":{"cpu":"486"},"id":"1"}],"completed":[{"id":"0"},{"id":"1"}]} + val expectedDataList = listOf( + DoesNotDisableDeferWithNullIfArgumentQuery.Data( + listOf( + DoesNotDisableDeferWithNullIfArgumentQuery.Computer("Computer", "Computer1", null), + DoesNotDisableDeferWithNullIfArgumentQuery.Computer("Computer", "Computer2", null), + ) + ), + DoesNotDisableDeferWithNullIfArgumentQuery.Data( + listOf( + DoesNotDisableDeferWithNullIfArgumentQuery.Computer("Computer", "Computer1", DoesNotDisableDeferWithNullIfArgumentQuery.OnComputer("386")), + DoesNotDisableDeferWithNullIfArgumentQuery.Computer("Computer", "Computer2", DoesNotDisableDeferWithNullIfArgumentQuery.OnComputer("486")), + ) + ) + ) + val actualDataList = + apolloClient.query(DoesNotDisableDeferWithNullIfArgumentQuery(Optional.Absent)).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun canDeferFragmentsOnTheTopLevelQueryField() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"__typename":"Query"},"pending":[{"id":"0","path":[]}],"hasNext":true} + // {"hasNext":false,"incremental":[{"data":{"computers":[{"id":"Computer1"},{"id":"Computer2"}]},"id":"0"}],"completed":[{"id":"0"}]} + val expectedDataList = listOf( + CanDeferFragmentsOnTheTopLevelQueryFieldQuery.Data( + "Query", + null + ), + CanDeferFragmentsOnTheTopLevelQueryFieldQuery.Data( + "Query", + FragmentOnQuery( + listOf( + FragmentOnQuery.Computer("Computer1"), + FragmentOnQuery.Computer("Computer2"), + ) + ) + ), + ) + val actualDataList = apolloClient.query(CanDeferFragmentsOnTheTopLevelQueryFieldQuery()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun deferFragmentThatIsAlsoNotDeferredIsSkipped1() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computer":{"screen":{"__typename":"Screen","isColor":false}}}} + val expectedDataList = listOf( + DeferFragmentThatIsAlsoNotDeferredIsSkipped1Query.Data( + DeferFragmentThatIsAlsoNotDeferredIsSkipped1Query.Computer( + DeferFragmentThatIsAlsoNotDeferredIsSkipped1Query.Screen("Screen", ScreenFields(false)) + ) + ), + ) + val actualDataList = apolloClient.query(DeferFragmentThatIsAlsoNotDeferredIsSkipped1Query()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun deferFragmentThatIsAlsoNotDeferredIsSkipped2() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computer":{"screen":{"__typename":"Screen","isColor":false}}}} + val expectedDataList = listOf( + DeferFragmentThatIsAlsoNotDeferredIsSkipped2Query.Data( + DeferFragmentThatIsAlsoNotDeferredIsSkipped2Query.Computer( + DeferFragmentThatIsAlsoNotDeferredIsSkipped2Query.Screen("Screen", ScreenFields(false)) + ) + ), + ) + val actualDataList = apolloClient.query(DeferFragmentThatIsAlsoNotDeferredIsSkipped2Query()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun handlesErrorsThrownInDeferredFragments() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computer":{"__typename":"Computer","id":"Computer1"}},"pending":[{"id":"0","path":["computer"]}],"hasNext":true} + // {"hasNext":false,"incremental":[{"data":{"errorField":null},"errors":[{"message":"Error field","locations":[{"line":3,"column":43}],"path":["computer","errorField"],"extensions":{"code":"INTERNAL_SERVER_ERROR","stacktrace":["Error: Error field"," at Object.errorField (file:///Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/computers.js:29:19)"," at field.resolve (file:///Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/@apollo/server/dist/esm/utils/schemaInstrumentation.js:36:28)"," at executeField (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:567:20)"," at executeFields (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:476:22)"," at executeExecutionGroup (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:1855:14)"," at executor (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:1803:7)"," at pendingExecutionGroup.result (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:1825:58)"," at IncrementalGraph._onExecutionGroup (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/IncrementalGraph.js:192:33)"," at IncrementalGraph._promoteNonEmptyToRoot (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/IncrementalGraph.js:146:20)"," at IncrementalGraph.getNewRootNodes (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/IncrementalGraph.js:25:17)"]}}],"id":"0"}],"completed":[{"id":"0"}]} + val query = HandlesErrorsThrownInDeferredFragmentsQuery() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ) + .data( + HandlesErrorsThrownInDeferredFragmentsQuery.Data( + HandlesErrorsThrownInDeferredFragmentsQuery.Computer( + "Computer", "Computer1", null + ) + ) + ) + .build(), + + ApolloResponse.Builder( + query, + uuid, + ) + .data( + HandlesErrorsThrownInDeferredFragmentsQuery.Data( + HandlesErrorsThrownInDeferredFragmentsQuery.Computer( + "Computer", "Computer1", ComputerErrorField(null) + ) + ) + ) + .errors( + listOf( + Error.Builder(message = "Error field") + .path(listOf("computer", "errorField")) + .build() + ) + ) + .build(), + ) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun handlesNonNullableErrorsThrownInDeferredFragments() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computer":{"__typename":"Computer","id":"Computer1"}},"pending":[{"id":"0","path":["computer"]}],"hasNext":true} + // {"hasNext":false,"completed":[{"id":"0","errors":[{"message":"Cannot return null for non-nullable field Computer.nonNullErrorField.","locations":[{"line":3,"column":54}],"path":["computer","nonNullErrorField"]}]}]} + val query = HandlesNonNullableErrorsThrownInDeferredFragmentsQuery() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + HandlesNonNullableErrorsThrownInDeferredFragmentsQuery.Data( + HandlesNonNullableErrorsThrownInDeferredFragmentsQuery.Computer( + "Computer", "Computer1", null + ) + ) + ) + .build(), + + ApolloResponse.Builder( + query, + uuid, + ) + .data( + HandlesNonNullableErrorsThrownInDeferredFragmentsQuery.Data( + HandlesNonNullableErrorsThrownInDeferredFragmentsQuery.Computer( + "Computer", "Computer1", null + ) + ) + ) + .errors(listOf(Error.Builder(message = "Cannot return null for non-nullable field Computer.nonNullErrorField.") + .path(listOf("computer", "nonNullErrorField")).build() + ) + ) + .build(), + ) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun handlesNonNullableErrorsThrownOutsideDeferredFragments() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"errors":[{"message":"Cannot return null for non-nullable field Computer.nonNullErrorField.","locations":[{"line":1,"column":108}],"path":["computer","nonNullErrorField"],"extensions":{"code":"INTERNAL_SERVER_ERROR","stacktrace":["Error: Cannot return null for non-nullable field Computer.nonNullErrorField."," at completeValue (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:716:13)"," at executeField (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:580:23)"," at executeFields (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:476:22)"," at collectAndExecuteSubfields (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:1491:21)"," at completeObjectValue (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:1395:10)"," at completeValue (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:760:12)"," at executeField (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:580:23)"," at executeFields (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:476:22)"," at executeRootGroupedFieldSet (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:373:14)"," at executeOperation (/Users/bod/gitrepo/apollo-kotlin-0/tests/defer/apollo-server/node_modules/graphql/execution/execute.js:159:30)"]}}],"data":{"computer":null}} + val query = HandlesNonNullableErrorsThrownOutsideDeferredFragmentsQuery() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + HandlesNonNullableErrorsThrownOutsideDeferredFragmentsQuery.Data( + null + ) + ) + .errors( + listOf( + Error.Builder(message = "Cannot return null for non-nullable field Computer.nonNullErrorField.") + .path(listOf("computer", "nonNullErrorField")) + .build() + ) + ) + .build() + ) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } +} From 9f4001170fa5b7109141b17fbe2669d4a481508e Mon Sep 17 00:00:00 2001 From: BoD Date: Tue, 17 Dec 2024 15:26:28 +0100 Subject: [PATCH 05/21] Add a few more edge case tests --- .../defer-with-apollo-server-tests.yml | 37 ---- .github/workflows/defer-with-router-tests.yml | 28 +++ tests/defer/build.gradle.kts | 30 +-- .../commonMain/graphql/base/operation.graphql | 43 +++++ .../graphql/noTypename/operation.graphql | 11 ++ .../graphql/noTypename/schema.graphqls | 31 +++ .../kotlin/test/DeferWithApolloServerTest.kt | 176 ++++++++++++++++++ 7 files changed, 306 insertions(+), 50 deletions(-) delete mode 100644 .github/workflows/defer-with-apollo-server-tests.yml create mode 100644 tests/defer/src/commonMain/graphql/noTypename/operation.graphql create mode 100644 tests/defer/src/commonMain/graphql/noTypename/schema.graphqls diff --git a/.github/workflows/defer-with-apollo-server-tests.yml b/.github/workflows/defer-with-apollo-server-tests.yml deleted file mode 100644 index 8397a845919..00000000000 --- a/.github/workflows/defer-with-apollo-server-tests.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: defer-router-tests - -on: - schedule: - - cron: '0 3 * * *' -env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - -jobs: - defer-with-router-tests: - runs-on: ubuntu-latest - if: github.repository == 'apollographql/apollo-kotlin' - steps: - - name: Checkout project - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 - - - name: Install and run graph - working-directory: tests/defer/apollo-server/ - run: | - npm install --legacy-peer-deps - npx patch-package - APOLLO_PORT=4000 npm start & - - - name: Setup Java - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 #v4.2.1 - with: - distribution: 'temurin' - java-version: 17 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda #v3.4.2 - - - name: Run Apollo Kotlin @defer tests - env: - DEFER_WITH_APOLLO_SERVER_TESTS: true - run: | - ./gradlew --no-daemon --console plain -p tests :defer:allTests diff --git a/.github/workflows/defer-with-router-tests.yml b/.github/workflows/defer-with-router-tests.yml index aa833443d69..084ffb6156d 100644 --- a/.github/workflows/defer-with-router-tests.yml +++ b/.github/workflows/defer-with-router-tests.yml @@ -28,3 +28,31 @@ jobs: DEFER_WITH_ROUTER_TESTS: true run: | ./gradlew --no-daemon --console plain -p tests :defer:allTests + defer-with-apollo-server-tests: + runs-on: ubuntu-latest + if: github.repository == 'apollographql/apollo-kotlin' + steps: + - name: Checkout project + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 + + - name: Install and run graph + working-directory: tests/defer/apollo-server/ + run: | + npm install --legacy-peer-deps + npx patch-package + APOLLO_PORT=4000 npm start & + + - name: Setup Java + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 #v4.2.1 + with: + distribution: 'temurin' + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda #v3.4.2 + + - name: Run Apollo Kotlin @defer tests + env: + DEFER_WITH_APOLLO_SERVER_TESTS: true + run: | + ./gradlew --no-daemon --console plain -p tests :defer:allTests diff --git a/tests/defer/build.gradle.kts b/tests/defer/build.gradle.kts index df6652fc118..d90c4f5fd6d 100644 --- a/tests/defer/build.gradle.kts +++ b/tests/defer/build.gradle.kts @@ -50,6 +50,14 @@ fun configureApollo(generateKotlinModels: Boolean) { } } +apollo { + service("noTypename") { + packageName.set("defer.notypename") + srcDir("src/commonMain/graphql/noTypename") + addTypename.set("ifPolymorphic") + } +} + configureApollo(true) if (System.getProperty("idea.sync.active") == null) { registerJavaCodegenTestTask() @@ -67,19 +75,15 @@ fun com.apollographql.apollo.gradle.api.Service.configureConnection(generateKotl } tasks.withType(AbstractTestTask::class.java) { - // Run the defer with Router tests only from a specific CI job + // Run the defer with Router and defer with Apollo Server tests only from a specific CI job val runDeferWithRouterTests = System.getenv("DEFER_WITH_ROUTER_TESTS").toBoolean() - if (runDeferWithRouterTests) { - filter.setIncludePatterns("test.DeferWithRouterTest") - } else { - filter.setExcludePatterns("test.DeferWithRouterTest") - } - - // Run the defer with Apollo Server tests only from a specific CI job val runDeferWithApolloServerTests = System.getenv("DEFER_WITH_APOLLO_SERVER_TESTS").toBoolean() - if (runDeferWithApolloServerTests) { - filter.setIncludePatterns("test.DeferWithApolloServerTest") - } else { - filter.setExcludePatterns("test.DeferWithApolloServerTest") - } + filter.setIncludePatterns(*buildList { + if (runDeferWithRouterTests) add("test.DeferWithRouterTest") + if (runDeferWithApolloServerTests) add("test.DeferWithApolloServerTest") + }.toTypedArray()) + filter.setExcludePatterns(*buildList { + if (!runDeferWithRouterTests) add("test.DeferWithRouterTest") + if (!runDeferWithApolloServerTests) add("test.DeferWithApolloServerTest") + }.toTypedArray()) } diff --git a/tests/defer/src/commonMain/graphql/base/operation.graphql b/tests/defer/src/commonMain/graphql/base/operation.graphql index 3f6c144b4a2..e89921fd895 100644 --- a/tests/defer/src/commonMain/graphql/base/operation.graphql +++ b/tests/defer/src/commonMain/graphql/base/operation.graphql @@ -168,3 +168,46 @@ query HandlesNonNullableErrorsThrownOutsideDeferredFragmentsQuery { fragment ComputerIdField on Computer { id } + +query OverlappingQuery { + computer(id: "Computer1") { + id + ... on Computer @defer(label: "a") { + id + ... on Computer @defer(label: "b") { + id + cpu + year + } + } + } +} + +query Overlapping2Query { + computer(id: "Computer1") { + id + ... on Computer @defer(label: "a") { + id + } + ... on Computer @defer(label: "b") { + id + cpu + year + } + } +} + +query SubPathQuery { + computer(id: "Computer1") { + id + } + ... on Query @defer(label: "a") { + MyFragment: __typename + computer(id: "Computer1") { + id + screen { + isColor + } + } + } +} diff --git a/tests/defer/src/commonMain/graphql/noTypename/operation.graphql b/tests/defer/src/commonMain/graphql/noTypename/operation.graphql new file mode 100644 index 00000000000..90ca0b72dda --- /dev/null +++ b/tests/defer/src/commonMain/graphql/noTypename/operation.graphql @@ -0,0 +1,11 @@ +query SkippingEmptyFragmentQuery { + computer(id: "Computer1") { + ... on Computer @defer(label: "a") { + ... on Computer @defer(label: "b") { + ... on Computer @defer(label: "c") { + id + } + } + } + } +} diff --git a/tests/defer/src/commonMain/graphql/noTypename/schema.graphqls b/tests/defer/src/commonMain/graphql/noTypename/schema.graphqls new file mode 100644 index 00000000000..0892f0f4075 --- /dev/null +++ b/tests/defer/src/commonMain/graphql/noTypename/schema.graphqls @@ -0,0 +1,31 @@ +type Query { + computers: [Computer!]! + computer(id: ID!): Computer +} + +type Mutation { + computers: [Computer!]! +} + +type Subscription { + count(to: Int!): Counter! +} + +type Counter { + value: Int! + valueTimesTwo: Int! +} + +type Computer { + id: ID! + cpu: String! + year: Int! + screen: Screen! + errorField: String + nonNullErrorField: String! +} + +type Screen { + resolution: String! + isColor: Boolean! +} diff --git a/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt index b04a788138b..c6ddf98bcdd 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt @@ -14,6 +14,9 @@ import defer.DoesNotDisableDeferWithNullIfArgumentQuery import defer.HandlesErrorsThrownInDeferredFragmentsQuery import defer.HandlesNonNullableErrorsThrownInDeferredFragmentsQuery import defer.HandlesNonNullableErrorsThrownOutsideDeferredFragmentsQuery +import defer.Overlapping2Query +import defer.OverlappingQuery +import defer.SubPathQuery import defer.WithFragmentSpreadsMutation import defer.WithFragmentSpreadsQuery import defer.WithInlineFragmentsQuery @@ -21,6 +24,7 @@ import defer.fragment.ComputerErrorField import defer.fragment.ComputerFields import defer.fragment.FragmentOnQuery import defer.fragment.ScreenFields +import defer.notypename.SkippingEmptyFragmentQuery import kotlinx.coroutines.flow.toList import kotlin.test.Test import kotlin.test.assertEquals @@ -357,4 +361,176 @@ class DeferWithApolloServerTest { val actualResponseList = apolloClient.query(query).toFlow().toList() assertResponseListEquals(expectedDataList, actualResponseList) } + + @Test + fun overlapping() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computer":{"__typename":"Computer","id":"Computer1"}},"pending":[{"id":"0","path":["computer"],"label":"b"}],"hasNext":true} + // {"hasNext":false,"incremental":[{"data":{"cpu":"386","year":1993},"id":"0"}],"completed":[{"id":"0"}]} + val query = OverlappingQuery() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + OverlappingQuery.Data( + OverlappingQuery.Computer( + "Computer", "Computer1", OverlappingQuery.OnComputer( + "Computer", "Computer1", null, + ) + ) + ) + ) + .build(), + + ApolloResponse.Builder( + query, + uuid, + ).data( + OverlappingQuery.Data( + OverlappingQuery.Computer( + "Computer", "Computer1", OverlappingQuery.OnComputer( + "Computer", "Computer1", OverlappingQuery.OnComputer1("Computer1", "386", 1993) + ) + ) + ) + ) + .build() + ) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun overlapping2() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computer":{"__typename":"Computer","id":"Computer1"}},"pending":[{"id":"0","path":["computer"],"label":"b"}],"hasNext":true} + // {"hasNext":false,"incremental":[{"data":{"cpu":"386","year":1993},"id":"0"}],"completed":[{"id":"0"}]} + val query = Overlapping2Query() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + Overlapping2Query.Data( + Overlapping2Query.Computer( + "Computer", "Computer1", Overlapping2Query.OnComputerDeferA("Computer1" + ), null + ) + ) + ) + .build(), + ApolloResponse.Builder( + query, + uuid, + ).data( + Overlapping2Query.Data( + Overlapping2Query.Computer( + "Computer", "Computer1", Overlapping2Query.OnComputerDeferA("Computer1" + ), Overlapping2Query.OnComputerDeferB( + "Computer1", "386", 1993 + ) + ) + ) + ) + .build() + ) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun subPath() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"__typename":"Query","computer":{"id":"Computer1"}},"pending":[{"id":"0","path":[],"label":"a"}],"hasNext":true} + // {"hasNext":false,"incremental":[{"data":{"screen":{"isColor":false}},"id":"0","subPath":["computer"]},{"data":{"MyFragment":"Query"},"id":"0"}],"completed":[{"id":"0"}]} + val query = SubPathQuery() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + SubPathQuery.Data( + "Query", SubPathQuery.Computer( + "Computer1" + ), null + ) + ) + .build(), + ApolloResponse.Builder( + query, + uuid, + ).data( + SubPathQuery.Data( + "Query", SubPathQuery.Computer( + "Computer1" + ), SubPathQuery.OnQuery( + "Query", SubPathQuery.Computer1( + "Computer1", + SubPathQuery.Screen(false + ) + ) + ) + ) + ) + .build() + ) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun skippingEmptyFragment() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computer":{}},"pending":[{"id":"0","path":["computer"],"label":"c"}],"hasNext":true} + // {"hasNext":false,"incremental":[{"data":{"id":"Computer1"},"id":"0"}],"completed":[{"id":"0"}]} + val query = SkippingEmptyFragmentQuery() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + SkippingEmptyFragmentQuery.Data( + SkippingEmptyFragmentQuery.Computer( + SkippingEmptyFragmentQuery.OnComputer( + SkippingEmptyFragmentQuery.OnComputer1( + null + ) + ) + ) + ) + ) + .build(), + + ApolloResponse.Builder( + query, + uuid, + ).data( + SkippingEmptyFragmentQuery.Data( + SkippingEmptyFragmentQuery.Computer( + SkippingEmptyFragmentQuery.OnComputer( + SkippingEmptyFragmentQuery.OnComputer1( + SkippingEmptyFragmentQuery.OnComputer2( + "Computer1" + ) + ) + ) + ) + ) + ) + .build() + ) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + } From 2259e5d62367976ed2e2cfe6f09159c20d416a3d Mon Sep 17 00:00:00 2001 From: BoD Date: Tue, 17 Dec 2024 16:08:43 +0100 Subject: [PATCH 06/21] Fix missed test --- .../src/jvmTest/kotlin/test/DeferJvmTest.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt b/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt index 40d36185026..03a39f6e738 100644 --- a/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt +++ b/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt @@ -4,11 +4,11 @@ import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.cache.http.HttpFetchPolicy import com.apollographql.apollo.cache.http.httpCache import com.apollographql.apollo.cache.http.httpFetchPolicy -import com.apollographql.mockserver.MockServer -import com.apollographql.mockserver.enqueueMultipart import com.apollographql.apollo.mpp.currentTimeMillis import com.apollographql.apollo.testing.awaitElement import com.apollographql.apollo.testing.internal.runTest +import com.apollographql.mockserver.MockServer +import com.apollographql.mockserver.enqueueMultipart import defer.WithFragmentSpreadsQuery import defer.fragment.ComputerFields import defer.fragment.ScreenFields @@ -60,11 +60,8 @@ class DeferJvmTest { } val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", - """{"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", - """{"incremental":[{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", - """{"incremental":[{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", - """{"incremental":[{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", + """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", ) for ((index, json) in jsonList.withIndex()) { @@ -83,10 +80,14 @@ class DeferJvmTest { listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false)))), + ScreenFields(false) + ) + ) + ), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", - ScreenFields(true)))), + ComputerFields.Screen("Screen", "800x600", ScreenFields(true)) + ) + ), ) ) From dccde73f7f3a88bc3971e6721e2dc6d323f7fc0d Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 18 Jul 2025 15:59:10 +0200 Subject: [PATCH 07/21] Support appending lists in DeferredJsonMerger (for @stream) --- .../apollo/internal/DeferredJsonMerger.kt | 31 +- .../test/defer/DeferredJsonMergerTest.kt | 2980 +++++++++++------ tests/defer/apollo-server/computers.graphqls | 7 + tests/defer/apollo-server/computers.js | 16 +- tests/defer/build.gradle.kts | 2 +- .../commonMain/graphql/base/operation.graphql | 13 + .../commonMain/graphql/base/schema.graphqls | 7 + .../kotlin/test/DeferWithApolloServerTest.kt | 116 + 8 files changed, 2052 insertions(+), 1120 deletions(-) diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt index c0d0e0d1d09..47a6fdb4204 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt @@ -114,22 +114,35 @@ class DeferredJsonMerger { } private fun mergeIncrementalData(incrementalItem: JsonMap) { - val id = incrementalItem["id"] as String? ?: error("No id found in incremental item") - val data = incrementalItem["data"] as JsonMap? ?: error("No data found in incremental item") + val id = incrementalItem["id"] as String? ?: error("No id found in incremental result") + val data = incrementalItem["data"] as JsonMap? + val items = incrementalItem["items"] as List? val subPath = incrementalItem["subPath"] as List? ?: emptyList() val path = (_pendingFragmentIds[id]?.path ?: error("Id '$id' not found in pending results")) + subPath val mergedData = merged["data"] as JsonMap - val nodeToMergeInto = nodeAtPath(mergedData, path) as MutableJsonMap - deepMerge(nodeToMergeInto, data) + val nodeToMergeInto = nodeAtPath(mergedData, path) + when { + data != null -> { + deepMergeObject(nodeToMergeInto as MutableJsonMap, data) + } + + items != null -> { + mergeList(nodeToMergeInto as MutableList, items) + } + + else -> { + error("Neither data nor items found in incremental result") + } + } } - private fun deepMerge(destination: MutableJsonMap, map: JsonMap) { - for ((key, value) in map) { + private fun deepMergeObject(destination: MutableJsonMap, obj: JsonMap) { + for ((key, value) in obj) { if (destination.containsKey(key) && destination[key] is MutableMap<*, *>) { // Objects: merge recursively val fieldDestination = destination[key] as MutableJsonMap val fieldMap = value as? JsonMap ?: error("'$key' is an object in destination but not in map") - deepMerge(destination = fieldDestination, map = fieldMap) + deepMergeObject(destination = fieldDestination, obj = fieldMap) } else { // Other types: add / overwrite destination[key] = value @@ -137,6 +150,10 @@ class DeferredJsonMerger { } } + private fun mergeList(destination: MutableList, items: List) { + destination.addAll(items) + } + private fun jsonToMap(json: BufferedSource): JsonMap = BufferedSourceJsonReader(json).readAny() as JsonMap diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt index c0a9c4cf480..f1c189ad919 100644 --- a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt +++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt @@ -1,5 +1,8 @@ +@file:OptIn(ApolloInternal::class) + package test.defer +import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.DeferredFragmentIdentifier import com.apollographql.apollo.api.json.BufferedSourceJsonReader import com.apollographql.apollo.api.json.readAny @@ -22,57 +25,57 @@ class DeferredJsonMergerTest { //language=JSON val payload1 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "screen": { - "isTouch": true - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true } - ] - }, - "pending": [ + }, { - "id": "0", - "path": [ - "computers", - 0 - ], - "label": "query:Query1:0" + "id": "Computer2", + "screen": { + "isTouch": false + } } - ], - "hasNext": true - } - """ + ] + }, + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "screen": { - "isTouch": true - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true } - ] - } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -84,73 +87,73 @@ class DeferredJsonMergerTest { //language=JSON val payload2 = """ - { - "incremental": [ - { - "data": { - "cpu": "386", - "year": 1993, - "screen": { - "resolution": "640x480" - } - }, - "id": "0" - } - ], - "completed": [ - { - "id": "0" - } - ], - "pending": [ - { - "id": "1", - "path": [ - "computers", - 1 - ], - "label": "query:Query1:0" - } - ], - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" - } - }, - "hasNext": true - } - """ + { + "incremental": [ + { + "data": { + "cpu": "386", + "year": 1993, + "screen": { + "resolution": "640x480" + } + }, + "id": "0" + } + ], + "completed": [ + { + "id": "0" + } + ], + "pending": [ + { + "id": "1", + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0" + } + ], + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + }, + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1_2 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false } - ] - }, - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" } + ] + }, + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" } - } - """ + } + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -162,77 +165,77 @@ class DeferredJsonMergerTest { //language=JSON val payload3 = """ - { - "incremental": [ - { - "data": { - "cpu": "486", - "year": 1996, - "screen": { - "resolution": "640x480" - } - }, - "id": "1" - } - ], - "completed": [ - { - "id": "1" - } - ], - "pending": [ - { - "id": "2", - "path": [ - "computers", - 0, - "screen" - ], - "label": "fragment:ComputerFields:0" - } - ], - "extensions": { - "duration": { - "amount": 25, - "unit": "ms" - } - }, - "hasNext": true - } - """ + { + "incremental": [ + { + "data": { + "cpu": "486", + "year": 1996, + "screen": { + "resolution": "640x480" + } + }, + "id": "1" + } + ], + "completed": [ + { + "id": "1" + } + ], + "pending": [ + { + "id": "2", + "path": [ + "computers", + 0, + "screen" + ], + "label": "fragment:ComputerFields:0" + } + ], + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" + } + }, + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, - { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480" - } + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480" } - ] - }, - "extensions": { - "duration": { - "amount": 25, - "unit": "ms" } + ] + }, + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( @@ -244,93 +247,93 @@ class DeferredJsonMergerTest { //language=JSON val payload4 = """ - { - "completed": [ + { + "completed": [ + { + "id": "2", + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + } + ] + } + ], + "pending": [ + { + "id": "3", + "path": [ + "computers", + 1, + "screen" + ], + "label": "fragment:ComputerFields:0" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4 = """ + { + "data": { + "computers": [ { - "id": "2", - "errors": [ - { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - } - ] - } - ], - "pending": [ + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, { - "id": "3", - "path": [ - "computers", - 1, - "screen" - ], - "label": "fragment:ComputerFields:0" + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480" + } } - ], - "hasNext": true - } - """ - //language=JSON - val mergedPayloads_1_2_3_4 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, + ] + }, + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480" - } + "line": 12, + "column": 11 } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" ] - }, - "errors": [ - { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - } - ], - "extensions": { - "duration": { - "amount": 25, - "unit": "ms" - } + } + ], + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload4.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3_4), deferredJsonMerger.merged) assertEquals( @@ -343,102 +346,102 @@ class DeferredJsonMergerTest { //language=JSON val payload5 = """ - { - "incremental": [ - { - "data": { - "isColor": false - }, - "id": "3", - "errors": [ - { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] - } - ] - } - ], - "completed": [ - { - "id": "3" - } - ], - "extensions": { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" - } - }, - "hasNext": false - } - """ - //language=JSON - val mergedPayloads_1_2_3_4_5 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, + { + "incremental": [ + { + "data": { + "isColor": false + }, + "id": "3", + "errors": [ { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480", - "isColor": false - } + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] } ] - }, - "errors": [ + } + ], + "completed": [ + { + "id": "3" + } + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + }, + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4_5 = """ + { + "data": { + "computers": [ { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } }, { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] - } - ], - "extensions": { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480", + "isColor": false + } } + ] + }, + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + }, + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + } + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload5.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) assertEquals( @@ -455,65 +458,65 @@ class DeferredJsonMergerTest { //language=JSON val payload1 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "screen": { - "isTouch": true - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } - } - ] - }, - "pending": [ + { + "data": { + "computers": [ { - "id": "0", - "path": [ - "computers", - 0 - ], - "label": "query:Query1:0" + "id": "Computer1", + "screen": { + "isTouch": true + } }, { - "id": "1", - "path": [ - "computers", - 1 - ], - "label": "query:Query1:0" + "id": "Computer2", + "screen": { + "isTouch": false + } } - ], - "hasNext": true - } - """ + ] + }, + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" + }, + { + "id": "1", + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "screen": { - "isTouch": true - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true } - ] - } - } - """ + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] + } + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -526,99 +529,99 @@ class DeferredJsonMergerTest { //language=JSON val payload2_3 = """ - { - "incremental": [ - { - "data": { - "cpu": "386", - "year": 1993, - "screen": { - "resolution": "640x480" - } - }, - "id": "0" - }, - { - "data": { - "cpu": "486", - "year": 1996, - "screen": { - "resolution": "640x480" - } - }, - "id": "1" - } - ], - "completed": [ - { - "id": "0" + { + "incremental": [ + { + "data": { + "cpu": "386", + "year": 1993, + "screen": { + "resolution": "640x480" + } }, - { - "id": "1" - } - ], - "pending": [ - { - "id": "2", - "path": [ - "computers", - 0, - "screen" - ], - "label": "fragment:ComputerFields:0" + "id": "0" + }, + { + "data": { + "cpu": "486", + "year": 1996, + "screen": { + "resolution": "640x480" + } }, - { - "id": "3", - "path": [ - "computers", - 1, - "screen" - ], - "label": "fragment:ComputerFields:0" - } - ], - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" - } + "id": "1" + } + ], + "completed": [ + { + "id": "0" }, - "hasNext": true - } - """ + { + "id": "1" + } + ], + "pending": [ + { + "id": "2", + "path": [ + "computers", + 0, + "screen" + ], + "label": "fragment:ComputerFields:0" + }, + { + "id": "3", + "path": [ + "computers", + 1, + "screen" + ], + "label": "fragment:ComputerFields:0" + } + ], + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + }, + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, - { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480" - } + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480" } - ] - }, - "extensions": { - "duration": { - "amount": 100, - "unit": "ms" } + ] + }, + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2_3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( @@ -631,122 +634,122 @@ class DeferredJsonMergerTest { //language=JSON val payload4_5 = """ - { - "incremental": [ - { - "data": { - "isColor": false - }, - "id": "3", - "errors": [ - { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] - } - ] - } - ], - "completed": [ - { - "id": "2", - "errors": [ - { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - } - ] + { + "incremental": [ + { + "data": { + "isColor": false }, - { - "id": "3" - } - ], - "extensions": { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" - } - }, - "hasNext": false - } - """ - //language=JSON - val mergedPayloads_1_2_3_4_5 = """ - { - "data": { - "computers": [ + "id": "3", + "errors": [ { - "id": "Computer1", - "cpu": "386", - "year": 1993, - "screen": { - "isTouch": true, - "resolution": "640x480" - } - }, + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + } + ] + } + ], + "completed": [ + { + "id": "2", + "errors": [ { - "id": "Computer2", - "cpu": "486", - "year": 1996, - "screen": { - "isTouch": false, - "resolution": "640x480", - "isColor": false - } + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] } ] }, - "errors": [ + { + "id": "3" + } + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + }, + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4_5 = """ + { + "data": { + "computers": [ { - "message": "Another error", - "locations": [ - { - "line": 1, - "column": 1 - } - ] + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } }, { - "message": "Cannot resolve isColor", - "locations": [ - { - "line": 12, - "column": 11 - } - ], - "path": [ - "computers", - 0, - "screen", - "isColor" - ] - } - ], - "extensions": { - "value": 42, - "duration": { - "amount": 130, - "unit": "ms" + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480", + "isColor": false + } } + ] + }, + "errors": [ + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + }, + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + } + ], + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload4_5.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) assertEquals( @@ -763,82 +766,82 @@ class DeferredJsonMergerTest { //language=JSON val payload1 = """ - { - "data": { - "computers": [ - { - "id": "Computer1", - "screen": { - "isTouch": true - } - }, - { - "id": "Computer2", - "screen": { - "isTouch": false - } - } - ] - }, - "pending": [ + { + "data": { + "computers": [ { - "id": "0", - "path": [ - "computers", - 0 - ], - "label": "query:Query1:0" + "id": "Computer1", + "screen": { + "isTouch": true + } }, { - "id": "1", - "path": [ - "computers", - 1 - ], - "label": "query:Query1:0" + "id": "Computer2", + "screen": { + "isTouch": false + } } - ], - "hasNext": true - } - """ + ] + }, + "pending": [ + { + "id": "0", + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0" + }, + { + "id": "1", + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0" + } + ], + "hasNext": true + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertFalse(deferredJsonMerger.isEmptyPayload) //language=JSON val payload2 = """ - { - "hasNext": true - } - """ + { + "hasNext": true + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertTrue(deferredJsonMerger.isEmptyPayload) //language=JSON val payload3 = """ - { - "incremental": [ - { - "data": { - "cpu": "386", - "year": 1993, - "screen": { - "resolution": "640x480" - } - }, - "id": "0" - } - ], - "hasNext": true - } - """ + { + "incremental": [ + { + "data": { + "cpu": "386", + "year": 1993, + "screen": { + "resolution": "640x480" + } + }, + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() deferredJsonMerger.merge(payload3.buffer()) assertFalse(deferredJsonMerger.isEmptyPayload) //language=JSON val payload4 = """ - { - "hasNext": false - } - """ + { + "hasNext": false + } + """.trimIndent() deferredJsonMerger.merge(payload4.buffer()) assertTrue(deferredJsonMerger.isEmptyPayload) } @@ -851,38 +854,49 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": { - "f2": { - "a": "a", - "b": "b", - "c": { - "d": "d", - "e": "e", - "f": { "h": "h", "i": "i" } + { + "data": { + "f2": { + "a": "a", + "b": "b", + "c": { + "d": "d", + "e": "e", + "f": { + "h": "h", + "i": "i" } } - }, - "pending": [{ "path": [], "id": "0" }], - "hasNext": true - } - """ + } + }, + "pending": [ + { + "path": [], + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "f2": { - "a": "a", - "b": "b", - "c": { - "d": "d", - "e": "e", - "f": { "h": "h", "i": "i" } + { + "data": { + "f2": { + "a": "a", + "b": "b", + "c": { + "d": "d", + "e": "e", + "f": { + "h": "h", + "i": "i" } } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -894,32 +908,55 @@ class DeferredJsonMergerTest { //language=JSON val payload2 = """ - { - "incremental": [ - { "id": "0", "data": { "MyFragment": "Query" } }, - { "id": "0", "subPath": ["f2", "c", "f"], "data": { "j": "j" } } - ], - "completed": [{ "id": "0" }], - "hasNext": false - } - """ + { + "incremental": [ + { + "id": "0", + "data": { + "MyFragment": "Query" + } + }, + { + "id": "0", + "subPath": [ + "f2", + "c", + "f" + ], + "data": { + "j": "j" + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() //language=JSON val mergedPayloads_1_2 = """ - { - "data": { - "f2": { - "a": "a", - "b": "b", - "c": { - "d": "d", - "e": "e", - "f": { "h": "h", "i": "i", "j": "j" } + { + "data": { + "f2": { + "a": "a", + "b": "b", + "c": { + "d": "d", + "e": "e", + "f": { + "h": "h", + "i": "i", + "j": "j" } - }, - "MyFragment": "Query" - } + } + }, + "MyFragment": "Query" } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -936,35 +973,50 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": {"f2": {"a": "A", "b": "B", "c": { - "d": "D", "e": "E", "f": { - "h": "H", "i": "I" + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I" + } } - }}}, - "pending": [{"id": "0", "path": [], "label": "D1"}], - "hasNext": true - } - """ + } + }, + "pending": [ + { + "id": "0", + "path": [], + "label": "D1" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "f2": { - "a": "A", - "b": "B", - "c": { - "d": "D", - "e": "E", - "f": { - "h": "H", - "i": "I" - } + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I" } } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -976,38 +1028,61 @@ class DeferredJsonMergerTest { //language=JSON val payload2 = """ - { - "incremental": [ - {"id": "0", "subPath": ["f2", "c", "f"], "data": {"j": "J", "k": "K"}} - ], - "pending": [{"id": "1", "path": ["f2", "c", "f"], "label": "D2"}], - "completed": [ - {"id": "0"} - ], - "hasNext": true - } - """ + { + "incremental": [ + { + "id": "0", + "subPath": [ + "f2", + "c", + "f" + ], + "data": { + "j": "J", + "k": "K" + } + } + ], + "pending": [ + { + "id": "1", + "path": [ + "f2", + "c", + "f" + ], + "label": "D2" + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1_2 = """ - { - "data": { - "f2": { - "a": "A", - "b": "B", - "c": { - "d": "D", - "e": "E", - "f": { - "h": "H", - "i": "I", - "j": "J", - "k": "K" - } + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I", + "j": "J", + "k": "K" } } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -1019,40 +1094,48 @@ class DeferredJsonMergerTest { //language=JSON val payload3 = """ - { - "incremental": [ - {"id": "1", "data": {"l": "L", "m": "M"}} - ], - "completed": [ - {"id": "1"} - ], - "hasNext": false - } - """ + { + "incremental": [ + { + "id": "1", + "data": { + "l": "L", + "m": "M" + } + } + ], + "completed": [ + { + "id": "1" + } + ], + "hasNext": false + } + """.trimIndent() //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "f2": { - "a": "A", - "b": "B", - "c": { - "d": "D", - "e": "E", - "f": { - "h": "H", - "i": "I", - "j": "J", - "k": "K", - "l": "L", - "m": "M" - } + { + "data": { + "f2": { + "a": "A", + "b": "B", + "c": { + "d": "D", + "e": "E", + "f": { + "h": "H", + "i": "I", + "j": "J", + "k": "K", + "l": "L", + "m": "M" } } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( @@ -1069,32 +1152,49 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": { - "a": { "b": { "c": { "d": "d" } } } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + } + } + } + }, + "pending": [ + { + "path": [], + "id": "0", + "label": "Blue" }, - "pending": [ - { "path": [], "id": "0", "label": "Blue" }, - { "path": ["a", "b"], "id": "1", "label": "Red" } - ], - "hasNext": true - } - """ + { + "path": [ + "a", + "b" + ], + "id": "1", + "label": "Red" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "a": { - "b": { - "c": { - "d": "d" - } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" } } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -1107,33 +1207,49 @@ class DeferredJsonMergerTest { //language=JSON val payload2 = """ - { - "incremental": [ - { "id": "1", "data": { "potentiallySlowFieldA": "potentiallySlowFieldA" } }, - { "id": "1", "data": { "e": { "f": "f" } } } - ], - "completed": [{ "id": "1" }], - "hasNext": true - } - """ + { + "incremental": [ + { + "id": "1", + "data": { + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + }, + { + "id": "1", + "data": { + "e": { + "f": "f" + } + } + } + ], + "completed": [ + { + "id": "1" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1_2 = """ - { - "data": { - "a": { - "b": { - "c": { - "d": "d" - }, - "e": { - "f": "f" - }, - "potentiallySlowFieldA": "potentiallySlowFieldA" - } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + }, + "potentiallySlowFieldA": "potentiallySlowFieldA" } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -1145,36 +1261,48 @@ class DeferredJsonMergerTest { //language=JSON val payload3 = """ - { - "incremental": [ - { "id": "0", "data": { "g": { "h": "h" }, "potentiallySlowFieldB": "potentiallySlowFieldB" } } - ], - "completed": [{ "id": "0" }], - "hasNext": false - } - """ + { + "incremental": [ + { + "id": "0", + "data": { + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "a": { - "b": { - "c": { - "d": "d" - }, - "e": { - "f": "f" - }, - "potentiallySlowFieldA": "potentiallySlowFieldA" - } - }, - "g": { - "h": "h" - }, - "potentiallySlowFieldB": "potentiallySlowFieldB" - } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + }, + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + }, + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( @@ -1191,32 +1319,49 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": { - "a": { "b": { "c": { "d": "d" } } } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + } + } + } + }, + "pending": [ + { + "path": [], + "id": "0", + "label": "Blue" }, - "pending": [ - { "path": [], "id": "0", "label": "Blue" }, - { "path": ["a", "b"], "id": "1", "label": "Red" } - ], - "hasNext": true - } - """ + { + "path": [ + "a", + "b" + ], + "id": "1", + "label": "Red" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "a": { - "b": { - "c": { - "d": "d" - } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" } } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -1229,36 +1374,55 @@ class DeferredJsonMergerTest { //language=JSON val payload2 = """ - { - "incremental": [ - { "id": "0", "data": { "g": { "h": "h" }, "potentiallySlowFieldB": "potentiallySlowFieldB" } }, - { "id": "1", "data": { "e": { "f": "f" } } } - ], - "completed": [{ "id": "0" }], - "hasNext": true - } - """ + { + "incremental": [ + { + "id": "0", + "data": { + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" + } + }, + { + "id": "1", + "data": { + "e": { + "f": "f" + } + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1_2 = """ - { - "data": { - "a": { - "b": { - "c": { - "d": "d" - }, - "e": { - "f": "f" - } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" } - }, - "g": { - "h": "h" - }, - "potentiallySlowFieldB": "potentiallySlowFieldB" - } + } + }, + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -1272,13 +1436,21 @@ class DeferredJsonMergerTest { val payload3 = """ { "incremental": [ - { "id": "1", "data": { "potentiallySlowFieldA": "potentiallySlowFieldA" } } + { + "id": "1", + "data": { + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + } + ], + "completed": [ + { + "id": "1" + } ], - "completed": [{ "id": "1" }], "hasNext": false } - """ - //language=JSON + """.trimIndent() val mergedPayloads_1_2_3 = """ { "data": { @@ -1316,23 +1488,33 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": { "me": {} }, - "pending": [ - { "path": [], "id": "0" }, - { "path": ["me"], "id": "1" } - ], - "hasNext": true - } - """ + { + "data": { + "me": {} + }, + "pending": [ + { + "path": [], + "id": "0" + }, + { + "path": [ + "me" + ], + "id": "1" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "me": {} - } + { + "data": { + "me": {} } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -1345,34 +1527,92 @@ class DeferredJsonMergerTest { //language=JSON val payload2 = """ - { - "incremental": [ - { - "id": "1", - "data": { "list": [{ "item": {} }, { "item": {} }, { "item": {} }] } - }, - { "id": "1", "subPath": ["list", 0, "item"], "data": { "id": "1" } }, - { "id": "1", "subPath": ["list", 1, "item"], "data": { "id": "2" } }, - { "id": "1", "subPath": ["list", 2, "item"], "data": { "id": "3" } } - ], - "completed": [{ "id": "1" }], - "hasNext": true - } - """ - //language=JSON - val mergedPayloads_1_2 = """ - { - "data": { - "me": { + { + "incremental": [ + { + "id": "1", + "data": { "list": [ - { "item": { "id": "1" } }, - { "item": { "id": "2" } }, - { "item": { "id": "3" } } + { + "item": {} + }, + { + "item": {} + }, + { + "item": {} + } ] } + }, + { + "id": "1", + "subPath": [ + "list", + 0, + "item" + ], + "data": { + "id": "1" + } + }, + { + "id": "1", + "subPath": [ + "list", + 1, + "item" + ], + "data": { + "id": "2" + } + }, + { + "id": "1", + "subPath": [ + "list", + 2, + "item" + ], + "data": { + "id": "3" + } + } + ], + "completed": [ + { + "id": "1" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "list": [ + { + "item": { + "id": "1" + } + }, + { + "item": { + "id": "2" + } + }, + { + "item": { + "id": "3" + } + } + ] } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -1384,30 +1624,82 @@ class DeferredJsonMergerTest { //language=JSON val payload3 = """ - { - "incremental": [ - { "id": "0", "subPath": ["me", "list", 0, "item"], "data": { "value": "Foo" } }, - { "id": "0", "subPath": ["me", "list", 1, "item"], "data": { "value": "Bar" } }, - { "id": "0", "subPath": ["me", "list", 2, "item"], "data": { "value": "Baz" } } - ], - "completed": [{ "id": "0" }], - "hasNext": false - } - """ + { + "incremental": [ + { + "id": "0", + "subPath": [ + "me", + "list", + 0, + "item" + ], + "data": { + "value": "Foo" + } + }, + { + "id": "0", + "subPath": [ + "me", + "list", + 1, + "item" + ], + "data": { + "value": "Bar" + } + }, + { + "id": "0", + "subPath": [ + "me", + "list", + 2, + "item" + ], + "data": { + "value": "Baz" + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "me": { - "list": [ - { "item": { "id": "1", "value": "Foo" } }, - { "item": { "id": "2", "value": "Bar" } }, - { "item": { "id": "3", "value": "Baz" } } - ] - } + { + "data": { + "me": { + "list": [ + { + "item": { + "id": "1", + "value": "Foo" + } + }, + { + "item": { + "id": "2", + "value": "Bar" + } + }, + { + "item": { + "id": "3", + "value": "Baz" + } + } + ] } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( @@ -1424,24 +1716,30 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": { - "me": {} - }, - "pending": [ - {"id": "0", "path": ["me"], "label": "B"} - ], - "hasNext": true - } - """ + { + "data": { + "me": {} + }, + "pending": [ + { + "id": "0", + "path": [ + "me" + ], + "label": "B" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "me": {} - } + { + "data": { + "me": {} } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -1453,27 +1751,35 @@ class DeferredJsonMergerTest { //language=JSON val payload2 = """ - { - "incremental": [ - {"id":"0" , "data": {"a": "A", "b": "B"}} - ], - "completed": [ - {"id": "0"} - ], - "hasNext": false - } - """ - //language=JSON - val mergedPayloads_1_2 = """ - { - "data": { - "me": { + { + "incremental": [ + { + "id": "0", + "data": { "a": "A", "b": "B" } } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "a": "A", + "b": "B" + } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -1490,33 +1796,53 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": { - "me": { - "id": 1, - "avatarUrl": "http://…", - "projects": [{ "name": "My Project" }] - } + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [ + { + "name": "My Project" + } + ] + } + }, + "pending": [ + { + "id": "0", + "path": [ + "me" + ], + "label": "Billing" }, - "pending": [ - { "id": "0", "path": ["me"], "label": "Billing" }, - { "id": "1", "path": ["me"], "label": "Prev" } - ], - "hasNext": true - } - """ + { + "id": "1", + "path": [ + "me" + ], + "label": "Prev" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "me": { - "id": 1, - "avatarUrl": "http://…", - "projects": [{ "name": "My Project" }] - } + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [ + { + "name": "My Project" + } + ] } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -1529,36 +1855,44 @@ class DeferredJsonMergerTest { //language=JSON val payload2 = """ - { - "incremental": [ - { - "id": "0", - "data": { - "tier": "BRONZE", - "renewalDate": "2023-03-20", - "latestInvoiceTotal": "${'$'}12.34" - } - } - ], - "completed": [{ "id": "0" }], - "hasNext": true - } - """ - //language=JSON - val mergedPayloads_1_2 = """ - { - "data": { - "me": { - "id": 1, - "avatarUrl": "http://…", - "projects": [{ "name": "My Project" }], + { + "incremental": [ + { + "id": "0", + "data": { "tier": "BRONZE", "renewalDate": "2023-03-20", "latestInvoiceTotal": "${'$'}12.34" } } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [ + { + "name": "My Project" + } + ], + "tier": "BRONZE", + "renewalDate": "2023-03-20", + "latestInvoiceTotal": "${'$'}12.34" + } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -1570,33 +1904,51 @@ class DeferredJsonMergerTest { //language=JSON val payload3 = """ - { - "incremental": [ - { - "id": "1", - "data": { "previousInvoices": [{ "name": "My Invoice" }] } + { + "incremental": [ + { + "id": "1", + "data": { + "previousInvoices": [ + { + "name": "My Invoice" + } + ] } - ], - "completed": [{ "id": "1" }], - "hasNext": false - } - """ + } + ], + "completed": [ + { + "id": "1" + } + ], + "hasNext": false + } + """.trimIndent() //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "me": { - "id": 1, - "avatarUrl": "http://…", - "projects": [{ "name": "My Project" }], - "tier": "BRONZE", - "renewalDate": "2023-03-20", - "latestInvoiceTotal": "${'$'}12.34", - "previousInvoices": [{ "name": "My Invoice" }] - } + { + "data": { + "me": { + "id": 1, + "avatarUrl": "http://…", + "projects": [ + { + "name": "My Project" + } + ], + "tier": "BRONZE", + "renewalDate": "2023-03-20", + "latestInvoiceTotal": "${'$'}12.34", + "previousInvoices": [ + { + "name": "My Invoice" + } + ] } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( @@ -1613,25 +1965,35 @@ class DeferredJsonMergerTest { val deferredJsonMerger = DeferredJsonMerger() //language=JSON val payload1 = """ - { - "data": { - "me": {} + { + "data": { + "me": {} + }, + "pending": [ + { + "id": "0", + "path": [], + "label": "A" }, - "pending": [ - {"id": "0", "path": [], "label": "A"}, - {"id": "1", "path": ["me"], "label": "B"} - ], - "hasNext": true - } - """ + { + "id": "1", + "path": [ + "me" + ], + "label": "B" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1 = """ - { - "data": { - "me": {} - } + { + "data": { + "me": {} } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) assertEquals( @@ -1644,41 +2006,53 @@ class DeferredJsonMergerTest { //language=JSON val payload2 = """ - { - "incremental": [ - { - "id": "0", - "subPath": ["me"], - "data": { "foo": { "bar": {} } } - }, - { - "id": "0", - "subPath": ["me", "foo", "bar"], - "data": { - "baz": "BAZ" + { + "incremental": [ + { + "id": "0", + "subPath": [ + "me" + ], + "data": { + "foo": { + "bar": {} } } - ], - "completed": [ - {"id": "0"} - ], - "hasNext": true - } - """ + }, + { + "id": "0", + "subPath": [ + "me", + "foo", + "bar" + ], + "data": { + "baz": "BAZ" + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": true + } + """.trimIndent() //language=JSON val mergedPayloads_1_2 = """ - { - "data": { - "me": { - "foo": { - "bar": { - "baz": "BAZ" - } + { + "data": { + "me": { + "foo": { + "bar": { + "baz": "BAZ" } } } } - """ + } + """.trimIndent() deferredJsonMerger.merge(payload2.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) assertEquals( @@ -1690,58 +2064,444 @@ class DeferredJsonMergerTest { //language=JSON val payload3 = """ - { - "completed": [ - { - "id": "1", - "errors": [ - { - "message": "Cannot return null for non-nullable field Bar.qux.", - "locations": [ - { - "line": 1, - "column": 1 - } - ], - "path": ["foo", "bar", "qux"] - } - ] - } - ], - "hasNext": false - } - """ + { + "completed": [ + { + "id": "1", + "errors": [ + { + "message": "Cannot return null for non-nullable field Bar.qux.", + "locations": [ + { + "line": 1, + "column": 1 + } + ], + "path": [ + "foo", + "bar", + "qux" + ] + } + ] + } + ], + "hasNext": false + } + """.trimIndent() //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "me": { - "foo": { - "bar": { - "baz": "BAZ" - } + { + "data": { + "me": { + "foo": { + "bar": { + "baz": "BAZ" } } + } + }, + "errors": [ + { + "message": "Cannot return null for non-nullable field Bar.qux.", + "locations": [ + { + "line": 1, + "column": 1 + } + ], + "path": [ + "foo", + "bar", + "qux" + ] + } + ] + } + """.trimIndent() + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + ), + deferredJsonMerger.pendingFragmentIds + ) + } + + /** + * Example I from https://github.com/graphql/defer-stream-wg/discussions/69 (Jul 18 2025 version) + */ + @Test + fun july2025ExampleI() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + } + ] + } + }, + "pending": [ + { + "id": "0", + "path": [ + "person" + ], + "label": "homeWorldDefer" }, - "errors": [ - { - "message": "Cannot return null for non-nullable field Bar.qux.", - "locations": [ - { - "line": 1, - "column": 1 - } - ], - "path": ["foo", "bar", "qux"] + { + "id": "1", + "path": [ + "person", + "films" + ], + "label": "filmsStream" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + } + ] + } + } + } + """.trimIndent() + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "1", + "items": [ + { + "title": "Return of the Jedi" + } + ] + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + }, + { + "title": "Return of the Jedi" + } + ] + } + } + } + """.trimIndent() + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload3 = """ + { + "completed": [ + { + "id": "1" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + }, + { + "title": "Return of the Jedi" + } + ] + } + } + } + """.trimIndent() + + deferredJsonMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload4 = """ + { + "incremental": [ + { + "id": "0", + "data": { + "homeworld": { + "name": "Tatooine" + } } - ] + } + ], + "completed": [ + { + "id": "0" + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4 = """ + { + "data": { + "person": { + "name": "Luke Skywalker", + "homeworld": { + "name": "Tatooine" + }, + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + }, + { + "title": "Return of the Jedi" + } + ] + } } - """ + } + """.trimIndent() + + deferredJsonMerger.merge(payload4.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4), deferredJsonMerger.merged) + assertEquals( + setOf(), + deferredJsonMerger.pendingFragmentIds + ) + } + + /** + * Example J from https://github.com/graphql/defer-stream-wg/discussions/69 (Jul 18 2025 version) + */ + @Test + fun july2025ExampleJ() { + val deferredJsonMerger = DeferredJsonMerger() + //language=JSON + val payload1 = """ + { + "data": { + "person": { + "films": [ + { + "title": "A New Hope" + } + ] + } + }, + "pending": [ + { + "id": "1", + "path": [ + "person", + "films" + ], + "label": "filmsStream" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1 = """ + { + "data": { + "person": { + "films": [ + { + "title": "A New Hope" + } + ] + } + } + } + """.trimIndent() + deferredJsonMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "id": "1", + "items": [ + { + "title": "The Empire Strikes Back" + } + ] + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "person": { + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + } + ] + } + } + } + """.trimIndent() + deferredJsonMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + deferredJsonMerger.pendingFragmentIds + ) + + //language=JSON + val payload3 = """ + { + "completed": [ + { + "id": "1", + "errors": [ + { + "message": "Cannot return null for non-nullable field Person.films.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "person", + "films" + ] + } + ] + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "person": { + "films": [ + { + "title": "A New Hope" + }, + { + "title": "The Empire Strikes Back" + } + ] + } + }, + "errors": [ + { + "message": "Cannot return null for non-nullable field Person.films.", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "person", + "films" + ] + } + ] + } + """.trimIndent() + deferredJsonMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), deferredJsonMerger.pendingFragmentIds ) diff --git a/tests/defer/apollo-server/computers.graphqls b/tests/defer/apollo-server/computers.graphqls index 4c410b7fe7e..a1875342b39 100644 --- a/tests/defer/apollo-server/computers.graphqls +++ b/tests/defer/apollo-server/computers.graphqls @@ -14,6 +14,7 @@ type Computer { screen: Screen! errorField: String nonNullErrorField: String! + peripherals: [String!]! } type Screen { @@ -25,3 +26,9 @@ directive @defer( if: Boolean! = true label: String ) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @stream( + label: String + if: Boolean! = true + initialCount: Int = 0 +) on FIELD diff --git a/tests/defer/apollo-server/computers.js b/tests/defer/apollo-server/computers.js index d5cedcd16b0..77e673b7985 100644 --- a/tests/defer/apollo-server/computers.js +++ b/tests/defer/apollo-server/computers.js @@ -5,8 +5,20 @@ import {readFileSync} from 'fs'; const port = process.env.APOLLO_PORT || 4000; const computers = [ - {id: 'Computer1', cpu: "386", year: 1993, screen: {resolution: "640x480", isColor: false}}, - {id: 'Computer2', cpu: "486", year: 1996, screen: {resolution: "800x600", isColor: true}}, + { + id: 'Computer1', + cpu: "386", + year: 1993, + screen: {resolution: "640x480", isColor: false}, + peripherals: ["Keyboard", "Mouse", "Printer"], + }, + { + id: 'Computer2', + cpu: "486", + year: 1996, + screen: {resolution: "800x600", isColor: true}, + peripherals: ["Keyboard", "Mouse", "Printer", "Scanner"], + }, ] const typeDefs = readFileSync('./computers.graphqls', {encoding: 'utf-8'}); diff --git a/tests/defer/build.gradle.kts b/tests/defer/build.gradle.kts index d90c4f5fd6d..0f1b0ea79c7 100644 --- a/tests/defer/build.gradle.kts +++ b/tests/defer/build.gradle.kts @@ -77,7 +77,7 @@ fun com.apollographql.apollo.gradle.api.Service.configureConnection(generateKotl tasks.withType(AbstractTestTask::class.java) { // Run the defer with Router and defer with Apollo Server tests only from a specific CI job val runDeferWithRouterTests = System.getenv("DEFER_WITH_ROUTER_TESTS").toBoolean() - val runDeferWithApolloServerTests = System.getenv("DEFER_WITH_APOLLO_SERVER_TESTS").toBoolean() + val runDeferWithApolloServerTests = true filter.setIncludePatterns(*buildList { if (runDeferWithRouterTests) add("test.DeferWithRouterTest") if (runDeferWithApolloServerTests) add("test.DeferWithApolloServerTest") diff --git a/tests/defer/src/commonMain/graphql/base/operation.graphql b/tests/defer/src/commonMain/graphql/base/operation.graphql index e89921fd895..c3b9b8d57b9 100644 --- a/tests/defer/src/commonMain/graphql/base/operation.graphql +++ b/tests/defer/src/commonMain/graphql/base/operation.graphql @@ -211,3 +211,16 @@ query SubPathQuery { } } } + +query SimpleStreamQuery($initialCount: Int!) { + computers @stream(initialCount: $initialCount) { + id + } +} + +query NestedStreamQuery($initialCount: Int!) { + computers @stream(initialCount: $initialCount) { + id + peripherals @stream(initialCount: $initialCount) + } +} diff --git a/tests/defer/src/commonMain/graphql/base/schema.graphqls b/tests/defer/src/commonMain/graphql/base/schema.graphqls index 0892f0f4075..9de38fc9a65 100644 --- a/tests/defer/src/commonMain/graphql/base/schema.graphqls +++ b/tests/defer/src/commonMain/graphql/base/schema.graphqls @@ -23,9 +23,16 @@ type Computer { screen: Screen! errorField: String nonNullErrorField: String! + peripherals: [String!]! } type Screen { resolution: String! isColor: Boolean! } + +directive @stream( + label: String + if: Boolean! = true + initialCount: Int = 0 +) on FIELD diff --git a/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt index c6ddf98bcdd..1746e941c95 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt @@ -14,8 +14,10 @@ import defer.DoesNotDisableDeferWithNullIfArgumentQuery import defer.HandlesErrorsThrownInDeferredFragmentsQuery import defer.HandlesNonNullableErrorsThrownInDeferredFragmentsQuery import defer.HandlesNonNullableErrorsThrownOutsideDeferredFragmentsQuery +import defer.NestedStreamQuery import defer.Overlapping2Query import defer.OverlappingQuery +import defer.SimpleStreamQuery import defer.SubPathQuery import defer.WithFragmentSpreadsMutation import defer.WithFragmentSpreadsQuery @@ -532,5 +534,119 @@ class DeferWithApolloServerTest { assertResponseListEquals(expectedDataList, actualResponseList) } + @Test + fun simpleStream0Initial() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[]},"pending":[{"id":"0","path":["computers"]}],"hasNext":true} + // {"hasNext":false,"incremental":[{"id":"0","items":[{"id":"Computer1"},{"id":"Computer2"}]}],"completed":[{"id":"0"}]} + val query = SimpleStreamQuery(0) + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + SimpleStreamQuery.Data( + listOf() + ) + ) + .build(), + + + ApolloResponse.Builder( + query, + uuid, + ).data( + SimpleStreamQuery.Data( + listOf( + SimpleStreamQuery.Computer("Computer1"), + SimpleStreamQuery.Computer("Computer2"), + ) + ) + ) + .build() + ) + + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun simpleStream1Initial() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"id":"Computer1"}]},"pending":[{"id":"0","path":["computers"]}],"hasNext":true} + // {"hasNext":false,"incremental":[{"id":"0","items":[{"id":"Computer2"}]}],"completed":[{"id":"0"}]} + val query = SimpleStreamQuery(1) + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + SimpleStreamQuery.Data( + listOf( + SimpleStreamQuery.Computer("Computer1"), + ) + ) + ) + .build(), + ApolloResponse.Builder( + query, + uuid, + ).data( + SimpleStreamQuery.Data( + listOf( + SimpleStreamQuery.Computer("Computer1"), + SimpleStreamQuery.Computer("Computer2"), + ) + ) + ) + .build() + ) + + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun nestedStream() = runTest(before = { setUp() }, after = { tearDown() }) { + // Expected payloads: + // {"data":{"computers":[{"id":"Computer1","peripherals":["Keyboard"]}]},"pending":[{"id":"0","path":["computers",0,"peripherals"]},{"id":"1","path":["computers"]}],"hasNext":true} + // {"hasNext":false,"pending":[{"id":"2","path":["computers",1,"peripherals"]}],"incremental":[{"id":"0","items":["Mouse","Printer"]},{"id":"1","items":[{"id":"Computer2","peripherals":["Keyboard"]}]},{"id":"2","items":["Mouse","Printer","Scanner"]}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"}]} + val query = NestedStreamQuery(1) + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data( + NestedStreamQuery.Data( + listOf( + NestedStreamQuery.Computer("Computer1", listOf("Keyboard")) + ) + ) + ) + .build(), + + ApolloResponse.Builder( + query, + uuid, + ).data( + NestedStreamQuery.Data( + listOf( + NestedStreamQuery.Computer("Computer1", listOf("Keyboard", "Mouse", "Printer")), + NestedStreamQuery.Computer("Computer2", listOf("Keyboard", "Mouse", "Printer", "Scanner")) + ) + ) + ) + .build() + ) + + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } } From bb505c16e0cab76856f0ccc3483c4bece0599d70 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 18 Jul 2025 18:24:58 +0200 Subject: [PATCH 08/21] Rename DeferredJsonMerger -> IncrementalResultsMerger and DeferredFragmentIdentifier -> IncrementalResultIdentifier --- libraries/apollo-api/api/apollo-api.api | 1 + libraries/apollo-api/api/apollo-api.klib.api | 1 + .../apollo/api/BooleanExpression.kt | 13 +- .../apollo/api/CustomScalarAdapters.kt | 19 +- .../apollographql/apollo/api/Executables.kt | 8 +- ...fier.kt => IncrementalResultIdentifier.kt} | 6 + .../apollographql/apollo/api/Operations.kt | 8 +- .../apollo/api/internal/ResponseParser.kt | 12 +- .../api/android/apollo-runtime.api | 6 +- .../api/apollo-runtime.klib.api | 32 +- .../apollo-runtime/api/jvm/apollo-runtime.api | 6 +- .../apollo/internal/DeferredJsonMerger.kt | 187 ---------- .../internal/IncrementalResultsMerger.kt | 186 ++++++++++ .../network/http/HttpNetworkTransport.kt | 19 +- .../websocket/WebSocketNetworkTransport.kt | 14 +- .../network/ws/WebSocketNetworkTransport.kt | 18 +- ...est.kt => IncrementalResultsMergerTest.kt} | 348 +++++++++--------- 17 files changed, 449 insertions(+), 435 deletions(-) rename libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/{DeferredFragmentIdentifier.kt => IncrementalResultIdentifier.kt} (54%) delete mode 100644 libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt create mode 100644 libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/IncrementalResultsMerger.kt rename libraries/apollo-runtime/src/commonTest/kotlin/test/defer/{DeferredJsonMergerTest.kt => IncrementalResultsMergerTest.kt} (78%) diff --git a/libraries/apollo-api/api/apollo-api.api b/libraries/apollo-api/api/apollo-api.api index 23317324bb8..f9259849daf 100644 --- a/libraries/apollo-api/api/apollo-api.api +++ b/libraries/apollo-api/api/apollo-api.api @@ -426,6 +426,7 @@ public final class com/apollographql/apollo/api/CustomScalarAdapters$Builder { public final fun deferredFragmentIdentifiers (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; public final fun errors (Ljava/util/List;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; public final fun falseVariables (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; + public final fun pendingResultIds (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; } public final class com/apollographql/apollo/api/CustomScalarAdapters$Key : com/apollographql/apollo/api/ExecutionContext$Key { diff --git a/libraries/apollo-api/api/apollo-api.klib.api b/libraries/apollo-api/api/apollo-api.klib.api index 46ddfe5e8c1..587765d443e 100644 --- a/libraries/apollo-api/api/apollo-api.klib.api +++ b/libraries/apollo-api/api/apollo-api.klib.api @@ -938,6 +938,7 @@ final class com.apollographql.apollo.api/CustomScalarAdapters : com.apollographq final fun deferredFragmentIdentifiers(kotlin.collections/Set?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.deferredFragmentIdentifiers|deferredFragmentIdentifiers(kotlin.collections.Set?){}[0] final fun errors(kotlin.collections/List?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.errors|errors(kotlin.collections.List?){}[0] final fun falseVariables(kotlin.collections/Set?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.falseVariables|falseVariables(kotlin.collections.Set?){}[0] + final fun pendingResultIds(kotlin.collections/Set?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.pendingResultIds|pendingResultIds(kotlin.collections.Set?){}[0] } final object Key : com.apollographql.apollo.api/ExecutionContext.Key { // com.apollographql.apollo.api/CustomScalarAdapters.Key|null[0] diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt index bddc85a8729..cfe2db00c53 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt @@ -50,7 +50,8 @@ fun and(vararg other: BooleanExpression): BooleanExpression = Bo fun not(other: BooleanExpression): BooleanExpression = BooleanExpression.Not(other) fun variable(name: String): BooleanExpression = BooleanExpression.Element(BVariable(name)) fun label(label: String? = null): BooleanExpression = BooleanExpression.Element(BLabel(label)) -fun possibleTypes(vararg typenames: String): BooleanExpression = BooleanExpression.Element(BPossibleTypes(typenames.toSet())) +fun possibleTypes(vararg typenames: String): BooleanExpression = + BooleanExpression.Element(BPossibleTypes(typenames.toSet())) internal fun BooleanExpression.evaluate(block: (T) -> Boolean): Boolean { return when (this) { @@ -66,7 +67,7 @@ internal fun BooleanExpression.evaluate(block: (T) -> Boolean): Boo fun BooleanExpression.evaluate( variables: Set?, typename: String?, - deferredFragmentIdentifiers: Set?, + pendingResultIds: Set?, path: List?, ): Boolean { // Remove "data" from the path @@ -74,22 +75,22 @@ fun BooleanExpression.evaluate( return evaluate { when (it) { is BVariable -> !(variables?.contains(it.name) ?: false) - is BLabel -> !isDeferredFragmentPending(deferredFragmentIdentifiers, croppedPath!!, it.label) + is BLabel -> !isDeferredFragmentPending(pendingResultIds, croppedPath!!, it.label) is BPossibleTypes -> it.possibleTypes.contains(typename) } } } private fun isDeferredFragmentPending( - deferredFragmentIdentifiers: Set?, + pendingResultIds: Set?, path: List, label: String?, ): Boolean { - if (deferredFragmentIdentifiers == null) { + if (pendingResultIds == null) { // By default, parse all deferred fragments - this is the case when parsing from the normalized cache. return false } - return deferredFragmentIdentifiers.contains(DeferredFragmentIdentifier(path, label)) + return pendingResultIds.contains(IncrementalResultIdentifier(path, label)) } /** diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt index a02faf1ac44..585aa94a4c3 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt @@ -19,10 +19,10 @@ class CustomScalarAdapters private constructor( @JvmField val falseVariables: Set?, /** - * Defer identifiers used to determine whether the parser must parse @defer fragments + * Pending incremental result identifiers used to determine whether the parser must parse deferred fragments */ @JvmField - val deferredFragmentIdentifiers: Set?, + val deferredFragmentIdentifiers: Set?, /** * Errors to use with @catch */ @@ -125,21 +125,26 @@ class CustomScalarAdapters private constructor( fun newBuilder(): Builder { return Builder().addAll(this) .falseVariables(falseVariables) - .deferredFragmentIdentifiers(deferredFragmentIdentifiers) + .pendingResultIds(deferredFragmentIdentifiers) } class Builder { private val adaptersMap: MutableMap> = mutableMapOf() private var falseVariables: Set? = null - private var deferredFragmentIdentifiers: Set? = null + private var pendingResultIds: Set? = null private var errors: List? = null fun falseVariables(falseVariables: Set?) = apply { this.falseVariables = falseVariables } - fun deferredFragmentIdentifiers(deferredFragmentIdentifiers: Set?) = apply { - this.deferredFragmentIdentifiers = deferredFragmentIdentifiers + @Deprecated("Use pendingResultIds instead", ReplaceWith("pendingResultIds(pendingResultIds = deferredFragmentIdentifiers)")) + fun deferredFragmentIdentifiers(deferredFragmentIdentifiers: Set?) = apply { + this.pendingResultIds = deferredFragmentIdentifiers + } + + fun pendingResultIds(pendingResultIds: Set?) = apply { + this.pendingResultIds = pendingResultIds } fun errors(errors: List?) = apply { @@ -173,7 +178,7 @@ class CustomScalarAdapters private constructor( return CustomScalarAdapters( adaptersMap, falseVariables, - deferredFragmentIdentifiers, + pendingResultIds, errors, ) } diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt index 28d0fe1c090..59fda6ca3ef 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt @@ -71,12 +71,12 @@ fun Executable.parseData( jsonReader: JsonReader, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, falseVariables: Set? = null, - deferredFragmentIds: Set? = null, - errors: List? = null + deferredFragmentIds: Set? = null, + errors: List? = null, ): D? { val customScalarAdapters1 = customScalarAdapters.newBuilder() .falseVariables(falseVariables) - .deferredFragmentIdentifiers(deferredFragmentIds) + .pendingResultIds(pendingResultIds = deferredFragmentIds) .errors(errors) .build() return adapter().nullable().fromJson(jsonReader, customScalarAdapters1) @@ -89,4 +89,4 @@ fun Executable.composeData( value: D ) { adapter().toJson(jsonWriter, customScalarAdapters, value) -} \ No newline at end of file +} diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/DeferredFragmentIdentifier.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt similarity index 54% rename from libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/DeferredFragmentIdentifier.kt rename to libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt index 13f5e2d4dac..0d3d6ec5d6a 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/DeferredFragmentIdentifier.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt @@ -7,3 +7,9 @@ data class DeferredFragmentIdentifier( val path: List, val label: String?, ) + +/** + * Identifies an incremental result. + * [DeferredFragmentIdentifier] is kept to not break the API/ABI, but this alias is more descriptive of its purpose. + */ +typealias IncrementalResultIdentifier = DeferredFragmentIdentifier diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt index 9dc586f8147..a97159aa4a0 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt @@ -70,7 +70,7 @@ fun Operation.composeJsonRequest( fun Operation.parseJsonResponse( jsonReader: JsonReader, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return jsonReader.use { ResponseParser.parse( @@ -103,7 +103,7 @@ fun Operation.parseResponse( jsonReader: JsonReader, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return try { ResponseParser.parse( @@ -177,7 +177,7 @@ fun JsonReader.toApolloResponse( operation: Operation, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return use { try { @@ -213,7 +213,7 @@ fun JsonReader.parseResponse( operation: Operation, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return try { ResponseParser.parse( diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt index 2ce1c62f70f..fc611c83e17 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt @@ -3,8 +3,8 @@ package com.apollographql.apollo.api.internal import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.CustomScalarAdapters -import com.apollographql.apollo.api.DeferredFragmentIdentifier import com.apollographql.apollo.api.Error +import com.apollographql.apollo.api.IncrementalResultIdentifier import com.apollographql.apollo.api.Operation import com.apollographql.apollo.api.falseVariables import com.apollographql.apollo.api.json.JsonReader @@ -24,7 +24,7 @@ internal object ResponseParser { operation: Operation, requestUuid: Uuid?, customScalarAdapters: CustomScalarAdapters, - deferredFragmentIds: Set?, + pendingResultIds: Set?, ): ApolloResponse { jsonReader.beginObject() @@ -36,8 +36,9 @@ internal object ResponseParser { when (val name = jsonReader.nextName()) { "data" -> { val falseVariables = operation.falseVariables(customScalarAdapters) - data = operation.parseData(jsonReader, customScalarAdapters, falseVariables, deferredFragmentIds, errors) + data = operation.parseData(jsonReader, customScalarAdapters, falseVariables, pendingResultIds, errors) } + "errors" -> errors = jsonReader.readErrors() "extensions" -> extensions = jsonReader.readAny() as? Map else -> { @@ -100,7 +101,8 @@ private fun JsonReader.readError(): Error { @Suppress("DEPRECATION") - return Error.Builder(message = message).locations(locations).path(path).extensions(extensions).nonStandardFields(nonStandardFields).build() + return Error.Builder(message = message).locations(locations).path(path).extensions(extensions).nonStandardFields(nonStandardFields) + .build() } private fun JsonReader.readPath(): List? { @@ -164,4 +166,4 @@ fun JsonReader.readErrors(): List { } endArray() return list -} \ No newline at end of file +} diff --git a/libraries/apollo-runtime/api/android/apollo-runtime.api b/libraries/apollo-runtime/api/android/apollo-runtime.api index 42df03b77cf..d4e65147106 100644 --- a/libraries/apollo-runtime/api/android/apollo-runtime.api +++ b/libraries/apollo-runtime/api/android/apollo-runtime.api @@ -215,12 +215,12 @@ public final class com/apollographql/apollo/interceptor/RetryOnErrorInterceptorK public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor; } -public final class com/apollographql/apollo/internal/DeferredJsonMerger { +public final class com/apollographql/apollo/internal/IncrementalResultsMerger { public fun ()V public final fun getHasNext ()Z public final fun getMerged ()Ljava/util/Map; - public final fun getPendingFragmentIds ()Ljava/util/Set; - public final fun isEmptyPayload ()Z + public final fun getPendingResultIds ()Ljava/util/Set; + public final fun isEmptyResponse ()Z public final fun merge (Ljava/util/Map;)Ljava/util/Map; public final fun merge (Lokio/BufferedSource;)Ljava/util/Map; public final fun reset ()V diff --git a/libraries/apollo-runtime/api/apollo-runtime.klib.api b/libraries/apollo-runtime/api/apollo-runtime.klib.api index 315cd896802..e82fb4ac562 100644 --- a/libraries/apollo-runtime/api/apollo-runtime.klib.api +++ b/libraries/apollo-runtime/api/apollo-runtime.klib.api @@ -200,22 +200,22 @@ final class com.apollographql.apollo.interceptor/AutoPersistedQueryInterceptor : } } -final class com.apollographql.apollo.internal/DeferredJsonMerger { // com.apollographql.apollo.internal/DeferredJsonMerger|null[0] - constructor () // com.apollographql.apollo.internal/DeferredJsonMerger.|(){}[0] - - final val merged // com.apollographql.apollo.internal/DeferredJsonMerger.merged|{}merged[0] - final fun (): kotlin.collections/Map // com.apollographql.apollo.internal/DeferredJsonMerger.merged.|(){}[0] - final val pendingFragmentIds // com.apollographql.apollo.internal/DeferredJsonMerger.pendingFragmentIds|{}pendingFragmentIds[0] - final fun (): kotlin.collections/Set // com.apollographql.apollo.internal/DeferredJsonMerger.pendingFragmentIds.|(){}[0] - - final var hasNext // com.apollographql.apollo.internal/DeferredJsonMerger.hasNext|{}hasNext[0] - final fun (): kotlin/Boolean // com.apollographql.apollo.internal/DeferredJsonMerger.hasNext.|(){}[0] - final var isEmptyPayload // com.apollographql.apollo.internal/DeferredJsonMerger.isEmptyPayload|{}isEmptyPayload[0] - final fun (): kotlin/Boolean // com.apollographql.apollo.internal/DeferredJsonMerger.isEmptyPayload.|(){}[0] - - final fun merge(kotlin.collections/Map): kotlin.collections/Map // com.apollographql.apollo.internal/DeferredJsonMerger.merge|merge(kotlin.collections.Map){}[0] - final fun merge(okio/BufferedSource): kotlin.collections/Map // com.apollographql.apollo.internal/DeferredJsonMerger.merge|merge(okio.BufferedSource){}[0] - final fun reset() // com.apollographql.apollo.internal/DeferredJsonMerger.reset|reset(){}[0] +final class com.apollographql.apollo.internal/IncrementalResultsMerger { // com.apollographql.apollo.internal/IncrementalResultsMerger|null[0] + constructor () // com.apollographql.apollo.internal/IncrementalResultsMerger.|(){}[0] + + final val merged // com.apollographql.apollo.internal/IncrementalResultsMerger.merged|{}merged[0] + final fun (): kotlin.collections/Map // com.apollographql.apollo.internal/IncrementalResultsMerger.merged.|(){}[0] + final val pendingResultIds // com.apollographql.apollo.internal/IncrementalResultsMerger.pendingResultIds|{}pendingResultIds[0] + final fun (): kotlin.collections/Set // com.apollographql.apollo.internal/IncrementalResultsMerger.pendingResultIds.|(){}[0] + + final var hasNext // com.apollographql.apollo.internal/IncrementalResultsMerger.hasNext|{}hasNext[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.internal/IncrementalResultsMerger.hasNext.|(){}[0] + final var isEmptyResponse // com.apollographql.apollo.internal/IncrementalResultsMerger.isEmptyResponse|{}isEmptyResponse[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.internal/IncrementalResultsMerger.isEmptyResponse.|(){}[0] + + final fun merge(kotlin.collections/Map): kotlin.collections/Map // com.apollographql.apollo.internal/IncrementalResultsMerger.merge|merge(kotlin.collections.Map){}[0] + final fun merge(okio/BufferedSource): kotlin.collections/Map // com.apollographql.apollo.internal/IncrementalResultsMerger.merge|merge(okio.BufferedSource){}[0] + final fun reset() // com.apollographql.apollo.internal/IncrementalResultsMerger.reset|reset(){}[0] } final class com.apollographql.apollo.internal/MultipartReader : okio/Closeable { // com.apollographql.apollo.internal/MultipartReader|null[0] diff --git a/libraries/apollo-runtime/api/jvm/apollo-runtime.api b/libraries/apollo-runtime/api/jvm/apollo-runtime.api index b0f372bd9de..8806ffc7efd 100644 --- a/libraries/apollo-runtime/api/jvm/apollo-runtime.api +++ b/libraries/apollo-runtime/api/jvm/apollo-runtime.api @@ -215,12 +215,12 @@ public final class com/apollographql/apollo/interceptor/RetryOnErrorInterceptorK public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor; } -public final class com/apollographql/apollo/internal/DeferredJsonMerger { +public final class com/apollographql/apollo/internal/IncrementalResultsMerger { public fun ()V public final fun getHasNext ()Z public final fun getMerged ()Ljava/util/Map; - public final fun getPendingFragmentIds ()Ljava/util/Set; - public final fun isEmptyPayload ()Z + public final fun getPendingResultIds ()Ljava/util/Set; + public final fun isEmptyResponse ()Z public final fun merge (Ljava/util/Map;)Ljava/util/Map; public final fun merge (Lokio/BufferedSource;)Ljava/util/Map; public final fun reset ()V diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt deleted file mode 100644 index 47a6fdb4204..00000000000 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/DeferredJsonMerger.kt +++ /dev/null @@ -1,187 +0,0 @@ -package com.apollographql.apollo.internal - -import com.apollographql.apollo.annotations.ApolloInternal -import com.apollographql.apollo.api.DeferredFragmentIdentifier -import com.apollographql.apollo.api.json.BufferedSourceJsonReader -import com.apollographql.apollo.api.json.readAny -import okio.BufferedSource - -private typealias JsonMap = Map -private typealias MutableJsonMap = MutableMap - -/** - * Utility class for merging GraphQL JSON payloads received in multiple chunks when using the `@defer` directive. - * - * Each call to [merge] will merge the given chunk into the [merged] Map, and will also update the [pendingFragmentIds] Set with the - * value of its `path` and `label` field. - * - * The fields in `data` are merged into the node found in [merged] at the path known by looking at the `id` field (for the first call to - * [merge], the payload is copied to [merged] as-is). - * - * `errors` in incremental and completed items (if present) are merged together in an array and then set to the `errors` field of the - * [merged] Map, at each call to [merge]. - * `extensions` in incremental items (if present) are merged together in an array and then set to the `extensions` field of the [merged] - * Map, at each call to [merge]. - */ -@ApolloInternal -@Suppress("UNCHECKED_CAST") -class DeferredJsonMerger { - private val _merged: MutableJsonMap = mutableMapOf() - val merged: JsonMap = _merged - - /** - * Map of identifiers to their corresponding DeferredFragmentIdentifier, found in `pending`. - */ - private val _pendingFragmentIds = mutableMapOf() - val pendingFragmentIds: Set get() = _pendingFragmentIds.values.toSet() - - var hasNext: Boolean = true - private set - - /** - * A payload can sometimes have no `incremental` field, e.g. when the server couldn't predict if there were more data after the last - * emitted payload. This field allows to test for this in order to ignore such payloads. - * See https://github.com/apollographql/router/issues/1687. - */ - var isEmptyPayload: Boolean = false - private set - - fun merge(payload: BufferedSource): JsonMap { - val payloadMap = jsonToMap(payload) - return merge(payloadMap) - } - - fun merge(payload: JsonMap): JsonMap { - val completed = payload["completed"] as? List - if (merged.isEmpty()) { - // Initial payload, no merging needed (strip some fields that should not appear in the final result) - _merged += payload - "hasNext" - "pending" - handlePending(payload) - handleCompleted(completed) - return merged - } - handlePending(payload) - - val incrementalList = payload["incremental"] as? List - if (incrementalList != null) { - for (incrementalItem in incrementalList) { - mergeIncrementalData(incrementalItem) - // Merge errors (if any) of the incremental item - (incrementalItem["errors"] as? List)?.let { getOrPutMergedErrors() += it } - } - } - isEmptyPayload = completed == null && incrementalList == null - - hasNext = payload["hasNext"] as Boolean? ?: false - - handleCompleted(completed) - - (payload["extensions"] as? JsonMap)?.let { getOrPutExtensions() += it } - - return merged - } - - private fun getOrPutMergedErrors() = _merged.getOrPut("errors") { mutableListOf() } as MutableList - - private fun getOrPutExtensions() = _merged.getOrPut("extensions") { mutableMapOf() } as MutableJsonMap - - private fun handlePending(payload: JsonMap) { - val pending = payload["pending"] as? List - if (pending != null) { - for (pendingItem in pending) { - val id = pendingItem["id"] as String - val path = pendingItem["path"] as List - val label = pendingItem["label"] as String? - _pendingFragmentIds[id] = DeferredFragmentIdentifier(path = path, label = label) - } - } - } - - private fun handleCompleted(completed: List?) { - if (completed != null) { - for (completedItem in completed) { - // Merge errors (if any) of the completed item - val errors = completedItem["errors"] as? List - if (errors != null) { - getOrPutMergedErrors() += errors - } else { - // Fragment is no longer pending - only if there were no errors - val id = completedItem["id"] as String - _pendingFragmentIds.remove(id) ?: error("Id '$id' not found in pending results") - } - } - } - } - - private fun mergeIncrementalData(incrementalItem: JsonMap) { - val id = incrementalItem["id"] as String? ?: error("No id found in incremental result") - val data = incrementalItem["data"] as JsonMap? - val items = incrementalItem["items"] as List? - val subPath = incrementalItem["subPath"] as List? ?: emptyList() - val path = (_pendingFragmentIds[id]?.path ?: error("Id '$id' not found in pending results")) + subPath - val mergedData = merged["data"] as JsonMap - val nodeToMergeInto = nodeAtPath(mergedData, path) - when { - data != null -> { - deepMergeObject(nodeToMergeInto as MutableJsonMap, data) - } - - items != null -> { - mergeList(nodeToMergeInto as MutableList, items) - } - - else -> { - error("Neither data nor items found in incremental result") - } - } - } - - private fun deepMergeObject(destination: MutableJsonMap, obj: JsonMap) { - for ((key, value) in obj) { - if (destination.containsKey(key) && destination[key] is MutableMap<*, *>) { - // Objects: merge recursively - val fieldDestination = destination[key] as MutableJsonMap - val fieldMap = value as? JsonMap ?: error("'$key' is an object in destination but not in map") - deepMergeObject(destination = fieldDestination, obj = fieldMap) - } else { - // Other types: add / overwrite - destination[key] = value - } - } - } - - private fun mergeList(destination: MutableList, items: List) { - destination.addAll(items) - } - - private fun jsonToMap(json: BufferedSource): JsonMap = BufferedSourceJsonReader(json).readAny() as JsonMap - - - /** - * Find the node in the [map] at the given [path]. - * @param path The path to the node to find, as a list of either `String` (name of field in object) or `Int` (index of element in array). - */ - private fun nodeAtPath(map: JsonMap, path: List): Any? { - var node: Any? = map - for (key in path) { - node = if (node is List<*>) { - node[key as Int] - } else { - node as JsonMap - node[key] - } - } - return node - } - - fun reset() { - _merged.clear() - _pendingFragmentIds.clear() - hasNext = true - isEmptyPayload = false - } -} - -internal fun JsonMap.isDeferred(): Boolean { - return keys.contains("hasNext") -} diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/IncrementalResultsMerger.kt new file mode 100644 index 00000000000..b49e1af3dd0 --- /dev/null +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/IncrementalResultsMerger.kt @@ -0,0 +1,186 @@ +package com.apollographql.apollo.internal + +import com.apollographql.apollo.annotations.ApolloInternal +import com.apollographql.apollo.api.IncrementalResultIdentifier +import com.apollographql.apollo.api.json.BufferedSourceJsonReader +import com.apollographql.apollo.api.json.readAny +import okio.BufferedSource + +private typealias JsonMap = Map +private typealias MutableJsonMap = MutableMap + +/** + * Utility class for merging GraphQL incremental results received in multiple chunks when using the `@defer` and/or `@stream` directives. + * + * Each call to [merge] will merge the given results into the [merged] Map, and will also update the [pendingResultIds] Set with the + * value of their `path` and `label` fields. + * + * The fields in `data` are merged into the node found in [merged] at the path known by looking at the `id` field. For the first call to + * [merge], the payload is copied to [merged] as-is. + * + * `errors` in incremental and completed results (if present) are merged together in an array and then set to the `errors` field of the + * [merged] Map. + * `extensions` in incremental results (if present) are merged together in an array and then set to the `extensions` field of the [merged] + * Map. + */ +@ApolloInternal +@Suppress("UNCHECKED_CAST") +class IncrementalResultsMerger { + private val _merged: MutableJsonMap = mutableMapOf() + val merged: JsonMap = _merged + + /** + * Map of identifiers to their corresponding IncrementalResultIdentifier, found in `pending`. + */ + private val _pendingResultIds = mutableMapOf() + val pendingResultIds: Set get() = _pendingResultIds.values.toSet() + + var hasNext: Boolean = true + private set + + /** + * A response can sometimes have no `incremental` field, e.g. when the server couldn't predict if there were more data after the last + * emitted payload. This field allows to test for this in order to ignore such payloads. + * See https://github.com/apollographql/router/issues/1687. + */ + var isEmptyResponse: Boolean = false + private set + + fun merge(part: BufferedSource): JsonMap { + return merge(part.toJsonMap()) + } + + fun merge(part: JsonMap): JsonMap { + val completed = part["completed"] as? List + if (merged.isEmpty()) { + // Initial part, no merging needed (strip some fields that should not appear in the final result) + _merged += part - "hasNext" - "pending" + handlePending(part) + handleCompleted(completed) + return merged + } + handlePending(part) + + val incremental = part["incremental"] as? List + if (incremental != null) { + for (incrementalResult in incremental) { + mergeIncrementalResult(incrementalResult) + // Merge errors (if any) of the incremental result + (incrementalResult["errors"] as? List)?.let { getOrPutMergedErrors() += it } + } + } + isEmptyResponse = completed == null && incremental == null + + hasNext = part["hasNext"] as Boolean? ?: false + + handleCompleted(completed) + + (part["extensions"] as? JsonMap)?.let { getOrPutExtensions() += it } + + return merged + } + + private fun getOrPutMergedErrors() = _merged.getOrPut("errors") { mutableListOf() } as MutableList + + private fun getOrPutExtensions() = _merged.getOrPut("extensions") { mutableMapOf() } as MutableJsonMap + + private fun handlePending(part: JsonMap) { + val pending = part["pending"] as? List + if (pending != null) { + for (pendingResult in pending) { + val id = pendingResult["id"] as String + val path = pendingResult["path"] as List + val label = pendingResult["label"] as String? + _pendingResultIds[id] = IncrementalResultIdentifier(path = path, label = label) + } + } + } + + private fun handleCompleted(completed: List?) { + if (completed != null) { + for (completedResult in completed) { + // Merge errors (if any) of the completed result + val errors = completedResult["errors"] as? List + if (errors != null) { + getOrPutMergedErrors() += errors + } else { + // Fragment is no longer pending - only if there were no errors + val id = completedResult["id"] as String + _pendingResultIds.remove(id) ?: error("Id '$id' not found in pending results") + } + } + } + } + + private fun mergeIncrementalResult(incrementalResult: JsonMap) { + val id = incrementalResult["id"] as String? ?: error("No id found in incremental result") + val data = incrementalResult["data"] as JsonMap? + val items = incrementalResult["items"] as List? + val subPath = incrementalResult["subPath"] as List? ?: emptyList() + val path = (_pendingResultIds[id]?.path ?: error("Id '$id' not found in pending results")) + subPath + val mergedData = merged["data"] as JsonMap + val nodeToMergeInto = nodeAtPath(mergedData, path) + when { + data != null -> { + deepMergeObject(nodeToMergeInto as MutableJsonMap, data) + } + + items != null -> { + mergeList(nodeToMergeInto as MutableList, items) + } + + else -> { + error("Neither data nor items found in incremental result") + } + } + } + + private fun deepMergeObject(destination: MutableJsonMap, obj: JsonMap) { + for ((key, value) in obj) { + if (destination.containsKey(key) && destination[key] is MutableMap<*, *>) { + // Objects: merge recursively + val fieldDestination = destination[key] as MutableJsonMap + val fieldMap = value as? JsonMap ?: error("'$key' is an object in destination but not in map") + deepMergeObject(destination = fieldDestination, obj = fieldMap) + } else { + // Other types: add / overwrite + destination[key] = value + } + } + } + + private fun mergeList(destination: MutableList, items: List) { + destination.addAll(items) + } + + private fun BufferedSource.toJsonMap(): JsonMap = BufferedSourceJsonReader(this).readAny() as JsonMap + + + /** + * Find the node in the [map] at the given [path]. + * @param path The path to the node to find, as a list of either `String` (name of field in object) or `Int` (index of element in array). + */ + private fun nodeAtPath(map: JsonMap, path: List): Any? { + var node: Any? = map + for (key in path) { + node = if (node is List<*>) { + node[key as Int] + } else { + node as JsonMap + node[key] + } + } + return node + } + + fun reset() { + _merged.clear() + _pendingResultIds.clear() + hasNext = true + isEmptyResponse = false + } +} + +internal fun JsonMap.isIncremental(): Boolean { + return keys.contains("hasNext") +} diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt index d96f14d39e5..128338697bb 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt @@ -21,7 +21,7 @@ import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloHttpException import com.apollographql.apollo.exception.ApolloNetworkException import com.apollographql.apollo.exception.RouterError -import com.apollographql.apollo.internal.DeferredJsonMerger +import com.apollographql.apollo.internal.IncrementalResultsMerger import com.apollographql.apollo.internal.isGraphQLResponse import com.apollographql.apollo.internal.isMultipart import com.apollographql.apollo.internal.multipartBodyFlow @@ -170,7 +170,7 @@ private constructor( customScalarAdapters: CustomScalarAdapters, httpResponse: HttpResponse, ): Flow> { - var jsonMerger: DeferredJsonMerger? = null + var incrementalResultsMerger: IncrementalResultsMerger? = null val operation = request.operation return multipartBodyFlow(httpResponse) @@ -218,21 +218,20 @@ private constructor( else -> null } } else { - if (jsonMerger == null) { - jsonMerger = DeferredJsonMerger() + if (incrementalResultsMerger == null) { + incrementalResultsMerger = IncrementalResultsMerger() } - val merged = jsonMerger.merge(part) - val deferredFragmentIds = jsonMerger.pendingFragmentIds - val isLast = !jsonMerger.hasNext + val merged = incrementalResultsMerger.merge(part) + val pendingResultIds = incrementalResultsMerger.pendingResultIds + val isLast = !incrementalResultsMerger.hasNext - if (jsonMerger.isEmptyPayload) { + if (incrementalResultsMerger.isEmptyResponse) { null } else { - @Suppress("DEPRECATION") merged.jsonReader().toApolloResponse( operation = operation, customScalarAdapters = customScalarAdapters, - deferredFragmentIdentifiers = deferredFragmentIds + deferredFragmentIdentifiers = pendingResultIds ).newBuilder().isLast(isLast).build() } } diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt index 1bfe1f0b98a..e6e2370c49f 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt @@ -12,7 +12,7 @@ import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloWebSocketForceCloseException import com.apollographql.apollo.exception.DefaultApolloException import com.apollographql.apollo.exception.SubscriptionOperationException -import com.apollographql.apollo.internal.DeferredJsonMerger +import com.apollographql.apollo.internal.IncrementalResultsMerger import com.apollographql.apollo.network.NetworkTransport import com.apollographql.apollo.network.websocket.internal.OperationListener import com.apollographql.apollo.network.websocket.internal.WebSocketPool @@ -202,7 +202,7 @@ private object DefaultSubscriptionParserFactory: SubscriptionParserFactory { } private class DefaultSubscriptionParser(private val request: ApolloRequest) : SubscriptionParser { - private var deferredJsonMerger: DeferredJsonMerger = DeferredJsonMerger() + private var incrementalResultsMerger: IncrementalResultsMerger = IncrementalResultsMerger() private val requestCustomScalarAdapters = request.executionContext[CustomScalarAdapters] ?: CustomScalarAdapters.Empty @Suppress("NAME_SHADOWING") @@ -215,7 +215,7 @@ private class DefaultSubscriptionParser(private val request: } val (payload, mergedFragmentIds) = if (responseMap.isDeferred()) { - deferredJsonMerger.merge(responseMap) to deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.merge(responseMap) to incrementalResultsMerger.pendingResultIds } else { responseMap to null } @@ -226,12 +226,12 @@ private class DefaultSubscriptionParser(private val request: deferredFragmentIdentifiers = mergedFragmentIds ) - if (!deferredJsonMerger.hasNext) { - // Last deferred payload: reset the deferredJsonMerger for potential subsequent responses - deferredJsonMerger.reset() + if (!incrementalResultsMerger.hasNext) { + // Last incremental result: reset the incrementalResultsMerger for potential subsequent responses + incrementalResultsMerger.reset() } - return if (deferredJsonMerger.isEmptyPayload) { + return if (incrementalResultsMerger.isEmptyResponse) { null } else { apolloResponse diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt index 8ffda6b9285..549638ed687 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt @@ -10,8 +10,8 @@ import com.apollographql.apollo.api.toApolloResponse import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloNetworkException import com.apollographql.apollo.exception.SubscriptionOperationException -import com.apollographql.apollo.internal.DeferredJsonMerger -import com.apollographql.apollo.internal.isDeferred +import com.apollographql.apollo.internal.IncrementalResultsMerger +import com.apollographql.apollo.internal.isIncremental import com.apollographql.apollo.internal.transformWhile import com.apollographql.apollo.network.NetworkTransport import com.apollographql.apollo.network.ws.internal.Command @@ -262,7 +262,7 @@ private constructor( override fun execute( request: ApolloRequest, ): Flow> { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() return events.onSubscription { messages.send(StartOperation(request)) @@ -302,8 +302,8 @@ private constructor( is OperationResponse -> { val responsePayload = response.payload val requestCustomScalarAdapters = request.executionContext[CustomScalarAdapters]!! - val (payload, mergedFragmentIds) = if (responsePayload.isDeferred()) { - deferredJsonMerger.merge(responsePayload) to deferredJsonMerger.pendingFragmentIds + val (payload, mergedFragmentIds) = if (responsePayload.isIncremental()) { + incrementalResultsMerger.merge(responsePayload) to incrementalResultsMerger.pendingResultIds } else { responsePayload to null } @@ -314,9 +314,9 @@ private constructor( deferredFragmentIdentifiers = mergedFragmentIds ) - if (!deferredJsonMerger.hasNext) { - // Last deferred payload: reset the deferredJsonMerger for potential subsequent responses - deferredJsonMerger.reset() + if (!incrementalResultsMerger.hasNext) { + // Last incremental result: reset the incrementalResultsMerger for potential subsequent responses + incrementalResultsMerger.reset() } apolloResponse } @@ -328,7 +328,7 @@ private constructor( is ConnectionReEstablished, is OperationComplete, is GeneralError -> error("Unexpected event $response") } }.filterNot { - deferredJsonMerger.isEmptyPayload + incrementalResultsMerger.isEmptyResponse }.onCompletion { messages.send(StopOperation(request)) } diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/IncrementalResultsMergerTest.kt similarity index 78% rename from libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt rename to libraries/apollo-runtime/src/commonTest/kotlin/test/defer/IncrementalResultsMergerTest.kt index f1c189ad919..e743fa6f2ac 100644 --- a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/DeferredJsonMergerTest.kt +++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/IncrementalResultsMergerTest.kt @@ -3,10 +3,10 @@ package test.defer import com.apollographql.apollo.annotations.ApolloInternal -import com.apollographql.apollo.api.DeferredFragmentIdentifier +import com.apollographql.apollo.api.IncrementalResultIdentifier import com.apollographql.apollo.api.json.BufferedSourceJsonReader import com.apollographql.apollo.api.json.readAny -import com.apollographql.apollo.internal.DeferredJsonMerger +import com.apollographql.apollo.internal.IncrementalResultsMerger import okio.Buffer import kotlin.test.Test import kotlin.test.assertEquals @@ -18,10 +18,10 @@ private fun String.buffer() = Buffer().writeUtf8(this) @Suppress("UNCHECKED_CAST") private fun jsonToMap(json: String): Map = BufferedSourceJsonReader(json.buffer()).readAny() as Map -class DeferredJsonMergerTest { +class IncrementalResultsMergerTest { @Test fun mergeJsonSingleIncrementalItem() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ @@ -76,13 +76,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0") + IncrementalResultIdentifier(path = listOf("computers", 0), label = "query:Query1:0") ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -154,13 +154,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0") + IncrementalResultIdentifier(path = listOf("computers", 1), label = "query:Query1:0") ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -236,13 +236,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -334,14 +334,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload4.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload4.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -442,19 +442,19 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload5.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload5.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @Test fun mergeJsonMultipleIncrementalItems() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ @@ -517,14 +517,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + IncrementalResultIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + IncrementalResultIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -622,14 +622,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2_3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2_3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), - DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -750,19 +750,19 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload4_5.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload4_5.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @Test fun emptyPayloads() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ @@ -804,8 +804,8 @@ class DeferredJsonMergerTest { "hasNext": true } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertFalse(deferredJsonMerger.isEmptyPayload) + incrementalResultsMerger.merge(payload1.buffer()) + assertFalse(incrementalResultsMerger.isEmptyResponse) //language=JSON val payload2 = """ @@ -813,8 +813,8 @@ class DeferredJsonMergerTest { "hasNext": true } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertTrue(deferredJsonMerger.isEmptyPayload) + incrementalResultsMerger.merge(payload2.buffer()) + assertTrue(incrementalResultsMerger.isEmptyResponse) //language=JSON val payload3 = """ { @@ -833,8 +833,8 @@ class DeferredJsonMergerTest { "hasNext": true } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertFalse(deferredJsonMerger.isEmptyPayload) + incrementalResultsMerger.merge(payload3.buffer()) + assertFalse(incrementalResultsMerger.isEmptyResponse) //language=JSON val payload4 = """ @@ -842,8 +842,8 @@ class DeferredJsonMergerTest { "hasNext": false } """.trimIndent() - deferredJsonMerger.merge(payload4.buffer()) - assertTrue(deferredJsonMerger.isEmptyPayload) + incrementalResultsMerger.merge(payload4.buffer()) + assertTrue(incrementalResultsMerger.isEmptyResponse) } /** @@ -851,7 +851,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleA() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -897,13 +897,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = null), + IncrementalResultIdentifier(path = listOf(), label = null), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -957,11 +957,11 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -970,7 +970,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleA2() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1017,13 +1017,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = "D1"), + IncrementalResultIdentifier(path = listOf(), label = "D1"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1083,13 +1083,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("f2", "c", "f"), label = "D2"), + IncrementalResultIdentifier(path = listOf("f2", "c", "f"), label = "D2"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1136,11 +1136,11 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -1149,7 +1149,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleB1() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1195,14 +1195,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = "Blue"), - DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + IncrementalResultIdentifier(path = listOf(), label = "Blue"), + IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1250,13 +1250,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + IncrementalResultIdentifier(path = listOf(), label = "Blue"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1303,11 +1303,11 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -1316,7 +1316,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleB2() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1362,14 +1362,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = "Blue"), - DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + IncrementalResultIdentifier(path = listOf(), label = "Blue"), + IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1423,13 +1423,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), + IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1472,11 +1472,11 @@ class DeferredJsonMergerTest { } } """ - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -1485,7 +1485,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleD() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1515,14 +1515,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = null), - DeferredFragmentIdentifier(path = listOf("me"), label = null), + IncrementalResultIdentifier(path = listOf(), label = null), + IncrementalResultIdentifier(path = listOf("me"), label = null), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1613,13 +1613,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = null), + IncrementalResultIdentifier(path = listOf(), label = null), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1700,11 +1700,11 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -1713,7 +1713,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleF() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1740,13 +1740,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + IncrementalResultIdentifier(path = listOf("me"), label = "B"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1780,11 +1780,11 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -1793,7 +1793,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleG() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1843,14 +1843,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = "Billing"), - DeferredFragmentIdentifier(path = listOf("me"), label = "Prev"), + IncrementalResultIdentifier(path = listOf("me"), label = "Billing"), + IncrementalResultIdentifier(path = listOf("me"), label = "Prev"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1893,13 +1893,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = "Prev"), + IncrementalResultIdentifier(path = listOf("me"), label = "Prev"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -1949,11 +1949,11 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -1962,7 +1962,7 @@ class DeferredJsonMergerTest { */ @Test fun june2023ExampleH() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1994,14 +1994,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf(), label = "A"), - DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + IncrementalResultIdentifier(path = listOf(), label = "A"), + IncrementalResultIdentifier(path = listOf("me"), label = "B"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -2053,13 +2053,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + IncrementalResultIdentifier(path = listOf("me"), label = "B"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -2119,13 +2119,13 @@ class DeferredJsonMergerTest { ] } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("me"), label = "B"), + IncrementalResultIdentifier(path = listOf("me"), label = "B"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -2134,7 +2134,7 @@ class DeferredJsonMergerTest { */ @Test fun july2025ExampleI() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -2189,14 +2189,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), - DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -2236,14 +2236,14 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), - DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -2279,13 +2279,13 @@ class DeferredJsonMergerTest { } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), + IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -2334,11 +2334,11 @@ class DeferredJsonMergerTest { } """.trimIndent() - deferredJsonMerger.merge(payload4.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload4.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4), incrementalResultsMerger.merged) assertEquals( setOf(), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } @@ -2347,7 +2347,7 @@ class DeferredJsonMergerTest { */ @Test fun july2025ExampleJ() { - val deferredJsonMerger = DeferredJsonMerger() + val incrementalResultsMerger = IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -2387,13 +2387,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -2429,13 +2429,13 @@ class DeferredJsonMergerTest { } } """.trimIndent() - deferredJsonMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) //language=JSON @@ -2497,13 +2497,13 @@ class DeferredJsonMergerTest { } """.trimIndent() - deferredJsonMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), deferredJsonMerger.merged) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - deferredJsonMerger.pendingFragmentIds + incrementalResultsMerger.pendingResultIds ) } } From 072059af23bf2a50570fed20971aeeb327bc8e85 Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 21 Jul 2025 10:00:31 +0200 Subject: [PATCH 09/21] Fix running tests hardcoded to true --- tests/defer/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/defer/build.gradle.kts b/tests/defer/build.gradle.kts index 0f1b0ea79c7..d90c4f5fd6d 100644 --- a/tests/defer/build.gradle.kts +++ b/tests/defer/build.gradle.kts @@ -77,7 +77,7 @@ fun com.apollographql.apollo.gradle.api.Service.configureConnection(generateKotl tasks.withType(AbstractTestTask::class.java) { // Run the defer with Router and defer with Apollo Server tests only from a specific CI job val runDeferWithRouterTests = System.getenv("DEFER_WITH_ROUTER_TESTS").toBoolean() - val runDeferWithApolloServerTests = true + val runDeferWithApolloServerTests = System.getenv("DEFER_WITH_APOLLO_SERVER_TESTS").toBoolean() filter.setIncludePatterns(*buildList { if (runDeferWithRouterTests) add("test.DeferWithRouterTest") if (runDeferWithApolloServerTests) add("test.DeferWithApolloServerTest") From 989e122df0f498bccf2add8b9cd03857582315b1 Mon Sep 17 00:00:00 2001 From: BoD Date: Wed, 24 Sep 2025 18:12:58 +0200 Subject: [PATCH 10/21] Support both legacy and modern incremental delivery protocols, with a configuration. Defaults to the legacy one. --- ...-tests.yml => defer-integration-tests.yml} | 2 + libraries/apollo-api/api/apollo-api.api | 12 +- libraries/apollo-api/api/apollo-api.klib.api | 12 +- .../apollo/api/BooleanExpression.kt | 21 +- .../apollo/api/CustomScalarAdapters.kt | 19 +- .../apollographql/apollo/api/Executables.kt | 4 +- .../apollo/api/IncrementalResultIdentifier.kt | 30 +- .../apollographql/apollo/api/Operations.kt | 8 +- .../api/http/DefaultHttpRequestComposer.kt | 32 +- .../apollo/api/internal/ResponseParser.kt | 6 +- .../api/android/apollo-runtime.api | 63 +- .../api/apollo-runtime.klib.api | 81 +- .../apollo-runtime/api/jvm/apollo-runtime.api | 63 +- .../Defer20220824IncrementalResultsMerger.kt | 92 +++ ...raphQL17Alpha9IncrementalResultsMerger.kt} | 85 +- .../incremental/IncrementalResultsMerger.kt | 41 + .../apollo/internal/incremental/JsonMap.kt | 45 ++ .../network/http/HttpNetworkTransport.kt | 77 +- .../websocket/WebSocketNetworkTransport.kt | 38 +- .../network/ws/WebSocketNetworkTransport.kt | 22 +- ...fer20220824IncrementalResultsMergerTest.kt | 747 ++++++++++++++++++ ...QL17Alpha9IncrementalResultsMergerTest.kt} | 628 +++++++-------- tests/defer/apollo-server/README.md | 2 +- tests/defer/apollo-server/package.json | 2 +- .../patches/@apollo+server+4.11.2.patch | 27 +- .../test/Defer20220824NormalizedCacheTest.kt | 548 +++++++++++++ .../kotlin/test/Defer20220824Test.kt | 446 +++++++++++ ...eferGraphQL17Alpha9NormalizedCacheTest.kt} | 200 +++-- ...ferTest.kt => DeferGraphQL17Alpha9Test.kt} | 21 +- .../kotlin/test/DeferWithApolloServerTest.kt | 9 +- .../src/jvmTest/kotlin/test/DeferJvmTest.kt | 11 +- 31 files changed, 2785 insertions(+), 609 deletions(-) rename .github/workflows/{defer-with-router-tests.yml => defer-integration-tests.yml} (98%) create mode 100644 libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger.kt rename libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/{IncrementalResultsMerger.kt => incremental/GraphQL17Alpha9IncrementalResultsMerger.kt} (56%) create mode 100644 libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalResultsMerger.kt create mode 100644 libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/JsonMap.kt create mode 100644 libraries/apollo-runtime/src/commonTest/kotlin/test/defer/Defer20220824IncrementalResultsMergerTest.kt rename libraries/apollo-runtime/src/commonTest/kotlin/test/defer/{IncrementalResultsMergerTest.kt => GraphQL17Alpha9IncrementalResultsMergerTest.kt} (78%) create mode 100644 tests/defer/src/commonTest/kotlin/test/Defer20220824NormalizedCacheTest.kt create mode 100644 tests/defer/src/commonTest/kotlin/test/Defer20220824Test.kt rename tests/defer/src/commonTest/kotlin/test/{DeferNormalizedCacheTest.kt => DeferGraphQL17Alpha9NormalizedCacheTest.kt} (86%) rename tests/defer/src/commonTest/kotlin/test/{DeferTest.kt => DeferGraphQL17Alpha9Test.kt} (95%) diff --git a/.github/workflows/defer-with-router-tests.yml b/.github/workflows/defer-integration-tests.yml similarity index 98% rename from .github/workflows/defer-with-router-tests.yml rename to .github/workflows/defer-integration-tests.yml index 084ffb6156d..313085f6379 100644 --- a/.github/workflows/defer-with-router-tests.yml +++ b/.github/workflows/defer-integration-tests.yml @@ -1,3 +1,5 @@ +name: defer-integration-tests + on: schedule: - cron: '0 3 * * *' diff --git a/libraries/apollo-api/api/apollo-api.api b/libraries/apollo-api/api/apollo-api.api index f9259849daf..82ac9960d60 100644 --- a/libraries/apollo-api/api/apollo-api.api +++ b/libraries/apollo-api/api/apollo-api.api @@ -426,7 +426,6 @@ public final class com/apollographql/apollo/api/CustomScalarAdapters$Builder { public final fun deferredFragmentIdentifiers (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; public final fun errors (Ljava/util/List;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; public final fun falseVariables (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; - public final fun pendingResultIds (Ljava/util/Set;)Lcom/apollographql/apollo/api/CustomScalarAdapters$Builder; } public final class com/apollographql/apollo/api/CustomScalarAdapters$Key : com/apollographql/apollo/api/ExecutionContext$Key { @@ -714,6 +713,12 @@ public final class com/apollographql/apollo/api/ImmutableMapBuilder { public final fun put (Ljava/lang/Object;Ljava/lang/Object;)Lcom/apollographql/apollo/api/ImmutableMapBuilder; } +public final class com/apollographql/apollo/api/IncrementalResultIdentifierKt { + public static final fun isPending (Ljava/util/Set;)Z + public static final fun nonPending (Ljava/util/Set;)Ljava/util/Set; + public static final fun pending (Ljava/util/Set;)Ljava/util/Set; +} + public final class com/apollographql/apollo/api/InputObjectType : com/apollographql/apollo/api/CompiledNamedType { public fun (Ljava/lang/String;)V } @@ -928,7 +933,12 @@ public final class com/apollographql/apollo/api/http/ByteStringHttpBody : com/ap public final class com/apollographql/apollo/api/http/DefaultHttpRequestComposer : com/apollographql/apollo/api/http/HttpRequestComposer { public static final field Companion Lcom/apollographql/apollo/api/http/DefaultHttpRequestComposer$Companion; + public static final field HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824 Ljava/lang/String; + public static final field HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20230621 Ljava/lang/String; + public static final field HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0 Ljava/lang/String; public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun compose (Lcom/apollographql/apollo/api/ApolloRequest;)Lcom/apollographql/apollo/api/http/HttpRequest; } diff --git a/libraries/apollo-api/api/apollo-api.klib.api b/libraries/apollo-api/api/apollo-api.klib.api index 587765d443e..f9bb9628590 100644 --- a/libraries/apollo-api/api/apollo-api.klib.api +++ b/libraries/apollo-api/api/apollo-api.klib.api @@ -493,10 +493,18 @@ final class com.apollographql.apollo.api.http/ByteStringHttpBody : com.apollogra final class com.apollographql.apollo.api.http/DefaultHttpRequestComposer : com.apollographql.apollo.api.http/HttpRequestComposer { // com.apollographql.apollo.api.http/DefaultHttpRequestComposer|null[0] constructor (kotlin/String) // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.|(kotlin.String){}[0] + constructor (kotlin/String, kotlin/String = ..., kotlin/String = ...) // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.|(kotlin.String;kotlin.String;kotlin.String){}[0] final fun <#A1: com.apollographql.apollo.api/Operation.Data> compose(com.apollographql.apollo.api/ApolloRequest<#A1>): com.apollographql.apollo.api.http/HttpRequest // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.compose|compose(com.apollographql.apollo.api.ApolloRequest<0:0>){0§}[0] final object Companion { // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion|null[0] + final const val HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824 // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824|{}HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824[0] + final fun (): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824.|(){}[0] + final const val HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20230621 // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20230621|{}HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20230621[0] + final fun (): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20230621.|(){}[0] + final const val HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0 // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0|{}HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0[0] + final fun (): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0.|(){}[0] + final val HEADER_ACCEPT_NAME // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_NAME|{}HEADER_ACCEPT_NAME[0] final fun (): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_NAME.|(){}[0] final val HEADER_ACCEPT_VALUE_DEFER // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_DEFER|{}HEADER_ACCEPT_VALUE_DEFER[0] @@ -938,7 +946,6 @@ final class com.apollographql.apollo.api/CustomScalarAdapters : com.apollographq final fun deferredFragmentIdentifiers(kotlin.collections/Set?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.deferredFragmentIdentifiers|deferredFragmentIdentifiers(kotlin.collections.Set?){}[0] final fun errors(kotlin.collections/List?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.errors|errors(kotlin.collections.List?){}[0] final fun falseVariables(kotlin.collections/Set?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.falseVariables|falseVariables(kotlin.collections.Set?){}[0] - final fun pendingResultIds(kotlin.collections/Set?): com.apollographql.apollo.api/CustomScalarAdapters.Builder // com.apollographql.apollo.api/CustomScalarAdapters.Builder.pendingResultIds|pendingResultIds(kotlin.collections.Set?){}[0] } final object Key : com.apollographql.apollo.api/ExecutionContext.Key { // com.apollographql.apollo.api/CustomScalarAdapters.Key|null[0] @@ -1430,6 +1437,9 @@ final fun (com.apollographql.apollo.api/Operation.Data).com.apollographql.apollo final fun (kotlin.collections/List).com.apollographql.apollo.api.http/get(kotlin/String): kotlin/String? // com.apollographql.apollo.api.http/get|get@kotlin.collections.List(kotlin.String){}[0] final fun (kotlin.collections/List).com.apollographql.apollo.api.http/valueOf(kotlin/String): kotlin/String? // com.apollographql.apollo.api.http/valueOf|valueOf@kotlin.collections.List(kotlin.String){}[0] final fun (kotlin.collections/Map).com.apollographql.apollo.api.json/jsonReader(): com.apollographql.apollo.api.json/JsonReader // com.apollographql.apollo.api.json/jsonReader|jsonReader@kotlin.collections.Map(){}[0] +final fun (kotlin.collections/Set).com.apollographql.apollo.api/isPending(): kotlin/Boolean // com.apollographql.apollo.api/isPending|isPending@kotlin.collections.Set(){}[0] +final fun (kotlin.collections/Set).com.apollographql.apollo.api/nonPending(): kotlin.collections/Set // com.apollographql.apollo.api/nonPending|nonPending@kotlin.collections.Set(){}[0] +final fun (kotlin.collections/Set).com.apollographql.apollo.api/pending(): kotlin.collections/Set // com.apollographql.apollo.api/pending|pending@kotlin.collections.Set(){}[0] final fun (kotlin/String).com.apollographql.apollo.api.http.internal/urlDecode(): kotlin/String // com.apollographql.apollo.api.http.internal/urlDecode|urlDecode@kotlin.String(){}[0] final fun (kotlin/String).com.apollographql.apollo.api.http.internal/urlEncode(): kotlin/String // com.apollographql.apollo.api.http.internal/urlEncode|urlEncode@kotlin.String(){}[0] final fun (okio/BufferedSource).com.apollographql.apollo.api.json/jsonReader(): com.apollographql.apollo.api.json/JsonReader // com.apollographql.apollo.api.json/jsonReader|jsonReader@okio.BufferedSource(){}[0] diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt index cfe2db00c53..8c33d3e88ad 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt @@ -67,7 +67,7 @@ internal fun BooleanExpression.evaluate(block: (T) -> Boolean): Boo fun BooleanExpression.evaluate( variables: Set?, typename: String?, - pendingResultIds: Set?, + deferredFragmentIdentifiers: IncrementalResultIdentifiers?, path: List?, ): Boolean { // Remove "data" from the path @@ -75,22 +75,29 @@ fun BooleanExpression.evaluate( return evaluate { when (it) { is BVariable -> !(variables?.contains(it.name) ?: false) - is BLabel -> !isDeferredFragmentPending(pendingResultIds, croppedPath!!, it.label) + is BLabel -> shouldParseFragment(deferredFragmentIdentifiers = deferredFragmentIdentifiers, path = croppedPath!!, label = it.label) is BPossibleTypes -> it.possibleTypes.contains(typename) } } } -private fun isDeferredFragmentPending( - pendingResultIds: Set?, +private fun shouldParseFragment( + deferredFragmentIdentifiers: IncrementalResultIdentifiers?, path: List, label: String?, ): Boolean { - if (pendingResultIds == null) { + if (deferredFragmentIdentifiers == null) { // By default, parse all deferred fragments - this is the case when parsing from the normalized cache. - return false + return true + } + val identifier = IncrementalResultIdentifier(path, label) + return if (deferredFragmentIdentifiers.isPending()) { + // Modern protocol: parse fragments that are _not_ pending + !deferredFragmentIdentifiers.contains(identifier) + } else { + // Legacy 20220824 protocol: parse fragments that have been merged + deferredFragmentIdentifiers.contains(identifier) } - return pendingResultIds.contains(IncrementalResultIdentifier(path, label)) } /** diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt index 585aa94a4c3..324d58c95a6 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt @@ -19,10 +19,10 @@ class CustomScalarAdapters private constructor( @JvmField val falseVariables: Set?, /** - * Pending incremental result identifiers used to determine whether the parser must parse deferred fragments + * Incremental result identifiers used to determine whether the parser must parse deferred fragments */ @JvmField - val deferredFragmentIdentifiers: Set?, + val deferredFragmentIdentifiers: IncrementalResultIdentifiers?, /** * Errors to use with @catch */ @@ -125,26 +125,21 @@ class CustomScalarAdapters private constructor( fun newBuilder(): Builder { return Builder().addAll(this) .falseVariables(falseVariables) - .pendingResultIds(deferredFragmentIdentifiers) + .deferredFragmentIdentifiers(deferredFragmentIdentifiers) } class Builder { private val adaptersMap: MutableMap> = mutableMapOf() private var falseVariables: Set? = null - private var pendingResultIds: Set? = null + private var deferredFragmentIdentifiers: IncrementalResultIdentifiers? = null private var errors: List? = null fun falseVariables(falseVariables: Set?) = apply { this.falseVariables = falseVariables } - @Deprecated("Use pendingResultIds instead", ReplaceWith("pendingResultIds(pendingResultIds = deferredFragmentIdentifiers)")) - fun deferredFragmentIdentifiers(deferredFragmentIdentifiers: Set?) = apply { - this.pendingResultIds = deferredFragmentIdentifiers - } - - fun pendingResultIds(pendingResultIds: Set?) = apply { - this.pendingResultIds = pendingResultIds + fun deferredFragmentIdentifiers(deferredFragmentIdentifiers: IncrementalResultIdentifiers?) = apply { + this.deferredFragmentIdentifiers = deferredFragmentIdentifiers } fun errors(errors: List?) = apply { @@ -178,7 +173,7 @@ class CustomScalarAdapters private constructor( return CustomScalarAdapters( adaptersMap, falseVariables, - pendingResultIds, + deferredFragmentIdentifiers, errors, ) } diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt index 59fda6ca3ef..0cd35e4c604 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt @@ -71,12 +71,12 @@ fun Executable.parseData( jsonReader: JsonReader, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, falseVariables: Set? = null, - deferredFragmentIds: Set? = null, + deferredFragmentIds: IncrementalResultIdentifiers? = null, errors: List? = null, ): D? { val customScalarAdapters1 = customScalarAdapters.newBuilder() .falseVariables(falseVariables) - .pendingResultIds(pendingResultIds = deferredFragmentIds) + .deferredFragmentIdentifiers(deferredFragmentIds) .errors(errors) .build() return adapter().nullable().fromJson(jsonReader, customScalarAdapters1) diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt index 0d3d6ec5d6a..b2455047c37 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt @@ -1,15 +1,41 @@ package com.apollographql.apollo.api +import com.apollographql.apollo.annotations.ApolloInternal + data class DeferredFragmentIdentifier( /** * Path of the fragment in the overall JSON response. The elements can either be Strings (names) or Integers (array indices). */ val path: List, val label: String?, -) +) { + internal companion object { + /** + * Special identifier to signal that the identifiers are pending, as in the modern version of the protocol. + */ + internal val Pending = DeferredFragmentIdentifier(emptyList(), "__pending") + } +} /** * Identifies an incremental result. - * [DeferredFragmentIdentifier] is kept to not break the API/ABI, but this alias is more descriptive of its purpose. + * [DeferredFragmentIdentifier] is kept to preserve the API/ABI, but this alias is more descriptive of its purpose. */ typealias IncrementalResultIdentifier = DeferredFragmentIdentifier + +typealias IncrementalResultIdentifiers = Set + +@ApolloInternal +fun IncrementalResultIdentifiers.isPending(): Boolean { + return any { it === DeferredFragmentIdentifier.Pending } +} + +@ApolloInternal +fun IncrementalResultIdentifiers.pending(): IncrementalResultIdentifiers { + return this + DeferredFragmentIdentifier.Pending +} + +@ApolloInternal +fun IncrementalResultIdentifiers.nonPending(): IncrementalResultIdentifiers { + return filter { it !== DeferredFragmentIdentifier.Pending }.toSet() +} diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt index a97159aa4a0..b9e4414818c 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt @@ -70,7 +70,7 @@ fun Operation.composeJsonRequest( fun Operation.parseJsonResponse( jsonReader: JsonReader, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: IncrementalResultIdentifiers? = null, ): ApolloResponse { return jsonReader.use { ResponseParser.parse( @@ -103,7 +103,7 @@ fun Operation.parseResponse( jsonReader: JsonReader, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: IncrementalResultIdentifiers? = null, ): ApolloResponse { return try { ResponseParser.parse( @@ -177,7 +177,7 @@ fun JsonReader.toApolloResponse( operation: Operation, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: IncrementalResultIdentifiers? = null, ): ApolloResponse { return use { try { @@ -213,7 +213,7 @@ fun JsonReader.parseResponse( operation: Operation, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: Set? = null, + deferredFragmentIdentifiers: IncrementalResultIdentifiers? = null, ): ApolloResponse { return try { ResponseParser.parse( diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt index 9170e69bc59..881910a027f 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt @@ -32,17 +32,25 @@ import okio.buffer */ class DefaultHttpRequestComposer( private val serverUrl: String, + private val acceptHeaderQueriesAndMutations: String = HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824, + private val acceptHeaderSubscriptions: String = HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0, ) : HttpRequestComposer { + constructor(serverUrl: String) : this( + serverUrl = serverUrl, + acceptHeaderQueriesAndMutations = HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824, + acceptHeaderSubscriptions = HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0, + ) + override fun compose(apolloRequest: ApolloRequest): HttpRequest { val operation = apolloRequest.operation val customScalarAdapters = apolloRequest.executionContext[CustomScalarAdapters] ?: CustomScalarAdapters.Empty val requestHeaders = mutableListOf().apply { if (apolloRequest.operation is Subscription<*>) { - add(HttpHeader(HEADER_ACCEPT_NAME, HEADER_ACCEPT_VALUE_MULTIPART)) + add(HttpHeader(HEADER_ACCEPT_NAME, acceptHeaderSubscriptions)) } else { - add(HttpHeader(HEADER_ACCEPT_NAME, HEADER_ACCEPT_VALUE_DEFER)) + add(HttpHeader(HEADER_ACCEPT_NAME, acceptHeaderQueriesAndMutations)) } if (apolloRequest.httpHeaders != null) { addAll(apolloRequest.httpHeaders) @@ -96,11 +104,21 @@ class DefaultHttpRequestComposer( val HEADER_ACCEPT_NAME = "Accept" - // TODO The deferSpec=20220824 part is a temporary measure so early backend implementations of the @defer directive - // can recognize early client implementations and potentially reply in a compatible way. - // This should be removed in later versions. - val HEADER_ACCEPT_VALUE_DEFER = "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" - val HEADER_ACCEPT_VALUE_MULTIPART = "multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json" + const val HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824 = + "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" + + // TODO To be agreed upon with the router and other clients + const val HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20230621 = + "multipart/mixed;incrementalDeliverySpec=20230621, application/graphql-response+json, application/json" + + const val HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0 = + "multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json" + + @Deprecated("Use HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0 instead", ReplaceWith("HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0")) + val HEADER_ACCEPT_VALUE_MULTIPART = HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0 + + @Deprecated("Use HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824 instead", ReplaceWith("HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824")) + val HEADER_ACCEPT_VALUE_DEFER = HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824 private fun buildGetUrl( serverUrl: String, diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt index fc611c83e17..0c92b233578 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt @@ -4,7 +4,7 @@ import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.CustomScalarAdapters import com.apollographql.apollo.api.Error -import com.apollographql.apollo.api.IncrementalResultIdentifier +import com.apollographql.apollo.api.IncrementalResultIdentifiers import com.apollographql.apollo.api.Operation import com.apollographql.apollo.api.falseVariables import com.apollographql.apollo.api.json.JsonReader @@ -24,7 +24,7 @@ internal object ResponseParser { operation: Operation, requestUuid: Uuid?, customScalarAdapters: CustomScalarAdapters, - pendingResultIds: Set?, + deferredFragmentIds: IncrementalResultIdentifiers?, ): ApolloResponse { jsonReader.beginObject() @@ -36,7 +36,7 @@ internal object ResponseParser { when (val name = jsonReader.nextName()) { "data" -> { val falseVariables = operation.falseVariables(customScalarAdapters) - data = operation.parseData(jsonReader, customScalarAdapters, falseVariables, pendingResultIds, errors) + data = operation.parseData(jsonReader, customScalarAdapters, falseVariables, deferredFragmentIds, errors) } "errors" -> errors = jsonReader.readErrors() diff --git a/libraries/apollo-runtime/api/android/apollo-runtime.api b/libraries/apollo-runtime/api/android/apollo-runtime.api index d4e65147106..3d9b141bcb0 100644 --- a/libraries/apollo-runtime/api/android/apollo-runtime.api +++ b/libraries/apollo-runtime/api/android/apollo-runtime.api @@ -215,17 +215,6 @@ public final class com/apollographql/apollo/interceptor/RetryOnErrorInterceptorK public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor; } -public final class com/apollographql/apollo/internal/IncrementalResultsMerger { - public fun ()V - public final fun getHasNext ()Z - public final fun getMerged ()Ljava/util/Map; - public final fun getPendingResultIds ()Ljava/util/Set; - public final fun isEmptyResponse ()Z - public final fun merge (Ljava/util/Map;)Ljava/util/Map; - public final fun merge (Lokio/BufferedSource;)Ljava/util/Map; - public final fun reset ()V -} - public final class com/apollographql/apollo/internal/MultipartReader : java/io/Closeable { public fun (Lokio/BufferedSource;Ljava/lang/String;)V public fun close ()V @@ -240,6 +229,38 @@ public final class com/apollographql/apollo/internal/MultipartReader$Part : java public final fun getHeaders ()Ljava/util/List; } +public final class com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger : com/apollographql/apollo/internal/incremental/IncrementalResultsMerger { + public fun ()V + public fun getHasNext ()Z + public fun getIncrementalResultIdentifiers ()Ljava/util/Set; + public fun getMerged ()Ljava/util/Map; + public fun isEmptyResponse ()Z + public fun merge (Ljava/util/Map;)Ljava/util/Map; + public fun merge (Lokio/BufferedSource;)Ljava/util/Map; + public fun reset ()V +} + +public final class com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger : com/apollographql/apollo/internal/incremental/IncrementalResultsMerger { + public fun ()V + public fun getHasNext ()Z + public fun getIncrementalResultIdentifiers ()Ljava/util/Set; + public fun getMerged ()Ljava/util/Map; + public fun isEmptyResponse ()Z + public fun merge (Ljava/util/Map;)Ljava/util/Map; + public fun merge (Lokio/BufferedSource;)Ljava/util/Map; + public fun reset ()V +} + +public abstract interface class com/apollographql/apollo/internal/incremental/IncrementalResultsMerger { + public abstract fun getHasNext ()Z + public abstract fun getIncrementalResultIdentifiers ()Ljava/util/Set; + public abstract fun getMerged ()Ljava/util/Map; + public abstract fun isEmptyResponse ()Z + public abstract fun merge (Ljava/util/Map;)Ljava/util/Map; + public abstract fun merge (Lokio/BufferedSource;)Ljava/util/Map; + public abstract fun reset ()V +} + public abstract interface class com/apollographql/apollo/network/NetworkMonitor : java/io/Closeable { public abstract fun isOnline ()Lkotlinx/coroutines/flow/StateFlow; } @@ -350,7 +371,7 @@ public abstract interface class com/apollographql/apollo/network/http/HttpInterc } public final class com/apollographql/apollo/network/http/HttpNetworkTransport : com/apollographql/apollo/network/NetworkTransport { - public synthetic fun (Lcom/apollographql/apollo/api/http/HttpRequestComposer;Lcom/apollographql/apollo/network/http/HttpEngine;Ljava/util/List;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lcom/apollographql/apollo/api/http/HttpRequestComposer;Lcom/apollographql/apollo/network/http/HttpEngine;Ljava/util/List;ZLcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun dispose ()V public fun execute (Lcom/apollographql/apollo/api/ApolloRequest;)Lkotlinx/coroutines/flow/Flow; public final fun getInterceptors ()Ljava/util/List; @@ -366,6 +387,7 @@ public final class com/apollographql/apollo/network/http/HttpNetworkTransport$Bu public final fun httpEngine (Lcom/apollographql/apollo/network/http/HttpEngine;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun httpHeaders (Ljava/util/List;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun httpRequestComposer (Lcom/apollographql/apollo/api/http/HttpRequestComposer;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; + public final fun incrementalDeliveryProtocol (Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun interceptors (Ljava/util/List;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun serverUrl (Ljava/lang/String;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; } @@ -375,6 +397,23 @@ public final class com/apollographql/apollo/network/http/HttpNetworkTransport$En public fun intercept (Lcom/apollographql/apollo/api/http/HttpRequest;Lcom/apollographql/apollo/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public abstract interface class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol { + public abstract fun getAcceptHeader ()Ljava/lang/String; + public abstract fun newIncrementalResultsMerger ()Lcom/apollographql/apollo/internal/incremental/IncrementalResultsMerger; +} + +public final class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$Defer20220824 : com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol { + public static final field INSTANCE Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$Defer20220824; + public fun getAcceptHeader ()Ljava/lang/String; + public fun newIncrementalResultsMerger ()Lcom/apollographql/apollo/internal/incremental/IncrementalResultsMerger; +} + +public final class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$GraphQL17Alpha9 : com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol { + public static final field INSTANCE Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$GraphQL17Alpha9; + public fun getAcceptHeader ()Ljava/lang/String; + public fun newIncrementalResultsMerger ()Lcom/apollographql/apollo/internal/incremental/IncrementalResultsMerger; +} + public final class com/apollographql/apollo/network/http/LoggingInterceptor : com/apollographql/apollo/network/http/HttpInterceptor { public fun ()V public fun (Lcom/apollographql/apollo/network/http/LoggingInterceptor$Level;Lkotlin/jvm/functions/Function1;)V diff --git a/libraries/apollo-runtime/api/apollo-runtime.klib.api b/libraries/apollo-runtime/api/apollo-runtime.klib.api index e82fb4ac562..84a7ad33872 100644 --- a/libraries/apollo-runtime/api/apollo-runtime.klib.api +++ b/libraries/apollo-runtime/api/apollo-runtime.klib.api @@ -33,6 +33,21 @@ abstract interface com.apollographql.apollo.interceptor/ApolloInterceptorChain { abstract fun <#A1: com.apollographql.apollo.api/Operation.Data> proceed(com.apollographql.apollo.api/ApolloRequest<#A1>): kotlinx.coroutines.flow/Flow> // com.apollographql.apollo.interceptor/ApolloInterceptorChain.proceed|proceed(com.apollographql.apollo.api.ApolloRequest<0:0>){0§}[0] } +abstract interface com.apollographql.apollo.internal.incremental/IncrementalResultsMerger { // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger|null[0] + abstract val hasNext // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.hasNext|{}hasNext[0] + abstract fun (): kotlin/Boolean // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.hasNext.|(){}[0] + abstract val incrementalResultIdentifiers // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.incrementalResultIdentifiers|{}incrementalResultIdentifiers[0] + abstract fun (): kotlin.collections/Set // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.incrementalResultIdentifiers.|(){}[0] + abstract val isEmptyResponse // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.isEmptyResponse|{}isEmptyResponse[0] + abstract fun (): kotlin/Boolean // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.isEmptyResponse.|(){}[0] + abstract val merged // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.merged|{}merged[0] + abstract fun (): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.merged.|(){}[0] + + abstract fun merge(kotlin.collections/Map): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.merge|merge(kotlin.collections.Map){}[0] + abstract fun merge(okio/BufferedSource): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.merge|merge(okio.BufferedSource){}[0] + abstract fun reset() // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.reset|reset(){}[0] +} + abstract interface com.apollographql.apollo.network.http/HttpEngine : okio/Closeable { // com.apollographql.apollo.network.http/HttpEngine|null[0] abstract suspend fun execute(com.apollographql.apollo.api.http/HttpRequest): com.apollographql.apollo.api.http/HttpResponse // com.apollographql.apollo.network.http/HttpEngine.execute|execute(com.apollographql.apollo.api.http.HttpRequest){}[0] open fun close() // com.apollographql.apollo.network.http/HttpEngine.close|close(){}[0] @@ -200,22 +215,40 @@ final class com.apollographql.apollo.interceptor/AutoPersistedQueryInterceptor : } } -final class com.apollographql.apollo.internal/IncrementalResultsMerger { // com.apollographql.apollo.internal/IncrementalResultsMerger|null[0] - constructor () // com.apollographql.apollo.internal/IncrementalResultsMerger.|(){}[0] +final class com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger : com.apollographql.apollo.internal.incremental/IncrementalResultsMerger { // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger|null[0] + constructor () // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.|(){}[0] + + final val incrementalResultIdentifiers // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.incrementalResultIdentifiers|{}incrementalResultIdentifiers[0] + final fun (): kotlin.collections/Set // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.incrementalResultIdentifiers.|(){}[0] + final val merged // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.merged|{}merged[0] + final fun (): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.merged.|(){}[0] + + final var hasNext // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.hasNext|{}hasNext[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.hasNext.|(){}[0] + final var isEmptyResponse // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.isEmptyResponse|{}isEmptyResponse[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.isEmptyResponse.|(){}[0] + + final fun merge(kotlin.collections/Map): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.merge|merge(kotlin.collections.Map){}[0] + final fun merge(okio/BufferedSource): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.merge|merge(okio.BufferedSource){}[0] + final fun reset() // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.reset|reset(){}[0] +} + +final class com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger : com.apollographql.apollo.internal.incremental/IncrementalResultsMerger { // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger|null[0] + constructor () // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.|(){}[0] - final val merged // com.apollographql.apollo.internal/IncrementalResultsMerger.merged|{}merged[0] - final fun (): kotlin.collections/Map // com.apollographql.apollo.internal/IncrementalResultsMerger.merged.|(){}[0] - final val pendingResultIds // com.apollographql.apollo.internal/IncrementalResultsMerger.pendingResultIds|{}pendingResultIds[0] - final fun (): kotlin.collections/Set // com.apollographql.apollo.internal/IncrementalResultsMerger.pendingResultIds.|(){}[0] + final val incrementalResultIdentifiers // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.incrementalResultIdentifiers|{}incrementalResultIdentifiers[0] + final fun (): kotlin.collections/Set // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.incrementalResultIdentifiers.|(){}[0] + final val merged // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.merged|{}merged[0] + final fun (): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.merged.|(){}[0] - final var hasNext // com.apollographql.apollo.internal/IncrementalResultsMerger.hasNext|{}hasNext[0] - final fun (): kotlin/Boolean // com.apollographql.apollo.internal/IncrementalResultsMerger.hasNext.|(){}[0] - final var isEmptyResponse // com.apollographql.apollo.internal/IncrementalResultsMerger.isEmptyResponse|{}isEmptyResponse[0] - final fun (): kotlin/Boolean // com.apollographql.apollo.internal/IncrementalResultsMerger.isEmptyResponse.|(){}[0] + final var hasNext // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.hasNext|{}hasNext[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.hasNext.|(){}[0] + final var isEmptyResponse // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.isEmptyResponse|{}isEmptyResponse[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.isEmptyResponse.|(){}[0] - final fun merge(kotlin.collections/Map): kotlin.collections/Map // com.apollographql.apollo.internal/IncrementalResultsMerger.merge|merge(kotlin.collections.Map){}[0] - final fun merge(okio/BufferedSource): kotlin.collections/Map // com.apollographql.apollo.internal/IncrementalResultsMerger.merge|merge(okio.BufferedSource){}[0] - final fun reset() // com.apollographql.apollo.internal/IncrementalResultsMerger.reset|reset(){}[0] + final fun merge(kotlin.collections/Map): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.merge|merge(kotlin.collections.Map){}[0] + final fun merge(okio/BufferedSource): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.merge|merge(okio.BufferedSource){}[0] + final fun reset() // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.reset|reset(){}[0] } final class com.apollographql.apollo.internal/MultipartReader : okio/Closeable { // com.apollographql.apollo.internal/MultipartReader|null[0] @@ -312,6 +345,27 @@ final class com.apollographql.apollo.network.http/HttpNetworkTransport : com.apo final fun dispose() // com.apollographql.apollo.network.http/HttpNetworkTransport.dispose|dispose(){}[0] final fun newBuilder(): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.newBuilder|newBuilder(){}[0] + sealed interface IncrementalDeliveryProtocol { // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol|null[0] + abstract val acceptHeader // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.acceptHeader|{}acceptHeader[0] + abstract fun (): kotlin/String // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.acceptHeader.|(){}[0] + + abstract fun newIncrementalResultsMerger(): com.apollographql.apollo.internal.incremental/IncrementalResultsMerger // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.newIncrementalResultsMerger|newIncrementalResultsMerger(){}[0] + + final object Defer20220824 : com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol { // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824|null[0] + final val acceptHeader // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824.acceptHeader|{}acceptHeader[0] + final fun (): kotlin/String // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824.acceptHeader.|(){}[0] + + final fun newIncrementalResultsMerger(): com.apollographql.apollo.internal.incremental/IncrementalResultsMerger // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824.newIncrementalResultsMerger|newIncrementalResultsMerger(){}[0] + } + + final object GraphQL17Alpha9 : com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol { // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9|null[0] + final val acceptHeader // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9.acceptHeader|{}acceptHeader[0] + final fun (): kotlin/String // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9.acceptHeader.|(){}[0] + + final fun newIncrementalResultsMerger(): com.apollographql.apollo.internal.incremental/IncrementalResultsMerger // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9.newIncrementalResultsMerger|newIncrementalResultsMerger(){}[0] + } + } + final class Builder { // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder|null[0] constructor () // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.|(){}[0] @@ -322,6 +376,7 @@ final class com.apollographql.apollo.network.http/HttpNetworkTransport : com.apo final fun httpEngine(com.apollographql.apollo.network.http/HttpEngine): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.httpEngine|httpEngine(com.apollographql.apollo.network.http.HttpEngine){}[0] final fun httpHeaders(kotlin.collections/List): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.httpHeaders|httpHeaders(kotlin.collections.List){}[0] final fun httpRequestComposer(com.apollographql.apollo.api.http/HttpRequestComposer): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.httpRequestComposer|httpRequestComposer(com.apollographql.apollo.api.http.HttpRequestComposer){}[0] + final fun incrementalDeliveryProtocol(com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.incrementalDeliveryProtocol|incrementalDeliveryProtocol(com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol){}[0] final fun interceptors(kotlin.collections/List): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.interceptors|interceptors(kotlin.collections.List){}[0] final fun serverUrl(kotlin/String): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.serverUrl|serverUrl(kotlin.String){}[0] } diff --git a/libraries/apollo-runtime/api/jvm/apollo-runtime.api b/libraries/apollo-runtime/api/jvm/apollo-runtime.api index 8806ffc7efd..eff51c03d5b 100644 --- a/libraries/apollo-runtime/api/jvm/apollo-runtime.api +++ b/libraries/apollo-runtime/api/jvm/apollo-runtime.api @@ -215,17 +215,6 @@ public final class com/apollographql/apollo/interceptor/RetryOnErrorInterceptorK public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor; } -public final class com/apollographql/apollo/internal/IncrementalResultsMerger { - public fun ()V - public final fun getHasNext ()Z - public final fun getMerged ()Ljava/util/Map; - public final fun getPendingResultIds ()Ljava/util/Set; - public final fun isEmptyResponse ()Z - public final fun merge (Ljava/util/Map;)Ljava/util/Map; - public final fun merge (Lokio/BufferedSource;)Ljava/util/Map; - public final fun reset ()V -} - public final class com/apollographql/apollo/internal/MultipartReader : java/io/Closeable { public fun (Lokio/BufferedSource;Ljava/lang/String;)V public fun close ()V @@ -240,6 +229,38 @@ public final class com/apollographql/apollo/internal/MultipartReader$Part : java public final fun getHeaders ()Ljava/util/List; } +public final class com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger : com/apollographql/apollo/internal/incremental/IncrementalResultsMerger { + public fun ()V + public fun getHasNext ()Z + public fun getIncrementalResultIdentifiers ()Ljava/util/Set; + public fun getMerged ()Ljava/util/Map; + public fun isEmptyResponse ()Z + public fun merge (Ljava/util/Map;)Ljava/util/Map; + public fun merge (Lokio/BufferedSource;)Ljava/util/Map; + public fun reset ()V +} + +public final class com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger : com/apollographql/apollo/internal/incremental/IncrementalResultsMerger { + public fun ()V + public fun getHasNext ()Z + public fun getIncrementalResultIdentifiers ()Ljava/util/Set; + public fun getMerged ()Ljava/util/Map; + public fun isEmptyResponse ()Z + public fun merge (Ljava/util/Map;)Ljava/util/Map; + public fun merge (Lokio/BufferedSource;)Ljava/util/Map; + public fun reset ()V +} + +public abstract interface class com/apollographql/apollo/internal/incremental/IncrementalResultsMerger { + public abstract fun getHasNext ()Z + public abstract fun getIncrementalResultIdentifiers ()Ljava/util/Set; + public abstract fun getMerged ()Ljava/util/Map; + public abstract fun isEmptyResponse ()Z + public abstract fun merge (Ljava/util/Map;)Ljava/util/Map; + public abstract fun merge (Lokio/BufferedSource;)Ljava/util/Map; + public abstract fun reset ()V +} + public abstract interface class com/apollographql/apollo/network/NetworkMonitor : java/io/Closeable { public abstract fun isOnline ()Lkotlinx/coroutines/flow/StateFlow; } @@ -346,7 +367,7 @@ public abstract interface class com/apollographql/apollo/network/http/HttpInterc } public final class com/apollographql/apollo/network/http/HttpNetworkTransport : com/apollographql/apollo/network/NetworkTransport { - public synthetic fun (Lcom/apollographql/apollo/api/http/HttpRequestComposer;Lcom/apollographql/apollo/network/http/HttpEngine;Ljava/util/List;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lcom/apollographql/apollo/api/http/HttpRequestComposer;Lcom/apollographql/apollo/network/http/HttpEngine;Ljava/util/List;ZLcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun dispose ()V public fun execute (Lcom/apollographql/apollo/api/ApolloRequest;)Lkotlinx/coroutines/flow/Flow; public final fun getInterceptors ()Ljava/util/List; @@ -362,6 +383,7 @@ public final class com/apollographql/apollo/network/http/HttpNetworkTransport$Bu public final fun httpEngine (Lcom/apollographql/apollo/network/http/HttpEngine;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun httpHeaders (Ljava/util/List;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun httpRequestComposer (Lcom/apollographql/apollo/api/http/HttpRequestComposer;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; + public final fun incrementalDeliveryProtocol (Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun interceptors (Ljava/util/List;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun serverUrl (Ljava/lang/String;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; } @@ -371,6 +393,23 @@ public final class com/apollographql/apollo/network/http/HttpNetworkTransport$En public fun intercept (Lcom/apollographql/apollo/api/http/HttpRequest;Lcom/apollographql/apollo/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public abstract interface class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol { + public abstract fun getAcceptHeader ()Ljava/lang/String; + public abstract fun newIncrementalResultsMerger ()Lcom/apollographql/apollo/internal/incremental/IncrementalResultsMerger; +} + +public final class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$Defer20220824 : com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol { + public static final field INSTANCE Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$Defer20220824; + public fun getAcceptHeader ()Ljava/lang/String; + public fun newIncrementalResultsMerger ()Lcom/apollographql/apollo/internal/incremental/IncrementalResultsMerger; +} + +public final class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$GraphQL17Alpha9 : com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol { + public static final field INSTANCE Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$GraphQL17Alpha9; + public fun getAcceptHeader ()Ljava/lang/String; + public fun newIncrementalResultsMerger ()Lcom/apollographql/apollo/internal/incremental/IncrementalResultsMerger; +} + public final class com/apollographql/apollo/network/http/LoggingInterceptor : com/apollographql/apollo/network/http/HttpInterceptor { public fun ()V public fun (Lcom/apollographql/apollo/network/http/LoggingInterceptor$Level;Lkotlin/jvm/functions/Function1;)V diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger.kt new file mode 100644 index 00000000000..e9db40042f9 --- /dev/null +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger.kt @@ -0,0 +1,92 @@ +package com.apollographql.apollo.internal.incremental + +import com.apollographql.apollo.annotations.ApolloInternal +import com.apollographql.apollo.api.IncrementalResultIdentifier +import com.apollographql.apollo.api.IncrementalResultIdentifiers +import okio.BufferedSource + +/** + * Merger for the [com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824] protocol format. + */ +@ApolloInternal +@Suppress("UNCHECKED_CAST") +class Defer20220824IncrementalResultsMerger : IncrementalResultsMerger { + private val _merged: MutableJsonMap = mutableMapOf() + override val merged: JsonMap = _merged + + private val _incrementalResultIds = mutableSetOf() + + /** + * For this protocol, this represents the set of fragment ids that are already merged. + */ + override val incrementalResultIdentifiers: IncrementalResultIdentifiers = _incrementalResultIds + + override var hasNext: Boolean = true + private set + + override var isEmptyResponse: Boolean = false + private set + + override fun merge(part: BufferedSource): JsonMap { + return merge(part.toJsonMap()) + } + + override fun merge(part: JsonMap): JsonMap { + if (merged.isEmpty()) { + // Initial part, no merging needed + _merged += part + return merged + } + + val incremental = part["incremental"] as? List + if (incremental == null) { + isEmptyResponse = true + } else { + isEmptyResponse = false + val mergedErrors = mutableListOf() + val mergedExtensions = mutableListOf() + for (incrementalResult in incremental) { + incrementalResult(incrementalResult) + // Merge errors and extensions (if any) of the incremental result + (incrementalResult["errors"] as? List)?.let { mergedErrors += it } + (incrementalResult["extensions"] as? JsonMap)?.let { mergedExtensions += it } + } + // Keep only this payload's errors and extensions, if any + if (mergedErrors.isNotEmpty()) { + _merged["errors"] = mergedErrors + } else { + _merged.remove("errors") + } + if (mergedExtensions.isNotEmpty()) { + _merged["extensions"] = mapOf("incremental" to mergedExtensions) + } else { + _merged.remove("extensions") + } + } + + hasNext = part["hasNext"] as Boolean? ?: false + + return merged + } + + private fun incrementalResult(incrementalResult: JsonMap) { + val data = incrementalResult["data"] as JsonMap? + val path = incrementalResult["path"] as List + val mergedData = merged["data"] as JsonMap + + // data can be null if there are errors + if (data != null) { + val nodeToMergeInto = nodeAtPath(mergedData, path) as MutableJsonMap + deepMergeObject(nodeToMergeInto, data) + + _incrementalResultIds += IncrementalResultIdentifier(path = path, label = incrementalResult["label"] as String?) + } + } + + override fun reset() { + _merged.clear() + _incrementalResultIds.clear() + hasNext = true + isEmptyResponse = false + } +} diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt similarity index 56% rename from libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/IncrementalResultsMerger.kt rename to libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt index b49e1af3dd0..5d4b88b2dca 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/IncrementalResultsMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt @@ -1,56 +1,41 @@ -package com.apollographql.apollo.internal +package com.apollographql.apollo.internal.incremental import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.IncrementalResultIdentifier -import com.apollographql.apollo.api.json.BufferedSourceJsonReader -import com.apollographql.apollo.api.json.readAny +import com.apollographql.apollo.api.IncrementalResultIdentifiers +import com.apollographql.apollo.api.pending import okio.BufferedSource -private typealias JsonMap = Map -private typealias MutableJsonMap = MutableMap - /** - * Utility class for merging GraphQL incremental results received in multiple chunks when using the `@defer` and/or `@stream` directives. - * - * Each call to [merge] will merge the given results into the [merged] Map, and will also update the [pendingResultIds] Set with the - * value of their `path` and `label` fields. - * - * The fields in `data` are merged into the node found in [merged] at the path known by looking at the `id` field. For the first call to - * [merge], the payload is copied to [merged] as-is. - * - * `errors` in incremental and completed results (if present) are merged together in an array and then set to the `errors` field of the - * [merged] Map. - * `extensions` in incremental results (if present) are merged together in an array and then set to the `extensions` field of the [merged] - * Map. + * Merger for the [com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9] protocol format. */ @ApolloInternal @Suppress("UNCHECKED_CAST") -class IncrementalResultsMerger { +class GraphQL17Alpha9IncrementalResultsMerger : IncrementalResultsMerger { private val _merged: MutableJsonMap = mutableMapOf() - val merged: JsonMap = _merged + override val merged: JsonMap = _merged /** * Map of identifiers to their corresponding IncrementalResultIdentifier, found in `pending`. */ private val _pendingResultIds = mutableMapOf() - val pendingResultIds: Set get() = _pendingResultIds.values.toSet() - - var hasNext: Boolean = true - private set /** - * A response can sometimes have no `incremental` field, e.g. when the server couldn't predict if there were more data after the last - * emitted payload. This field allows to test for this in order to ignore such payloads. - * See https://github.com/apollographql/router/issues/1687. + * For this protocol, this represents the set of ids that are pending. */ - var isEmptyResponse: Boolean = false + override val incrementalResultIdentifiers: IncrementalResultIdentifiers get() = _pendingResultIds.values.toSet().pending() + + override var hasNext: Boolean = true + private set + + override var isEmptyResponse: Boolean = false private set - fun merge(part: BufferedSource): JsonMap { + override fun merge(part: BufferedSource): JsonMap { return merge(part.toJsonMap()) } - fun merge(part: JsonMap): JsonMap { + override fun merge(part: JsonMap): JsonMap { val completed = part["completed"] as? List if (merged.isEmpty()) { // Initial part, no merging needed (strip some fields that should not appear in the final result) @@ -135,52 +120,14 @@ class IncrementalResultsMerger { } } - private fun deepMergeObject(destination: MutableJsonMap, obj: JsonMap) { - for ((key, value) in obj) { - if (destination.containsKey(key) && destination[key] is MutableMap<*, *>) { - // Objects: merge recursively - val fieldDestination = destination[key] as MutableJsonMap - val fieldMap = value as? JsonMap ?: error("'$key' is an object in destination but not in map") - deepMergeObject(destination = fieldDestination, obj = fieldMap) - } else { - // Other types: add / overwrite - destination[key] = value - } - } - } - private fun mergeList(destination: MutableList, items: List) { destination.addAll(items) } - private fun BufferedSource.toJsonMap(): JsonMap = BufferedSourceJsonReader(this).readAny() as JsonMap - - - /** - * Find the node in the [map] at the given [path]. - * @param path The path to the node to find, as a list of either `String` (name of field in object) or `Int` (index of element in array). - */ - private fun nodeAtPath(map: JsonMap, path: List): Any? { - var node: Any? = map - for (key in path) { - node = if (node is List<*>) { - node[key as Int] - } else { - node as JsonMap - node[key] - } - } - return node - } - - fun reset() { + override fun reset() { _merged.clear() _pendingResultIds.clear() hasNext = true isEmptyResponse = false } } - -internal fun JsonMap.isIncremental(): Boolean { - return keys.contains("hasNext") -} diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalResultsMerger.kt new file mode 100644 index 00000000000..c2fa48e989f --- /dev/null +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalResultsMerger.kt @@ -0,0 +1,41 @@ +package com.apollographql.apollo.internal.incremental + +import com.apollographql.apollo.annotations.ApolloInternal +import com.apollographql.apollo.api.IncrementalResultIdentifiers +import okio.BufferedSource + +/** + * Utility for merging GraphQL incremental results received in multiple chunks when using the `@defer` and/or `@stream` directives. + * + * Each call to [merge] will merge the given results into the [merged] Map, and will also update [incrementalResultIdentifiers] with the + * value of their `path` and `label` fields. + * + * The fields in `data` are merged into the node found in [merged] at the path known by looking at the `id` field. For the first call to + * [merge], the payload is copied to [merged] as-is. + * + * `errors` in incremental and completed results (if present) are merged together in an array and then set to the `errors` field of the + * [merged] Map. + * `extensions` in incremental results (if present) are merged together in an array and then set to the `extensions` field of the [merged] + * Map. + */ +@ApolloInternal +interface IncrementalResultsMerger { + val merged: JsonMap + + val incrementalResultIdentifiers: IncrementalResultIdentifiers + + val hasNext: Boolean + + /** + * A response can sometimes have no `incremental` field, e.g. when the server couldn't predict if there were more data after the last + * emitted payload. This field allows to test for this in order to ignore such payloads. + * See https://github.com/apollographql/router/issues/1687. + */ + val isEmptyResponse: Boolean + + fun merge(part: BufferedSource): JsonMap + + fun merge(part: JsonMap): JsonMap + + fun reset() +} diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/JsonMap.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/JsonMap.kt new file mode 100644 index 00000000000..a7aea249326 --- /dev/null +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/JsonMap.kt @@ -0,0 +1,45 @@ +@file:Suppress("UNCHECKED_CAST") + +package com.apollographql.apollo.internal.incremental + +import com.apollographql.apollo.api.json.BufferedSourceJsonReader +import com.apollographql.apollo.api.json.readAny +import okio.BufferedSource + +internal typealias JsonMap = Map +internal typealias MutableJsonMap = MutableMap + + +/** + * Find the node in the [map] at the given [path]. + * @param path The path to the node to find, as a list of either `String` (name of field in object) or `Int` (index of element in array). + */ +internal fun nodeAtPath(map: JsonMap, path: List): Any? { + var node: Any? = map + for (key in path) { + node = if (node is List<*>) { + node[key as Int] + } else { + node as JsonMap + node[key] + } + } + return node +} + +internal fun deepMergeObject(destination: MutableJsonMap, obj: JsonMap) { + for ((key, value) in obj) { + if (destination.containsKey(key) && destination[key] is MutableMap<*, *>) { + // Objects: merge recursively + val fieldDestination = destination[key] as MutableJsonMap + val fieldMap = value as? JsonMap ?: error("'$key' is an object in destination but not in map") + deepMergeObject(destination = fieldDestination, obj = fieldMap) + } else { + // Other types: add / overwrite + destination[key] = value + } + } +} + +internal fun BufferedSource.toJsonMap(): JsonMap = BufferedSourceJsonReader(this).readAny() as JsonMap + diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt index 128338697bb..279785f57f5 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt @@ -1,6 +1,7 @@ package com.apollographql.apollo.network.http import com.apollographql.apollo.annotations.ApolloDeprecatedSince +import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.ApolloRequest import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.CustomScalarAdapters @@ -21,7 +22,9 @@ import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloHttpException import com.apollographql.apollo.exception.ApolloNetworkException import com.apollographql.apollo.exception.RouterError -import com.apollographql.apollo.internal.IncrementalResultsMerger +import com.apollographql.apollo.internal.incremental.Defer20220824IncrementalResultsMerger +import com.apollographql.apollo.internal.incremental.GraphQL17Alpha9IncrementalResultsMerger +import com.apollographql.apollo.internal.incremental.IncrementalResultsMerger import com.apollographql.apollo.internal.isGraphQLResponse import com.apollographql.apollo.internal.isMultipart import com.apollographql.apollo.internal.multipartBodyFlow @@ -44,6 +47,7 @@ private constructor( private val engine: HttpEngine, val interceptors: List, private val exposeErrorBody: Boolean, + private val incrementalDeliveryProtocol: IncrementalDeliveryProtocol, ) : NetworkTransport { private val engineInterceptor = EngineInterceptor() @@ -157,10 +161,10 @@ private constructor( val response = httpResponse.body!!.jsonReader() .apply { ignoreUnknownKeys(request.ignoreUnknownKeys ?: true) } .toApolloResponse( - operation, - customScalarAdapters = customScalarAdapters, - deferredFragmentIdentifiers = null, - ) + operation, + customScalarAdapters = customScalarAdapters, + deferredFragmentIdentifiers = null, + ) return flowOf(response.newBuilder().isLast(true).build()) } @@ -219,10 +223,10 @@ private constructor( } } else { if (incrementalResultsMerger == null) { - incrementalResultsMerger = IncrementalResultsMerger() + incrementalResultsMerger = incrementalDeliveryProtocol.newIncrementalResultsMerger() } val merged = incrementalResultsMerger.merge(part) - val pendingResultIds = incrementalResultsMerger.pendingResultIds + val deferredFragmentIds = incrementalResultsMerger.incrementalResultIdentifiers val isLast = !incrementalResultsMerger.hasNext if (incrementalResultsMerger.isEmptyResponse) { @@ -231,7 +235,7 @@ private constructor( merged.jsonReader().toApolloResponse( operation = operation, customScalarAdapters = customScalarAdapters, - deferredFragmentIdentifiers = pendingResultIds + deferredFragmentIdentifiers = deferredFragmentIds ).newBuilder().isLast(isLast).build() } } @@ -249,8 +253,8 @@ private constructor( this } else { ApolloNetworkException( - message = "Error while reading response", - platformCause = this + message = "Error while reading response", + platformCause = this ) } } @@ -310,6 +314,7 @@ private constructor( private var engine: HttpEngine? = null private val interceptors: MutableList = mutableListOf() private var exposeErrorBody: Boolean = false + private var incrementalDeliveryProtocol: IncrementalDeliveryProtocol = IncrementalDeliveryProtocol.Defer20220824 private val headers: MutableList = mutableListOf() fun httpRequestComposer(httpRequestComposer: HttpRequestComposer) = apply { @@ -353,6 +358,10 @@ private constructor( this.engine = httpEngine } + fun incrementalDeliveryProtocol(incrementalDeliveryProtocol: IncrementalDeliveryProtocol) = apply { + this.incrementalDeliveryProtocol = incrementalDeliveryProtocol + } + fun interceptors(interceptors: List) = apply { this.interceptors.clear() this.interceptors.addAll(interceptors) @@ -367,7 +376,12 @@ private constructor( "It is an error to set both 'httpRequestComposer' and 'serverUrl'" } val composer = httpRequestComposer - ?: serverUrl?.let { DefaultHttpRequestComposer(it) } + ?: serverUrl?.let { + DefaultHttpRequestComposer( + serverUrl = it, + acceptHeaderQueriesAndMutations = incrementalDeliveryProtocol.acceptHeader, + ) + } ?: error("No HttpRequestComposer found. Use 'httpRequestComposer' or 'serverUrl'") if (headers.isNotEmpty()) { @@ -379,6 +393,7 @@ private constructor( engine = engine ?: DefaultHttpEngine(), interceptors = interceptors, exposeErrorBody = exposeErrorBody, + incrementalDeliveryProtocol = incrementalDeliveryProtocol, ) } } @@ -391,4 +406,44 @@ private constructor( return chain.proceed(request.newBuilder().addHeaders(headers).build()) } } + + /** + * The protocol to use for incremental delivery (`@defer` and `@stream`). + */ + sealed interface IncrementalDeliveryProtocol { + @ApolloInternal + val acceptHeader: String + + @ApolloInternal + fun newIncrementalResultsMerger(): IncrementalResultsMerger + + /** + * Format specified in this historical commit: + * https://github.com/graphql/graphql-spec/tree/48cf7263a71a683fab03d45d309fd42d8d9a6659/spec + * + * Only `@defer` is supported with this format. + * + * This is the default. + */ + object Defer20220824 : IncrementalDeliveryProtocol { + @ApolloInternal + override val acceptHeader: String = DefaultHttpRequestComposer.HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824 + + @ApolloInternal + override fun newIncrementalResultsMerger(): IncrementalResultsMerger = Defer20220824IncrementalResultsMerger() + } + + /** + * Newer format as implemented by graphql.js version `17.0.0-alpha.9`. + * + * Both `@defer` and `@stream` are supported with this format. + */ + object GraphQL17Alpha9 : IncrementalDeliveryProtocol { + @ApolloInternal + override val acceptHeader: String = DefaultHttpRequestComposer.HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20230621 + + @ApolloInternal + override fun newIncrementalResultsMerger(): IncrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() + } + } } diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt index e6e2370c49f..631d40e6dfc 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt @@ -12,7 +12,6 @@ import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloWebSocketForceCloseException import com.apollographql.apollo.exception.DefaultApolloException import com.apollographql.apollo.exception.SubscriptionOperationException -import com.apollographql.apollo.internal.IncrementalResultsMerger import com.apollographql.apollo.network.NetworkTransport import com.apollographql.apollo.network.websocket.internal.OperationListener import com.apollographql.apollo.network.websocket.internal.WebSocketPool @@ -41,7 +40,7 @@ class WebSocketNetworkTransport private constructor( private val connectionAcknowledgeTimeout: Duration, private val pingInterval: Duration?, private val idleTimeout: Duration, - private val parserFactory: SubscriptionParserFactory + private val parserFactory: SubscriptionParserFactory, ) : NetworkTransport { private val pool = WebSocketPool( @@ -195,14 +194,13 @@ class WebSocketNetworkTransport private constructor( } } -private object DefaultSubscriptionParserFactory: SubscriptionParserFactory { +private object DefaultSubscriptionParserFactory : SubscriptionParserFactory { override fun createParser(request: ApolloRequest): SubscriptionParser { return DefaultSubscriptionParser(request) } } private class DefaultSubscriptionParser(private val request: ApolloRequest) : SubscriptionParser { - private var incrementalResultsMerger: IncrementalResultsMerger = IncrementalResultsMerger() private val requestCustomScalarAdapters = request.executionContext[CustomScalarAdapters] ?: CustomScalarAdapters.Empty @Suppress("NAME_SHADOWING") @@ -214,35 +212,20 @@ private class DefaultSubscriptionParser(private val request: .exception(DefaultApolloException("Invalid payload")).build() } - val (payload, mergedFragmentIds) = if (responseMap.isDeferred()) { - incrementalResultsMerger.merge(responseMap) to incrementalResultsMerger.pendingResultIds - } else { - responseMap to null - } - val apolloResponse: ApolloResponse = payload.jsonReader().toApolloResponse( + val apolloResponse: ApolloResponse = responseMap.jsonReader().toApolloResponse( operation = request.operation, requestUuid = request.requestUuid, customScalarAdapters = requestCustomScalarAdapters, - deferredFragmentIdentifiers = mergedFragmentIds ) - if (!incrementalResultsMerger.hasNext) { - // Last incremental result: reset the incrementalResultsMerger for potential subsequent responses - incrementalResultsMerger.reset() - } - - return if (incrementalResultsMerger.isEmptyResponse) { - null - } else { - apolloResponse - } + return apolloResponse } } private class DefaultOperationListener( private val request: ApolloRequest, private val producerScope: ProducerScope>, - private val parser: SubscriptionParser + private val parser: SubscriptionParser, ) : OperationListener { override fun onResponse(response: ApolloJsonElement) { parser.parse(response)?.let { @@ -270,11 +253,6 @@ private class DefaultOperationListener( producerScope.close() } } - -private fun Map.isDeferred(): Boolean { - return keys.contains("hasNext") -} - /** * Closes the websocket connection if the transport is a [WebSocketNetworkTransport]. * @@ -289,7 +267,8 @@ private fun Map.isDeferred(): Boolean { */ @ApolloExperimental fun NetworkTransport.closeConnection(exception: ApolloException) { - val webSocketNetworkTransport = (this as? WebSocketNetworkTransport) ?: throw IllegalArgumentException("'$this' is not an instance of com.apollographql.apollo.websocket.WebSocketNetworkTransport") + val webSocketNetworkTransport = (this as? WebSocketNetworkTransport) + ?: throw IllegalArgumentException("'$this' is not an instance of com.apollographql.apollo.websocket.WebSocketNetworkTransport") webSocketNetworkTransport.closeConnection(exception) } @@ -301,7 +280,8 @@ fun NetworkTransport.closeConnection(exception: ApolloException) { */ @ApolloExperimental fun NetworkTransport.closeConnection() { - val webSocketNetworkTransport = (this as? WebSocketNetworkTransport) ?: throw IllegalArgumentException("'$this' is not an instance of com.apollographql.apollo.websocket.WebSocketNetworkTransport") + val webSocketNetworkTransport = (this as? WebSocketNetworkTransport) + ?: throw IllegalArgumentException("'$this' is not an instance of com.apollographql.apollo.websocket.WebSocketNetworkTransport") webSocketNetworkTransport.closeConnection(ApolloWebSocketForceCloseException) } diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt index 549638ed687..f432a47aeb4 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt @@ -10,8 +10,6 @@ import com.apollographql.apollo.api.toApolloResponse import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloNetworkException import com.apollographql.apollo.exception.SubscriptionOperationException -import com.apollographql.apollo.internal.IncrementalResultsMerger -import com.apollographql.apollo.internal.isIncremental import com.apollographql.apollo.internal.transformWhile import com.apollographql.apollo.network.NetworkTransport import com.apollographql.apollo.network.ws.internal.Command @@ -40,7 +38,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onSubscription @@ -61,7 +58,7 @@ private constructor( 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 { /** @@ -262,8 +259,6 @@ private constructor( override fun execute( request: ApolloRequest, ): Flow> { - val incrementalResultsMerger = IncrementalResultsMerger() - return events.onSubscription { messages.send(StartOperation(request)) }.filter { @@ -300,24 +295,13 @@ private constructor( }.map { response -> when (response) { is OperationResponse -> { - val responsePayload = response.payload val requestCustomScalarAdapters = request.executionContext[CustomScalarAdapters]!! - val (payload, mergedFragmentIds) = if (responsePayload.isIncremental()) { - incrementalResultsMerger.merge(responsePayload) to incrementalResultsMerger.pendingResultIds - } else { - responsePayload to null - } - val apolloResponse: ApolloResponse = payload.jsonReader().toApolloResponse( + val apolloResponse: ApolloResponse = response.payload.jsonReader().toApolloResponse( operation = request.operation, requestUuid = request.requestUuid, customScalarAdapters = requestCustomScalarAdapters, - deferredFragmentIdentifiers = mergedFragmentIds ) - if (!incrementalResultsMerger.hasNext) { - // Last incremental result: reset the incrementalResultsMerger for potential subsequent responses - incrementalResultsMerger.reset() - } apolloResponse } @@ -327,8 +311,6 @@ private constructor( // Cannot happen as these events are filtered out upstream is ConnectionReEstablished, is OperationComplete, is GeneralError -> error("Unexpected event $response") } - }.filterNot { - incrementalResultsMerger.isEmptyResponse }.onCompletion { messages.send(StopOperation(request)) } diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/Defer20220824IncrementalResultsMergerTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/Defer20220824IncrementalResultsMergerTest.kt new file mode 100644 index 00000000000..e33a5e34f8f --- /dev/null +++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/Defer20220824IncrementalResultsMergerTest.kt @@ -0,0 +1,747 @@ +@file:OptIn(ApolloInternal::class) + +package test.defer + +import com.apollographql.apollo.annotations.ApolloInternal +import com.apollographql.apollo.api.DeferredFragmentIdentifier +import com.apollographql.apollo.api.json.BufferedSourceJsonReader +import com.apollographql.apollo.api.json.readAny +import com.apollographql.apollo.internal.incremental.Defer20220824IncrementalResultsMerger +import okio.Buffer +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +private fun String.buffer() = Buffer().writeUtf8(this) + +@Suppress("UNCHECKED_CAST") +private fun jsonToMap(json: String): Map = BufferedSourceJsonReader(json.buffer()).readAny() as Map + +class Defer20220824IncrementalResultsMergerTest { + @Test + fun mergeJsonSingleIncrementalItem() { + val incrementalResultsMerger = Defer20220824IncrementalResultsMerger() + + //language=JSON + val payload1 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true + } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] + }, + "hasNext": true + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(payload1), incrementalResultsMerger.merged) + assertEquals( + setOf(), + incrementalResultsMerger.incrementalResultIdentifiers + ) + + //language=JSON + val payload2 = """ + { + "incremental": [ + { + "data": { + "cpu": "386", + "year": 1993, + "screen": { + "resolution": "640x480" + } + }, + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0", + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + } + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] + }, + "hasNext": true, + "extensions": { + "incremental": [ + { + "duration": { + "amount": 100, + "unit": "ms" + } + } + ] + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + ), + incrementalResultsMerger.incrementalResultIdentifiers + ) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { + "data": { + "cpu": "486", + "year": 1996, + "screen": { + "resolution": "640x480" + } + }, + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0", + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" + } + } + } + ], + "hasNext": true + } + """.trimIndent() + + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480" + } + } + ] + }, + "hasNext": true, + "extensions": { + "incremental": [ + { + "duration": { + "amount": 25, + "unit": "ms" + } + } + ] + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + ), + incrementalResultsMerger.incrementalResultIdentifiers + ) + + //language=JSON + val payload4 = """ + { + "incremental": [ + { + "data": null, + "path": [ + "computers", + 0, + "screen" + ], + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + } + ], + "label": "fragment:ComputerFields:0" + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480" + } + } + ] + }, + "hasNext": true, + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + } + ] + } + """.trimIndent() + incrementalResultsMerger.merge(payload4.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4), incrementalResultsMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + ), + incrementalResultsMerger.incrementalResultIdentifiers + ) + + //language=JSON + val payload5 = """ + { + "incremental": [ + { + "data": { + "isColor": false + }, + "path": [ + "computers", + 1, + "screen" + ], + "errors": [ + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + } + ], + "label": "fragment:ComputerFields:0", + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + } + } + ], + "hasNext": false + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4_5 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480", + "isColor": false + } + } + ] + }, + "hasNext": true, + "extensions": { + "incremental": [ + { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + } + ] + }, + "errors": [ + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + } + ] + } + """.trimIndent() + incrementalResultsMerger.merge(payload5.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), incrementalResultsMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), + ), + incrementalResultsMerger.incrementalResultIdentifiers + ) + } + + @Test + fun mergeJsonMultipleIncrementalItems() { + val incrementalResultsMerger = Defer20220824IncrementalResultsMerger() + + //language=JSON + val payload1 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true + } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] + }, + "hasNext": true + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(payload1), incrementalResultsMerger.merged) + assertEquals( + setOf(), + incrementalResultsMerger.incrementalResultIdentifiers + ) + + //language=JSON + val payload2_3 = """ + { + "incremental": [ + { + "data": { + "cpu": "386", + "year": 1993, + "screen": { + "resolution": "640x480" + } + }, + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0", + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + } + }, + { + "data": { + "cpu": "486", + "year": 1996, + "screen": { + "resolution": "640x480" + } + }, + "path": [ + "computers", + 1 + ], + "label": "query:Query1:0", + "extensions": { + "duration": { + "amount": 25, + "unit": "ms" + } + } + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480" + } + } + ] + }, + "hasNext": true, + "extensions": { + "incremental": [ + { + "duration": { + "amount": 100, + "unit": "ms" + } + }, + { + "duration": { + "amount": 25, + "unit": "ms" + } + } + ] + } + } + """.trimIndent() + incrementalResultsMerger.merge(payload2_3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + ), + incrementalResultsMerger.incrementalResultIdentifiers + ) + + //language=JSON + val payload4_5 = """ + { + "incremental": [ + { + "data": null, + "path": [ + "computers", + 0, + "screen" + ], + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + } + ], + "label": "fragment:ComputerFields:0" + }, + { + "data": { + "isColor": false + }, + "path": [ + "computers", + 1, + "screen" + ], + "errors": [ + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + } + ], + "label": "fragment:ComputerFields:0", + "extensions": { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + } + } + ], + "hasNext": true + } + """.trimIndent() + //language=JSON + val mergedPayloads_1_2_3_4_5 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "cpu": "386", + "year": 1993, + "screen": { + "isTouch": true, + "resolution": "640x480" + } + }, + { + "id": "Computer2", + "cpu": "486", + "year": 1996, + "screen": { + "isTouch": false, + "resolution": "640x480", + "isColor": false + } + } + ] + }, + "hasNext": true, + "extensions": { + "incremental": [ + { + "value": 42, + "duration": { + "amount": 130, + "unit": "ms" + } + } + ] + }, + "errors": [ + { + "message": "Cannot resolve isColor", + "locations": [ + { + "line": 12, + "column": 11 + } + ], + "path": [ + "computers", + 0, + "screen", + "isColor" + ] + }, + { + "message": "Another error", + "locations": [ + { + "line": 1, + "column": 1 + } + ] + } + ] + } + """.trimIndent() + incrementalResultsMerger.merge(payload4_5.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), incrementalResultsMerger.merged) + assertEquals( + setOf( + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), + ), + incrementalResultsMerger.incrementalResultIdentifiers + ) + } + + @Test + fun emptyPayloads() { + val incrementalResultsMerger = Defer20220824IncrementalResultsMerger() + + //language=JSON + val payload1 = """ + { + "data": { + "computers": [ + { + "id": "Computer1", + "screen": { + "isTouch": true + } + }, + { + "id": "Computer2", + "screen": { + "isTouch": false + } + } + ] + }, + "hasNext": true + } + """.trimIndent() + incrementalResultsMerger.merge(payload1.buffer()) + assertFalse(incrementalResultsMerger.isEmptyResponse) + + //language=JSON + val payload2 = """ + { + "hasNext": true + } + """.trimIndent() + incrementalResultsMerger.merge(payload2.buffer()) + assertTrue(incrementalResultsMerger.isEmptyResponse) + + //language=JSON + val payload3 = """ + { + "incremental": [ + { + "data": { + "cpu": "386", + "year": 1993, + "screen": { + "resolution": "640x480" + } + }, + "path": [ + "computers", + 0 + ], + "label": "query:Query1:0", + "extensions": { + "duration": { + "amount": 100, + "unit": "ms" + } + } + } + ], + "hasNext": true + } + """.trimIndent() + incrementalResultsMerger.merge(payload3.buffer()) + assertFalse(incrementalResultsMerger.isEmptyResponse) + + //language=JSON + val payload4 = """ + { + "hasNext": false + } + """.trimIndent() + incrementalResultsMerger.merge(payload4.buffer()) + assertTrue(incrementalResultsMerger.isEmptyResponse) + } +} diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/IncrementalResultsMergerTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/GraphQL17Alpha9IncrementalResultsMergerTest.kt similarity index 78% rename from libraries/apollo-runtime/src/commonTest/kotlin/test/defer/IncrementalResultsMergerTest.kt rename to libraries/apollo-runtime/src/commonTest/kotlin/test/defer/GraphQL17Alpha9IncrementalResultsMergerTest.kt index e743fa6f2ac..cf07c635dc3 100644 --- a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/IncrementalResultsMergerTest.kt +++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/GraphQL17Alpha9IncrementalResultsMergerTest.kt @@ -6,7 +6,8 @@ import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.IncrementalResultIdentifier import com.apollographql.apollo.api.json.BufferedSourceJsonReader import com.apollographql.apollo.api.json.readAny -import com.apollographql.apollo.internal.IncrementalResultsMerger +import com.apollographql.apollo.api.nonPending +import com.apollographql.apollo.internal.incremental.GraphQL17Alpha9IncrementalResultsMerger import okio.Buffer import kotlin.test.Test import kotlin.test.assertEquals @@ -18,10 +19,10 @@ private fun String.buffer() = Buffer().writeUtf8(this) @Suppress("UNCHECKED_CAST") private fun jsonToMap(json: String): Map = BufferedSourceJsonReader(json.buffer()).readAny() as Map -class IncrementalResultsMergerTest { +class GraphQL17Alpha9IncrementalResultsMergerTest { @Test fun mergeJsonSingleIncrementalItem() { - val incrementalResultsMerger = IncrementalResultsMerger() + val incrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() //language=JSON val payload1 = """ @@ -78,12 +79,12 @@ class IncrementalResultsMergerTest { """.trimIndent() incrementalResultsMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf("computers", 0), label = "query:Query1:0") - ), - incrementalResultsMerger.pendingResultIds - ) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("computers", 0), label = "query:Query1:0") + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) //language=JSON val payload2 = """ @@ -160,7 +161,7 @@ class IncrementalResultsMergerTest { setOf( IncrementalResultIdentifier(path = listOf("computers", 1), label = "query:Query1:0") ), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) //language=JSON @@ -242,7 +243,7 @@ class IncrementalResultsMergerTest { setOf( IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) //language=JSON @@ -341,7 +342,7 @@ class IncrementalResultsMergerTest { IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), IncrementalResultIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), ), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) //language=JSON @@ -448,13 +449,13 @@ class IncrementalResultsMergerTest { setOf( IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) } @Test fun mergeJsonMultipleIncrementalItems() { - val incrementalResultsMerger = IncrementalResultsMerger() + val incrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() //language=JSON val payload1 = """ @@ -519,13 +520,13 @@ class IncrementalResultsMergerTest { """.trimIndent() incrementalResultsMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - IncrementalResultIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), - ), - incrementalResultsMerger.pendingResultIds - ) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + IncrementalResultIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) //language=JSON val payload2_3 = """ @@ -629,7 +630,7 @@ class IncrementalResultsMergerTest { IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), IncrementalResultIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), ), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) //language=JSON @@ -756,13 +757,13 @@ class IncrementalResultsMergerTest { setOf( IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) } @Test fun emptyPayloads() { - val incrementalResultsMerger = IncrementalResultsMerger() + val incrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() //language=JSON val payload1 = """ @@ -851,7 +852,7 @@ class IncrementalResultsMergerTest { */ @Test fun june2023ExampleA() { - val incrementalResultsMerger = IncrementalResultsMerger() + val incrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -899,12 +900,12 @@ class IncrementalResultsMergerTest { """.trimIndent() incrementalResultsMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf(), label = null), - ), - incrementalResultsMerger.pendingResultIds - ) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf(), label = null), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) //language=JSON val payload2 = """ @@ -961,7 +962,7 @@ class IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) } @@ -970,7 +971,7 @@ class IncrementalResultsMergerTest { */ @Test fun june2023ExampleA2() { - val incrementalResultsMerger = IncrementalResultsMerger() + val incrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1019,12 +1020,12 @@ class IncrementalResultsMergerTest { """.trimIndent() incrementalResultsMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf(), label = "D1"), - ), - incrementalResultsMerger.pendingResultIds - ) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf(), label = "D1"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) //language=JSON val payload2 = """ @@ -1089,7 +1090,7 @@ class IncrementalResultsMergerTest { setOf( IncrementalResultIdentifier(path = listOf("f2", "c", "f"), label = "D2"), ), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) //language=JSON @@ -1140,7 +1141,7 @@ class IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) } @@ -1149,7 +1150,7 @@ class IncrementalResultsMergerTest { */ @Test fun june2023ExampleB1() { - val incrementalResultsMerger = IncrementalResultsMerger() + val incrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1197,13 +1198,13 @@ class IncrementalResultsMergerTest { """.trimIndent() incrementalResultsMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf(), label = "Blue"), - IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), - ), - incrementalResultsMerger.pendingResultIds - ) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf(), label = "Blue"), + IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) //language=JSON val payload2 = """ @@ -1256,7 +1257,7 @@ class IncrementalResultsMergerTest { setOf( IncrementalResultIdentifier(path = listOf(), label = "Blue"), ), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) //language=JSON @@ -1307,7 +1308,7 @@ class IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) } @@ -1316,7 +1317,7 @@ class IncrementalResultsMergerTest { */ @Test fun june2023ExampleB2() { - val incrementalResultsMerger = IncrementalResultsMerger() + val incrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1364,13 +1365,13 @@ class IncrementalResultsMergerTest { """.trimIndent() incrementalResultsMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf(), label = "Blue"), - IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), - ), - incrementalResultsMerger.pendingResultIds - ) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf(), label = "Blue"), + IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) //language=JSON val payload2 = """ @@ -1429,54 +1430,55 @@ class IncrementalResultsMergerTest { setOf( IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), ), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) //language=JSON val payload3 = """ - { - "incremental": [ - { - "id": "1", - "data": { - "potentiallySlowFieldA": "potentiallySlowFieldA" - } - } - ], - "completed": [ - { - "id": "1" + { + "incremental": [ + { + "id": "1", + "data": { + "potentiallySlowFieldA": "potentiallySlowFieldA" } - ], - "hasNext": false - } + } + ], + "completed": [ + { + "id": "1" + } + ], + "hasNext": false + } """.trimIndent() + //language=JSON val mergedPayloads_1_2_3 = """ - { - "data": { - "a": { - "b": { - "c": { - "d": "d" - }, - "e": { - "f": "f" - }, - "potentiallySlowFieldA": "potentiallySlowFieldA" - } - }, - "g": { - "h": "h" - }, - "potentiallySlowFieldB": "potentiallySlowFieldB" - } + { + "data": { + "a": { + "b": { + "c": { + "d": "d" + }, + "e": { + "f": "f" + }, + "potentiallySlowFieldA": "potentiallySlowFieldA" + } + }, + "g": { + "h": "h" + }, + "potentiallySlowFieldB": "potentiallySlowFieldB" } - """ + } + """.trimIndent() incrementalResultsMerger.merge(payload3.buffer()) assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) } @@ -1485,7 +1487,7 @@ class IncrementalResultsMergerTest { */ @Test fun june2023ExampleD() { - val incrementalResultsMerger = IncrementalResultsMerger() + val incrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() //language=JSON val payload1 = """ { @@ -1517,13 +1519,13 @@ class IncrementalResultsMergerTest { """.trimIndent() incrementalResultsMerger.merge(payload1.buffer()) assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf(), label = null), - IncrementalResultIdentifier(path = listOf("me"), label = null), - ), - incrementalResultsMerger.pendingResultIds - ) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf(), label = null), + IncrementalResultIdentifier(path = listOf("me"), label = null), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) //language=JSON val payload2 = """ @@ -1619,7 +1621,7 @@ class IncrementalResultsMergerTest { setOf( IncrementalResultIdentifier(path = listOf(), label = null), ), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) //language=JSON @@ -1704,18 +1706,18 @@ class IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) } - /** - * Example F from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) - */ - @Test - fun june2023ExampleF() { - val incrementalResultsMerger = IncrementalResultsMerger() - //language=JSON - val payload1 = """ + /** + * Example F from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleF() { + val incrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() + //language=JSON + val payload1 = """ { "data": { "me": {} @@ -1732,25 +1734,25 @@ class IncrementalResultsMergerTest { "hasNext": true } """.trimIndent() - //language=JSON - val mergedPayloads_1 = """ + //language=JSON + val mergedPayloads_1 = """ { "data": { "me": {} } } """.trimIndent() - incrementalResultsMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) - assertEquals( + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( setOf( IncrementalResultIdentifier(path = listOf("me"), label = "B"), ), - incrementalResultsMerger.pendingResultIds - ) + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) - //language=JSON - val payload2 = """ + //language=JSON + val payload2 = """ { "incremental": [ { @@ -1769,8 +1771,8 @@ class IncrementalResultsMergerTest { "hasNext": false } """.trimIndent() - //language=JSON - val mergedPayloads_1_2 = """ + //language=JSON + val mergedPayloads_1_2 = """ { "data": { "me": { @@ -1780,22 +1782,22 @@ class IncrementalResultsMergerTest { } } """.trimIndent() - incrementalResultsMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) - assertEquals( - setOf(), - incrementalResultsMerger.pendingResultIds + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf(), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() ) } - /** - * Example G from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) - */ - @Test - fun june2023ExampleG() { - val incrementalResultsMerger = IncrementalResultsMerger() - //language=JSON - val payload1 = """ + /** + * Example G from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleG() { + val incrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() + //language=JSON + val payload1 = """ { "data": { "me": { @@ -1827,8 +1829,8 @@ class IncrementalResultsMergerTest { "hasNext": true } """.trimIndent() - //language=JSON - val mergedPayloads_1 = """ + //language=JSON + val mergedPayloads_1 = """ { "data": { "me": { @@ -1843,18 +1845,18 @@ class IncrementalResultsMergerTest { } } """.trimIndent() - incrementalResultsMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf("me"), label = "Billing"), - IncrementalResultIdentifier(path = listOf("me"), label = "Prev"), - ), - incrementalResultsMerger.pendingResultIds - ) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("me"), label = "Billing"), + IncrementalResultIdentifier(path = listOf("me"), label = "Prev"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) - //language=JSON - val payload2 = """ + //language=JSON + val payload2 = """ { "incremental": [ { @@ -1874,8 +1876,8 @@ class IncrementalResultsMergerTest { "hasNext": true } """.trimIndent() - //language=JSON - val mergedPayloads_1_2 = """ + //language=JSON + val mergedPayloads_1_2 = """ { "data": { "me": { @@ -1893,17 +1895,17 @@ class IncrementalResultsMergerTest { } } """.trimIndent() - incrementalResultsMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf("me"), label = "Prev"), - ), - incrementalResultsMerger.pendingResultIds - ) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("me"), label = "Prev"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) - //language=JSON - val payload3 = """ + //language=JSON + val payload3 = """ { "incremental": [ { @@ -1925,8 +1927,8 @@ class IncrementalResultsMergerTest { "hasNext": false } """.trimIndent() - //language=JSON - val mergedPayloads_1_2_3 = """ + //language=JSON + val mergedPayloads_1_2_3 = """ { "data": { "me": { @@ -1949,22 +1951,22 @@ class IncrementalResultsMergerTest { } } """.trimIndent() - incrementalResultsMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) - assertEquals( - setOf(), - incrementalResultsMerger.pendingResultIds - ) - } + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf(), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) + } - /** - * Example H from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) - */ - @Test - fun june2023ExampleH() { - val incrementalResultsMerger = IncrementalResultsMerger() - //language=JSON - val payload1 = """ + /** + * Example H from https://github.com/graphql/defer-stream-wg/discussions/69 (Dec 13 2024 version) + */ + @Test + fun june2023ExampleH() { + val incrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() + //language=JSON + val payload1 = """ { "data": { "me": {} @@ -1986,26 +1988,26 @@ class IncrementalResultsMergerTest { "hasNext": true } """.trimIndent() - //language=JSON - val mergedPayloads_1 = """ + //language=JSON + val mergedPayloads_1 = """ { "data": { "me": {} } } """.trimIndent() - incrementalResultsMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf(), label = "A"), - IncrementalResultIdentifier(path = listOf("me"), label = "B"), - ), - incrementalResultsMerger.pendingResultIds - ) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf(), label = "A"), + IncrementalResultIdentifier(path = listOf("me"), label = "B"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) - //language=JSON - val payload2 = """ + //language=JSON + val payload2 = """ { "incremental": [ { @@ -2039,8 +2041,8 @@ class IncrementalResultsMergerTest { "hasNext": true } """.trimIndent() - //language=JSON - val mergedPayloads_1_2 = """ + //language=JSON + val mergedPayloads_1_2 = """ { "data": { "me": { @@ -2053,17 +2055,17 @@ class IncrementalResultsMergerTest { } } """.trimIndent() - incrementalResultsMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf("me"), label = "B"), - ), - incrementalResultsMerger.pendingResultIds - ) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("me"), label = "B"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) - //language=JSON - val payload3 = """ + //language=JSON + val payload3 = """ { "completed": [ { @@ -2089,8 +2091,8 @@ class IncrementalResultsMergerTest { "hasNext": false } """.trimIndent() - //language=JSON - val mergedPayloads_1_2_3 = """ + //language=JSON + val mergedPayloads_1_2_3 = """ { "data": { "me": { @@ -2119,24 +2121,24 @@ class IncrementalResultsMergerTest { ] } """.trimIndent() - incrementalResultsMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf("me"), label = "B"), - ), - incrementalResultsMerger.pendingResultIds - ) - } + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("me"), label = "B"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) + } - /** - * Example I from https://github.com/graphql/defer-stream-wg/discussions/69 (Jul 18 2025 version) - */ - @Test - fun july2025ExampleI() { - val incrementalResultsMerger = IncrementalResultsMerger() - //language=JSON - val payload1 = """ + /** + * Example I from https://github.com/graphql/defer-stream-wg/discussions/69 (Jul 18 2025 version) + */ + @Test + fun july2025ExampleI() { + val incrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() + //language=JSON + val payload1 = """ { "data": { "person": { @@ -2171,8 +2173,8 @@ class IncrementalResultsMergerTest { "hasNext": true } """.trimIndent() - //language=JSON - val mergedPayloads_1 = """ + //language=JSON + val mergedPayloads_1 = """ { "data": { "person": { @@ -2189,18 +2191,18 @@ class IncrementalResultsMergerTest { } } """.trimIndent() - incrementalResultsMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), - IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), - ), - incrementalResultsMerger.pendingResultIds - ) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) - //language=JSON - val payload2 = """ + //language=JSON + val payload2 = """ { "incremental": [ { @@ -2215,8 +2217,8 @@ class IncrementalResultsMergerTest { "hasNext": true } """.trimIndent() - //language=JSON - val mergedPayloads_1_2 = """ + //language=JSON + val mergedPayloads_1_2 = """ { "data": { "person": { @@ -2236,18 +2238,18 @@ class IncrementalResultsMergerTest { } } """.trimIndent() - incrementalResultsMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), - IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), - ), - incrementalResultsMerger.pendingResultIds - ) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) - //language=JSON - val payload3 = """ + //language=JSON + val payload3 = """ { "completed": [ { @@ -2257,8 +2259,8 @@ class IncrementalResultsMergerTest { "hasNext": true } """.trimIndent() - //language=JSON - val mergedPayloads_1_2_3 = """ + //language=JSON + val mergedPayloads_1_2_3 = """ { "data": { "person": { @@ -2279,17 +2281,17 @@ class IncrementalResultsMergerTest { } """.trimIndent() - incrementalResultsMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), - ), - incrementalResultsMerger.pendingResultIds - ) + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) - //language=JSON - val payload4 = """ + //language=JSON + val payload4 = """ { "incremental": [ { @@ -2309,8 +2311,8 @@ class IncrementalResultsMergerTest { "hasNext": false } """.trimIndent() - //language=JSON - val mergedPayloads_1_2_3_4 = """ + //language=JSON + val mergedPayloads_1_2_3_4 = """ { "data": { "person": { @@ -2334,22 +2336,22 @@ class IncrementalResultsMergerTest { } """.trimIndent() - incrementalResultsMerger.merge(payload4.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3_4), incrementalResultsMerger.merged) - assertEquals( - setOf(), - incrementalResultsMerger.pendingResultIds - ) - } + incrementalResultsMerger.merge(payload4.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3_4), incrementalResultsMerger.merged) + assertEquals( + setOf(), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) + } - /** - * Example J from https://github.com/graphql/defer-stream-wg/discussions/69 (Jul 18 2025 version) - */ - @Test - fun july2025ExampleJ() { - val incrementalResultsMerger = IncrementalResultsMerger() - //language=JSON - val payload1 = """ + /** + * Example J from https://github.com/graphql/defer-stream-wg/discussions/69 (Jul 18 2025 version) + */ + @Test + fun july2025ExampleJ() { + val incrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() + //language=JSON + val payload1 = """ { "data": { "person": { @@ -2373,8 +2375,8 @@ class IncrementalResultsMergerTest { "hasNext": true } """.trimIndent() - //language=JSON - val mergedPayloads_1 = """ + //language=JSON + val mergedPayloads_1 = """ { "data": { "person": { @@ -2387,17 +2389,17 @@ class IncrementalResultsMergerTest { } } """.trimIndent() - incrementalResultsMerger.merge(payload1.buffer()) - assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), - ), - incrementalResultsMerger.pendingResultIds - ) + incrementalResultsMerger.merge(payload1.buffer()) + assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) - //language=JSON - val payload2 = """ + //language=JSON + val payload2 = """ { "incremental": [ { @@ -2412,8 +2414,8 @@ class IncrementalResultsMergerTest { "hasNext": true } """.trimIndent() - //language=JSON - val mergedPayloads_1_2 = """ + //language=JSON + val mergedPayloads_1_2 = """ { "data": { "person": { @@ -2429,17 +2431,17 @@ class IncrementalResultsMergerTest { } } """.trimIndent() - incrementalResultsMerger.merge(payload2.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), - ), - incrementalResultsMerger.pendingResultIds - ) + incrementalResultsMerger.merge(payload2.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) - //language=JSON - val payload3 = """ + //language=JSON + val payload3 = """ { "completed": [ { @@ -2464,8 +2466,8 @@ class IncrementalResultsMergerTest { "hasNext": false } """.trimIndent() - //language=JSON - val mergedPayloads_1_2_3 = """ + //language=JSON + val mergedPayloads_1_2_3 = """ { "data": { "person": { @@ -2497,13 +2499,13 @@ class IncrementalResultsMergerTest { } """.trimIndent() - incrementalResultsMerger.merge(payload3.buffer()) - assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) - assertEquals( - setOf( - IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), - ), - incrementalResultsMerger.pendingResultIds - ) - } + incrementalResultsMerger.merge(payload3.buffer()) + assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) + assertEquals( + setOf( + IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + ), + incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + ) + } } diff --git a/tests/defer/apollo-server/README.md b/tests/defer/apollo-server/README.md index ef149563b19..5111b87d42e 100644 --- a/tests/defer/apollo-server/README.md +++ b/tests/defer/apollo-server/README.md @@ -1,4 +1,4 @@ # Test server using Apollo Server, for `@defer` tests -- This uses graphql-js `17.0.0-alpha.7`, which implements the latest draft of the `@defer` incremental format (as of 2024-12-16). +- This uses graphql-js `17.0.0-alpha.9`, which implements the latest draft of the `@defer` incremental format (as of 2025-09-24). - Apollo Server `4.11.2` needs a patch (in `patches`) to surface this format in the responses. diff --git a/tests/defer/apollo-server/package.json b/tests/defer/apollo-server/package.json index 1b469e77c4e..57416828baf 100644 --- a/tests/defer/apollo-server/package.json +++ b/tests/defer/apollo-server/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@apollo/server": "4.11.2", - "graphql": "17.0.0-alpha.7", + "graphql": "17.0.0-alpha.9", "patch-package": "^8.0.0" }, "keywords": [], diff --git a/tests/defer/apollo-server/patches/@apollo+server+4.11.2.patch b/tests/defer/apollo-server/patches/@apollo+server+4.11.2.patch index d6a742855b7..a516a8c8867 100644 --- a/tests/defer/apollo-server/patches/@apollo+server+4.11.2.patch +++ b/tests/defer/apollo-server/patches/@apollo+server+4.11.2.patch @@ -1,7 +1,32 @@ +diff --git a/node_modules/@apollo/server/dist/esm/ApolloServer.js b/node_modules/@apollo/server/dist/esm/ApolloServer.js +index 7665490..c32a28c 100644 +--- a/node_modules/@apollo/server/dist/esm/ApolloServer.js ++++ b/node_modules/@apollo/server/dist/esm/ApolloServer.js +@@ -631,7 +631,7 @@ export const MEDIA_TYPES = { + APPLICATION_JSON_GRAPHQL_CALLBACK: 'application/json; callbackSpec=1.0; charset=utf-8', + APPLICATION_GRAPHQL_RESPONSE_JSON: 'application/graphql-response+json; charset=utf-8', + MULTIPART_MIXED_NO_DEFER_SPEC: 'multipart/mixed', +- MULTIPART_MIXED_EXPERIMENTAL: 'multipart/mixed; deferSpec=20220824', ++ MULTIPART_MIXED_EXPERIMENTAL: 'multipart/mixed; incrementalDeliverySpec=20230621', + TEXT_HTML: 'text/html', + }; + export function chooseContentTypeForSingleResultResponse(head) { diff --git a/node_modules/@apollo/server/dist/esm/runHttpQuery.js b/node_modules/@apollo/server/dist/esm/runHttpQuery.js -index 96ef0ab..0d341fa 100644 +index 96ef0ab..d816750 100644 --- a/node_modules/@apollo/server/dist/esm/runHttpQuery.js +++ b/node_modules/@apollo/server/dist/esm/runHttpQuery.js +@@ -161,9 +161,9 @@ export async function runHttpQuery({ server, httpRequest, contextValue, schemaDe + throw new BadRequestError('Apollo server received an operation that uses incremental delivery ' + + '(@defer or @stream), but the client does not accept multipart/mixed ' + + 'HTTP responses. To enable incremental delivery support, add the HTTP ' + +- "header 'Accept: multipart/mixed; deferSpec=20220824'.", { extensions: { http: { status: 406 } } }); ++ "header 'Accept: multipart/mixed; incrementalDeliverySpec=20230621'.", { extensions: { http: { status: 406 } } }); + } +- graphQLResponse.http.headers.set('content-type', 'multipart/mixed; boundary="-"; deferSpec=20220824'); ++ graphQLResponse.http.headers.set('content-type', 'multipart/mixed; boundary="-"; incrementalDeliverySpec=20230621'); + return { + ...graphQLResponse.http, + body: { @@ -187,6 +187,7 @@ function orderExecutionResultFields(result) { } function orderInitialIncrementalExecutionResultFields(result) { diff --git a/tests/defer/src/commonTest/kotlin/test/Defer20220824NormalizedCacheTest.kt b/tests/defer/src/commonTest/kotlin/test/Defer20220824NormalizedCacheTest.kt new file mode 100644 index 00000000000..be5cb5ac1e4 --- /dev/null +++ b/tests/defer/src/commonTest/kotlin/test/Defer20220824NormalizedCacheTest.kt @@ -0,0 +1,548 @@ +package test + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.ApolloRequest +import com.apollographql.apollo.api.ApolloResponse +import com.apollographql.apollo.api.Error +import com.apollographql.apollo.api.Operation +import com.apollographql.apollo.cache.normalized.ApolloStore +import com.apollographql.apollo.cache.normalized.FetchPolicy +import com.apollographql.apollo.cache.normalized.api.CacheHeaders +import com.apollographql.apollo.cache.normalized.api.MemoryCacheFactory +import com.apollographql.apollo.cache.normalized.apolloStore +import com.apollographql.apollo.cache.normalized.fetchPolicy +import com.apollographql.apollo.cache.normalized.optimisticUpdates +import com.apollographql.apollo.cache.normalized.store +import com.apollographql.apollo.exception.ApolloException +import com.apollographql.apollo.exception.ApolloHttpException +import com.apollographql.apollo.exception.ApolloNetworkException +import com.apollographql.apollo.exception.CacheMissException +import com.apollographql.apollo.network.NetworkTransport +import com.apollographql.apollo.testing.internal.runTest +import com.apollographql.mockserver.MockServer +import com.apollographql.mockserver.assertNoRequest +import com.apollographql.mockserver.awaitRequest +import com.apollographql.mockserver.enqueueError +import com.apollographql.mockserver.enqueueMultipart +import com.apollographql.mockserver.enqueueStrings +import com.benasher44.uuid.uuid4 +import defer.SimpleDeferQuery +import defer.WithFragmentSpreadsMutation +import defer.WithFragmentSpreadsQuery +import defer.fragment.ComputerFields +import defer.fragment.ScreenFields +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import okio.ByteString.Companion.encodeUtf8 +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertTrue + +class Defer20220824NormalizedCacheTest { + private lateinit var mockServer: MockServer + private lateinit var apolloClient: ApolloClient + private lateinit var store: ApolloStore + + private suspend fun setUp() { + store = ApolloStore(MemoryCacheFactory()) + mockServer = MockServer() + apolloClient = ApolloClient.Builder().serverUrl(mockServer.url()).store(store).build() + } + + private fun tearDown() { + mockServer.close() + apolloClient.close() + } + + @Test + fun cacheOnly() = runTest(before = { setUp() }, after = { tearDown() }) { + apolloClient = apolloClient.newBuilder().fetchPolicy(FetchPolicy.CacheOnly).build() + + // Cache is empty + assertIs( + apolloClient.query(WithFragmentSpreadsQuery()).execute().exception + ) + + // Fill the cache by doing a network only request + val jsonList = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + ) + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) + apolloClient.query(WithFragmentSpreadsQuery()).fetchPolicy(FetchPolicy.NetworkOnly).toFlow().collect() + mockServer.awaitRequest() + + // Cache is not empty, so this doesn't go to the server + val cacheActual = apolloClient.query(WithFragmentSpreadsQuery()).execute().dataOrThrow() + mockServer.assertNoRequest() + + // We get the last/fully formed data + val cacheExpected = WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ) + ) + ) + assertEquals(cacheExpected, cacheActual) + } + + @Test + fun networkOnly() = runTest(before = { setUp() }, after = { tearDown() }) { + apolloClient = apolloClient.newBuilder().fetchPolicy(FetchPolicy.NetworkOnly).build() + + // Fill the cache by doing a first request + val jsonList = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + ) + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) + apolloClient.query(WithFragmentSpreadsQuery()).fetchPolicy(FetchPolicy.NetworkOnly).toFlow().collect() + mockServer.awaitRequest() + + // Cache is not empty, but NetworkOnly still goes to the server + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) + val networkActual = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().toList().map { it.dataOrThrow() } + mockServer.awaitRequest() + + val networkExpected = listOf( + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) + ), + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ) + ) + ), + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ) + ) + ), + ) + assertEquals(networkExpected, networkActual) + } + + @Test + fun cacheFirst() = runTest(before = { setUp() }, after = { tearDown() }) { + apolloClient = apolloClient.newBuilder().fetchPolicy(FetchPolicy.CacheFirst).build() + + val jsonList = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + ) + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) + + // Cache is empty, so this goes to the server + val responses = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().toList() + assertTrue(responses[0].exception is CacheMissException) + val networkActual = responses.drop(1).map { it.dataOrThrow() } + mockServer.awaitRequest() + + val networkExpected = listOf( + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) + ), + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ) + ) + ), + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ) + ) + ), + ) + assertEquals(networkExpected, networkActual) + + // Cache is not empty, so this doesn't go to the server + val cacheActual = apolloClient.query(WithFragmentSpreadsQuery()).execute().dataOrThrow() + assertFails { mockServer.takeRequest() } + + // We get the last/fully formed data + val cacheExpected = networkExpected.last() + assertEquals(cacheExpected, cacheActual) + } + + @Test + fun networkFirst() = runTest(before = { setUp() }, after = { tearDown() }) { + apolloClient = apolloClient.newBuilder().fetchPolicy(FetchPolicy.NetworkFirst).build() + + val jsonList = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + ) + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) + + // Cache is empty, so this goes to the server + val networkActual = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().toList().map { it.dataOrThrow() } + mockServer.awaitRequest() + + val networkExpected = listOf( + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) + ), + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ) + ) + ), + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ) + ) + ), + ) + assertEquals(networkExpected, networkActual) + + mockServer.enqueueError(statusCode = 500) + // Network will fail, so we get the cached version + val cacheActual = apolloClient.query(WithFragmentSpreadsQuery()).execute().dataOrThrow() + + // We get the last/fully formed data + val cacheExpected = networkExpected.last() + assertEquals(cacheExpected, cacheActual) + } + + @Test + fun cacheAndNetwork() = runTest(before = { setUp() }, after = { tearDown() }) { + apolloClient = apolloClient.newBuilder().fetchPolicy(FetchPolicy.CacheAndNetwork).build() + + val jsonList1 = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + ) + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList1) + + // Cache is empty + val responses = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().toList() + assertTrue(responses[0].exception is CacheMissException) + val networkActual = responses.drop(1).map { it.dataOrThrow() } + mockServer.awaitRequest() + + val networkExpected = listOf( + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) + ), + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ) + ) + ), + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ) + ) + ), + ) + assertEquals(networkExpected, networkActual) + + val jsonList2 = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",0]}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":true},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + ) + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList2) + + // Cache is not empty + val cacheAndNetworkActual = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().toList().map { it.dataOrThrow() } + mockServer.awaitRequest() + + // We get a combination of the last/fully formed data from the cache + the new network data + val cacheAndNetworkExpected = listOf( + networkExpected.last(), + + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null)) + ), + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", null) + ) + ) + ) + ), + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ) + ) + ), + ) + + assertEquals(cacheAndNetworkExpected, cacheAndNetworkActual) + } + + @Test + fun cacheFirstWithMissingFragmentDueToError() = runTest(before = { setUp() }, after = { tearDown() }) { + apolloClient = apolloClient.newBuilder().fetchPolicy(FetchPolicy.CacheFirst).build() + + val jsonList = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", + """{"incremental": [{"data":null,"path":["computers",0,"screen"],"label":"b","errors":[{"message":"Cannot resolve isColor","locations":[{"line":1,"column":119}],"path":["computers",0,"screen","isColor"]}]}],"hasNext":false}""", + ) + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) + + // Cache is empty, so this goes to the server + val networkActual = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().toList().drop(1) + mockServer.awaitRequest() + + val query = WithFragmentSpreadsQuery() + val uuid = uuid4() + + val networkExpected = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data(WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) + ) + ).build(), + + ApolloResponse.Builder( + query, + uuid, + ).data(WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ) + ) + ) + ).build(), + + ApolloResponse.Builder( + query, + uuid, + ) + .data( + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ) + ) + ) + ) + .errors( + listOf( + Error.Builder(message = "Cannot resolve isColor") + .locations(listOf(Error.Location(1, 119))) + .path(listOf("computers", 0, "screen", "isColor")) + .build() + ) + ) + .build(), + ) + assertResponseListEquals(networkExpected, networkActual) + + mockServer.enqueueError(statusCode = 500) + // Because of the error the cache is missing some fields, so we get a cache miss, and fallback to the network (which also fails) + val exception = apolloClient.query(WithFragmentSpreadsQuery()).execute().exception + check(exception is CacheMissException) + assertIs(exception.suppressedExceptions.first()) + assertEquals("Object 'computers.0.screen' has no field named 'isColor'", exception.message) + mockServer.awaitRequest() + } + + @Test + fun networkFirstWithNetworkError() = runTest(before = { setUp() }, after = { tearDown() }) { + val query = WithFragmentSpreadsQuery() + val uuid = uuid4() + val networkResponses = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data(WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) + ) + ).build(), + + ApolloResponse.Builder( + query, + uuid, + ).data(WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ) + ) + ) + ).build(), + ) + + apolloClient = ApolloClient.Builder() + .store(store) + .fetchPolicy(FetchPolicy.NetworkFirst) + .networkTransport( + object : NetworkTransport { + @Suppress("UNCHECKED_CAST") + override fun execute(request: ApolloRequest): Flow> { + // Emit a few items then an exception + return flow { + for (networkResponse in networkResponses) { + emit(networkResponse as ApolloResponse) + } + delay(10) + emit(ApolloResponse.Builder(requestUuid = uuid, operation = query) + .exception(ApolloNetworkException("Network error")) + .isLast(true) + .build() as ApolloResponse + ) + } + } + + override fun dispose() {} + } + ) + .build() + + // - get the first few responses + // - an exception happens + // - fallback to the cache + // - because of the error the cache is missing some fields, so we get a cache miss + val actual = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().toList() + + assertResponseListEquals(networkResponses, actual.dropLast(2)) + val networkExceptionResponse = actual[actual.size - 2] + val cacheExceptionResponse = actual.last() + assertIs(networkExceptionResponse.exception) + assertIs(cacheExceptionResponse.exception) + assertEquals("Object 'computers.0.screen' has no field named 'isColor'", cacheExceptionResponse.exception!!.message) + } + + @Test + fun mutation() = runTest(before = { setUp() }, after = { tearDown() }) { + val jsonList = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0],"label":"c"}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + ) + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) + val networkActual = apolloClient.mutation(WithFragmentSpreadsMutation()).toFlow().toList().map { it.dataOrThrow() } + mockServer.awaitRequest() + + val networkExpected = listOf( + WithFragmentSpreadsMutation.Data( + listOf(WithFragmentSpreadsMutation.Computer("Computer", "Computer1", null)) + ), + WithFragmentSpreadsMutation.Data( + listOf(WithFragmentSpreadsMutation.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ) + ) + ), + WithFragmentSpreadsMutation.Data( + listOf(WithFragmentSpreadsMutation.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ) + ) + ), + ) + assertEquals(networkExpected, networkActual) + + // Now cache is not empty + val cacheActual = apolloClient.query(WithFragmentSpreadsQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute().dataOrThrow() + + // We get the last/fully formed data + val cacheExpected = WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ) + ) + ) + assertEquals(cacheExpected, cacheActual) + } + + @Test + fun mutationWithOptimisticDataFails() = runTest(before = { setUp() }, after = { tearDown() }) { + val jsonList = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0],"label":"c"}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":false}""", + ) + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) + val responses = apolloClient.mutation(WithFragmentSpreadsMutation()).optimisticUpdates( + WithFragmentSpreadsMutation.Data( + listOf(WithFragmentSpreadsMutation.Computer("Computer", "Computer1", null)) + ) + ).toFlow() + + val exception = assertFailsWith { + responses.collect() + } + assertEquals("Apollo: optimistic updates can only be applied with one network response", exception.message) + } + + @Test + fun intermediatePayloadsAreCached() = runTest(before = { setUp() }, after = { tearDown() }) { + @Suppress("DEPRECATION") + if (com.apollographql.apollo.testing.platform() == com.apollographql.apollo.testing.Platform.Js) { + // TODO For now chunked is not supported on JS - remove this check when it is + return@runTest + } + val jsonList = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386"},"path":["computers",0]}],"hasNext":false}""", + ) + val multipartBody = mockServer.enqueueMultipart("application/json") + multipartBody.enqueuePart(jsonList[0].encodeUtf8(), false) + val recordFields = apolloClient.query(SimpleDeferQuery()).fetchPolicy(FetchPolicy.NetworkOnly).toFlow().map { + apolloClient.apolloStore.accessCache { it.loadRecord("computers.0", CacheHeaders.NONE)!!.fields }.also { + multipartBody.enqueuePart(jsonList[1].encodeUtf8(), true) + } + }.toList() + assertEquals(mapOf("__typename" to "Computer", "id" to "Computer1"), recordFields[0]) + assertEquals(mapOf("__typename" to "Computer", "id" to "Computer1", "cpu" to "386"), recordFields[1]) + } +} diff --git a/tests/defer/src/commonTest/kotlin/test/Defer20220824Test.kt b/tests/defer/src/commonTest/kotlin/test/Defer20220824Test.kt new file mode 100644 index 00000000000..1feaf99fa5f --- /dev/null +++ b/tests/defer/src/commonTest/kotlin/test/Defer20220824Test.kt @@ -0,0 +1,446 @@ +package test + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.ApolloResponse +import com.apollographql.apollo.api.Error +import com.apollographql.apollo.autoPersistedQueryInfo +import com.apollographql.apollo.mpp.currentTimeMillis +import com.apollographql.apollo.testing.internal.runTest +import com.apollographql.mockserver.MockServer +import com.apollographql.mockserver.enqueueMultipart +import com.apollographql.mockserver.enqueueString +import com.apollographql.mockserver.enqueueStrings +import com.benasher44.uuid.uuid4 +import defer.SimpleDeferQuery +import defer.WithFragmentSpreadsQuery +import defer.WithInlineFragmentsQuery +import defer.fragment.ComputerFields +import defer.fragment.ScreenFields +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import okio.ByteString.Companion.encodeUtf8 +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class Defer20220824Test { + private lateinit var mockServer: MockServer + private lateinit var apolloClient: ApolloClient + + private suspend fun setUp() { + mockServer = MockServer() + apolloClient = ApolloClient.Builder() + .serverUrl(mockServer.url()) + .build() + } + + private fun tearDown() { + mockServer.close() + } + + @Test + fun deferWithFragmentSpreads() = runTest(before = { setUp() }, after = { tearDown() }) { + val jsonList = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + ) + + val expectedDataList = listOf( + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) + ), + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) + ), + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", null) + ) + ), + ) + ), + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", null) + ) + ), + ) + ), + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) + ), + ) + + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) + val actualDataList = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun deferWithInlineFragments() = runTest(before = { setUp() }, after = { tearDown() }) { + val jsonList = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"b"}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"b"}],"hasNext":false}""", + ) + + val expectedDataList = listOf( + WithInlineFragmentsQuery.Data( + listOf( + WithInlineFragmentsQuery.Computer("Computer", "Computer1", null), + WithInlineFragmentsQuery.Computer("Computer", "Computer2", null), + ) + ), + WithInlineFragmentsQuery.Data( + listOf( + WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, + WithInlineFragmentsQuery.Screen("Screen", "640x480", null) + ) + ), + WithInlineFragmentsQuery.Computer("Computer", "Computer2", null), + ) + ), + WithInlineFragmentsQuery.Data( + listOf( + WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, + WithInlineFragmentsQuery.Screen("Screen", "640x480", null) + ) + ), + WithInlineFragmentsQuery.Computer("Computer", "Computer2", WithInlineFragmentsQuery.OnComputer("486", 1996, + WithInlineFragmentsQuery.Screen("Screen", "800x600", null) + ) + ), + ) + ), + WithInlineFragmentsQuery.Data( + listOf( + WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, + WithInlineFragmentsQuery.Screen("Screen", "640x480", + WithInlineFragmentsQuery.OnScreen(false) + ) + ) + ), + WithInlineFragmentsQuery.Computer("Computer", "Computer2", WithInlineFragmentsQuery.OnComputer("486", 1996, + WithInlineFragmentsQuery.Screen("Screen", "800x600", null) + ) + ), + ) + ), + WithInlineFragmentsQuery.Data( + listOf( + WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, + WithInlineFragmentsQuery.Screen("Screen", "640x480", + WithInlineFragmentsQuery.OnScreen(false) + ) + ) + ), + WithInlineFragmentsQuery.Computer("Computer", "Computer2", WithInlineFragmentsQuery.OnComputer("486", 1996, + WithInlineFragmentsQuery.Screen("Screen", "800x600", + WithInlineFragmentsQuery.OnScreen(true) + ) + ) + ), + ) + ), + ) + + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) + val actualDataList = apolloClient.query(WithInlineFragmentsQuery()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun deferWithFragmentSpreadsAndError() = runTest(before = { setUp() }, after = { tearDown() }) { + val jsonList = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", + """{"incremental": [{"data":null,"path":["computers",0,"screen"],"label":"b","errors":[{"message":"Cannot resolve isColor","locations":[{"line":1,"column":119}],"path":["computers",0,"screen","isColor"]}]}],"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + ) + + val query = WithFragmentSpreadsQuery() + val uuid = uuid4() + + val expectedDataList = listOf( + ApolloResponse.Builder( + query, + uuid, + ).data(WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) + ) + ).build(), + + ApolloResponse.Builder( + query, + uuid, + ).data( + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) + ) + ).build(), + + ApolloResponse.Builder( + query, + uuid, + ) + .data( + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) + ) + ) + .errors( + listOf( + Error.Builder(message = "Cannot resolve isColor") + .locations(listOf(Error.Location(1, 119))) + .path(listOf("computers", 0, "screen", "isColor")) + .build() + ) + ) + .build(), + + ApolloResponse.Builder( + query, + uuid, + ).data( + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", null) + ) + ), + ) + ) + ).build(), + + ApolloResponse.Builder( + query, + uuid, + ).data( + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) + ) + ).build(), + ) + + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) + val actualResponseList = apolloClient.query(query).toFlow().toList() + assertResponseListEquals(expectedDataList, actualResponseList) + } + + @Test + fun payloadsAreReceivedIncrementally() = runTest(before = { setUp() }, after = { tearDown() }) { + @Suppress("DEPRECATION") + if (com.apollographql.apollo.testing.platform() == com.apollographql.apollo.testing.Platform.Js) { + // TODO For now chunked is not supported on JS - remove this check when it is + return@runTest + } + val delayMillis = 200L + + val multipartBody = mockServer.enqueueMultipart("application/json") + + val syncChannel = Channel() + val job = launch { + apolloClient.query(WithFragmentSpreadsQuery()).toFlow().collect { + syncChannel.send(Unit) + } + } + + val jsonList = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + ) + + jsonList.withIndex().forEach { (index, value) -> + multipartBody.enqueueDelay(delayMillis) + multipartBody.enqueuePart(value.encodeUtf8(), index == jsonList.lastIndex) + + val timeBeforeReceive = currentTimeMillis() + syncChannel.receive() + assertTrue(currentTimeMillis() - timeBeforeReceive >= delayMillis) + } + + job.cancel() + } + + @Test + fun emptyPayloadsAreIgnored() = runTest(before = { setUp() }, after = { tearDown() }) { + val jsonWithEmptyPayload = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"computer1"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386"},"path":["computers",0]}],"hasNext":true}""", + """{"hasNext":false}""", + ) + val jsonWithoutEmptyPayload = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"computer1"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386"},"path":["computers",0]}],"hasNext":false}""", + ) + + val expectedDataList = listOf( + SimpleDeferQuery.Data( + listOf(SimpleDeferQuery.Computer("Computer", "computer1", null)) + ), + SimpleDeferQuery.Data( + listOf(SimpleDeferQuery.Computer("Computer", "computer1", SimpleDeferQuery.OnComputer("386"))) + ), + ) + + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonWithEmptyPayload) + var actualDataList = apolloClient.query(SimpleDeferQuery()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonWithoutEmptyPayload) + actualDataList = apolloClient.query(SimpleDeferQuery()).toFlow().toList().map { it.dataOrThrow() } + assertEquals(expectedDataList, actualDataList) + } + + @Test + fun deferWithApqFound() = runTest(before = { setUp() }, after = { tearDown() }) { + apolloClient = ApolloClient.Builder() + .serverUrl(mockServer.url()) + .autoPersistedQueries() + .build() + + val jsonList = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + ) + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) + val finalResponse = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().last() + assertEquals(true, finalResponse.autoPersistedQueryInfo?.hit) + assertEquals( + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) + ), + finalResponse.dataOrThrow() + ) + } + + @Test + fun deferWithApqNotFound() = runTest(before = { setUp() }, after = { tearDown() }) { + apolloClient = ApolloClient.Builder() + .serverUrl(mockServer.url()) + .autoPersistedQueries() + .build() + + mockServer.enqueueString("""{"errors":[{"message":"PersistedQueryNotFound"}]}""") + val jsonList = listOf( + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", + """{"incremental": [{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", + """{"incremental": [{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", + ) + mockServer.enqueueMultipart("application/json").enqueueStrings(jsonList) + val finalResponse = apolloClient.query(WithFragmentSpreadsQuery()).toFlow().last() + assertEquals(false, finalResponse.autoPersistedQueryInfo?.hit) + assertEquals( + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ), + ) + ), + finalResponse.dataOrThrow() + ) + } +} diff --git a/tests/defer/src/commonTest/kotlin/test/DeferNormalizedCacheTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha9NormalizedCacheTest.kt similarity index 86% rename from tests/defer/src/commonTest/kotlin/test/DeferNormalizedCacheTest.kt rename to tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha9NormalizedCacheTest.kt index e6bced95233..0958e4f7835 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferNormalizedCacheTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha9NormalizedCacheTest.kt @@ -19,6 +19,8 @@ import com.apollographql.apollo.exception.ApolloHttpException import com.apollographql.apollo.exception.ApolloNetworkException import com.apollographql.apollo.exception.CacheMissException import com.apollographql.apollo.network.NetworkTransport +import com.apollographql.apollo.network.http.HttpNetworkTransport +import com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol import com.apollographql.apollo.testing.internal.runTest import com.apollographql.mockserver.MockServer import com.apollographql.mockserver.assertNoRequest @@ -46,7 +48,7 @@ import kotlin.test.assertFailsWith import kotlin.test.assertIs import kotlin.test.assertTrue -class DeferNormalizedCacheTest { +class DeferGraphQL17Alpha9NormalizedCacheTest { private lateinit var mockServer: MockServer private lateinit var apolloClient: ApolloClient private lateinit var store: ApolloStore @@ -54,7 +56,14 @@ class DeferNormalizedCacheTest { private suspend fun setUp() { store = ApolloStore(MemoryCacheFactory()) mockServer = MockServer() - apolloClient = ApolloClient.Builder().serverUrl(mockServer.url()).store(store).build() + apolloClient = ApolloClient.Builder() + .networkTransport( + HttpNetworkTransport.Builder() + .serverUrl(mockServer.url()) + .incrementalDeliveryProtocol(IncrementalDeliveryProtocol.GraphQL17Alpha9) + .build() + ) + .store(store).build() } private fun tearDown() { @@ -87,17 +96,19 @@ class DeferNormalizedCacheTest { // We get the last/fully formed data val cacheExpected = WithFragmentSpreadsQuery.Data( listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) - ) - ) + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", + ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) ), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", - ScreenFields(true) + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", + ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) ) - ) ), ) ) @@ -131,17 +142,19 @@ class DeferNormalizedCacheTest { ), WithFragmentSpreadsQuery.Data( listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", + ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) ) - ) ), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", - ScreenFields(true) + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", + ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) ) - ) ), ) ), @@ -174,17 +187,19 @@ class DeferNormalizedCacheTest { ), WithFragmentSpreadsQuery.Data( listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", + ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) ) - ) ), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", - ScreenFields(true) + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", + ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) ) - ) ), ) ), @@ -223,17 +238,19 @@ class DeferNormalizedCacheTest { ), WithFragmentSpreadsQuery.Data( listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", + ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) ) - ) ), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", - ScreenFields(true) + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", + ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) ) - ) ), ) ), @@ -270,9 +287,15 @@ class DeferNormalizedCacheTest { listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) ), WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false))))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", + ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) + ) + ) + ) ), ) assertEquals(networkExpected, networkActual) @@ -295,9 +318,15 @@ class DeferNormalizedCacheTest { listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null)) ), WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", - ScreenFields(true))))) + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", + ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) + ) + ) + ) ), ) @@ -325,12 +354,13 @@ class DeferNormalizedCacheTest { ApolloResponse.Builder( query, uuid, - ).data(WithFragmentSpreadsQuery.Data( - listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ).data( + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), + ) ) - ) ).build(), @@ -340,15 +370,17 @@ class DeferNormalizedCacheTest { ).data( WithFragmentSpreadsQuery.Data( listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", + ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) ), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", - ScreenFields(true) + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", + ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) ) - ) ), ) ) @@ -380,17 +412,26 @@ class DeferNormalizedCacheTest { ApolloResponse.Builder( query, uuid, - ).data(WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) - )).build(), + ).data( + WithFragmentSpreadsQuery.Data( + listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) + ) + ).build(), ApolloResponse.Builder( query, uuid, - ).data(WithFragmentSpreadsQuery.Data( - listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null)))) - )).build(), + ).data( + WithFragmentSpreadsQuery.Data( + listOf( + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", + ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", null) + ) + ) + ) + ) + ).build(), ) apolloClient = ApolloClient.Builder() @@ -409,7 +450,8 @@ class DeferNormalizedCacheTest { emit(ApolloResponse.Builder(requestUuid = uuid, operation = query) .exception(ApolloNetworkException("Network error")) .isLast(true) - .build() as ApolloResponse) + .build() as ApolloResponse + ) } } @@ -450,18 +492,20 @@ class DeferNormalizedCacheTest { ) ), WithFragmentSpreadsMutation.Data( - listOf(WithFragmentSpreadsMutation.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) + listOf(WithFragmentSpreadsMutation.Computer("Computer", "Computer1", + ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) ) - ) ), - WithFragmentSpreadsMutation.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", - ScreenFields(true) + WithFragmentSpreadsMutation.Computer("Computer", "Computer2", + ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) ) ) - ) ) ), ) @@ -473,17 +517,19 @@ class DeferNormalizedCacheTest { // We get the last/fully formed data val cacheExpected = WithFragmentSpreadsQuery.Data( listOf( - WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) + WithFragmentSpreadsQuery.Computer("Computer", "Computer1", + ComputerFields("386", 1993, + ComputerFields.Screen("Screen", "640x480", + ScreenFields(false) + ) ) - ) ), - WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", - ScreenFields(true) + WithFragmentSpreadsQuery.Computer("Computer", "Computer2", + ComputerFields("486", 1996, + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) ) - ) ), ) ) diff --git a/tests/defer/src/commonTest/kotlin/test/DeferTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha9Test.kt similarity index 95% rename from tests/defer/src/commonTest/kotlin/test/DeferTest.kt rename to tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha9Test.kt index 9d7a80a4d00..423ab53cd02 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha9Test.kt @@ -6,7 +6,11 @@ import com.apollographql.apollo.api.Error import com.apollographql.apollo.api.Error.Builder import com.apollographql.apollo.autoPersistedQueryInfo import com.apollographql.apollo.mpp.currentTimeMillis +import com.apollographql.apollo.network.http.HttpNetworkTransport +import com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol +import com.apollographql.apollo.testing.Platform import com.apollographql.apollo.testing.internal.runTest +import com.apollographql.apollo.testing.platform import com.apollographql.mockserver.MockServer import com.apollographql.mockserver.enqueueMultipart import com.apollographql.mockserver.enqueueString @@ -26,14 +30,19 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -class DeferTest { +class DeferGraphQL17Alpha9Test { private lateinit var mockServer: MockServer private lateinit var apolloClient: ApolloClient private suspend fun setUp() { mockServer = MockServer() apolloClient = ApolloClient.Builder() - .serverUrl(mockServer.url()) + .networkTransport( + HttpNetworkTransport.Builder() + .serverUrl(mockServer.url()) + .incrementalDeliveryProtocol(IncrementalDeliveryProtocol.GraphQL17Alpha9) + .build() + ) .build() } @@ -174,7 +183,7 @@ class DeferTest { @Test fun payloadsAreReceivedIncrementally() = runTest(before = { setUp() }, after = { tearDown() }) { @Suppress("DEPRECATION") - if (com.apollographql.apollo.testing.platform() == com.apollographql.apollo.testing.Platform.Js) { + if (platform() == Platform.Js) { // TODO For now chunked is not supported on JS - remove this check when it is return@runTest } @@ -244,8 +253,7 @@ class DeferTest { @Test fun deferWithApqFound() = runTest(before = { setUp() }, after = { tearDown() }) { - apolloClient = ApolloClient.Builder() - .serverUrl(mockServer.url()) + apolloClient = apolloClient.newBuilder() .autoPersistedQueries() .build() @@ -279,8 +287,7 @@ class DeferTest { @Test fun deferWithApqNotFound() = runTest(before = { setUp() }, after = { tearDown() }) { - apolloClient = ApolloClient.Builder() - .serverUrl(mockServer.url()) + apolloClient = apolloClient.newBuilder() .autoPersistedQueries() .build() diff --git a/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt index 1746e941c95..09c954030d6 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt @@ -4,6 +4,8 @@ import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.Error import com.apollographql.apollo.api.Optional +import com.apollographql.apollo.network.http.HttpNetworkTransport +import com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol import com.apollographql.apollo.testing.internal.runTest import com.benasher44.uuid.uuid4 import defer.CanDeferFragmentsOnTheTopLevelQueryFieldQuery @@ -44,7 +46,12 @@ class DeferWithApolloServerTest { private fun setUp() { apolloClient = ApolloClient.Builder() - .serverUrl("http://127.0.0.1:4000/") + .networkTransport( + HttpNetworkTransport.Builder() + .serverUrl("http://127.0.0.1:4000/") + .incrementalDeliveryProtocol(IncrementalDeliveryProtocol.GraphQL17Alpha9) + .build() + ) .build() } diff --git a/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt b/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt index 03a39f6e738..79a4e52eb87 100644 --- a/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt +++ b/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt @@ -60,8 +60,11 @@ class DeferJvmTest { } val jsonList = listOf( - """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"pending":[{"id":"0","path":["computers",0]},{"id":"1","path":["computers",1]}],"hasNext":true}""", - """{"hasNext":true,"pending":[{"id":"2","path":["computers",0,"screen"],"label":"a"},{"id":"3","path":["computers",1,"screen"],"label":"a"}],"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"id":"0"},{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"id":"1"},{"data":{"isColor":false},"id":"2"},{"data":{"isColor":true},"id":"3"}],"completed":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}]}""", + """{"data":{"computers":[{"__typename":"Computer","id":"Computer1"},{"__typename":"Computer","id":"Computer2"}]},"hasNext":true}""", + """{"incremental":[{"data":{"cpu":"386","year":1993,"screen":{"__typename":"Screen","resolution":"640x480"}},"path":["computers",0]}],"hasNext":true}""", + """{"incremental":[{"data":{"cpu":"486","year":1996,"screen":{"__typename":"Screen","resolution":"800x600"}},"path":["computers",1]}],"hasNext":true}""", + """{"incremental":[{"data":{"isColor":false},"path":["computers",0,"screen"],"label":"a"}],"hasNext":true}""", + """{"incremental":[{"data":{"isColor":true},"path":["computers",1,"screen"],"label":"a"}],"hasNext":false}""", ) for ((index, json) in jsonList.withIndex()) { @@ -85,7 +88,9 @@ class DeferJvmTest { ) ), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", ScreenFields(true)) + ComputerFields.Screen("Screen", "800x600", + ScreenFields(true) + ) ) ), ) From a9bf9b2ccb100a5604a0febbc0f0ffe898bc9aaa Mon Sep 17 00:00:00 2001 From: BoD Date: Thu, 25 Sep 2025 11:48:54 +0200 Subject: [PATCH 11/21] Simplify CI job --- .github/workflows/defer-integration-tests.yml | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/.github/workflows/defer-integration-tests.yml b/.github/workflows/defer-integration-tests.yml index 313085f6379..16b84fdc9db 100644 --- a/.github/workflows/defer-integration-tests.yml +++ b/.github/workflows/defer-integration-tests.yml @@ -24,8 +24,6 @@ jobs: - run: | ./router --supergraph tests/defer/router/simple-supergraph.graphqls & - - uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda #v3.4.2 - - env: DEFER_WITH_ROUTER_TESTS: true run: | @@ -34,27 +32,15 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'apollographql/apollo-kotlin' steps: - - name: Checkout project - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 - - name: Install and run graph - working-directory: tests/defer/apollo-server/ + - working-directory: tests/defer/apollo-server/ run: | npm install --legacy-peer-deps npx patch-package APOLLO_PORT=4000 npm start & - - name: Setup Java - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 #v4.2.1 - with: - distribution: 'temurin' - java-version: 17 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda #v3.4.2 - - - name: Run Apollo Kotlin @defer tests - env: + - env: DEFER_WITH_APOLLO_SERVER_TESTS: true run: | ./gradlew --no-daemon --console plain -p tests :defer:allTests From 8bf128dcbf0bb395a3b4995c295524fbaf4c7a65 Mon Sep 17 00:00:00 2001 From: BoD Date: Thu, 25 Sep 2025 12:18:40 +0200 Subject: [PATCH 12/21] Do not expose accept headers in DefaultHttpRequestComposer's public API --- .../api/http/DefaultHttpRequestComposer.kt | 31 +++++-------------- .../network/http/HttpNetworkTransport.kt | 5 +-- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt index 881910a027f..dc75dc719f8 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt @@ -7,6 +7,7 @@ import com.apollographql.apollo.api.CustomScalarAdapters import com.apollographql.apollo.api.Operation import com.apollographql.apollo.api.Subscription import com.apollographql.apollo.api.Upload +import com.apollographql.apollo.api.http.DefaultHttpRequestComposer.Companion.composePostParams import com.apollographql.apollo.api.http.internal.urlEncode import com.apollographql.apollo.api.json.JsonWriter import com.apollographql.apollo.api.json.buildJsonByteString @@ -32,14 +33,12 @@ import okio.buffer */ class DefaultHttpRequestComposer( private val serverUrl: String, - private val acceptHeaderQueriesAndMutations: String = HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824, - private val acceptHeaderSubscriptions: String = HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0, + private val acceptHeaderQueriesAndMutations: String = HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS, ) : HttpRequestComposer { constructor(serverUrl: String) : this( serverUrl = serverUrl, - acceptHeaderQueriesAndMutations = HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824, - acceptHeaderSubscriptions = HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0, + acceptHeaderQueriesAndMutations = HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS, ) override fun compose(apolloRequest: ApolloRequest): HttpRequest { @@ -48,7 +47,7 @@ class DefaultHttpRequestComposer( val requestHeaders = mutableListOf().apply { if (apolloRequest.operation is Subscription<*>) { - add(HttpHeader(HEADER_ACCEPT_NAME, acceptHeaderSubscriptions)) + add(HttpHeader(HEADER_ACCEPT_NAME, HEADER_ACCEPT_VALUE_SUBSCRIPTIONS)) } else { add(HttpHeader(HEADER_ACCEPT_NAME, acceptHeaderQueriesAndMutations)) } @@ -100,25 +99,11 @@ class DefaultHttpRequestComposer( // and thus is safe to execute. // See https://www.apollographql.com/docs/apollo-server/security/cors/#preventing-cross-site-request-forgery-csrf // for details. - internal val HEADER_APOLLO_REQUIRE_PREFLIGHT = "Apollo-Require-Preflight" + private const val HEADER_APOLLO_REQUIRE_PREFLIGHT = "Apollo-Require-Preflight" - val HEADER_ACCEPT_NAME = "Accept" - - const val HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824 = - "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" - - // TODO To be agreed upon with the router and other clients - const val HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20230621 = - "multipart/mixed;incrementalDeliverySpec=20230621, application/graphql-response+json, application/json" - - const val HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0 = - "multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json" - - @Deprecated("Use HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0 instead", ReplaceWith("HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0")) - val HEADER_ACCEPT_VALUE_MULTIPART = HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0 - - @Deprecated("Use HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824 instead", ReplaceWith("HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824")) - val HEADER_ACCEPT_VALUE_DEFER = HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824 + private const val HEADER_ACCEPT_NAME = "Accept" + private const val HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS = "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" + private const val HEADER_ACCEPT_VALUE_SUBSCRIPTIONS = "multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json" private fun buildGetUrl( serverUrl: String, diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt index 279785f57f5..4a9a6002ff3 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt @@ -427,7 +427,7 @@ private constructor( */ object Defer20220824 : IncrementalDeliveryProtocol { @ApolloInternal - override val acceptHeader: String = DefaultHttpRequestComposer.HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824 + override val acceptHeader: String = "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" @ApolloInternal override fun newIncrementalResultsMerger(): IncrementalResultsMerger = Defer20220824IncrementalResultsMerger() @@ -440,7 +440,8 @@ private constructor( */ object GraphQL17Alpha9 : IncrementalDeliveryProtocol { @ApolloInternal - override val acceptHeader: String = DefaultHttpRequestComposer.HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20230621 + // TODO To be agreed upon with the router and other clients + override val acceptHeader: String = "multipart/mixed;incrementalDeliverySpec=20230621, application/graphql-response+json, application/json" @ApolloInternal override fun newIncrementalResultsMerger(): IncrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() From 1dcd3b4dd4988bdcabb45ab11e5106eef4d6971c Mon Sep 17 00:00:00 2001 From: BoD Date: Thu, 25 Sep 2025 15:34:13 +0200 Subject: [PATCH 13/21] Make more symbols internal --- libraries/apollo-api/api/apollo-api.api | 10 +-- libraries/apollo-api/api/apollo-api.klib.api | 16 +--- .../api/android/apollo-runtime.api | 55 ++------------ .../api/apollo-runtime.klib.api | 75 ++----------------- .../apollo-runtime/api/jvm/apollo-runtime.api | 55 ++------------ .../Defer20220824IncrementalResultsMerger.kt | 4 +- ...GraphQL17Alpha9IncrementalResultsMerger.kt | 4 +- .../IncrementalDeliveryProtocolImpl.kt | 42 +++++++++++ .../incremental/IncrementalResultsMerger.kt | 5 +- .../network/http/HttpNetworkTransport.kt | 42 ++++------- 10 files changed, 84 insertions(+), 224 deletions(-) create mode 100644 libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt diff --git a/libraries/apollo-api/api/apollo-api.api b/libraries/apollo-api/api/apollo-api.api index 82ac9960d60..aa424e446c6 100644 --- a/libraries/apollo-api/api/apollo-api.api +++ b/libraries/apollo-api/api/apollo-api.api @@ -933,12 +933,9 @@ public final class com/apollographql/apollo/api/http/ByteStringHttpBody : com/ap public final class com/apollographql/apollo/api/http/DefaultHttpRequestComposer : com/apollographql/apollo/api/http/HttpRequestComposer { public static final field Companion Lcom/apollographql/apollo/api/http/DefaultHttpRequestComposer$Companion; - public static final field HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824 Ljava/lang/String; - public static final field HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20230621 Ljava/lang/String; - public static final field HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0 Ljava/lang/String; public fun (Ljava/lang/String;)V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun compose (Lcom/apollographql/apollo/api/ApolloRequest;)Lcom/apollographql/apollo/api/http/HttpRequest; } @@ -949,9 +946,6 @@ public final class com/apollographql/apollo/api/http/DefaultHttpRequestComposer$ public final fun buildPostBody (Lcom/apollographql/apollo/api/Operation;Lcom/apollographql/apollo/api/CustomScalarAdapters;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/apollographql/apollo/api/http/HttpBody; public final fun buildPostBody (Lcom/apollographql/apollo/api/Operation;Lcom/apollographql/apollo/api/CustomScalarAdapters;ZZLjava/lang/String;)Lcom/apollographql/apollo/api/http/HttpBody; public final fun composePayload (Lcom/apollographql/apollo/api/ApolloRequest;)Ljava/util/Map; - public final fun getHEADER_ACCEPT_NAME ()Ljava/lang/String; - public final fun getHEADER_ACCEPT_VALUE_DEFER ()Ljava/lang/String; - public final fun getHEADER_ACCEPT_VALUE_MULTIPART ()Ljava/lang/String; } public final class com/apollographql/apollo/api/http/DefaultWebSocketPayloadComposer : com/apollographql/apollo/api/http/WebSocketPayloadComposer { diff --git a/libraries/apollo-api/api/apollo-api.klib.api b/libraries/apollo-api/api/apollo-api.klib.api index f9bb9628590..d56ba7371e4 100644 --- a/libraries/apollo-api/api/apollo-api.klib.api +++ b/libraries/apollo-api/api/apollo-api.klib.api @@ -493,25 +493,11 @@ final class com.apollographql.apollo.api.http/ByteStringHttpBody : com.apollogra final class com.apollographql.apollo.api.http/DefaultHttpRequestComposer : com.apollographql.apollo.api.http/HttpRequestComposer { // com.apollographql.apollo.api.http/DefaultHttpRequestComposer|null[0] constructor (kotlin/String) // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.|(kotlin.String){}[0] - constructor (kotlin/String, kotlin/String = ..., kotlin/String = ...) // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.|(kotlin.String;kotlin.String;kotlin.String){}[0] + constructor (kotlin/String, kotlin/String = ...) // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.|(kotlin.String;kotlin.String){}[0] final fun <#A1: com.apollographql.apollo.api/Operation.Data> compose(com.apollographql.apollo.api/ApolloRequest<#A1>): com.apollographql.apollo.api.http/HttpRequest // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.compose|compose(com.apollographql.apollo.api.ApolloRequest<0:0>){0§}[0] final object Companion { // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion|null[0] - final const val HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824 // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824|{}HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824[0] - final fun (): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20220824.|(){}[0] - final const val HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20230621 // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20230621|{}HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20230621[0] - final fun (): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS_20230621.|(){}[0] - final const val HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0 // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0|{}HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0[0] - final fun (): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_SUBSCRIPTIONS_1_0.|(){}[0] - - final val HEADER_ACCEPT_NAME // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_NAME|{}HEADER_ACCEPT_NAME[0] - final fun (): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_NAME.|(){}[0] - final val HEADER_ACCEPT_VALUE_DEFER // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_DEFER|{}HEADER_ACCEPT_VALUE_DEFER[0] - final fun (): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_DEFER.|(){}[0] - final val HEADER_ACCEPT_VALUE_MULTIPART // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_MULTIPART|{}HEADER_ACCEPT_VALUE_MULTIPART[0] - final fun (): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_MULTIPART.|(){}[0] - final fun (kotlin/String).appendQueryParameters(kotlin.collections/Map): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.appendQueryParameters|appendQueryParameters@kotlin.String(kotlin.collections.Map){}[0] final fun <#A2: com.apollographql.apollo.api/Operation.Data> buildParamsMap(com.apollographql.apollo.api/Operation<#A2>, com.apollographql.apollo.api/CustomScalarAdapters, kotlin/Boolean, kotlin/Boolean): okio/ByteString // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.buildParamsMap|buildParamsMap(com.apollographql.apollo.api.Operation<0:0>;com.apollographql.apollo.api.CustomScalarAdapters;kotlin.Boolean;kotlin.Boolean){0§}[0] final fun <#A2: com.apollographql.apollo.api/Operation.Data> buildParamsMap(com.apollographql.apollo.api/Operation<#A2>, com.apollographql.apollo.api/CustomScalarAdapters, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean): okio/ByteString // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.buildParamsMap|buildParamsMap(com.apollographql.apollo.api.Operation<0:0>;com.apollographql.apollo.api.CustomScalarAdapters;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean){0§}[0] diff --git a/libraries/apollo-runtime/api/android/apollo-runtime.api b/libraries/apollo-runtime/api/android/apollo-runtime.api index 3d9b141bcb0..2aded9b55ea 100644 --- a/libraries/apollo-runtime/api/android/apollo-runtime.api +++ b/libraries/apollo-runtime/api/android/apollo-runtime.api @@ -229,38 +229,6 @@ public final class com/apollographql/apollo/internal/MultipartReader$Part : java public final fun getHeaders ()Ljava/util/List; } -public final class com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger : com/apollographql/apollo/internal/incremental/IncrementalResultsMerger { - public fun ()V - public fun getHasNext ()Z - public fun getIncrementalResultIdentifiers ()Ljava/util/Set; - public fun getMerged ()Ljava/util/Map; - public fun isEmptyResponse ()Z - public fun merge (Ljava/util/Map;)Ljava/util/Map; - public fun merge (Lokio/BufferedSource;)Ljava/util/Map; - public fun reset ()V -} - -public final class com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger : com/apollographql/apollo/internal/incremental/IncrementalResultsMerger { - public fun ()V - public fun getHasNext ()Z - public fun getIncrementalResultIdentifiers ()Ljava/util/Set; - public fun getMerged ()Ljava/util/Map; - public fun isEmptyResponse ()Z - public fun merge (Ljava/util/Map;)Ljava/util/Map; - public fun merge (Lokio/BufferedSource;)Ljava/util/Map; - public fun reset ()V -} - -public abstract interface class com/apollographql/apollo/internal/incremental/IncrementalResultsMerger { - public abstract fun getHasNext ()Z - public abstract fun getIncrementalResultIdentifiers ()Ljava/util/Set; - public abstract fun getMerged ()Ljava/util/Map; - public abstract fun isEmptyResponse ()Z - public abstract fun merge (Ljava/util/Map;)Ljava/util/Map; - public abstract fun merge (Lokio/BufferedSource;)Ljava/util/Map; - public abstract fun reset ()V -} - public abstract interface class com/apollographql/apollo/network/NetworkMonitor : java/io/Closeable { public abstract fun isOnline ()Lkotlinx/coroutines/flow/StateFlow; } @@ -371,7 +339,7 @@ public abstract interface class com/apollographql/apollo/network/http/HttpInterc } public final class com/apollographql/apollo/network/http/HttpNetworkTransport : com/apollographql/apollo/network/NetworkTransport { - public synthetic fun (Lcom/apollographql/apollo/api/http/HttpRequestComposer;Lcom/apollographql/apollo/network/http/HttpEngine;Ljava/util/List;ZLcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lcom/apollographql/apollo/api/http/HttpRequestComposer;Lcom/apollographql/apollo/network/http/HttpEngine;Ljava/util/List;ZLcom/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun dispose ()V public fun execute (Lcom/apollographql/apollo/api/ApolloRequest;)Lkotlinx/coroutines/flow/Flow; public final fun getInterceptors ()Ljava/util/List; @@ -397,21 +365,12 @@ public final class com/apollographql/apollo/network/http/HttpNetworkTransport$En public fun intercept (Lcom/apollographql/apollo/api/http/HttpRequest;Lcom/apollographql/apollo/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public abstract interface class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol { - public abstract fun getAcceptHeader ()Ljava/lang/String; - public abstract fun newIncrementalResultsMerger ()Lcom/apollographql/apollo/internal/incremental/IncrementalResultsMerger; -} - -public final class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$Defer20220824 : com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol { - public static final field INSTANCE Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$Defer20220824; - public fun getAcceptHeader ()Ljava/lang/String; - public fun newIncrementalResultsMerger ()Lcom/apollographql/apollo/internal/incremental/IncrementalResultsMerger; -} - -public final class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$GraphQL17Alpha9 : com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol { - public static final field INSTANCE Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$GraphQL17Alpha9; - public fun getAcceptHeader ()Ljava/lang/String; - public fun newIncrementalResultsMerger ()Lcom/apollographql/apollo/internal/incremental/IncrementalResultsMerger; +public final class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol : java/lang/Enum { + public static final field Defer20220824 Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; + public static final field GraphQL17Alpha9 Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; + public static fun values ()[Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; } public final class com/apollographql/apollo/network/http/LoggingInterceptor : com/apollographql/apollo/network/http/HttpInterceptor { diff --git a/libraries/apollo-runtime/api/apollo-runtime.klib.api b/libraries/apollo-runtime/api/apollo-runtime.klib.api index 84a7ad33872..ec840a21c73 100644 --- a/libraries/apollo-runtime/api/apollo-runtime.klib.api +++ b/libraries/apollo-runtime/api/apollo-runtime.klib.api @@ -33,21 +33,6 @@ abstract interface com.apollographql.apollo.interceptor/ApolloInterceptorChain { abstract fun <#A1: com.apollographql.apollo.api/Operation.Data> proceed(com.apollographql.apollo.api/ApolloRequest<#A1>): kotlinx.coroutines.flow/Flow> // com.apollographql.apollo.interceptor/ApolloInterceptorChain.proceed|proceed(com.apollographql.apollo.api.ApolloRequest<0:0>){0§}[0] } -abstract interface com.apollographql.apollo.internal.incremental/IncrementalResultsMerger { // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger|null[0] - abstract val hasNext // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.hasNext|{}hasNext[0] - abstract fun (): kotlin/Boolean // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.hasNext.|(){}[0] - abstract val incrementalResultIdentifiers // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.incrementalResultIdentifiers|{}incrementalResultIdentifiers[0] - abstract fun (): kotlin.collections/Set // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.incrementalResultIdentifiers.|(){}[0] - abstract val isEmptyResponse // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.isEmptyResponse|{}isEmptyResponse[0] - abstract fun (): kotlin/Boolean // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.isEmptyResponse.|(){}[0] - abstract val merged // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.merged|{}merged[0] - abstract fun (): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.merged.|(){}[0] - - abstract fun merge(kotlin.collections/Map): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.merge|merge(kotlin.collections.Map){}[0] - abstract fun merge(okio/BufferedSource): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.merge|merge(okio.BufferedSource){}[0] - abstract fun reset() // com.apollographql.apollo.internal.incremental/IncrementalResultsMerger.reset|reset(){}[0] -} - abstract interface com.apollographql.apollo.network.http/HttpEngine : okio/Closeable { // com.apollographql.apollo.network.http/HttpEngine|null[0] abstract suspend fun execute(com.apollographql.apollo.api.http/HttpRequest): com.apollographql.apollo.api.http/HttpResponse // com.apollographql.apollo.network.http/HttpEngine.execute|execute(com.apollographql.apollo.api.http.HttpRequest){}[0] open fun close() // com.apollographql.apollo.network.http/HttpEngine.close|close(){}[0] @@ -215,42 +200,6 @@ final class com.apollographql.apollo.interceptor/AutoPersistedQueryInterceptor : } } -final class com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger : com.apollographql.apollo.internal.incremental/IncrementalResultsMerger { // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger|null[0] - constructor () // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.|(){}[0] - - final val incrementalResultIdentifiers // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.incrementalResultIdentifiers|{}incrementalResultIdentifiers[0] - final fun (): kotlin.collections/Set // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.incrementalResultIdentifiers.|(){}[0] - final val merged // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.merged|{}merged[0] - final fun (): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.merged.|(){}[0] - - final var hasNext // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.hasNext|{}hasNext[0] - final fun (): kotlin/Boolean // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.hasNext.|(){}[0] - final var isEmptyResponse // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.isEmptyResponse|{}isEmptyResponse[0] - final fun (): kotlin/Boolean // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.isEmptyResponse.|(){}[0] - - final fun merge(kotlin.collections/Map): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.merge|merge(kotlin.collections.Map){}[0] - final fun merge(okio/BufferedSource): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.merge|merge(okio.BufferedSource){}[0] - final fun reset() // com.apollographql.apollo.internal.incremental/Defer20220824IncrementalResultsMerger.reset|reset(){}[0] -} - -final class com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger : com.apollographql.apollo.internal.incremental/IncrementalResultsMerger { // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger|null[0] - constructor () // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.|(){}[0] - - final val incrementalResultIdentifiers // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.incrementalResultIdentifiers|{}incrementalResultIdentifiers[0] - final fun (): kotlin.collections/Set // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.incrementalResultIdentifiers.|(){}[0] - final val merged // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.merged|{}merged[0] - final fun (): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.merged.|(){}[0] - - final var hasNext // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.hasNext|{}hasNext[0] - final fun (): kotlin/Boolean // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.hasNext.|(){}[0] - final var isEmptyResponse // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.isEmptyResponse|{}isEmptyResponse[0] - final fun (): kotlin/Boolean // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.isEmptyResponse.|(){}[0] - - final fun merge(kotlin.collections/Map): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.merge|merge(kotlin.collections.Map){}[0] - final fun merge(okio/BufferedSource): kotlin.collections/Map // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.merge|merge(okio.BufferedSource){}[0] - final fun reset() // com.apollographql.apollo.internal.incremental/GraphQL17Alpha9IncrementalResultsMerger.reset|reset(){}[0] -} - final class com.apollographql.apollo.internal/MultipartReader : okio/Closeable { // com.apollographql.apollo.internal/MultipartReader|null[0] constructor (okio/BufferedSource, kotlin/String) // com.apollographql.apollo.internal/MultipartReader.|(okio.BufferedSource;kotlin.String){}[0] @@ -345,25 +294,15 @@ final class com.apollographql.apollo.network.http/HttpNetworkTransport : com.apo final fun dispose() // com.apollographql.apollo.network.http/HttpNetworkTransport.dispose|dispose(){}[0] final fun newBuilder(): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.newBuilder|newBuilder(){}[0] - sealed interface IncrementalDeliveryProtocol { // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol|null[0] - abstract val acceptHeader // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.acceptHeader|{}acceptHeader[0] - abstract fun (): kotlin/String // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.acceptHeader.|(){}[0] - - abstract fun newIncrementalResultsMerger(): com.apollographql.apollo.internal.incremental/IncrementalResultsMerger // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.newIncrementalResultsMerger|newIncrementalResultsMerger(){}[0] - - final object Defer20220824 : com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol { // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824|null[0] - final val acceptHeader // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824.acceptHeader|{}acceptHeader[0] - final fun (): kotlin/String // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824.acceptHeader.|(){}[0] - - final fun newIncrementalResultsMerger(): com.apollographql.apollo.internal.incremental/IncrementalResultsMerger // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824.newIncrementalResultsMerger|newIncrementalResultsMerger(){}[0] - } + final enum class IncrementalDeliveryProtocol : kotlin/Enum { // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol|null[0] + enum entry Defer20220824 // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824|null[0] + enum entry GraphQL17Alpha9 // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9|null[0] - final object GraphQL17Alpha9 : com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol { // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9|null[0] - final val acceptHeader // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9.acceptHeader|{}acceptHeader[0] - final fun (): kotlin/String // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9.acceptHeader.|(){}[0] + final val entries // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.entries.|#static(){}[0] - final fun newIncrementalResultsMerger(): com.apollographql.apollo.internal.incremental/IncrementalResultsMerger // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9.newIncrementalResultsMerger|newIncrementalResultsMerger(){}[0] - } + final fun valueOf(kotlin/String): com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.values|values#static(){}[0] } final class Builder { // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder|null[0] diff --git a/libraries/apollo-runtime/api/jvm/apollo-runtime.api b/libraries/apollo-runtime/api/jvm/apollo-runtime.api index eff51c03d5b..e3bb83328b7 100644 --- a/libraries/apollo-runtime/api/jvm/apollo-runtime.api +++ b/libraries/apollo-runtime/api/jvm/apollo-runtime.api @@ -229,38 +229,6 @@ public final class com/apollographql/apollo/internal/MultipartReader$Part : java public final fun getHeaders ()Ljava/util/List; } -public final class com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger : com/apollographql/apollo/internal/incremental/IncrementalResultsMerger { - public fun ()V - public fun getHasNext ()Z - public fun getIncrementalResultIdentifiers ()Ljava/util/Set; - public fun getMerged ()Ljava/util/Map; - public fun isEmptyResponse ()Z - public fun merge (Ljava/util/Map;)Ljava/util/Map; - public fun merge (Lokio/BufferedSource;)Ljava/util/Map; - public fun reset ()V -} - -public final class com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger : com/apollographql/apollo/internal/incremental/IncrementalResultsMerger { - public fun ()V - public fun getHasNext ()Z - public fun getIncrementalResultIdentifiers ()Ljava/util/Set; - public fun getMerged ()Ljava/util/Map; - public fun isEmptyResponse ()Z - public fun merge (Ljava/util/Map;)Ljava/util/Map; - public fun merge (Lokio/BufferedSource;)Ljava/util/Map; - public fun reset ()V -} - -public abstract interface class com/apollographql/apollo/internal/incremental/IncrementalResultsMerger { - public abstract fun getHasNext ()Z - public abstract fun getIncrementalResultIdentifiers ()Ljava/util/Set; - public abstract fun getMerged ()Ljava/util/Map; - public abstract fun isEmptyResponse ()Z - public abstract fun merge (Ljava/util/Map;)Ljava/util/Map; - public abstract fun merge (Lokio/BufferedSource;)Ljava/util/Map; - public abstract fun reset ()V -} - public abstract interface class com/apollographql/apollo/network/NetworkMonitor : java/io/Closeable { public abstract fun isOnline ()Lkotlinx/coroutines/flow/StateFlow; } @@ -367,7 +335,7 @@ public abstract interface class com/apollographql/apollo/network/http/HttpInterc } public final class com/apollographql/apollo/network/http/HttpNetworkTransport : com/apollographql/apollo/network/NetworkTransport { - public synthetic fun (Lcom/apollographql/apollo/api/http/HttpRequestComposer;Lcom/apollographql/apollo/network/http/HttpEngine;Ljava/util/List;ZLcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lcom/apollographql/apollo/api/http/HttpRequestComposer;Lcom/apollographql/apollo/network/http/HttpEngine;Ljava/util/List;ZLcom/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun dispose ()V public fun execute (Lcom/apollographql/apollo/api/ApolloRequest;)Lkotlinx/coroutines/flow/Flow; public final fun getInterceptors ()Ljava/util/List; @@ -393,21 +361,12 @@ public final class com/apollographql/apollo/network/http/HttpNetworkTransport$En public fun intercept (Lcom/apollographql/apollo/api/http/HttpRequest;Lcom/apollographql/apollo/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public abstract interface class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol { - public abstract fun getAcceptHeader ()Ljava/lang/String; - public abstract fun newIncrementalResultsMerger ()Lcom/apollographql/apollo/internal/incremental/IncrementalResultsMerger; -} - -public final class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$Defer20220824 : com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol { - public static final field INSTANCE Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$Defer20220824; - public fun getAcceptHeader ()Ljava/lang/String; - public fun newIncrementalResultsMerger ()Lcom/apollographql/apollo/internal/incremental/IncrementalResultsMerger; -} - -public final class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$GraphQL17Alpha9 : com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol { - public static final field INSTANCE Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol$GraphQL17Alpha9; - public fun getAcceptHeader ()Ljava/lang/String; - public fun newIncrementalResultsMerger ()Lcom/apollographql/apollo/internal/incremental/IncrementalResultsMerger; +public final class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol : java/lang/Enum { + public static final field Defer20220824 Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; + public static final field GraphQL17Alpha9 Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; + public static fun values ()[Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; } public final class com/apollographql/apollo/network/http/LoggingInterceptor : com/apollographql/apollo/network/http/HttpInterceptor { diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger.kt index e9db40042f9..760d5311655 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger.kt @@ -1,6 +1,5 @@ package com.apollographql.apollo.internal.incremental -import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.IncrementalResultIdentifier import com.apollographql.apollo.api.IncrementalResultIdentifiers import okio.BufferedSource @@ -8,9 +7,8 @@ import okio.BufferedSource /** * Merger for the [com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824] protocol format. */ -@ApolloInternal @Suppress("UNCHECKED_CAST") -class Defer20220824IncrementalResultsMerger : IncrementalResultsMerger { +internal class Defer20220824IncrementalResultsMerger : IncrementalResultsMerger { private val _merged: MutableJsonMap = mutableMapOf() override val merged: JsonMap = _merged diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt index 5d4b88b2dca..bb5c33e3e6a 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt @@ -1,6 +1,5 @@ package com.apollographql.apollo.internal.incremental -import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.IncrementalResultIdentifier import com.apollographql.apollo.api.IncrementalResultIdentifiers import com.apollographql.apollo.api.pending @@ -9,9 +8,8 @@ import okio.BufferedSource /** * Merger for the [com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9] protocol format. */ -@ApolloInternal @Suppress("UNCHECKED_CAST") -class GraphQL17Alpha9IncrementalResultsMerger : IncrementalResultsMerger { +internal class GraphQL17Alpha9IncrementalResultsMerger : IncrementalResultsMerger { private val _merged: MutableJsonMap = mutableMapOf() override val merged: JsonMap = _merged diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt new file mode 100644 index 00000000000..97e3ba66a95 --- /dev/null +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt @@ -0,0 +1,42 @@ +package com.apollographql.apollo.internal.incremental + +import com.apollographql.apollo.network.http.HttpNetworkTransport + +internal sealed interface IncrementalDeliveryProtocolImpl { + val acceptHeader: String + + fun newIncrementalResultsMerger(): IncrementalResultsMerger + + /** + * Format specified in this historical commit: + * https://github.com/graphql/graphql-spec/tree/48cf7263a71a683fab03d45d309fd42d8d9a6659/spec + * + * Only `@defer` is supported with this format. + * + * This is the default. + */ + object Defer20220824 : IncrementalDeliveryProtocolImpl { + override val acceptHeader: String = "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" + + override fun newIncrementalResultsMerger(): IncrementalResultsMerger = Defer20220824IncrementalResultsMerger() + } + + /** + * Newer format as implemented by graphql.js version `17.0.0-alpha.9`. + * + * Both `@defer` and `@stream` are supported with this format. + */ + object GraphQL17Alpha9 : IncrementalDeliveryProtocolImpl { + // TODO To be agreed upon with the router and other clients + override val acceptHeader: String = + "multipart/mixed;incrementalDeliverySpec=20230621, application/graphql-response+json, application/json" + + override fun newIncrementalResultsMerger(): IncrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() + } +} + +internal val HttpNetworkTransport.IncrementalDeliveryProtocol.impl: IncrementalDeliveryProtocolImpl + get() = when (this) { + HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824 -> IncrementalDeliveryProtocolImpl.Defer20220824 + HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9 -> IncrementalDeliveryProtocolImpl.GraphQL17Alpha9 + } diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalResultsMerger.kt index c2fa48e989f..fc9ecd23446 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalResultsMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalResultsMerger.kt @@ -1,6 +1,5 @@ package com.apollographql.apollo.internal.incremental -import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.IncrementalResultIdentifiers import okio.BufferedSource @@ -18,8 +17,8 @@ import okio.BufferedSource * `extensions` in incremental results (if present) are merged together in an array and then set to the `extensions` field of the [merged] * Map. */ -@ApolloInternal -interface IncrementalResultsMerger { + +internal sealed interface IncrementalResultsMerger { val merged: JsonMap val incrementalResultIdentifiers: IncrementalResultIdentifiers diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt index 4a9a6002ff3..6a6fa809a52 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt @@ -1,7 +1,6 @@ package com.apollographql.apollo.network.http import com.apollographql.apollo.annotations.ApolloDeprecatedSince -import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.ApolloRequest import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.CustomScalarAdapters @@ -22,9 +21,9 @@ import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloHttpException import com.apollographql.apollo.exception.ApolloNetworkException import com.apollographql.apollo.exception.RouterError -import com.apollographql.apollo.internal.incremental.Defer20220824IncrementalResultsMerger -import com.apollographql.apollo.internal.incremental.GraphQL17Alpha9IncrementalResultsMerger +import com.apollographql.apollo.internal.incremental.IncrementalDeliveryProtocolImpl import com.apollographql.apollo.internal.incremental.IncrementalResultsMerger +import com.apollographql.apollo.internal.incremental.impl import com.apollographql.apollo.internal.isGraphQLResponse import com.apollographql.apollo.internal.isMultipart import com.apollographql.apollo.internal.multipartBodyFlow @@ -47,7 +46,7 @@ private constructor( private val engine: HttpEngine, val interceptors: List, private val exposeErrorBody: Boolean, - private val incrementalDeliveryProtocol: IncrementalDeliveryProtocol, + private val incrementalDeliveryProtocolImpl: IncrementalDeliveryProtocolImpl, ) : NetworkTransport { private val engineInterceptor = EngineInterceptor() @@ -223,7 +222,7 @@ private constructor( } } else { if (incrementalResultsMerger == null) { - incrementalResultsMerger = incrementalDeliveryProtocol.newIncrementalResultsMerger() + incrementalResultsMerger = incrementalDeliveryProtocolImpl.newIncrementalResultsMerger() } val merged = incrementalResultsMerger.merge(part) val deferredFragmentIds = incrementalResultsMerger.incrementalResultIdentifiers @@ -358,6 +357,11 @@ private constructor( this.engine = httpEngine } + /** + * The incremental delivery protocol to use when using `@defer` and/or `@stream`. + * + * Default: [IncrementalDeliveryProtocol.Defer20220824] + */ fun incrementalDeliveryProtocol(incrementalDeliveryProtocol: IncrementalDeliveryProtocol) = apply { this.incrementalDeliveryProtocol = incrementalDeliveryProtocol } @@ -379,7 +383,7 @@ private constructor( ?: serverUrl?.let { DefaultHttpRequestComposer( serverUrl = it, - acceptHeaderQueriesAndMutations = incrementalDeliveryProtocol.acceptHeader, + acceptHeaderQueriesAndMutations = incrementalDeliveryProtocol.impl.acceptHeader, ) } ?: error("No HttpRequestComposer found. Use 'httpRequestComposer' or 'serverUrl'") @@ -393,7 +397,7 @@ private constructor( engine = engine ?: DefaultHttpEngine(), interceptors = interceptors, exposeErrorBody = exposeErrorBody, - incrementalDeliveryProtocol = incrementalDeliveryProtocol, + incrementalDeliveryProtocolImpl = incrementalDeliveryProtocol.impl, ) } } @@ -410,12 +414,7 @@ private constructor( /** * The protocol to use for incremental delivery (`@defer` and `@stream`). */ - sealed interface IncrementalDeliveryProtocol { - @ApolloInternal - val acceptHeader: String - - @ApolloInternal - fun newIncrementalResultsMerger(): IncrementalResultsMerger + enum class IncrementalDeliveryProtocol { /** * Format specified in this historical commit: @@ -425,26 +424,13 @@ private constructor( * * This is the default. */ - object Defer20220824 : IncrementalDeliveryProtocol { - @ApolloInternal - override val acceptHeader: String = "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" - - @ApolloInternal - override fun newIncrementalResultsMerger(): IncrementalResultsMerger = Defer20220824IncrementalResultsMerger() - } + Defer20220824, /** * Newer format as implemented by graphql.js version `17.0.0-alpha.9`. * * Both `@defer` and `@stream` are supported with this format. */ - object GraphQL17Alpha9 : IncrementalDeliveryProtocol { - @ApolloInternal - // TODO To be agreed upon with the router and other clients - override val acceptHeader: String = "multipart/mixed;incrementalDeliverySpec=20230621, application/graphql-response+json, application/json" - - @ApolloInternal - override fun newIncrementalResultsMerger(): IncrementalResultsMerger = GraphQL17Alpha9IncrementalResultsMerger() - } + GraphQL17Alpha9 } } From 48b18e40add42562a53978d0cbe433f4904b784b Mon Sep 17 00:00:00 2001 From: BoD Date: Thu, 25 Sep 2025 15:43:15 +0200 Subject: [PATCH 14/21] Mark IncrementalDeliveryProtocol @ApolloExperimental --- .../apollographql/apollo/network/http/HttpNetworkTransport.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt index 6a6fa809a52..008ec02cc2f 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt @@ -1,6 +1,7 @@ package com.apollographql.apollo.network.http import com.apollographql.apollo.annotations.ApolloDeprecatedSince +import com.apollographql.apollo.annotations.ApolloExperimental import com.apollographql.apollo.api.ApolloRequest import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.CustomScalarAdapters @@ -362,6 +363,7 @@ private constructor( * * Default: [IncrementalDeliveryProtocol.Defer20220824] */ + @ApolloExperimental fun incrementalDeliveryProtocol(incrementalDeliveryProtocol: IncrementalDeliveryProtocol) = apply { this.incrementalDeliveryProtocol = incrementalDeliveryProtocol } @@ -414,6 +416,7 @@ private constructor( /** * The protocol to use for incremental delivery (`@defer` and `@stream`). */ + @ApolloExperimental enum class IncrementalDeliveryProtocol { /** From b323879ea0ccb81e7e88ac936547907ecc659f5e Mon Sep 17 00:00:00 2001 From: BoD Date: Thu, 25 Sep 2025 16:20:19 +0200 Subject: [PATCH 15/21] Rename Defer20220824 protocol to GraphQL17Alpha2 fpr consistency --- .../com/apollographql/apollo/api/BooleanExpression.kt | 2 +- .../apollo-runtime/api/android/apollo-runtime.api | 2 +- libraries/apollo-runtime/api/apollo-runtime.klib.api | 2 +- libraries/apollo-runtime/api/jvm/apollo-runtime.api | 2 +- ...r.kt => GraphQL17Alpha2IncrementalResultsMerger.kt} | 4 ++-- .../incremental/IncrementalDeliveryProtocolImpl.kt | 4 ++-- .../apollo/network/http/HttpNetworkTransport.kt | 8 ++++---- ... => GraphQL17Alpha2IncrementalResultsMergerTest.kt} | 10 +++++----- ...t.kt => DeferGraphQL17Alpha2NormalizedCacheTest.kt} | 2 +- ...efer20220824Test.kt => DeferGraphQL17Alpha2Test.kt} | 2 +- 10 files changed, 19 insertions(+), 19 deletions(-) rename libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/{Defer20220824IncrementalResultsMerger.kt => GraphQL17Alpha2IncrementalResultsMerger.kt} (94%) rename libraries/apollo-runtime/src/commonTest/kotlin/test/defer/{Defer20220824IncrementalResultsMergerTest.kt => GraphQL17Alpha2IncrementalResultsMergerTest.kt} (97%) rename tests/defer/src/commonTest/kotlin/test/{Defer20220824NormalizedCacheTest.kt => DeferGraphQL17Alpha2NormalizedCacheTest.kt} (99%) rename tests/defer/src/commonTest/kotlin/test/{Defer20220824Test.kt => DeferGraphQL17Alpha2Test.kt} (99%) diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt index 8c33d3e88ad..afb109b7335 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt @@ -95,7 +95,7 @@ private fun shouldParseFragment( // Modern protocol: parse fragments that are _not_ pending !deferredFragmentIdentifiers.contains(identifier) } else { - // Legacy 20220824 protocol: parse fragments that have been merged + // Legacy GraphQL17Alpha2 protocol: parse fragments that have been merged deferredFragmentIdentifiers.contains(identifier) } } diff --git a/libraries/apollo-runtime/api/android/apollo-runtime.api b/libraries/apollo-runtime/api/android/apollo-runtime.api index 2aded9b55ea..54526749352 100644 --- a/libraries/apollo-runtime/api/android/apollo-runtime.api +++ b/libraries/apollo-runtime/api/android/apollo-runtime.api @@ -366,7 +366,7 @@ public final class com/apollographql/apollo/network/http/HttpNetworkTransport$En } public final class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol : java/lang/Enum { - public static final field Defer20220824 Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; + public static final field GraphQL17Alpha2 Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; public static final field GraphQL17Alpha9 Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; diff --git a/libraries/apollo-runtime/api/apollo-runtime.klib.api b/libraries/apollo-runtime/api/apollo-runtime.klib.api index ec840a21c73..13cec134861 100644 --- a/libraries/apollo-runtime/api/apollo-runtime.klib.api +++ b/libraries/apollo-runtime/api/apollo-runtime.klib.api @@ -295,7 +295,7 @@ final class com.apollographql.apollo.network.http/HttpNetworkTransport : com.apo final fun newBuilder(): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.newBuilder|newBuilder(){}[0] final enum class IncrementalDeliveryProtocol : kotlin/Enum { // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol|null[0] - enum entry Defer20220824 // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824|null[0] + enum entry GraphQL17Alpha2 // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha2|null[0] enum entry GraphQL17Alpha9 // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9|null[0] final val entries // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.entries|#static{}entries[0] diff --git a/libraries/apollo-runtime/api/jvm/apollo-runtime.api b/libraries/apollo-runtime/api/jvm/apollo-runtime.api index e3bb83328b7..496dc0e660c 100644 --- a/libraries/apollo-runtime/api/jvm/apollo-runtime.api +++ b/libraries/apollo-runtime/api/jvm/apollo-runtime.api @@ -362,7 +362,7 @@ public final class com/apollographql/apollo/network/http/HttpNetworkTransport$En } public final class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol : java/lang/Enum { - public static final field Defer20220824 Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; + public static final field GraphQL17Alpha2 Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; public static final field GraphQL17Alpha9 Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha2IncrementalResultsMerger.kt similarity index 94% rename from libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger.kt rename to libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha2IncrementalResultsMerger.kt index 760d5311655..159823a6206 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/Defer20220824IncrementalResultsMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha2IncrementalResultsMerger.kt @@ -5,10 +5,10 @@ import com.apollographql.apollo.api.IncrementalResultIdentifiers import okio.BufferedSource /** - * Merger for the [com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824] protocol format. + * Merger for the [com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha2] protocol format. */ @Suppress("UNCHECKED_CAST") -internal class Defer20220824IncrementalResultsMerger : IncrementalResultsMerger { +internal class GraphQL17Alpha2IncrementalResultsMerger : IncrementalResultsMerger { private val _merged: MutableJsonMap = mutableMapOf() override val merged: JsonMap = _merged diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt index 97e3ba66a95..e70f297582d 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt @@ -18,7 +18,7 @@ internal sealed interface IncrementalDeliveryProtocolImpl { object Defer20220824 : IncrementalDeliveryProtocolImpl { override val acceptHeader: String = "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" - override fun newIncrementalResultsMerger(): IncrementalResultsMerger = Defer20220824IncrementalResultsMerger() + override fun newIncrementalResultsMerger(): IncrementalResultsMerger = GraphQL17Alpha2IncrementalResultsMerger() } /** @@ -37,6 +37,6 @@ internal sealed interface IncrementalDeliveryProtocolImpl { internal val HttpNetworkTransport.IncrementalDeliveryProtocol.impl: IncrementalDeliveryProtocolImpl get() = when (this) { - HttpNetworkTransport.IncrementalDeliveryProtocol.Defer20220824 -> IncrementalDeliveryProtocolImpl.Defer20220824 + HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha2 -> IncrementalDeliveryProtocolImpl.Defer20220824 HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9 -> IncrementalDeliveryProtocolImpl.GraphQL17Alpha9 } diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt index 008ec02cc2f..7f2b0625e29 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt @@ -314,7 +314,7 @@ private constructor( private var engine: HttpEngine? = null private val interceptors: MutableList = mutableListOf() private var exposeErrorBody: Boolean = false - private var incrementalDeliveryProtocol: IncrementalDeliveryProtocol = IncrementalDeliveryProtocol.Defer20220824 + private var incrementalDeliveryProtocol: IncrementalDeliveryProtocol = IncrementalDeliveryProtocol.GraphQL17Alpha2 private val headers: MutableList = mutableListOf() fun httpRequestComposer(httpRequestComposer: HttpRequestComposer) = apply { @@ -361,7 +361,7 @@ private constructor( /** * The incremental delivery protocol to use when using `@defer` and/or `@stream`. * - * Default: [IncrementalDeliveryProtocol.Defer20220824] + * Default: [IncrementalDeliveryProtocol.GraphQL17Alpha2] */ @ApolloExperimental fun incrementalDeliveryProtocol(incrementalDeliveryProtocol: IncrementalDeliveryProtocol) = apply { @@ -420,14 +420,14 @@ private constructor( enum class IncrementalDeliveryProtocol { /** - * Format specified in this historical commit: + * Newer format as implemented by graphql.js version `17.0.0-alpha.2` and specified in this historical commit: * https://github.com/graphql/graphql-spec/tree/48cf7263a71a683fab03d45d309fd42d8d9a6659/spec * * Only `@defer` is supported with this format. * * This is the default. */ - Defer20220824, + GraphQL17Alpha2, /** * Newer format as implemented by graphql.js version `17.0.0-alpha.9`. diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/Defer20220824IncrementalResultsMergerTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/GraphQL17Alpha2IncrementalResultsMergerTest.kt similarity index 97% rename from libraries/apollo-runtime/src/commonTest/kotlin/test/defer/Defer20220824IncrementalResultsMergerTest.kt rename to libraries/apollo-runtime/src/commonTest/kotlin/test/defer/GraphQL17Alpha2IncrementalResultsMergerTest.kt index e33a5e34f8f..85d5d25b538 100644 --- a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/Defer20220824IncrementalResultsMergerTest.kt +++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/GraphQL17Alpha2IncrementalResultsMergerTest.kt @@ -6,7 +6,7 @@ import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.DeferredFragmentIdentifier import com.apollographql.apollo.api.json.BufferedSourceJsonReader import com.apollographql.apollo.api.json.readAny -import com.apollographql.apollo.internal.incremental.Defer20220824IncrementalResultsMerger +import com.apollographql.apollo.internal.incremental.GraphQL17Alpha2IncrementalResultsMerger import okio.Buffer import kotlin.test.Test import kotlin.test.assertEquals @@ -18,10 +18,10 @@ private fun String.buffer() = Buffer().writeUtf8(this) @Suppress("UNCHECKED_CAST") private fun jsonToMap(json: String): Map = BufferedSourceJsonReader(json.buffer()).readAny() as Map -class Defer20220824IncrementalResultsMergerTest { +class GraphQL17Alpha2IncrementalResultsMergerTest { @Test fun mergeJsonSingleIncrementalItem() { - val incrementalResultsMerger = Defer20220824IncrementalResultsMerger() + val incrementalResultsMerger = GraphQL17Alpha2IncrementalResultsMerger() //language=JSON val payload1 = """ @@ -392,7 +392,7 @@ class Defer20220824IncrementalResultsMergerTest { @Test fun mergeJsonMultipleIncrementalItems() { - val incrementalResultsMerger = Defer20220824IncrementalResultsMerger() + val incrementalResultsMerger = GraphQL17Alpha2IncrementalResultsMerger() //language=JSON val payload1 = """ @@ -668,7 +668,7 @@ class Defer20220824IncrementalResultsMergerTest { @Test fun emptyPayloads() { - val incrementalResultsMerger = Defer20220824IncrementalResultsMerger() + val incrementalResultsMerger = GraphQL17Alpha2IncrementalResultsMerger() //language=JSON val payload1 = """ diff --git a/tests/defer/src/commonTest/kotlin/test/Defer20220824NormalizedCacheTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha2NormalizedCacheTest.kt similarity index 99% rename from tests/defer/src/commonTest/kotlin/test/Defer20220824NormalizedCacheTest.kt rename to tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha2NormalizedCacheTest.kt index be5cb5ac1e4..515337cc662 100644 --- a/tests/defer/src/commonTest/kotlin/test/Defer20220824NormalizedCacheTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha2NormalizedCacheTest.kt @@ -45,7 +45,7 @@ import kotlin.test.assertFailsWith import kotlin.test.assertIs import kotlin.test.assertTrue -class Defer20220824NormalizedCacheTest { +class DeferGraphQL17Alpha2NormalizedCacheTest { private lateinit var mockServer: MockServer private lateinit var apolloClient: ApolloClient private lateinit var store: ApolloStore diff --git a/tests/defer/src/commonTest/kotlin/test/Defer20220824Test.kt b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha2Test.kt similarity index 99% rename from tests/defer/src/commonTest/kotlin/test/Defer20220824Test.kt rename to tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha2Test.kt index 1feaf99fa5f..c4c232b1a0f 100644 --- a/tests/defer/src/commonTest/kotlin/test/Defer20220824Test.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha2Test.kt @@ -25,7 +25,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -class Defer20220824Test { +class DeferGraphQL17Alpha2Test { private lateinit var mockServer: MockServer private lateinit var apolloClient: ApolloClient From 7fb253abd3c227a740a6436ea6dc6769486ea17e Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 26 Sep 2025 10:08:55 +0200 Subject: [PATCH 16/21] Keep symbols public but deprecated --- libraries/apollo-api/api/apollo-api.api | 3 +++ libraries/apollo-api/api/apollo-api.klib.api | 7 ++++++ .../api/http/DefaultHttpRequestComposer.kt | 25 +++++++++++++------ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/libraries/apollo-api/api/apollo-api.api b/libraries/apollo-api/api/apollo-api.api index aa424e446c6..2ffb6303d41 100644 --- a/libraries/apollo-api/api/apollo-api.api +++ b/libraries/apollo-api/api/apollo-api.api @@ -946,6 +946,9 @@ public final class com/apollographql/apollo/api/http/DefaultHttpRequestComposer$ public final fun buildPostBody (Lcom/apollographql/apollo/api/Operation;Lcom/apollographql/apollo/api/CustomScalarAdapters;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/apollographql/apollo/api/http/HttpBody; public final fun buildPostBody (Lcom/apollographql/apollo/api/Operation;Lcom/apollographql/apollo/api/CustomScalarAdapters;ZZLjava/lang/String;)Lcom/apollographql/apollo/api/http/HttpBody; public final fun composePayload (Lcom/apollographql/apollo/api/ApolloRequest;)Ljava/util/Map; + public final fun getHEADER_ACCEPT_NAME ()Ljava/lang/String; + public final fun getHEADER_ACCEPT_VALUE_DEFER ()Ljava/lang/String; + public final fun getHEADER_ACCEPT_VALUE_MULTIPART ()Ljava/lang/String; } public final class com/apollographql/apollo/api/http/DefaultWebSocketPayloadComposer : com/apollographql/apollo/api/http/WebSocketPayloadComposer { diff --git a/libraries/apollo-api/api/apollo-api.klib.api b/libraries/apollo-api/api/apollo-api.klib.api index d56ba7371e4..caadd1f4b7a 100644 --- a/libraries/apollo-api/api/apollo-api.klib.api +++ b/libraries/apollo-api/api/apollo-api.klib.api @@ -498,6 +498,13 @@ final class com.apollographql.apollo.api.http/DefaultHttpRequestComposer : com.a final fun <#A1: com.apollographql.apollo.api/Operation.Data> compose(com.apollographql.apollo.api/ApolloRequest<#A1>): com.apollographql.apollo.api.http/HttpRequest // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.compose|compose(com.apollographql.apollo.api.ApolloRequest<0:0>){0§}[0] final object Companion { // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion|null[0] + final val HEADER_ACCEPT_NAME // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_NAME|{}HEADER_ACCEPT_NAME[0] + final fun (): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_NAME.|(){}[0] + final val HEADER_ACCEPT_VALUE_DEFER // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_DEFER|{}HEADER_ACCEPT_VALUE_DEFER[0] + final fun (): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_DEFER.|(){}[0] + final val HEADER_ACCEPT_VALUE_MULTIPART // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_MULTIPART|{}HEADER_ACCEPT_VALUE_MULTIPART[0] + final fun (): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.HEADER_ACCEPT_VALUE_MULTIPART.|(){}[0] + final fun (kotlin/String).appendQueryParameters(kotlin.collections/Map): kotlin/String // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.appendQueryParameters|appendQueryParameters@kotlin.String(kotlin.collections.Map){}[0] final fun <#A2: com.apollographql.apollo.api/Operation.Data> buildParamsMap(com.apollographql.apollo.api/Operation<#A2>, com.apollographql.apollo.api/CustomScalarAdapters, kotlin/Boolean, kotlin/Boolean): okio/ByteString // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.buildParamsMap|buildParamsMap(com.apollographql.apollo.api.Operation<0:0>;com.apollographql.apollo.api.CustomScalarAdapters;kotlin.Boolean;kotlin.Boolean){0§}[0] final fun <#A2: com.apollographql.apollo.api/Operation.Data> buildParamsMap(com.apollographql.apollo.api/Operation<#A2>, com.apollographql.apollo.api/CustomScalarAdapters, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean): okio/ByteString // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.Companion.buildParamsMap|buildParamsMap(com.apollographql.apollo.api.Operation<0:0>;com.apollographql.apollo.api.CustomScalarAdapters;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean){0§}[0] diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt index dc75dc719f8..95c351b441f 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt @@ -46,11 +46,7 @@ class DefaultHttpRequestComposer( val customScalarAdapters = apolloRequest.executionContext[CustomScalarAdapters] ?: CustomScalarAdapters.Empty val requestHeaders = mutableListOf().apply { - if (apolloRequest.operation is Subscription<*>) { - add(HttpHeader(HEADER_ACCEPT_NAME, HEADER_ACCEPT_VALUE_SUBSCRIPTIONS)) - } else { - add(HttpHeader(HEADER_ACCEPT_NAME, acceptHeaderQueriesAndMutations)) - } + add(HttpHeader("Accept", if (apolloRequest.operation is Subscription<*>) HEADER_ACCEPT_VALUE_SUBSCRIPTIONS else acceptHeaderQueriesAndMutations)) if (apolloRequest.httpHeaders != null) { addAll(apolloRequest.httpHeaders) } @@ -101,9 +97,22 @@ class DefaultHttpRequestComposer( // for details. private const val HEADER_APOLLO_REQUIRE_PREFLIGHT = "Apollo-Require-Preflight" - private const val HEADER_ACCEPT_NAME = "Accept" - private const val HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS = "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" - private const val HEADER_ACCEPT_VALUE_SUBSCRIPTIONS = "multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json" + @Deprecated("This was made public by mistake and will be removed in a future version, please use your own constants instead") + @ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v5_0_0) + val HEADER_ACCEPT_NAME = "Accept" + + @Deprecated("This was made public by mistake and will be removed in a future version, please use your own constants instead") + @ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v5_0_0) + val HEADER_ACCEPT_VALUE_DEFER = "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" + + @Deprecated("This was made public by mistake and will be removed in a future version, please use your own constants instead") + @ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v5_0_0) + val HEADER_ACCEPT_VALUE_MULTIPART = "multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json" + + private const val HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS = + "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" + private const val HEADER_ACCEPT_VALUE_SUBSCRIPTIONS = + "multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json" private fun buildGetUrl( serverUrl: String, From 35cef063f4b13474453647f47fbde39db6ba737d Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 26 Sep 2025 11:38:13 +0200 Subject: [PATCH 17/21] Let HttpNetworkTransport handle the Accept header --- libraries/apollo-api/api/apollo-api.api | 2 -- libraries/apollo-api/api/apollo-api.klib.api | 1 - .../api/http/DefaultHttpRequestComposer.kt | 23 +++++++++---------- .../network/http/HttpNetworkTransport.kt | 18 ++++++++++----- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/libraries/apollo-api/api/apollo-api.api b/libraries/apollo-api/api/apollo-api.api index 2ffb6303d41..b0f05188a8f 100644 --- a/libraries/apollo-api/api/apollo-api.api +++ b/libraries/apollo-api/api/apollo-api.api @@ -934,8 +934,6 @@ public final class com/apollographql/apollo/api/http/ByteStringHttpBody : com/ap public final class com/apollographql/apollo/api/http/DefaultHttpRequestComposer : com/apollographql/apollo/api/http/HttpRequestComposer { public static final field Companion Lcom/apollographql/apollo/api/http/DefaultHttpRequestComposer$Companion; public fun (Ljava/lang/String;)V - public fun (Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun compose (Lcom/apollographql/apollo/api/ApolloRequest;)Lcom/apollographql/apollo/api/http/HttpRequest; } diff --git a/libraries/apollo-api/api/apollo-api.klib.api b/libraries/apollo-api/api/apollo-api.klib.api index caadd1f4b7a..1d90582a580 100644 --- a/libraries/apollo-api/api/apollo-api.klib.api +++ b/libraries/apollo-api/api/apollo-api.klib.api @@ -493,7 +493,6 @@ final class com.apollographql.apollo.api.http/ByteStringHttpBody : com.apollogra final class com.apollographql.apollo.api.http/DefaultHttpRequestComposer : com.apollographql.apollo.api.http/HttpRequestComposer { // com.apollographql.apollo.api.http/DefaultHttpRequestComposer|null[0] constructor (kotlin/String) // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.|(kotlin.String){}[0] - constructor (kotlin/String, kotlin/String = ...) // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.|(kotlin.String;kotlin.String){}[0] final fun <#A1: com.apollographql.apollo.api/Operation.Data> compose(com.apollographql.apollo.api/ApolloRequest<#A1>): com.apollographql.apollo.api.http/HttpRequest // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.compose|compose(com.apollographql.apollo.api.ApolloRequest<0:0>){0§}[0] diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt index 95c351b441f..7e9ab9b27b7 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt @@ -33,23 +33,27 @@ import okio.buffer */ class DefaultHttpRequestComposer( private val serverUrl: String, - private val acceptHeaderQueriesAndMutations: String = HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS, ) : HttpRequestComposer { - constructor(serverUrl: String) : this( - serverUrl = serverUrl, - acceptHeaderQueriesAndMutations = HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS, - ) - override fun compose(apolloRequest: ApolloRequest): HttpRequest { val operation = apolloRequest.operation val customScalarAdapters = apolloRequest.executionContext[CustomScalarAdapters] ?: CustomScalarAdapters.Empty val requestHeaders = mutableListOf().apply { - add(HttpHeader("Accept", if (apolloRequest.operation is Subscription<*>) HEADER_ACCEPT_VALUE_SUBSCRIPTIONS else acceptHeaderQueriesAndMutations)) if (apolloRequest.httpHeaders != null) { addAll(apolloRequest.httpHeaders) } + if (get("accept") == null) { + add( + HttpHeader("accept", + if (apolloRequest.operation is Subscription<*>) { + "multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json" + } else { + "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" + } + ) + ) + } } val sendApqExtensions = apolloRequest.sendApqExtensions ?: false @@ -109,11 +113,6 @@ class DefaultHttpRequestComposer( @ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v5_0_0) val HEADER_ACCEPT_VALUE_MULTIPART = "multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json" - private const val HEADER_ACCEPT_VALUE_QUERIES_AND_MUTATIONS = - "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" - private const val HEADER_ACCEPT_VALUE_SUBSCRIPTIONS = - "multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json" - private fun buildGetUrl( serverUrl: String, operation: Operation, diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt index 7f2b0625e29..2f4e17d4f1c 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt @@ -55,6 +55,17 @@ private constructor( request: ApolloRequest, ): Flow> { val customScalarAdapters = request.executionContext[CustomScalarAdapters]!! + + val request = if (request.httpHeaders.orEmpty().none { it.name.lowercase() == "accept" }) { + val accept = if (request.operation is Subscription<*>) { + "multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json" + } else { + incrementalDeliveryProtocolImpl.acceptHeader + } + request.newBuilder().addHttpHeader("accept", accept).build() + } else { + request + } val httpRequest = httpRequestComposer.compose(request) return execute(request, httpRequest, customScalarAdapters) @@ -382,12 +393,7 @@ private constructor( "It is an error to set both 'httpRequestComposer' and 'serverUrl'" } val composer = httpRequestComposer - ?: serverUrl?.let { - DefaultHttpRequestComposer( - serverUrl = it, - acceptHeaderQueriesAndMutations = incrementalDeliveryProtocol.impl.acceptHeader, - ) - } + ?: serverUrl?.let { DefaultHttpRequestComposer(it) } ?: error("No HttpRequestComposer found. Use 'httpRequestComposer' or 'serverUrl'") if (headers.isNotEmpty()) { From 08c7a051ea4965fd5af52206c085a888285889a3 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 26 Sep 2025 11:40:33 +0200 Subject: [PATCH 18/21] Rename Defer20220824 protocol to GraphQL17Alpha2 for consistency --- .../IncrementalDeliveryProtocolImpl.kt | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt index e70f297582d..4a2aa2ca0f8 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt @@ -7,25 +7,12 @@ internal sealed interface IncrementalDeliveryProtocolImpl { fun newIncrementalResultsMerger(): IncrementalResultsMerger - /** - * Format specified in this historical commit: - * https://github.com/graphql/graphql-spec/tree/48cf7263a71a683fab03d45d309fd42d8d9a6659/spec - * - * Only `@defer` is supported with this format. - * - * This is the default. - */ - object Defer20220824 : IncrementalDeliveryProtocolImpl { + object GraphQL17Alpha2 : IncrementalDeliveryProtocolImpl { override val acceptHeader: String = "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" override fun newIncrementalResultsMerger(): IncrementalResultsMerger = GraphQL17Alpha2IncrementalResultsMerger() } - /** - * Newer format as implemented by graphql.js version `17.0.0-alpha.9`. - * - * Both `@defer` and `@stream` are supported with this format. - */ object GraphQL17Alpha9 : IncrementalDeliveryProtocolImpl { // TODO To be agreed upon with the router and other clients override val acceptHeader: String = @@ -37,6 +24,6 @@ internal sealed interface IncrementalDeliveryProtocolImpl { internal val HttpNetworkTransport.IncrementalDeliveryProtocol.impl: IncrementalDeliveryProtocolImpl get() = when (this) { - HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha2 -> IncrementalDeliveryProtocolImpl.Defer20220824 + HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha2 -> IncrementalDeliveryProtocolImpl.GraphQL17Alpha2 HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9 -> IncrementalDeliveryProtocolImpl.GraphQL17Alpha9 } From a14e3b3d803fafe6651799ec937fff936fc19bee Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 26 Sep 2025 14:29:25 +0200 Subject: [PATCH 19/21] Revert typealiases and use DeferredFragmentIdentifier again --- libraries/apollo-api/api/apollo-api.api | 11 +- libraries/apollo-api/api/apollo-api.klib.api | 8 +- .../apollo/api/BooleanExpression.kt | 16 +- .../apollo/api/CustomScalarAdapters.kt | 8 +- .../apollo/api/DeferredFragmentIdentifier.kt | 19 +++ .../apollographql/apollo/api/Executables.kt | 6 +- .../apollo/api/IncrementalResultIdentifier.kt | 41 ----- .../apollographql/apollo/api/Operations.kt | 8 +- .../apollo/api/internal/ResponseParser.kt | 4 +- ...GraphQL17Alpha2IncrementalResultsMerger.kt | 11 +- ...GraphQL17Alpha9IncrementalResultsMerger.kt | 10 +- .../incremental/IncrementalResultsMerger.kt | 7 +- .../apollo/internal/incremental/JsonMap.kt | 1 - .../network/http/HttpNetworkTransport.kt | 4 +- ...hQL17Alpha2IncrementalResultsMergerTest.kt | 16 +- ...hQL17Alpha9IncrementalResultsMergerTest.kt | 159 +++++++++--------- ...DeferGraphQL17Alpha2NormalizedCacheTest.kt | 108 +++--------- .../kotlin/test/DeferGraphQL17Alpha2Test.kt | 110 +++--------- .../src/jvmTest/kotlin/test/DeferJvmTest.kt | 10 +- 19 files changed, 202 insertions(+), 355 deletions(-) create mode 100644 libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/DeferredFragmentIdentifier.kt delete mode 100644 libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt diff --git a/libraries/apollo-api/api/apollo-api.api b/libraries/apollo-api/api/apollo-api.api index b0f05188a8f..a1b27e79109 100644 --- a/libraries/apollo-api/api/apollo-api.api +++ b/libraries/apollo-api/api/apollo-api.api @@ -530,6 +530,7 @@ public final class com/apollographql/apollo/api/DefaultUploadKt { } public final class com/apollographql/apollo/api/DeferredFragmentIdentifier { + public static final field Companion Lcom/apollographql/apollo/api/DeferredFragmentIdentifier$Companion; public fun (Ljava/util/List;Ljava/lang/String;)V public final fun component1 ()Ljava/util/List; public final fun component2 ()Ljava/lang/String; @@ -542,6 +543,10 @@ public final class com/apollographql/apollo/api/DeferredFragmentIdentifier { public fun toString ()Ljava/lang/String; } +public final class com/apollographql/apollo/api/DeferredFragmentIdentifier$Companion { + public final fun getPending ()Lcom/apollographql/apollo/api/DeferredFragmentIdentifier; +} + public final class com/apollographql/apollo/api/EnumType : com/apollographql/apollo/api/CompiledNamedType { public fun (Ljava/lang/String;Ljava/util/List;)V public final fun getValues ()Ljava/util/List; @@ -713,12 +718,6 @@ public final class com/apollographql/apollo/api/ImmutableMapBuilder { public final fun put (Ljava/lang/Object;Ljava/lang/Object;)Lcom/apollographql/apollo/api/ImmutableMapBuilder; } -public final class com/apollographql/apollo/api/IncrementalResultIdentifierKt { - public static final fun isPending (Ljava/util/Set;)Z - public static final fun nonPending (Ljava/util/Set;)Ljava/util/Set; - public static final fun pending (Ljava/util/Set;)Ljava/util/Set; -} - public final class com/apollographql/apollo/api/InputObjectType : com/apollographql/apollo/api/CompiledNamedType { public fun (Ljava/lang/String;)V } diff --git a/libraries/apollo-api/api/apollo-api.klib.api b/libraries/apollo-api/api/apollo-api.klib.api index 1d90582a580..2bb22714d6d 100644 --- a/libraries/apollo-api/api/apollo-api.klib.api +++ b/libraries/apollo-api/api/apollo-api.klib.api @@ -994,6 +994,11 @@ final class com.apollographql.apollo.api/DeferredFragmentIdentifier { // com.apo final fun equals(kotlin/Any?): kotlin/Boolean // com.apollographql.apollo.api/DeferredFragmentIdentifier.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // com.apollographql.apollo.api/DeferredFragmentIdentifier.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // com.apollographql.apollo.api/DeferredFragmentIdentifier.toString|toString(){}[0] + + final object Companion { // com.apollographql.apollo.api/DeferredFragmentIdentifier.Companion|null[0] + final val Pending // com.apollographql.apollo.api/DeferredFragmentIdentifier.Companion.Pending|{}Pending[0] + final fun (): com.apollographql.apollo.api/DeferredFragmentIdentifier // com.apollographql.apollo.api/DeferredFragmentIdentifier.Companion.Pending.|(){}[0] + } } final class com.apollographql.apollo.api/EnumType : com.apollographql.apollo.api/CompiledNamedType { // com.apollographql.apollo.api/EnumType|null[0] @@ -1429,9 +1434,6 @@ final fun (com.apollographql.apollo.api/Operation.Data).com.apollographql.apollo final fun (kotlin.collections/List).com.apollographql.apollo.api.http/get(kotlin/String): kotlin/String? // com.apollographql.apollo.api.http/get|get@kotlin.collections.List(kotlin.String){}[0] final fun (kotlin.collections/List).com.apollographql.apollo.api.http/valueOf(kotlin/String): kotlin/String? // com.apollographql.apollo.api.http/valueOf|valueOf@kotlin.collections.List(kotlin.String){}[0] final fun (kotlin.collections/Map).com.apollographql.apollo.api.json/jsonReader(): com.apollographql.apollo.api.json/JsonReader // com.apollographql.apollo.api.json/jsonReader|jsonReader@kotlin.collections.Map(){}[0] -final fun (kotlin.collections/Set).com.apollographql.apollo.api/isPending(): kotlin/Boolean // com.apollographql.apollo.api/isPending|isPending@kotlin.collections.Set(){}[0] -final fun (kotlin.collections/Set).com.apollographql.apollo.api/nonPending(): kotlin.collections/Set // com.apollographql.apollo.api/nonPending|nonPending@kotlin.collections.Set(){}[0] -final fun (kotlin.collections/Set).com.apollographql.apollo.api/pending(): kotlin.collections/Set // com.apollographql.apollo.api/pending|pending@kotlin.collections.Set(){}[0] final fun (kotlin/String).com.apollographql.apollo.api.http.internal/urlDecode(): kotlin/String // com.apollographql.apollo.api.http.internal/urlDecode|urlDecode@kotlin.String(){}[0] final fun (kotlin/String).com.apollographql.apollo.api.http.internal/urlEncode(): kotlin/String // com.apollographql.apollo.api.http.internal/urlEncode|urlEncode@kotlin.String(){}[0] final fun (okio/BufferedSource).com.apollographql.apollo.api.json/jsonReader(): com.apollographql.apollo.api.json/JsonReader // com.apollographql.apollo.api.json/jsonReader|jsonReader@okio.BufferedSource(){}[0] diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt index afb109b7335..c9a1cd0a5b7 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/BooleanExpression.kt @@ -67,7 +67,7 @@ internal fun BooleanExpression.evaluate(block: (T) -> Boolean): Boo fun BooleanExpression.evaluate( variables: Set?, typename: String?, - deferredFragmentIdentifiers: IncrementalResultIdentifiers?, + deferredFragmentIdentifiers: Set?, path: List?, ): Boolean { // Remove "data" from the path @@ -75,22 +75,18 @@ fun BooleanExpression.evaluate( return evaluate { when (it) { is BVariable -> !(variables?.contains(it.name) ?: false) - is BLabel -> shouldParseFragment(deferredFragmentIdentifiers = deferredFragmentIdentifiers, path = croppedPath!!, label = it.label) + is BLabel -> shouldParseFragment(deferredFragmentIdentifiers, croppedPath!!, it.label) is BPossibleTypes -> it.possibleTypes.contains(typename) } } } -private fun shouldParseFragment( - deferredFragmentIdentifiers: IncrementalResultIdentifiers?, - path: List, - label: String?, -): Boolean { +private fun shouldParseFragment(deferredFragmentIdentifiers: Set?, path: List, label: String?): Boolean { if (deferredFragmentIdentifiers == null) { // By default, parse all deferred fragments - this is the case when parsing from the normalized cache. return true } - val identifier = IncrementalResultIdentifier(path, label) + val identifier = DeferredFragmentIdentifier(path, label) return if (deferredFragmentIdentifiers.isPending()) { // Modern protocol: parse fragments that are _not_ pending !deferredFragmentIdentifiers.contains(identifier) @@ -100,6 +96,10 @@ private fun shouldParseFragment( } } +private fun Set.isPending(): Boolean { + return any { it === DeferredFragmentIdentifier.Pending } +} + /** * A generic term in a [BooleanExpression] */ diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt index 324d58c95a6..a684756cf4b 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/CustomScalarAdapters.kt @@ -19,10 +19,10 @@ class CustomScalarAdapters private constructor( @JvmField val falseVariables: Set?, /** - * Incremental result identifiers used to determine whether the parser must parse deferred fragments + * Identifiers used to determine whether the parser must parse deferred fragments */ @JvmField - val deferredFragmentIdentifiers: IncrementalResultIdentifiers?, + val deferredFragmentIdentifiers: Set?, /** * Errors to use with @catch */ @@ -131,14 +131,14 @@ class CustomScalarAdapters private constructor( class Builder { private val adaptersMap: MutableMap> = mutableMapOf() private var falseVariables: Set? = null - private var deferredFragmentIdentifiers: IncrementalResultIdentifiers? = null + private var deferredFragmentIdentifiers: Set? = null private var errors: List? = null fun falseVariables(falseVariables: Set?) = apply { this.falseVariables = falseVariables } - fun deferredFragmentIdentifiers(deferredFragmentIdentifiers: IncrementalResultIdentifiers?) = apply { + fun deferredFragmentIdentifiers(deferredFragmentIdentifiers: Set?) = apply { this.deferredFragmentIdentifiers = deferredFragmentIdentifiers } diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/DeferredFragmentIdentifier.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/DeferredFragmentIdentifier.kt new file mode 100644 index 00000000000..a71efb5328c --- /dev/null +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/DeferredFragmentIdentifier.kt @@ -0,0 +1,19 @@ +package com.apollographql.apollo.api + +import com.apollographql.apollo.annotations.ApolloInternal + +data class DeferredFragmentIdentifier( + /** + * Path of the fragment in the overall JSON response. The elements can either be Strings (names) or Integers (array indices). + */ + val path: List, + val label: String?, +) { + companion object { + /** + * Special identifier to signal that the identifiers are pending, as in the modern version of the protocol. + */ + @ApolloInternal + val Pending = DeferredFragmentIdentifier(emptyList(), "__pending") + } +} diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt index 0cd35e4c604..28d0fe1c090 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Executables.kt @@ -71,8 +71,8 @@ fun Executable.parseData( jsonReader: JsonReader, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, falseVariables: Set? = null, - deferredFragmentIds: IncrementalResultIdentifiers? = null, - errors: List? = null, + deferredFragmentIds: Set? = null, + errors: List? = null ): D? { val customScalarAdapters1 = customScalarAdapters.newBuilder() .falseVariables(falseVariables) @@ -89,4 +89,4 @@ fun Executable.composeData( value: D ) { adapter().toJson(jsonWriter, customScalarAdapters, value) -} +} \ No newline at end of file diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt deleted file mode 100644 index b2455047c37..00000000000 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/IncrementalResultIdentifier.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.apollographql.apollo.api - -import com.apollographql.apollo.annotations.ApolloInternal - -data class DeferredFragmentIdentifier( - /** - * Path of the fragment in the overall JSON response. The elements can either be Strings (names) or Integers (array indices). - */ - val path: List, - val label: String?, -) { - internal companion object { - /** - * Special identifier to signal that the identifiers are pending, as in the modern version of the protocol. - */ - internal val Pending = DeferredFragmentIdentifier(emptyList(), "__pending") - } -} - -/** - * Identifies an incremental result. - * [DeferredFragmentIdentifier] is kept to preserve the API/ABI, but this alias is more descriptive of its purpose. - */ -typealias IncrementalResultIdentifier = DeferredFragmentIdentifier - -typealias IncrementalResultIdentifiers = Set - -@ApolloInternal -fun IncrementalResultIdentifiers.isPending(): Boolean { - return any { it === DeferredFragmentIdentifier.Pending } -} - -@ApolloInternal -fun IncrementalResultIdentifiers.pending(): IncrementalResultIdentifiers { - return this + DeferredFragmentIdentifier.Pending -} - -@ApolloInternal -fun IncrementalResultIdentifiers.nonPending(): IncrementalResultIdentifiers { - return filter { it !== DeferredFragmentIdentifier.Pending }.toSet() -} diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt index b9e4414818c..9dc586f8147 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/Operations.kt @@ -70,7 +70,7 @@ fun Operation.composeJsonRequest( fun Operation.parseJsonResponse( jsonReader: JsonReader, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: IncrementalResultIdentifiers? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return jsonReader.use { ResponseParser.parse( @@ -103,7 +103,7 @@ fun Operation.parseResponse( jsonReader: JsonReader, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: IncrementalResultIdentifiers? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return try { ResponseParser.parse( @@ -177,7 +177,7 @@ fun JsonReader.toApolloResponse( operation: Operation, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: IncrementalResultIdentifiers? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return use { try { @@ -213,7 +213,7 @@ fun JsonReader.parseResponse( operation: Operation, requestUuid: Uuid? = null, customScalarAdapters: CustomScalarAdapters = CustomScalarAdapters.Empty, - deferredFragmentIdentifiers: IncrementalResultIdentifiers? = null, + deferredFragmentIdentifiers: Set? = null, ): ApolloResponse { return try { ResponseParser.parse( diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt index 0c92b233578..f60fa9d5bcc 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/internal/ResponseParser.kt @@ -3,8 +3,8 @@ package com.apollographql.apollo.api.internal import com.apollographql.apollo.annotations.ApolloInternal import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.CustomScalarAdapters +import com.apollographql.apollo.api.DeferredFragmentIdentifier import com.apollographql.apollo.api.Error -import com.apollographql.apollo.api.IncrementalResultIdentifiers import com.apollographql.apollo.api.Operation import com.apollographql.apollo.api.falseVariables import com.apollographql.apollo.api.json.JsonReader @@ -24,7 +24,7 @@ internal object ResponseParser { operation: Operation, requestUuid: Uuid?, customScalarAdapters: CustomScalarAdapters, - deferredFragmentIds: IncrementalResultIdentifiers?, + deferredFragmentIds: Set?, ): ApolloResponse { jsonReader.beginObject() diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha2IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha2IncrementalResultsMerger.kt index 159823a6206..70e4a7ab924 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha2IncrementalResultsMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha2IncrementalResultsMerger.kt @@ -1,7 +1,6 @@ package com.apollographql.apollo.internal.incremental -import com.apollographql.apollo.api.IncrementalResultIdentifier -import com.apollographql.apollo.api.IncrementalResultIdentifiers +import com.apollographql.apollo.api.DeferredFragmentIdentifier import okio.BufferedSource /** @@ -12,12 +11,12 @@ internal class GraphQL17Alpha2IncrementalResultsMerger : IncrementalResultsMerge private val _merged: MutableJsonMap = mutableMapOf() override val merged: JsonMap = _merged - private val _incrementalResultIds = mutableSetOf() + private val _deferredFragmentIdentifiers = mutableSetOf() /** * For this protocol, this represents the set of fragment ids that are already merged. */ - override val incrementalResultIdentifiers: IncrementalResultIdentifiers = _incrementalResultIds + override val deferredFragmentIdentifiers: Set = _deferredFragmentIdentifiers override var hasNext: Boolean = true private set @@ -77,13 +76,13 @@ internal class GraphQL17Alpha2IncrementalResultsMerger : IncrementalResultsMerge val nodeToMergeInto = nodeAtPath(mergedData, path) as MutableJsonMap deepMergeObject(nodeToMergeInto, data) - _incrementalResultIds += IncrementalResultIdentifier(path = path, label = incrementalResult["label"] as String?) + _deferredFragmentIdentifiers += DeferredFragmentIdentifier(path = path, label = incrementalResult["label"] as String?) } } override fun reset() { _merged.clear() - _incrementalResultIds.clear() + _deferredFragmentIdentifiers.clear() hasNext = true isEmptyResponse = false } diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt index bb5c33e3e6a..f6ef485146b 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt @@ -1,8 +1,6 @@ package com.apollographql.apollo.internal.incremental -import com.apollographql.apollo.api.IncrementalResultIdentifier -import com.apollographql.apollo.api.IncrementalResultIdentifiers -import com.apollographql.apollo.api.pending +import com.apollographql.apollo.api.DeferredFragmentIdentifier import okio.BufferedSource /** @@ -16,12 +14,12 @@ internal class GraphQL17Alpha9IncrementalResultsMerger : IncrementalResultsMerge /** * Map of identifiers to their corresponding IncrementalResultIdentifier, found in `pending`. */ - private val _pendingResultIds = mutableMapOf() + private val _pendingResultIds = mutableMapOf() /** * For this protocol, this represents the set of ids that are pending. */ - override val incrementalResultIdentifiers: IncrementalResultIdentifiers get() = _pendingResultIds.values.toSet().pending() + override val deferredFragmentIdentifiers: Set get() = _pendingResultIds.values.toSet() + DeferredFragmentIdentifier.Pending override var hasNext: Boolean = true private set @@ -74,7 +72,7 @@ internal class GraphQL17Alpha9IncrementalResultsMerger : IncrementalResultsMerge val id = pendingResult["id"] as String val path = pendingResult["path"] as List val label = pendingResult["label"] as String? - _pendingResultIds[id] = IncrementalResultIdentifier(path = path, label = label) + _pendingResultIds[id] = DeferredFragmentIdentifier(path = path, label = label) } } } diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalResultsMerger.kt index fc9ecd23446..5577849e335 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalResultsMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalResultsMerger.kt @@ -1,12 +1,12 @@ package com.apollographql.apollo.internal.incremental -import com.apollographql.apollo.api.IncrementalResultIdentifiers +import com.apollographql.apollo.api.DeferredFragmentIdentifier import okio.BufferedSource /** * Utility for merging GraphQL incremental results received in multiple chunks when using the `@defer` and/or `@stream` directives. * - * Each call to [merge] will merge the given results into the [merged] Map, and will also update [incrementalResultIdentifiers] with the + * Each call to [merge] will merge the given results into the [merged] Map, and will also update [deferredFragmentIdentifiers] with the * value of their `path` and `label` fields. * * The fields in `data` are merged into the node found in [merged] at the path known by looking at the `id` field. For the first call to @@ -17,11 +17,10 @@ import okio.BufferedSource * `extensions` in incremental results (if present) are merged together in an array and then set to the `extensions` field of the [merged] * Map. */ - internal sealed interface IncrementalResultsMerger { val merged: JsonMap - val incrementalResultIdentifiers: IncrementalResultIdentifiers + val deferredFragmentIdentifiers: Set val hasNext: Boolean diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/JsonMap.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/JsonMap.kt index a7aea249326..3d9d62ac17e 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/JsonMap.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/JsonMap.kt @@ -9,7 +9,6 @@ import okio.BufferedSource internal typealias JsonMap = Map internal typealias MutableJsonMap = MutableMap - /** * Find the node in the [map] at the given [path]. * @param path The path to the node to find, as a list of either `String` (name of field in object) or `Int` (index of element in array). diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt index 2f4e17d4f1c..b36091b25c9 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt @@ -237,7 +237,7 @@ private constructor( incrementalResultsMerger = incrementalDeliveryProtocolImpl.newIncrementalResultsMerger() } val merged = incrementalResultsMerger.merge(part) - val deferredFragmentIds = incrementalResultsMerger.incrementalResultIdentifiers + val deferredFragmentIdentifiers = incrementalResultsMerger.deferredFragmentIdentifiers val isLast = !incrementalResultsMerger.hasNext if (incrementalResultsMerger.isEmptyResponse) { @@ -246,7 +246,7 @@ private constructor( merged.jsonReader().toApolloResponse( operation = operation, customScalarAdapters = customScalarAdapters, - deferredFragmentIdentifiers = deferredFragmentIds + deferredFragmentIdentifiers = deferredFragmentIdentifiers ).newBuilder().isLast(isLast).build() } } diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/GraphQL17Alpha2IncrementalResultsMergerTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/GraphQL17Alpha2IncrementalResultsMergerTest.kt index 85d5d25b538..b5226574b78 100644 --- a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/GraphQL17Alpha2IncrementalResultsMergerTest.kt +++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/GraphQL17Alpha2IncrementalResultsMergerTest.kt @@ -49,7 +49,7 @@ class GraphQL17Alpha2IncrementalResultsMergerTest { assertEquals(jsonToMap(payload1), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.incrementalResultIdentifiers + incrementalResultsMerger.deferredFragmentIdentifiers ) //language=JSON @@ -121,7 +121,7 @@ class GraphQL17Alpha2IncrementalResultsMergerTest { setOf( DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), ), - incrementalResultsMerger.incrementalResultIdentifiers + incrementalResultsMerger.deferredFragmentIdentifiers ) //language=JSON @@ -198,7 +198,7 @@ class GraphQL17Alpha2IncrementalResultsMergerTest { DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), ), - incrementalResultsMerger.incrementalResultIdentifiers + incrementalResultsMerger.deferredFragmentIdentifiers ) //language=JSON @@ -287,7 +287,7 @@ class GraphQL17Alpha2IncrementalResultsMergerTest { DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), ), - incrementalResultsMerger.incrementalResultIdentifiers + incrementalResultsMerger.deferredFragmentIdentifiers ) //language=JSON @@ -386,7 +386,7 @@ class GraphQL17Alpha2IncrementalResultsMergerTest { DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), ), - incrementalResultsMerger.incrementalResultIdentifiers + incrementalResultsMerger.deferredFragmentIdentifiers ) } @@ -420,7 +420,7 @@ class GraphQL17Alpha2IncrementalResultsMergerTest { assertEquals(jsonToMap(payload1), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.incrementalResultIdentifiers + incrementalResultsMerger.deferredFragmentIdentifiers ) //language=JSON @@ -522,7 +522,7 @@ class GraphQL17Alpha2IncrementalResultsMergerTest { DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), ), - incrementalResultsMerger.incrementalResultIdentifiers + incrementalResultsMerger.deferredFragmentIdentifiers ) //language=JSON @@ -662,7 +662,7 @@ class GraphQL17Alpha2IncrementalResultsMergerTest { DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), ), - incrementalResultsMerger.incrementalResultIdentifiers + incrementalResultsMerger.deferredFragmentIdentifiers ) } diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/GraphQL17Alpha9IncrementalResultsMergerTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/GraphQL17Alpha9IncrementalResultsMergerTest.kt index cf07c635dc3..796f3d039e0 100644 --- a/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/GraphQL17Alpha9IncrementalResultsMergerTest.kt +++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/defer/GraphQL17Alpha9IncrementalResultsMergerTest.kt @@ -3,10 +3,9 @@ package test.defer import com.apollographql.apollo.annotations.ApolloInternal -import com.apollographql.apollo.api.IncrementalResultIdentifier +import com.apollographql.apollo.api.DeferredFragmentIdentifier import com.apollographql.apollo.api.json.BufferedSourceJsonReader import com.apollographql.apollo.api.json.readAny -import com.apollographql.apollo.api.nonPending import com.apollographql.apollo.internal.incremental.GraphQL17Alpha9IncrementalResultsMerger import okio.Buffer import kotlin.test.Test @@ -81,9 +80,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("computers", 0), label = "query:Query1:0") + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0") ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -159,9 +158,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("computers", 1), label = "query:Query1:0") + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0") ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -241,9 +240,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -339,10 +338,10 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3_4), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), - IncrementalResultIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -447,9 +446,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) } @@ -522,10 +521,10 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), - IncrementalResultIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 0), label = "query:Query1:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1), label = "query:Query1:0"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -627,10 +626,10 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), - IncrementalResultIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + DeferredFragmentIdentifier(path = listOf("computers", 1, "screen"), label = "fragment:ComputerFields:0"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -755,9 +754,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3_4_5), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), + DeferredFragmentIdentifier(path = listOf("computers", 0, "screen"), label = "fragment:ComputerFields:0"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) } @@ -902,9 +901,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf(), label = null), + DeferredFragmentIdentifier(path = listOf(), label = null), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -962,7 +961,7 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) } @@ -1022,9 +1021,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf(), label = "D1"), + DeferredFragmentIdentifier(path = listOf(), label = "D1"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -1088,9 +1087,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("f2", "c", "f"), label = "D2"), + DeferredFragmentIdentifier(path = listOf("f2", "c", "f"), label = "D2"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -1141,7 +1140,7 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) } @@ -1200,10 +1199,10 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf(), label = "Blue"), - IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -1255,9 +1254,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf(), label = "Blue"), + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -1308,7 +1307,7 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) } @@ -1367,10 +1366,10 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf(), label = "Blue"), - IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), + DeferredFragmentIdentifier(path = listOf(), label = "Blue"), + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -1428,9 +1427,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("a", "b"), label = "Red"), + DeferredFragmentIdentifier(path = listOf("a", "b"), label = "Red"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -1478,7 +1477,7 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) } @@ -1521,10 +1520,10 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf(), label = null), - IncrementalResultIdentifier(path = listOf("me"), label = null), + DeferredFragmentIdentifier(path = listOf(), label = null), + DeferredFragmentIdentifier(path = listOf("me"), label = null), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -1619,9 +1618,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf(), label = null), + DeferredFragmentIdentifier(path = listOf(), label = null), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -1706,7 +1705,7 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) } @@ -1746,9 +1745,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("me"), label = "B"), + DeferredFragmentIdentifier(path = listOf("me"), label = "B"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -1786,7 +1785,7 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) } @@ -1849,10 +1848,10 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("me"), label = "Billing"), - IncrementalResultIdentifier(path = listOf("me"), label = "Prev"), + DeferredFragmentIdentifier(path = listOf("me"), label = "Billing"), + DeferredFragmentIdentifier(path = listOf("me"), label = "Prev"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -1899,9 +1898,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("me"), label = "Prev"), + DeferredFragmentIdentifier(path = listOf("me"), label = "Prev"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -1955,7 +1954,7 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) } @@ -2000,10 +1999,10 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf(), label = "A"), - IncrementalResultIdentifier(path = listOf("me"), label = "B"), + DeferredFragmentIdentifier(path = listOf(), label = "A"), + DeferredFragmentIdentifier(path = listOf("me"), label = "B"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -2059,9 +2058,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("me"), label = "B"), + DeferredFragmentIdentifier(path = listOf("me"), label = "B"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -2125,9 +2124,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("me"), label = "B"), + DeferredFragmentIdentifier(path = listOf("me"), label = "B"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) } @@ -2195,10 +2194,10 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), - IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -2242,10 +2241,10 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), - IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -2285,9 +2284,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("person"), label = "homeWorldDefer"), + DeferredFragmentIdentifier(path = listOf("person"), label = "homeWorldDefer"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -2340,7 +2339,7 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3_4), incrementalResultsMerger.merged) assertEquals( setOf(), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) } @@ -2393,9 +2392,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -2435,9 +2434,9 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) //language=JSON @@ -2503,9 +2502,13 @@ class GraphQL17Alpha9IncrementalResultsMergerTest { assertEquals(jsonToMap(mergedPayloads_1_2_3), incrementalResultsMerger.merged) assertEquals( setOf( - IncrementalResultIdentifier(path = listOf("person", "films"), label = "filmsStream"), + DeferredFragmentIdentifier(path = listOf("person", "films"), label = "filmsStream"), ), - incrementalResultsMerger.incrementalResultIdentifiers.nonPending() + incrementalResultsMerger.deferredFragmentIdentifiers.nonPending() ) } } + +private fun Set.nonPending(): Set { + return filter { it !== DeferredFragmentIdentifier.Pending }.toSet() +} diff --git a/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha2NormalizedCacheTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha2NormalizedCacheTest.kt index 515337cc662..9ca3d8da8b2 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha2NormalizedCacheTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha2NormalizedCacheTest.kt @@ -88,11 +88,7 @@ class DeferGraphQL17Alpha2NormalizedCacheTest { val cacheExpected = WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) - ) - ) - ) - ) + ScreenFields(false))))) ) assertEquals(cacheExpected, cacheActual) } @@ -122,19 +118,12 @@ class DeferGraphQL17Alpha2NormalizedCacheTest { ), WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) - ) - ) + ComputerFields.Screen("Screen", "640x480", null)))) ), WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) - ) - ) - ) - ) + ScreenFields(false))))) ), ) assertEquals(networkExpected, networkActual) @@ -163,19 +152,12 @@ class DeferGraphQL17Alpha2NormalizedCacheTest { ), WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) - ) - ) + ComputerFields.Screen("Screen", "640x480", null)))) ), WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) - ) - ) - ) - ) + ScreenFields(false))))) ), ) assertEquals(networkExpected, networkActual) @@ -210,19 +192,12 @@ class DeferGraphQL17Alpha2NormalizedCacheTest { ), WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) - ) - ) + ComputerFields.Screen("Screen", "640x480", null)))) ), WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) - ) - ) - ) - ) + ScreenFields(false))))) ), ) assertEquals(networkExpected, networkActual) @@ -259,19 +234,12 @@ class DeferGraphQL17Alpha2NormalizedCacheTest { ), WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) - ) - ) + ComputerFields.Screen("Screen", "640x480", null)))) ), WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) - ) - ) - ) - ) + ScreenFields(false))))) ), ) assertEquals(networkExpected, networkActual) @@ -296,19 +264,12 @@ class DeferGraphQL17Alpha2NormalizedCacheTest { ), WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", null) - ) - ) - ) + ComputerFields.Screen("Screen", "800x600", null)))) ), WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", - ScreenFields(true) - ) - ) - ) - ) + ScreenFields(true))))) ), ) @@ -339,20 +300,15 @@ class DeferGraphQL17Alpha2NormalizedCacheTest { uuid, ).data(WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) - ) - ).build(), + )).build(), ApolloResponse.Builder( query, uuid, ).data(WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) - ) - ) - ) - ).build(), + ComputerFields.Screen("Screen", "640x480", null)))) + )).build(), ApolloResponse.Builder( query, @@ -361,10 +317,7 @@ class DeferGraphQL17Alpha2NormalizedCacheTest { .data( WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) - ) - ) + ComputerFields.Screen("Screen", "640x480", null)))) ) ) .errors( @@ -398,20 +351,15 @@ class DeferGraphQL17Alpha2NormalizedCacheTest { uuid, ).data(WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null)) - ) - ).build(), + )).build(), ApolloResponse.Builder( query, uuid, ).data(WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) - ) - ) - ) - ).build(), + ComputerFields.Screen("Screen", "640x480", null)))) + )).build(), ) apolloClient = ApolloClient.Builder() @@ -430,8 +378,7 @@ class DeferGraphQL17Alpha2NormalizedCacheTest { emit(ApolloResponse.Builder(requestUuid = uuid, operation = query) .exception(ApolloNetworkException("Network error")) .isLast(true) - .build() as ApolloResponse - ) + .build() as ApolloResponse) } } @@ -471,19 +418,12 @@ class DeferGraphQL17Alpha2NormalizedCacheTest { ), WithFragmentSpreadsMutation.Data( listOf(WithFragmentSpreadsMutation.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) - ) - ) + ComputerFields.Screen("Screen", "640x480", null)))) ), WithFragmentSpreadsMutation.Data( listOf(WithFragmentSpreadsMutation.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) - ) - ) - ) - ) + ScreenFields(false))))) ), ) assertEquals(networkExpected, networkActual) @@ -495,11 +435,7 @@ class DeferGraphQL17Alpha2NormalizedCacheTest { val cacheExpected = WithFragmentSpreadsQuery.Data( listOf(WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) - ) - ) - ) - ) + ScreenFields(false))))) ) assertEquals(cacheExpected, cacheActual) } diff --git a/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha2Test.kt b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha2Test.kt index c4c232b1a0f..155b785f0fa 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha2Test.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha2Test.kt @@ -60,52 +60,35 @@ class DeferGraphQL17Alpha2Test { WithFragmentSpreadsQuery.Data( listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) - ), + ComputerFields.Screen("Screen", "640x480", null))), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), ) ), WithFragmentSpreadsQuery.Data( listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) - ), + ComputerFields.Screen("Screen", "640x480", null))), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", null) - ) - ), + ComputerFields.Screen("Screen", "800x600", null))), ) ), WithFragmentSpreadsQuery.Data( listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) - ) - ) - ), + ScreenFields(false)))), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", null) - ) - ), + ComputerFields.Screen("Screen", "800x600", null))), ) ), WithFragmentSpreadsQuery.Data( listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) - ) - ) - ), + ScreenFields(false)))), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", - ScreenFields(true) - ) - ) - ), + ScreenFields(true)))), ) ), ) @@ -135,52 +118,35 @@ class DeferGraphQL17Alpha2Test { WithInlineFragmentsQuery.Data( listOf( WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, - WithInlineFragmentsQuery.Screen("Screen", "640x480", null) - ) - ), + WithInlineFragmentsQuery.Screen("Screen", "640x480", null))), WithInlineFragmentsQuery.Computer("Computer", "Computer2", null), ) ), WithInlineFragmentsQuery.Data( listOf( WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, - WithInlineFragmentsQuery.Screen("Screen", "640x480", null) - ) - ), + WithInlineFragmentsQuery.Screen("Screen", "640x480", null))), WithInlineFragmentsQuery.Computer("Computer", "Computer2", WithInlineFragmentsQuery.OnComputer("486", 1996, - WithInlineFragmentsQuery.Screen("Screen", "800x600", null) - ) - ), + WithInlineFragmentsQuery.Screen("Screen", "800x600", null))), ) ), WithInlineFragmentsQuery.Data( listOf( WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, WithInlineFragmentsQuery.Screen("Screen", "640x480", - WithInlineFragmentsQuery.OnScreen(false) - ) - ) - ), + WithInlineFragmentsQuery.OnScreen(false)))), WithInlineFragmentsQuery.Computer("Computer", "Computer2", WithInlineFragmentsQuery.OnComputer("486", 1996, - WithInlineFragmentsQuery.Screen("Screen", "800x600", null) - ) - ), + WithInlineFragmentsQuery.Screen("Screen", "800x600", null))), ) ), WithInlineFragmentsQuery.Data( listOf( WithInlineFragmentsQuery.Computer("Computer", "Computer1", WithInlineFragmentsQuery.OnComputer("386", 1993, WithInlineFragmentsQuery.Screen("Screen", "640x480", - WithInlineFragmentsQuery.OnScreen(false) - ) - ) - ), + WithInlineFragmentsQuery.OnScreen(false)))), WithInlineFragmentsQuery.Computer("Computer", "Computer2", WithInlineFragmentsQuery.OnComputer("486", 1996, WithInlineFragmentsQuery.Screen("Screen", "800x600", - WithInlineFragmentsQuery.OnScreen(true) - ) - ) - ), + WithInlineFragmentsQuery.OnScreen(true)))), ) ), ) @@ -212,8 +178,7 @@ class DeferGraphQL17Alpha2Test { WithFragmentSpreadsQuery.Computer("Computer", "Computer1", null), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), ) - ) - ).build(), + )).build(), ApolloResponse.Builder( query, @@ -222,9 +187,7 @@ class DeferGraphQL17Alpha2Test { WithFragmentSpreadsQuery.Data( listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) - ), + ComputerFields.Screen("Screen", "640x480", null))), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), ) ) @@ -238,9 +201,7 @@ class DeferGraphQL17Alpha2Test { WithFragmentSpreadsQuery.Data( listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) - ), + ComputerFields.Screen("Screen", "640x480", null))), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", null), ) ) @@ -262,13 +223,9 @@ class DeferGraphQL17Alpha2Test { WithFragmentSpreadsQuery.Data( listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) - ), + ComputerFields.Screen("Screen", "640x480", null))), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, - ComputerFields.Screen("Screen", "800x600", null) - ) - ), + ComputerFields.Screen("Screen", "800x600", null))), ) ) ).build(), @@ -280,15 +237,10 @@ class DeferGraphQL17Alpha2Test { WithFragmentSpreadsQuery.Data( listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, - ComputerFields.Screen("Screen", "640x480", null) - ) - ), + ComputerFields.Screen("Screen", "640x480", null))), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", - ScreenFields(true) - ) - ) - ), + ScreenFields(true)))), ) ) ).build(), @@ -389,16 +341,10 @@ class DeferGraphQL17Alpha2Test { listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) - ) - ) - ), + ScreenFields(false)))), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", - ScreenFields(true) - ) - ) - ), + ScreenFields(true)))), ) ), finalResponse.dataOrThrow() @@ -428,16 +374,10 @@ class DeferGraphQL17Alpha2Test { listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) - ) - ) - ), + ScreenFields(false)))), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", - ScreenFields(true) - ) - ) - ), + ScreenFields(true)))), ) ), finalResponse.dataOrThrow() diff --git a/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt b/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt index 79a4e52eb87..2eab2f9d815 100644 --- a/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt +++ b/tests/defer/src/jvmTest/kotlin/test/DeferJvmTest.kt @@ -83,16 +83,10 @@ class DeferJvmTest { listOf( WithFragmentSpreadsQuery.Computer("Computer", "Computer1", ComputerFields("386", 1993, ComputerFields.Screen("Screen", "640x480", - ScreenFields(false) - ) - ) - ), + ScreenFields(false)))), WithFragmentSpreadsQuery.Computer("Computer", "Computer2", ComputerFields("486", 1996, ComputerFields.Screen("Screen", "800x600", - ScreenFields(true) - ) - ) - ), + ScreenFields(true)))), ) ) From eec7001fa566997bf901c8df84d7503f84d89f94 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 26 Sep 2025 15:30:07 +0200 Subject: [PATCH 20/21] Revert handling incremental results in WebSocket --- .../api/android/apollo-runtime.api | 22 ++++---- .../api/apollo-runtime.klib.api | 26 +++++----- .../apollo-runtime/api/jvm/apollo-runtime.api | 22 ++++---- ...GraphQL17Alpha2IncrementalResultsMerger.kt | 2 +- ...GraphQL17Alpha9IncrementalResultsMerger.kt | 2 +- .../IncrementalDeliveryProtocolImpl.kt | 8 +-- .../network/IncrementalDeliveryProtocol.kt | 27 ++++++++++ .../network/http/HttpNetworkTransport.kt | 25 +-------- .../websocket/WebSocketNetworkTransport.kt | 51 ++++++++++++++++--- .../network/ws/WebSocketNetworkTransport.kt | 41 ++++++++++++++- ...DeferGraphQL17Alpha9NormalizedCacheTest.kt | 2 +- .../kotlin/test/DeferGraphQL17Alpha9Test.kt | 2 +- .../kotlin/test/DeferWithApolloServerTest.kt | 2 +- 13 files changed, 159 insertions(+), 73 deletions(-) create mode 100644 libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/IncrementalDeliveryProtocol.kt diff --git a/libraries/apollo-runtime/api/android/apollo-runtime.api b/libraries/apollo-runtime/api/android/apollo-runtime.api index 54526749352..9c08f721c49 100644 --- a/libraries/apollo-runtime/api/android/apollo-runtime.api +++ b/libraries/apollo-runtime/api/android/apollo-runtime.api @@ -229,6 +229,14 @@ public final class com/apollographql/apollo/internal/MultipartReader$Part : java public final fun getHeaders ()Ljava/util/List; } +public final class com/apollographql/apollo/network/IncrementalDeliveryProtocol : java/lang/Enum { + public static final field GraphQL17Alpha2 Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol; + public static final field GraphQL17Alpha9 Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol; + public static fun values ()[Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol; +} + public abstract interface class com/apollographql/apollo/network/NetworkMonitor : java/io/Closeable { public abstract fun isOnline ()Lkotlinx/coroutines/flow/StateFlow; } @@ -355,7 +363,7 @@ public final class com/apollographql/apollo/network/http/HttpNetworkTransport$Bu public final fun httpEngine (Lcom/apollographql/apollo/network/http/HttpEngine;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun httpHeaders (Ljava/util/List;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun httpRequestComposer (Lcom/apollographql/apollo/api/http/HttpRequestComposer;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; - public final fun incrementalDeliveryProtocol (Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; + public final fun incrementalDeliveryProtocol (Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun interceptors (Ljava/util/List;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun serverUrl (Ljava/lang/String;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; } @@ -365,14 +373,6 @@ public final class com/apollographql/apollo/network/http/HttpNetworkTransport$En public fun intercept (Lcom/apollographql/apollo/api/http/HttpRequest;Lcom/apollographql/apollo/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol : java/lang/Enum { - public static final field GraphQL17Alpha2 Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; - public static final field GraphQL17Alpha9 Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; - public static fun values ()[Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; -} - public final class com/apollographql/apollo/network/http/LoggingInterceptor : com/apollographql/apollo/network/http/HttpInterceptor { public fun ()V public fun (Lcom/apollographql/apollo/network/http/LoggingInterceptor$Level;Lkotlin/jvm/functions/Function1;)V @@ -544,6 +544,7 @@ public final class com/apollographql/apollo/network/websocket/WebSocketNetworkTr public final fun build ()Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport; public final fun connectionAcknowledgeTimeout-BwNAW2A (Lkotlin/time/Duration;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder; public final fun idleTimeout-BwNAW2A (Lkotlin/time/Duration;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder; + public final fun incrementalDeliveryProtocol (Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder; public final fun parserFactory (Lcom/apollographql/apollo/network/websocket/SubscriptionParserFactory;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder; public final fun pingInterval-BwNAW2A (Lkotlin/time/Duration;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder; public final fun serverUrl (Ljava/lang/String;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder; @@ -672,7 +673,7 @@ public final class com/apollographql/apollo/network/ws/WebSocketEngineKt { } public final class com/apollographql/apollo/network/ws/WebSocketNetworkTransport : com/apollographql/apollo/network/NetworkTransport { - public synthetic fun (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lcom/apollographql/apollo/network/ws/WebSocketEngine;JLcom/apollographql/apollo/network/ws/WsProtocol$Factory;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lcom/apollographql/apollo/network/ws/WebSocketEngine;JLcom/apollographql/apollo/network/ws/WsProtocol$Factory;Lkotlin/jvm/functions/Function3;Lcom/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun closeConnection (Ljava/lang/Throwable;)V public fun dispose ()V public fun execute (Lcom/apollographql/apollo/api/ApolloRequest;)Lkotlinx/coroutines/flow/Flow; @@ -686,6 +687,7 @@ public final class com/apollographql/apollo/network/ws/WebSocketNetworkTransport public final fun build ()Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport; public final fun headers (Ljava/util/List;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder; public final fun idleTimeoutMillis (J)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder; + public final fun incrementalDeliveryProtocol (Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder; public final fun protocol (Lcom/apollographql/apollo/network/ws/WsProtocol$Factory;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder; public final fun reopenWhen (Lkotlin/jvm/functions/Function3;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder; public final fun serverUrl (Ljava/lang/String;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder; diff --git a/libraries/apollo-runtime/api/apollo-runtime.klib.api b/libraries/apollo-runtime/api/apollo-runtime.klib.api index 13cec134861..0c58f9a6c43 100644 --- a/libraries/apollo-runtime/api/apollo-runtime.klib.api +++ b/libraries/apollo-runtime/api/apollo-runtime.klib.api @@ -21,6 +21,17 @@ final enum class com.apollographql.apollo.network.ws/WsFrameType : kotlin/Enum // com.apollographql.apollo.network.ws/WsFrameType.values|values#static(){}[0] } +final enum class com.apollographql.apollo.network/IncrementalDeliveryProtocol : kotlin/Enum { // com.apollographql.apollo.network/IncrementalDeliveryProtocol|null[0] + enum entry GraphQL17Alpha2 // com.apollographql.apollo.network/IncrementalDeliveryProtocol.GraphQL17Alpha2|null[0] + enum entry GraphQL17Alpha9 // com.apollographql.apollo.network/IncrementalDeliveryProtocol.GraphQL17Alpha9|null[0] + + final val entries // com.apollographql.apollo.network/IncrementalDeliveryProtocol.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // com.apollographql.apollo.network/IncrementalDeliveryProtocol.entries.|#static(){}[0] + + final fun valueOf(kotlin/String): com.apollographql.apollo.network/IncrementalDeliveryProtocol // com.apollographql.apollo.network/IncrementalDeliveryProtocol.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // com.apollographql.apollo.network/IncrementalDeliveryProtocol.values|values#static(){}[0] +} + abstract interface <#A: com.apollographql.apollo.api/Operation.Data> com.apollographql.apollo.network.websocket/SubscriptionParser { // com.apollographql.apollo.network.websocket/SubscriptionParser|null[0] abstract fun parse(kotlin/Any?): com.apollographql.apollo.api/ApolloResponse<#A>? // com.apollographql.apollo.network.websocket/SubscriptionParser.parse|parse(kotlin.Any?){}[0] } @@ -294,17 +305,6 @@ final class com.apollographql.apollo.network.http/HttpNetworkTransport : com.apo final fun dispose() // com.apollographql.apollo.network.http/HttpNetworkTransport.dispose|dispose(){}[0] final fun newBuilder(): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.newBuilder|newBuilder(){}[0] - final enum class IncrementalDeliveryProtocol : kotlin/Enum { // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol|null[0] - enum entry GraphQL17Alpha2 // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha2|null[0] - enum entry GraphQL17Alpha9 // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9|null[0] - - final val entries // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.entries|#static{}entries[0] - final fun (): kotlin.enums/EnumEntries // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.entries.|#static(){}[0] - - final fun valueOf(kotlin/String): com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.valueOf|valueOf#static(kotlin.String){}[0] - final fun values(): kotlin/Array // com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol.values|values#static(){}[0] - } - final class Builder { // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder|null[0] constructor () // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.|(){}[0] @@ -315,7 +315,7 @@ final class com.apollographql.apollo.network.http/HttpNetworkTransport : com.apo final fun httpEngine(com.apollographql.apollo.network.http/HttpEngine): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.httpEngine|httpEngine(com.apollographql.apollo.network.http.HttpEngine){}[0] final fun httpHeaders(kotlin.collections/List): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.httpHeaders|httpHeaders(kotlin.collections.List){}[0] final fun httpRequestComposer(com.apollographql.apollo.api.http/HttpRequestComposer): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.httpRequestComposer|httpRequestComposer(com.apollographql.apollo.api.http.HttpRequestComposer){}[0] - final fun incrementalDeliveryProtocol(com.apollographql.apollo.network.http/HttpNetworkTransport.IncrementalDeliveryProtocol): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.incrementalDeliveryProtocol|incrementalDeliveryProtocol(com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol){}[0] + final fun incrementalDeliveryProtocol(com.apollographql.apollo.network/IncrementalDeliveryProtocol): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.incrementalDeliveryProtocol|incrementalDeliveryProtocol(com.apollographql.apollo.network.IncrementalDeliveryProtocol){}[0] final fun interceptors(kotlin.collections/List): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.interceptors|interceptors(kotlin.collections.List){}[0] final fun serverUrl(kotlin/String): com.apollographql.apollo.network.http/HttpNetworkTransport.Builder // com.apollographql.apollo.network.http/HttpNetworkTransport.Builder.serverUrl|serverUrl(kotlin.String){}[0] } @@ -463,6 +463,7 @@ final class com.apollographql.apollo.network.websocket/WebSocketNetworkTransport final fun build(): com.apollographql.apollo.network.websocket/WebSocketNetworkTransport // com.apollographql.apollo.network.websocket/WebSocketNetworkTransport.Builder.build|build(){}[0] final fun connectionAcknowledgeTimeout(kotlin.time/Duration?): com.apollographql.apollo.network.websocket/WebSocketNetworkTransport.Builder // com.apollographql.apollo.network.websocket/WebSocketNetworkTransport.Builder.connectionAcknowledgeTimeout|connectionAcknowledgeTimeout(kotlin.time.Duration?){}[0] final fun idleTimeout(kotlin.time/Duration?): com.apollographql.apollo.network.websocket/WebSocketNetworkTransport.Builder // com.apollographql.apollo.network.websocket/WebSocketNetworkTransport.Builder.idleTimeout|idleTimeout(kotlin.time.Duration?){}[0] + final fun incrementalDeliveryProtocol(com.apollographql.apollo.network/IncrementalDeliveryProtocol): com.apollographql.apollo.network.websocket/WebSocketNetworkTransport.Builder // com.apollographql.apollo.network.websocket/WebSocketNetworkTransport.Builder.incrementalDeliveryProtocol|incrementalDeliveryProtocol(com.apollographql.apollo.network.IncrementalDeliveryProtocol){}[0] final fun parserFactory(com.apollographql.apollo.network.websocket/SubscriptionParserFactory?): com.apollographql.apollo.network.websocket/WebSocketNetworkTransport.Builder // com.apollographql.apollo.network.websocket/WebSocketNetworkTransport.Builder.parserFactory|parserFactory(com.apollographql.apollo.network.websocket.SubscriptionParserFactory?){}[0] final fun pingInterval(kotlin.time/Duration?): com.apollographql.apollo.network.websocket/WebSocketNetworkTransport.Builder // com.apollographql.apollo.network.websocket/WebSocketNetworkTransport.Builder.pingInterval|pingInterval(kotlin.time.Duration?){}[0] final fun serverUrl(kotlin/String?): com.apollographql.apollo.network.websocket/WebSocketNetworkTransport.Builder // com.apollographql.apollo.network.websocket/WebSocketNetworkTransport.Builder.serverUrl|serverUrl(kotlin.String?){}[0] @@ -565,6 +566,7 @@ final class com.apollographql.apollo.network.ws/WebSocketNetworkTransport : com. final fun build(): com.apollographql.apollo.network.ws/WebSocketNetworkTransport // com.apollographql.apollo.network.ws/WebSocketNetworkTransport.Builder.build|build(){}[0] final fun headers(kotlin.collections/List): com.apollographql.apollo.network.ws/WebSocketNetworkTransport.Builder // com.apollographql.apollo.network.ws/WebSocketNetworkTransport.Builder.headers|headers(kotlin.collections.List){}[0] final fun idleTimeoutMillis(kotlin/Long): com.apollographql.apollo.network.ws/WebSocketNetworkTransport.Builder // com.apollographql.apollo.network.ws/WebSocketNetworkTransport.Builder.idleTimeoutMillis|idleTimeoutMillis(kotlin.Long){}[0] + final fun incrementalDeliveryProtocol(com.apollographql.apollo.network/IncrementalDeliveryProtocol): com.apollographql.apollo.network.ws/WebSocketNetworkTransport.Builder // com.apollographql.apollo.network.ws/WebSocketNetworkTransport.Builder.incrementalDeliveryProtocol|incrementalDeliveryProtocol(com.apollographql.apollo.network.IncrementalDeliveryProtocol){}[0] final fun protocol(com.apollographql.apollo.network.ws/WsProtocol.Factory): com.apollographql.apollo.network.ws/WebSocketNetworkTransport.Builder // com.apollographql.apollo.network.ws/WebSocketNetworkTransport.Builder.protocol|protocol(com.apollographql.apollo.network.ws.WsProtocol.Factory){}[0] final fun reopenWhen(kotlin.coroutines/SuspendFunction2?): com.apollographql.apollo.network.ws/WebSocketNetworkTransport.Builder // com.apollographql.apollo.network.ws/WebSocketNetworkTransport.Builder.reopenWhen|reopenWhen(kotlin.coroutines.SuspendFunction2?){}[0] final fun serverUrl(kotlin.coroutines/SuspendFunction0?): com.apollographql.apollo.network.ws/WebSocketNetworkTransport.Builder // com.apollographql.apollo.network.ws/WebSocketNetworkTransport.Builder.serverUrl|serverUrl(kotlin.coroutines.SuspendFunction0?){}[0] diff --git a/libraries/apollo-runtime/api/jvm/apollo-runtime.api b/libraries/apollo-runtime/api/jvm/apollo-runtime.api index 496dc0e660c..bab09e9aed9 100644 --- a/libraries/apollo-runtime/api/jvm/apollo-runtime.api +++ b/libraries/apollo-runtime/api/jvm/apollo-runtime.api @@ -229,6 +229,14 @@ public final class com/apollographql/apollo/internal/MultipartReader$Part : java public final fun getHeaders ()Ljava/util/List; } +public final class com/apollographql/apollo/network/IncrementalDeliveryProtocol : java/lang/Enum { + public static final field GraphQL17Alpha2 Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol; + public static final field GraphQL17Alpha9 Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol; + public static fun values ()[Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol; +} + public abstract interface class com/apollographql/apollo/network/NetworkMonitor : java/io/Closeable { public abstract fun isOnline ()Lkotlinx/coroutines/flow/StateFlow; } @@ -351,7 +359,7 @@ public final class com/apollographql/apollo/network/http/HttpNetworkTransport$Bu public final fun httpEngine (Lcom/apollographql/apollo/network/http/HttpEngine;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun httpHeaders (Ljava/util/List;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun httpRequestComposer (Lcom/apollographql/apollo/api/http/HttpRequestComposer;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; - public final fun incrementalDeliveryProtocol (Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; + public final fun incrementalDeliveryProtocol (Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun interceptors (Ljava/util/List;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; public final fun serverUrl (Ljava/lang/String;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$Builder; } @@ -361,14 +369,6 @@ public final class com/apollographql/apollo/network/http/HttpNetworkTransport$En public fun intercept (Lcom/apollographql/apollo/api/http/HttpRequest;Lcom/apollographql/apollo/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class com/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol : java/lang/Enum { - public static final field GraphQL17Alpha2 Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; - public static final field GraphQL17Alpha9 Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; - public static fun values ()[Lcom/apollographql/apollo/network/http/HttpNetworkTransport$IncrementalDeliveryProtocol; -} - public final class com/apollographql/apollo/network/http/LoggingInterceptor : com/apollographql/apollo/network/http/HttpInterceptor { public fun ()V public fun (Lcom/apollographql/apollo/network/http/LoggingInterceptor$Level;Lkotlin/jvm/functions/Function1;)V @@ -540,6 +540,7 @@ public final class com/apollographql/apollo/network/websocket/WebSocketNetworkTr public final fun build ()Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport; public final fun connectionAcknowledgeTimeout-BwNAW2A (Lkotlin/time/Duration;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder; public final fun idleTimeout-BwNAW2A (Lkotlin/time/Duration;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder; + public final fun incrementalDeliveryProtocol (Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder; public final fun parserFactory (Lcom/apollographql/apollo/network/websocket/SubscriptionParserFactory;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder; public final fun pingInterval-BwNAW2A (Lkotlin/time/Duration;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder; public final fun serverUrl (Ljava/lang/String;)Lcom/apollographql/apollo/network/websocket/WebSocketNetworkTransport$Builder; @@ -668,7 +669,7 @@ public final class com/apollographql/apollo/network/ws/WebSocketEngineKt { } public final class com/apollographql/apollo/network/ws/WebSocketNetworkTransport : com/apollographql/apollo/network/NetworkTransport { - public synthetic fun (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lcom/apollographql/apollo/network/ws/WebSocketEngine;JLcom/apollographql/apollo/network/ws/WsProtocol$Factory;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lcom/apollographql/apollo/network/ws/WebSocketEngine;JLcom/apollographql/apollo/network/ws/WsProtocol$Factory;Lkotlin/jvm/functions/Function3;Lcom/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun closeConnection (Ljava/lang/Throwable;)V public fun dispose ()V public fun execute (Lcom/apollographql/apollo/api/ApolloRequest;)Lkotlinx/coroutines/flow/Flow; @@ -682,6 +683,7 @@ public final class com/apollographql/apollo/network/ws/WebSocketNetworkTransport public final fun build ()Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport; public final fun headers (Ljava/util/List;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder; public final fun idleTimeoutMillis (J)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder; + public final fun incrementalDeliveryProtocol (Lcom/apollographql/apollo/network/IncrementalDeliveryProtocol;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder; public final fun protocol (Lcom/apollographql/apollo/network/ws/WsProtocol$Factory;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder; public final fun reopenWhen (Lkotlin/jvm/functions/Function3;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder; public final fun serverUrl (Ljava/lang/String;)Lcom/apollographql/apollo/network/ws/WebSocketNetworkTransport$Builder; diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha2IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha2IncrementalResultsMerger.kt index 70e4a7ab924..b741a6df394 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha2IncrementalResultsMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha2IncrementalResultsMerger.kt @@ -4,7 +4,7 @@ import com.apollographql.apollo.api.DeferredFragmentIdentifier import okio.BufferedSource /** - * Merger for the [com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha2] protocol format. + * Merger for the [com.apollographql.apollo.network.IncrementalDeliveryProtocol.GraphQL17Alpha2] protocol format. */ @Suppress("UNCHECKED_CAST") internal class GraphQL17Alpha2IncrementalResultsMerger : IncrementalResultsMerger { diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt index f6ef485146b..07952b1d227 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/GraphQL17Alpha9IncrementalResultsMerger.kt @@ -4,7 +4,7 @@ import com.apollographql.apollo.api.DeferredFragmentIdentifier import okio.BufferedSource /** - * Merger for the [com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9] protocol format. + * Merger for the [com.apollographql.apollo.network.IncrementalDeliveryProtocol.GraphQL17Alpha9] protocol format. */ @Suppress("UNCHECKED_CAST") internal class GraphQL17Alpha9IncrementalResultsMerger : IncrementalResultsMerger { diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt index 4a2aa2ca0f8..3bc8da45bfa 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/incremental/IncrementalDeliveryProtocolImpl.kt @@ -1,6 +1,6 @@ package com.apollographql.apollo.internal.incremental -import com.apollographql.apollo.network.http.HttpNetworkTransport +import com.apollographql.apollo.network.IncrementalDeliveryProtocol internal sealed interface IncrementalDeliveryProtocolImpl { val acceptHeader: String @@ -22,8 +22,8 @@ internal sealed interface IncrementalDeliveryProtocolImpl { } } -internal val HttpNetworkTransport.IncrementalDeliveryProtocol.impl: IncrementalDeliveryProtocolImpl +internal val IncrementalDeliveryProtocol.impl: IncrementalDeliveryProtocolImpl get() = when (this) { - HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha2 -> IncrementalDeliveryProtocolImpl.GraphQL17Alpha2 - HttpNetworkTransport.IncrementalDeliveryProtocol.GraphQL17Alpha9 -> IncrementalDeliveryProtocolImpl.GraphQL17Alpha9 + IncrementalDeliveryProtocol.GraphQL17Alpha2 -> IncrementalDeliveryProtocolImpl.GraphQL17Alpha2 + IncrementalDeliveryProtocol.GraphQL17Alpha9 -> IncrementalDeliveryProtocolImpl.GraphQL17Alpha9 } diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/IncrementalDeliveryProtocol.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/IncrementalDeliveryProtocol.kt new file mode 100644 index 00000000000..f01d205a1b6 --- /dev/null +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/IncrementalDeliveryProtocol.kt @@ -0,0 +1,27 @@ +package com.apollographql.apollo.network + +import com.apollographql.apollo.annotations.ApolloExperimental + +/** + * The protocol to use for incremental delivery (`@defer` and `@stream`). + */ +@ApolloExperimental +enum class IncrementalDeliveryProtocol { + + /** + * Newer format as implemented by graphql.js version `17.0.0-alpha.2` and specified in this historical commit: + * https://github.com/graphql/graphql-spec/tree/48cf7263a71a683fab03d45d309fd42d8d9a6659/spec + * + * Only `@defer` is supported with this format. + * + * This is the default. + */ + GraphQL17Alpha2, + + /** + * Newer format as implemented by graphql.js version `17.0.0-alpha.9`. + * + * Both `@defer` and `@stream` are supported with this format. + */ + GraphQL17Alpha9 +} diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt index b36091b25c9..f15c866dea6 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt @@ -29,6 +29,7 @@ import com.apollographql.apollo.internal.isGraphQLResponse import com.apollographql.apollo.internal.isMultipart import com.apollographql.apollo.internal.multipartBodyFlow import com.apollographql.apollo.mpp.currentTimeMillis +import com.apollographql.apollo.network.IncrementalDeliveryProtocol import com.apollographql.apollo.network.NetworkTransport import com.benasher44.uuid.Uuid import com.benasher44.uuid.uuid4 @@ -418,28 +419,4 @@ private constructor( return chain.proceed(request.newBuilder().addHeaders(headers).build()) } } - - /** - * The protocol to use for incremental delivery (`@defer` and `@stream`). - */ - @ApolloExperimental - enum class IncrementalDeliveryProtocol { - - /** - * Newer format as implemented by graphql.js version `17.0.0-alpha.2` and specified in this historical commit: - * https://github.com/graphql/graphql-spec/tree/48cf7263a71a683fab03d45d309fd42d8d9a6659/spec - * - * Only `@defer` is supported with this format. - * - * This is the default. - */ - GraphQL17Alpha2, - - /** - * Newer format as implemented by graphql.js version `17.0.0-alpha.9`. - * - * Both `@defer` and `@stream` are supported with this format. - */ - GraphQL17Alpha9 - } } diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt index 631d40e6dfc..907fa2bc949 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/websocket/WebSocketNetworkTransport.kt @@ -12,6 +12,9 @@ import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloWebSocketForceCloseException import com.apollographql.apollo.exception.DefaultApolloException import com.apollographql.apollo.exception.SubscriptionOperationException +import com.apollographql.apollo.internal.incremental.IncrementalDeliveryProtocolImpl +import com.apollographql.apollo.internal.incremental.impl +import com.apollographql.apollo.network.IncrementalDeliveryProtocol import com.apollographql.apollo.network.NetworkTransport import com.apollographql.apollo.network.websocket.internal.OperationListener import com.apollographql.apollo.network.websocket.internal.WebSocketPool @@ -113,6 +116,7 @@ class WebSocketNetworkTransport private constructor( private var pingInterval: Duration? = null private var idleTimeout: Duration? = null private var parserFactory: SubscriptionParserFactory? = null + private var incrementalDeliveryProtocol: IncrementalDeliveryProtocol = IncrementalDeliveryProtocol.GraphQL17Alpha2 /** * @param serverUrl a server url that is called every time a WebSocket @@ -176,6 +180,15 @@ class WebSocketNetworkTransport private constructor( this.parserFactory = parserFactory } + /** + * The incremental delivery protocol to use when using `@defer` and/or `@stream`. + * + * Default: [IncrementalDeliveryProtocol.GraphQL17Alpha2] + */ + @ApolloExperimental + fun incrementalDeliveryProtocol(incrementalDeliveryProtocol: IncrementalDeliveryProtocol) = apply { + this.incrementalDeliveryProtocol = incrementalDeliveryProtocol + } /** * Builds the [WebSocketNetworkTransport] @@ -188,19 +201,25 @@ class WebSocketNetworkTransport private constructor( wsProtocol = wsProtocol ?: GraphQLWsProtocol { null }, pingInterval = pingInterval, connectionAcknowledgeTimeout = connectionAcknowledgeTimeout ?: 10.seconds, - parserFactory = parserFactory ?: DefaultSubscriptionParserFactory + parserFactory = parserFactory ?: DefaultSubscriptionParserFactory(incrementalDeliveryProtocol.impl), ) } } } -private object DefaultSubscriptionParserFactory : SubscriptionParserFactory { +private class DefaultSubscriptionParserFactory( + private val incrementalDeliveryProtocolImpl: IncrementalDeliveryProtocolImpl, +) : SubscriptionParserFactory { override fun createParser(request: ApolloRequest): SubscriptionParser { - return DefaultSubscriptionParser(request) + return DefaultSubscriptionParser(incrementalDeliveryProtocolImpl, request) } } -private class DefaultSubscriptionParser(private val request: ApolloRequest) : SubscriptionParser { +private class DefaultSubscriptionParser( + incrementalDeliveryProtocolImpl: IncrementalDeliveryProtocolImpl, + private val request: ApolloRequest, +) : SubscriptionParser { + private val incrementalResultsMerger = incrementalDeliveryProtocolImpl.newIncrementalResultsMerger() private val requestCustomScalarAdapters = request.executionContext[CustomScalarAdapters] ?: CustomScalarAdapters.Empty @Suppress("NAME_SHADOWING") @@ -212,13 +231,28 @@ private class DefaultSubscriptionParser(private val request: .exception(DefaultApolloException("Invalid payload")).build() } - val apolloResponse: ApolloResponse = responseMap.jsonReader().toApolloResponse( + val (payload, deferredFragmentIdentifiers) = if (responseMap.isDeferred()) { + incrementalResultsMerger.merge(responseMap) to incrementalResultsMerger.deferredFragmentIdentifiers + } else { + responseMap to null + } + val apolloResponse: ApolloResponse = payload.jsonReader().toApolloResponse( operation = request.operation, requestUuid = request.requestUuid, customScalarAdapters = requestCustomScalarAdapters, + deferredFragmentIdentifiers = deferredFragmentIdentifiers, ) - return apolloResponse + if (!incrementalResultsMerger.hasNext) { + // Last deferred payload: reset the incrementalResultsMerger for potential subsequent responses + incrementalResultsMerger.reset() + } + + return if (incrementalResultsMerger.isEmptyResponse) { + null + } else { + apolloResponse + } } } @@ -253,6 +287,11 @@ private class DefaultOperationListener( producerScope.close() } } + +private fun Map.isDeferred(): Boolean { + return keys.contains("hasNext") +} + /** * Closes the websocket connection if the transport is a [WebSocketNetworkTransport]. * diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt index f432a47aeb4..f9244718878 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/ws/WebSocketNetworkTransport.kt @@ -1,5 +1,6 @@ package com.apollographql.apollo.network.ws +import com.apollographql.apollo.annotations.ApolloExperimental import com.apollographql.apollo.api.ApolloRequest import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.CustomScalarAdapters @@ -10,7 +11,10 @@ import com.apollographql.apollo.api.toApolloResponse import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloNetworkException import com.apollographql.apollo.exception.SubscriptionOperationException +import com.apollographql.apollo.internal.incremental.IncrementalDeliveryProtocolImpl +import com.apollographql.apollo.internal.incremental.impl import com.apollographql.apollo.internal.transformWhile +import com.apollographql.apollo.network.IncrementalDeliveryProtocol import com.apollographql.apollo.network.NetworkTransport import com.apollographql.apollo.network.ws.internal.Command import com.apollographql.apollo.network.ws.internal.ConnectionReEstablished @@ -38,6 +42,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onSubscription @@ -59,6 +64,7 @@ private constructor( private val idleTimeoutMillis: Long = 60_000, private val protocolFactory: WsProtocol.Factory = SubscriptionWsProtocol.Factory(), private val reopenWhen: (suspend (Throwable, attempt: Long) -> Boolean)?, + private val incrementalDeliveryProtocolImpl: IncrementalDeliveryProtocolImpl, ) : NetworkTransport { /** @@ -259,6 +265,8 @@ private constructor( override fun execute( request: ApolloRequest, ): Flow> { + val incrementalResultsMerger = incrementalDeliveryProtocolImpl.newIncrementalResultsMerger() + return events.onSubscription { messages.send(StartOperation(request)) }.filter { @@ -295,13 +303,24 @@ private constructor( }.map { response -> when (response) { is OperationResponse -> { + val responsePayload = response.payload val requestCustomScalarAdapters = request.executionContext[CustomScalarAdapters]!! - val apolloResponse: ApolloResponse = response.payload.jsonReader().toApolloResponse( + val (payload, deferredFragmentIdentifiers) = if (responsePayload.isDeferred()) { + incrementalResultsMerger.merge(responsePayload) to incrementalResultsMerger.deferredFragmentIdentifiers + } else { + responsePayload to null + } + val apolloResponse: ApolloResponse = payload.jsonReader().toApolloResponse( operation = request.operation, requestUuid = request.requestUuid, customScalarAdapters = requestCustomScalarAdapters, + deferredFragmentIdentifiers = deferredFragmentIdentifiers ) + if (!incrementalResultsMerger.hasNext) { + // Last deferred payload: reset the incrementalResultsMerger for potential subsequent responses + incrementalResultsMerger.reset() + } apolloResponse } @@ -311,6 +330,8 @@ private constructor( // Cannot happen as these events are filtered out upstream is ConnectionReEstablished, is OperationComplete, is GeneralError -> error("Unexpected event $response") } + }.filterNot { + incrementalResultsMerger.isEmptyResponse }.onCompletion { messages.send(StopOperation(request)) } @@ -353,6 +374,7 @@ private constructor( private var idleTimeoutMillis: Long? = null private var protocolFactory: WsProtocol.Factory? = null private var reopenWhen: (suspend (Throwable, attempt: Long) -> Boolean)? = null + private var incrementalDeliveryProtocol: IncrementalDeliveryProtocol = IncrementalDeliveryProtocol.GraphQL17Alpha2 /** * Configure the server URL. @@ -419,6 +441,16 @@ private constructor( this.reopenWhen = reopenWhen } + /** + * The incremental delivery protocol to use when using `@defer` and/or `@stream`. + * + * Default: [IncrementalDeliveryProtocol.GraphQL17Alpha2] + */ + @ApolloExperimental + fun incrementalDeliveryProtocol(incrementalDeliveryProtocol: IncrementalDeliveryProtocol) = apply { + this.incrementalDeliveryProtocol = incrementalDeliveryProtocol + } + fun build(): WebSocketNetworkTransport { return WebSocketNetworkTransport( serverUrl = serverUrl ?: error("No serverUrl specified"), @@ -426,7 +458,8 @@ private constructor( webSocketEngine = webSocketEngine ?: DefaultWebSocketEngine(), idleTimeoutMillis = idleTimeoutMillis ?: 60_000, protocolFactory = protocolFactory ?: SubscriptionWsProtocol.Factory(), - reopenWhen = reopenWhen + reopenWhen = reopenWhen, + incrementalDeliveryProtocolImpl = incrementalDeliveryProtocol.impl, ) } } @@ -441,3 +474,7 @@ fun NetworkTransport.closeConnection(reason: Throwable) { (this as? WebSocketNetworkTransport ?: throw IllegalArgumentException("'$this' is not an instance of com.apollographql.apollo.ws.WebSocketNetworkTransport")).closeConnection(reason) } + +private fun Map.isDeferred(): Boolean { + return keys.contains("hasNext") +} diff --git a/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha9NormalizedCacheTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha9NormalizedCacheTest.kt index 0958e4f7835..992fd4eb1f8 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha9NormalizedCacheTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha9NormalizedCacheTest.kt @@ -20,7 +20,7 @@ import com.apollographql.apollo.exception.ApolloNetworkException import com.apollographql.apollo.exception.CacheMissException import com.apollographql.apollo.network.NetworkTransport import com.apollographql.apollo.network.http.HttpNetworkTransport -import com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol +import com.apollographql.apollo.network.IncrementalDeliveryProtocol import com.apollographql.apollo.testing.internal.runTest import com.apollographql.mockserver.MockServer import com.apollographql.mockserver.assertNoRequest diff --git a/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha9Test.kt b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha9Test.kt index 423ab53cd02..f841ab6e188 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha9Test.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferGraphQL17Alpha9Test.kt @@ -7,7 +7,7 @@ import com.apollographql.apollo.api.Error.Builder import com.apollographql.apollo.autoPersistedQueryInfo import com.apollographql.apollo.mpp.currentTimeMillis import com.apollographql.apollo.network.http.HttpNetworkTransport -import com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol +import com.apollographql.apollo.network.IncrementalDeliveryProtocol import com.apollographql.apollo.testing.Platform import com.apollographql.apollo.testing.internal.runTest import com.apollographql.apollo.testing.platform diff --git a/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt index 09c954030d6..9f74798de9e 100644 --- a/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt +++ b/tests/defer/src/commonTest/kotlin/test/DeferWithApolloServerTest.kt @@ -5,7 +5,7 @@ import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.api.Error import com.apollographql.apollo.api.Optional import com.apollographql.apollo.network.http.HttpNetworkTransport -import com.apollographql.apollo.network.http.HttpNetworkTransport.IncrementalDeliveryProtocol +import com.apollographql.apollo.network.IncrementalDeliveryProtocol import com.apollographql.apollo.testing.internal.runTest import com.benasher44.uuid.uuid4 import defer.CanDeferFragmentsOnTheTopLevelQueryFieldQuery From af5a3a44bd95f8d25779669e294077a97da234ef Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 26 Sep 2025 15:48:49 +0200 Subject: [PATCH 21/21] Default to `application/graphql-response+json, application/json` in DefaultHttpRequestComposer --- .../apollographql/apollo/api/http/DefaultHttpRequestComposer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt index 7e9ab9b27b7..331198242fd 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt @@ -49,7 +49,7 @@ class DefaultHttpRequestComposer( if (apolloRequest.operation is Subscription<*>) { "multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json" } else { - "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json" + "application/graphql-response+json, application/json" } ) )