expo 56.0.7 → 56.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/fetch/ExpoFetchModule.kt +1 -1
- package/android/src/main/java/expo/modules/fetch/{CompressionInterceptor.kt → TransparentCompressionInterceptor.kt} +18 -15
- package/android/src/test/java/expo/modules/fetch/{CompressionInterceptorTest.kt → TransparentCompressionInterceptorTest.kt} +38 -9
- package/build/winter/fetch/FetchResponse.d.ts.map +1 -1
- package/bundledNativeModules.json +14 -14
- package/package.json +5 -5
- package/src/winter/fetch/FetchResponse.ts +43 -8
- package/src/winter/fetch/__tests__/FetchResponse-test.ts +80 -3
- package/template.tgz +0 -0
package/android/build.gradle
CHANGED
|
@@ -10,7 +10,7 @@ buildscript {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
group = 'host.exp.exponent'
|
|
13
|
-
version = '56.0.
|
|
13
|
+
version = '56.0.8'
|
|
14
14
|
|
|
15
15
|
expoModule {
|
|
16
16
|
// We can't prebuild the module because it depends on the generated files.
|
|
@@ -21,7 +21,7 @@ android {
|
|
|
21
21
|
namespace "expo.core"
|
|
22
22
|
defaultConfig {
|
|
23
23
|
versionCode 1
|
|
24
|
-
versionName "56.0.
|
|
24
|
+
versionName "56.0.8"
|
|
25
25
|
consumerProguardFiles("proguard-rules.pro")
|
|
26
26
|
}
|
|
27
27
|
testOptions {
|
|
@@ -26,7 +26,7 @@ class ExpoFetchModule : Module() {
|
|
|
26
26
|
OkHttpClientProvider.createClient(reactContext)
|
|
27
27
|
.newBuilder()
|
|
28
28
|
.addInterceptor(OkHttpFileUrlInterceptor(reactContext))
|
|
29
|
-
.addInterceptor(
|
|
29
|
+
.addInterceptor(TransparentCompressionInterceptor)
|
|
30
30
|
.build()
|
|
31
31
|
}
|
|
32
32
|
private val cookieHandler by lazy { ForwardingCookieHandler(reactContext) }
|
|
@@ -34,23 +34,26 @@ import org.brotli.dec.BrotliInputStream
|
|
|
34
34
|
* header, and decompresses (and strips `Content-Encoding`/`Content-Length` from) responses
|
|
35
35
|
* encoded with any of the three.
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* Unlike OkHttp, which disables transparent decompression when `Accept-Encoding` is explicitly set by the caller,
|
|
38
|
+
* this interceptor continues to decompress responses to match the behavior of the Fetch API, iOS `URLSession`,
|
|
39
|
+
* and [React Native fetch](https://github.com/facebook/react-native/blob/622941d9dca684ecfc8f5086eb42c8178c3062d1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.kt#L656-L691).
|
|
40
|
+
*
|
|
41
|
+
* The `Content-Encoding` and `Content-Length` headers are removed after
|
|
42
|
+
* decompression.
|
|
40
43
|
*/
|
|
41
|
-
object
|
|
42
|
-
override fun intercept(chain: Interceptor.Chain): Response
|
|
43
|
-
|
|
44
|
-
val request = chain.request().newBuilder()
|
|
45
|
-
.header("Accept-Encoding", "zstd, br, gzip")
|
|
46
|
-
.build()
|
|
47
|
-
|
|
48
|
-
val response = chain.proceed(request)
|
|
44
|
+
object TransparentCompressionInterceptor : Interceptor {
|
|
45
|
+
override fun intercept(chain: Interceptor.Chain): Response {
|
|
46
|
+
val request = chain.request()
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
return uncompress(
|
|
49
|
+
chain.proceed(
|
|
50
|
+
when (request.header("Accept-Encoding")) {
|
|
51
|
+
null -> request.newBuilder().header("Accept-Encoding", "zstd, br, gzip").build()
|
|
52
|
+
else -> request
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
}
|
|
54
57
|
|
|
55
58
|
internal fun uncompress(response: Response): Response {
|
|
56
59
|
if (!response.promisesBody()) {
|
|
@@ -17,7 +17,7 @@ import okio.GzipSink
|
|
|
17
17
|
import okio.buffer
|
|
18
18
|
import org.junit.Test
|
|
19
19
|
|
|
20
|
-
class
|
|
20
|
+
class TransparentCompressionInterceptorTest {
|
|
21
21
|
// brotli of "hello world" produced offline with `printf 'hello world' | brotli`.
|
|
22
22
|
// Avoids pulling in a brotli encoder just for this test; the decoder side is what we exercise.
|
|
23
23
|
private val brotliHelloWorld = hexToBytes("0f058068656c6c6f20776f726c6403")
|
|
@@ -32,12 +32,41 @@ class CompressionInterceptorTest {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
@Test
|
|
35
|
-
fun `should
|
|
35
|
+
fun `should leave caller-set Accept-Encoding header untouched`() {
|
|
36
36
|
val capturedRequest = interceptAndCapture(initialAcceptEncoding = "gzip")
|
|
37
37
|
|
|
38
38
|
assertThat(capturedRequest.header("Accept-Encoding")).isEqualTo("gzip")
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
@Test
|
|
42
|
+
fun `should still decompress response when caller set Accept-Encoding`() {
|
|
43
|
+
val payload = "hello world"
|
|
44
|
+
|
|
45
|
+
val request = Request.Builder()
|
|
46
|
+
.url("https://example.test/")
|
|
47
|
+
.header("Accept-Encoding", "gzip")
|
|
48
|
+
.build()
|
|
49
|
+
|
|
50
|
+
val chain = mockk<Interceptor.Chain>()
|
|
51
|
+
|
|
52
|
+
every { chain.request() } returns request
|
|
53
|
+
every { chain.proceed(any()) } answers {
|
|
54
|
+
Response.Builder()
|
|
55
|
+
.request(request)
|
|
56
|
+
.protocol(Protocol.HTTP_1_1)
|
|
57
|
+
.code(200)
|
|
58
|
+
.message("OK")
|
|
59
|
+
.header("Content-Encoding", "gzip")
|
|
60
|
+
.body(payload.toByteArray().gzipCompressed().toResponseBody("application/octet-stream".toMediaType()))
|
|
61
|
+
.build()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
val response = TransparentCompressionInterceptor.intercept(chain)
|
|
65
|
+
|
|
66
|
+
assertThat(response.body!!.string()).isEqualTo(payload)
|
|
67
|
+
assertThat(response.header("Content-Encoding")).isNull()
|
|
68
|
+
}
|
|
69
|
+
|
|
41
70
|
// endregion
|
|
42
71
|
|
|
43
72
|
// region uncompress() — decompression
|
|
@@ -47,7 +76,7 @@ class CompressionInterceptorTest {
|
|
|
47
76
|
val payload = "hello world"
|
|
48
77
|
val response = encodedResponse(payload.toByteArray().gzipCompressed(), "gzip")
|
|
49
78
|
|
|
50
|
-
val uncompressed =
|
|
79
|
+
val uncompressed = TransparentCompressionInterceptor.uncompress(response)
|
|
51
80
|
|
|
52
81
|
assertThat(uncompressed.body!!.string()).isEqualTo(payload)
|
|
53
82
|
assertThat(uncompressed.header("Content-Encoding")).isNull()
|
|
@@ -58,7 +87,7 @@ class CompressionInterceptorTest {
|
|
|
58
87
|
fun `should decompress br response and strip Content-Encoding and Content-Length headers`() {
|
|
59
88
|
val response = encodedResponse(brotliHelloWorld, "br")
|
|
60
89
|
|
|
61
|
-
val uncompressed =
|
|
90
|
+
val uncompressed = TransparentCompressionInterceptor.uncompress(response)
|
|
62
91
|
|
|
63
92
|
assertThat(uncompressed.body!!.string()).isEqualTo("hello world")
|
|
64
93
|
assertThat(uncompressed.header("Content-Encoding")).isNull()
|
|
@@ -70,7 +99,7 @@ class CompressionInterceptorTest {
|
|
|
70
99
|
val payload = "hello world"
|
|
71
100
|
val response = encodedResponse(payload.toByteArray().gzipCompressed(), "GZIP")
|
|
72
101
|
|
|
73
|
-
val uncompressed =
|
|
102
|
+
val uncompressed = TransparentCompressionInterceptor.uncompress(response)
|
|
74
103
|
|
|
75
104
|
assertThat(uncompressed.body!!.string()).isEqualTo(payload)
|
|
76
105
|
}
|
|
@@ -85,7 +114,7 @@ class CompressionInterceptorTest {
|
|
|
85
114
|
.body("plain".toResponseBody("text/plain".toMediaType()))
|
|
86
115
|
.build()
|
|
87
116
|
|
|
88
|
-
val result =
|
|
117
|
+
val result = TransparentCompressionInterceptor.uncompress(response)
|
|
89
118
|
|
|
90
119
|
assertThat(result).isSameInstanceAs(response)
|
|
91
120
|
}
|
|
@@ -94,7 +123,7 @@ class CompressionInterceptorTest {
|
|
|
94
123
|
fun `should leave response unchanged when Content-Encoding is unknown`() {
|
|
95
124
|
val response = encodedResponse("noop".toByteArray(), "lz4")
|
|
96
125
|
|
|
97
|
-
val result =
|
|
126
|
+
val result = TransparentCompressionInterceptor.uncompress(response)
|
|
98
127
|
|
|
99
128
|
assertThat(result).isSameInstanceAs(response)
|
|
100
129
|
}
|
|
@@ -108,7 +137,7 @@ class CompressionInterceptorTest {
|
|
|
108
137
|
.header("Content-Encoding", "gzip")
|
|
109
138
|
.build()
|
|
110
139
|
|
|
111
|
-
val result =
|
|
140
|
+
val result = TransparentCompressionInterceptor.uncompress(response)
|
|
112
141
|
|
|
113
142
|
assertThat(result).isSameInstanceAs(response)
|
|
114
143
|
}
|
|
@@ -138,7 +167,7 @@ class CompressionInterceptorTest {
|
|
|
138
167
|
.build()
|
|
139
168
|
}
|
|
140
169
|
|
|
141
|
-
|
|
170
|
+
TransparentCompressionInterceptor.intercept(chain)
|
|
142
171
|
return captured.captured
|
|
143
172
|
}
|
|
144
173
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FetchResponse.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/FetchResponse.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEzE,QAAA,MAAM,sBAAsB,EAAqC,OAAO,cAAc,CAAC;AACvF,MAAM,MAAM,gCAAgC,GAAG,MAAM,IAAI,CAAC;AAI1D,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AACvE,KAAK,iBAAiB,GAAG,UAAU,CAAC,QAAQ,GAAG,UAAU,CAAC;AAW1D,QAAA,MAAM,QAAQ,eAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"FetchResponse.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/FetchResponse.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEzE,QAAA,MAAM,sBAAsB,EAAqC,OAAO,cAAc,CAAC;AACvF,MAAM,MAAM,gCAAgC,GAAG,MAAM,IAAI,CAAC;AAI1D,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AACvE,KAAK,iBAAiB,GAAG,UAAU,CAAC,QAAQ,GAAG,UAAU,CAAC;AAW1D,QAAA,MAAM,QAAQ,eAAgC,CAAC;AAqI/C;;GAEG;AACH,qBAAa,aAAc,SAAQ,sBAAuB,YAAW,QAAQ;IAM/D,OAAO,CAAC,QAAQ,CAAC,oBAAoB;IALjD,OAAO,CAAC,CAAC,QAAQ,CAAC,CAGhB;gBAE2B,oBAAoB,EAAE,gCAAgC;IAQnF,IAAa,WAAW,IAAI,iBAAiB,CAE5C;IAED,IAAa,MAAM,IAAI,MAAM,CAE5B;IAED,IAAa,UAAU,IAAI,MAAM,CAEhC;IAED,IAAa,GAAG,IAAI,MAAM,CAEzB;IAED,IAAa,UAAU,IAAI,OAAO,CAEjC;IAED,IAAI,IAAI,IAAI,SAAS,CAEpB;IAED,IAAI,IAAI,IAAI,cAAc,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,GAAG,IAAI,CA0EzD;IAED,IAAa,QAAQ,IAAI,OAAO,CAE/B;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,EAAE,IAAI,OAAO,CAEhB;IAED;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAMrB,QAAQ,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAgBtC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC;IAMpB,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;IAKhC,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC;IAanC,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;IAatC,QAAQ,IAAI,MAAM;IAIlB,MAAM,IAAI,MAAM;IAShB,KAAK,IAAI,aAAa;IAgDtB,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,QAAQ,CAQd;CACH"}
|
|
@@ -16,19 +16,19 @@
|
|
|
16
16
|
"expo-analytics-amplitude": "~11.3.0",
|
|
17
17
|
"expo-app-auth": "~11.1.0",
|
|
18
18
|
"expo-app-loader-provider": "~8.0.0",
|
|
19
|
-
"expo-app-metrics": "~56.0.
|
|
19
|
+
"expo-app-metrics": "~56.0.16",
|
|
20
20
|
"expo-apple-authentication": "~56.0.4",
|
|
21
21
|
"expo-application": "~56.0.3",
|
|
22
22
|
"expo-asset": "~56.0.15",
|
|
23
23
|
"expo-audio": "~56.0.11",
|
|
24
24
|
"expo-auth-session": "~56.0.13",
|
|
25
|
-
"expo-background-fetch": "~56.0.
|
|
26
|
-
"expo-background-task": "~56.0.
|
|
25
|
+
"expo-background-fetch": "~56.0.16",
|
|
26
|
+
"expo-background-task": "~56.0.16",
|
|
27
27
|
"expo-battery": "~56.0.4",
|
|
28
28
|
"expo-blur": "~56.0.3",
|
|
29
29
|
"expo-brightness": "~56.0.5",
|
|
30
|
-
"expo-brownfield": "~56.0.
|
|
31
|
-
"expo-build-properties": "~56.0.
|
|
30
|
+
"expo-brownfield": "~56.0.17",
|
|
31
|
+
"expo-build-properties": "~56.0.16",
|
|
32
32
|
"expo-calendar": "~56.0.8",
|
|
33
33
|
"expo-camera": "~56.0.7",
|
|
34
34
|
"expo-cellular": "~56.0.5",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"expo-constants": "~56.0.16",
|
|
38
38
|
"expo-contacts": "~56.0.7",
|
|
39
39
|
"expo-crypto": "~56.0.4",
|
|
40
|
-
"expo-dev-client": "~56.0.
|
|
40
|
+
"expo-dev-client": "~56.0.18",
|
|
41
41
|
"expo-device": "~56.0.4",
|
|
42
42
|
"expo-document-picker": "~56.0.4",
|
|
43
43
|
"expo-file-system": "~56.0.7",
|
|
@@ -48,16 +48,16 @@
|
|
|
48
48
|
"expo-haptics": "~56.0.3",
|
|
49
49
|
"expo-image": "~56.0.9",
|
|
50
50
|
"expo-image-loader": "~56.0.3",
|
|
51
|
-
"expo-image-manipulator": "~56.0.
|
|
52
|
-
"expo-image-picker": "~56.0.
|
|
51
|
+
"expo-image-manipulator": "~56.0.16",
|
|
52
|
+
"expo-image-picker": "~56.0.15",
|
|
53
53
|
"expo-intent-launcher": "~56.0.4",
|
|
54
|
-
"expo-insights": "~56.0.
|
|
54
|
+
"expo-insights": "~56.0.15",
|
|
55
55
|
"expo-keep-awake": "~56.0.3",
|
|
56
56
|
"expo-linear-gradient": "~56.0.4",
|
|
57
57
|
"expo-linking": "~56.0.13",
|
|
58
58
|
"expo-local-authentication": "~56.0.4",
|
|
59
59
|
"expo-localization": "~56.0.6",
|
|
60
|
-
"expo-location": "~56.0.
|
|
60
|
+
"expo-location": "~56.0.15",
|
|
61
61
|
"expo-mail-composer": "~56.0.4",
|
|
62
62
|
"expo-manifests": "~56.0.4",
|
|
63
63
|
"expo-maps": "~56.0.6",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
"expo-modules-core": "~56.0.14",
|
|
69
69
|
"expo-navigation-bar": "~56.0.3",
|
|
70
70
|
"expo-network": "~56.0.4",
|
|
71
|
-
"expo-notifications": "~56.0.
|
|
72
|
-
"expo-observe": "~56.0.
|
|
71
|
+
"expo-notifications": "~56.0.15",
|
|
72
|
+
"expo-observe": "~56.0.18",
|
|
73
73
|
"expo-print": "~56.0.3",
|
|
74
74
|
"expo-live-photo": "~56.0.3",
|
|
75
75
|
"expo-router": "~56.2.8",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"expo-secure-store": "~56.0.4",
|
|
79
79
|
"expo-sensors": "~56.0.5",
|
|
80
80
|
"expo-server": "~56.0.4",
|
|
81
|
-
"expo-sharing": "~56.0.
|
|
81
|
+
"expo-sharing": "~56.0.15",
|
|
82
82
|
"expo-sms": "~56.0.3",
|
|
83
83
|
"expo-speech": "~56.0.3",
|
|
84
84
|
"expo-splash-screen": "~56.0.10",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"expo-store-review": "~56.0.3",
|
|
88
88
|
"expo-symbols": "~56.0.5",
|
|
89
89
|
"expo-system-ui": "~56.0.5",
|
|
90
|
-
"expo-task-manager": "~56.0.
|
|
90
|
+
"expo-task-manager": "~56.0.16",
|
|
91
91
|
"expo-tracking-transparency": "~56.0.5",
|
|
92
92
|
"expo-updates": "~56.0.17",
|
|
93
93
|
"expo-video-thumbnails": "~56.0.3",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo",
|
|
3
|
-
"version": "56.0.
|
|
3
|
+
"version": "56.0.8",
|
|
4
4
|
"description": "The Expo SDK",
|
|
5
5
|
"main": "src/Expo.ts",
|
|
6
6
|
"module": "src/Expo.ts",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"@expo/metro": "~56.0.0",
|
|
80
80
|
"@expo/metro-config": "~56.0.13",
|
|
81
81
|
"@ungap/structured-clone": "^1.3.0",
|
|
82
|
-
"babel-preset-expo": "~56.0.
|
|
82
|
+
"babel-preset-expo": "~56.0.14",
|
|
83
83
|
"expo-asset": "~56.0.15",
|
|
84
84
|
"expo-constants": "~56.0.16",
|
|
85
85
|
"expo-file-system": "~56.0.7",
|
|
@@ -101,8 +101,8 @@
|
|
|
101
101
|
"rimraf": "^6.1.2",
|
|
102
102
|
"web-streams-polyfill": "^3.3.2",
|
|
103
103
|
"@expo/dom-webview": "56.0.5",
|
|
104
|
-
"
|
|
105
|
-
"expo-
|
|
104
|
+
"expo-updates": "56.0.17",
|
|
105
|
+
"@expo/metro-runtime": "56.0.13"
|
|
106
106
|
},
|
|
107
107
|
"peerDependencies": {
|
|
108
108
|
"@expo/dom-webview": "*",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"scripts/resolveAppEntry.js"
|
|
140
140
|
]
|
|
141
141
|
},
|
|
142
|
-
"gitHead": "
|
|
142
|
+
"gitHead": "7df487c3b35d509e8988c87c0abcf8a7d76aa202",
|
|
143
143
|
"scripts": {
|
|
144
144
|
"build": "tsc",
|
|
145
145
|
"clean": "rimraf build",
|
|
@@ -20,17 +20,25 @@ interface ResponseMetadata {
|
|
|
20
20
|
|
|
21
21
|
const stateKey = Symbol('FetchResponse.state');
|
|
22
22
|
|
|
23
|
+
interface ConsumptionWrapper {
|
|
24
|
+
stream: ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
25
|
+
// Stops the wrapper from marking its body as consumed. Called by clone()
|
|
26
|
+
// when reads start coming through tee internals instead of from the user.
|
|
27
|
+
detach: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
function wrapWithConsumption(
|
|
24
31
|
source: ReadableStream<Uint8Array<ArrayBuffer>>,
|
|
25
32
|
body: Body
|
|
26
|
-
):
|
|
33
|
+
): ConsumptionWrapper {
|
|
27
34
|
const reader = source.getReader();
|
|
28
35
|
let markedConsumed = false;
|
|
36
|
+
let markedDetached = false;
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
const stream = new ReadableStream(
|
|
31
39
|
{
|
|
32
40
|
async pull(controller) {
|
|
33
|
-
if (!markedConsumed) {
|
|
41
|
+
if (!markedConsumed && !markedDetached) {
|
|
34
42
|
markedConsumed = true;
|
|
35
43
|
body.consumed = true;
|
|
36
44
|
}
|
|
@@ -50,7 +58,7 @@ function wrapWithConsumption(
|
|
|
50
58
|
}
|
|
51
59
|
},
|
|
52
60
|
cancel(reason) {
|
|
53
|
-
if (!markedConsumed) {
|
|
61
|
+
if (!markedConsumed && !markedDetached) {
|
|
54
62
|
markedConsumed = true;
|
|
55
63
|
body.consumed = true;
|
|
56
64
|
}
|
|
@@ -67,6 +75,13 @@ function wrapWithConsumption(
|
|
|
67
75
|
highWaterMark: 0,
|
|
68
76
|
}
|
|
69
77
|
);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
stream,
|
|
81
|
+
detach: () => {
|
|
82
|
+
markedDetached = true;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
70
85
|
}
|
|
71
86
|
|
|
72
87
|
// JS-side body state. Held behind the stateKey symbol slot.
|
|
@@ -76,6 +91,10 @@ class Body {
|
|
|
76
91
|
cloned: boolean;
|
|
77
92
|
consumed = false;
|
|
78
93
|
|
|
94
|
+
// Detach fn for the wrapper currently held in `stream`. Null until the
|
|
95
|
+
// first clone wraps the native stream.
|
|
96
|
+
detach: (() => void) | null = null;
|
|
97
|
+
|
|
79
98
|
constructor({ cloned }: { cloned: boolean }) {
|
|
80
99
|
this.cloned = cloned;
|
|
81
100
|
}
|
|
@@ -196,13 +215,19 @@ export class FetchResponse extends ConcreteNativeResponse implements Response {
|
|
|
196
215
|
});
|
|
197
216
|
|
|
198
217
|
this.addListener('didComplete', () => {
|
|
199
|
-
|
|
218
|
+
if (isControllerClosed) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
200
221
|
isControllerClosed = true;
|
|
222
|
+
controller.close();
|
|
201
223
|
});
|
|
202
224
|
|
|
203
225
|
this.addListener('didFailWithError', (error: string) => {
|
|
204
|
-
|
|
226
|
+
if (isControllerClosed) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
205
229
|
isControllerClosed = true;
|
|
230
|
+
controller.error(new Error(error));
|
|
206
231
|
});
|
|
207
232
|
},
|
|
208
233
|
|
|
@@ -358,9 +383,19 @@ export class FetchResponse extends ConcreteNativeResponse implements Response {
|
|
|
358
383
|
// Tee so both responses can be read independently. Each branch is wrapped
|
|
359
384
|
// so the first read flips the right consumed flag (otherwise bodyUsed lies).
|
|
360
385
|
if (this.body != null) {
|
|
386
|
+
// Detach the existing wrapper so reads via the new tee don't flip
|
|
387
|
+
// this body's consumed flag through it.
|
|
388
|
+
state.body.detach?.();
|
|
389
|
+
|
|
361
390
|
const [stream1, stream2] = this.body.tee();
|
|
362
|
-
|
|
363
|
-
|
|
391
|
+
const own = wrapWithConsumption(stream1, state.body);
|
|
392
|
+
const sibling = wrapWithConsumption(stream2, cloneState.body);
|
|
393
|
+
|
|
394
|
+
state.body.stream = own.stream;
|
|
395
|
+
state.body.detach = own.detach;
|
|
396
|
+
|
|
397
|
+
cloneState.body.stream = sibling.stream;
|
|
398
|
+
cloneState.body.detach = sibling.detach;
|
|
364
399
|
}
|
|
365
400
|
|
|
366
401
|
state.body.cloned = true;
|
|
@@ -13,6 +13,7 @@ jest.mock('../ExpoFetchModule', () => {
|
|
|
13
13
|
const helloWorld = new TextEncoder().encode('hello world');
|
|
14
14
|
|
|
15
15
|
class StubNativeResponse {
|
|
16
|
+
private listeners = new Map<string, Set<(...args: any[]) => void>>();
|
|
16
17
|
private _bodyUsed = false;
|
|
17
18
|
|
|
18
19
|
// Getters on the prototype, like the real native binding, so super.x works.
|
|
@@ -36,9 +37,31 @@ jest.mock('../ExpoFetchModule', () => {
|
|
|
36
37
|
return this._bodyUsed;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
addListener() {
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
addListener(event: string, listener: (...args: any[]) => void) {
|
|
41
|
+
if (!this.listeners.has(event)) {
|
|
42
|
+
this.listeners.set(event, new Set());
|
|
43
|
+
}
|
|
44
|
+
this.listeners.get(event)!.add(listener);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
removeListener(event: string, listener: (...args: any[]) => void) {
|
|
48
|
+
this.listeners.get(event)?.delete(listener);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
removeAllListeners(event: string) {
|
|
52
|
+
this.listeners.delete(event);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
emit(event: string, ...args: any[]) {
|
|
56
|
+
const listeners = this.listeners.get(event);
|
|
57
|
+
if (!listeners) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const listener of listeners) {
|
|
62
|
+
listener(...args);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
42
65
|
|
|
43
66
|
async arrayBuffer(): Promise<ArrayBuffer> {
|
|
44
67
|
this._bodyUsed = true;
|
|
@@ -79,6 +102,41 @@ describe('FetchResponse', () => {
|
|
|
79
102
|
expect(Object.prototype.toString.call(FetchResponse.prototype)).toBe('[object Response]');
|
|
80
103
|
});
|
|
81
104
|
|
|
105
|
+
it('does not throw when native emits didComplete after the stream was canceled', async () => {
|
|
106
|
+
// Repros expo/expo#34804: native can deliver didComplete after the JS
|
|
107
|
+
// consumer has already canceled the stream. Before the fix this would
|
|
108
|
+
// call controller.close() on an already-closed controller and throw
|
|
109
|
+
// "The stream is not in a state that permits close" out of the event
|
|
110
|
+
// listener, surfacing as a fatal unhandled error.
|
|
111
|
+
const response = makeResponse();
|
|
112
|
+
const body = response.body!;
|
|
113
|
+
const reader = body.getReader();
|
|
114
|
+
|
|
115
|
+
await reader.cancel('consumer canceled');
|
|
116
|
+
expect(() => (response as any).emit('didComplete')).not.toThrow();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('does not throw when native emits didFailWithError after the stream was canceled', async () => {
|
|
120
|
+
const response = makeResponse();
|
|
121
|
+
const body = response.body!;
|
|
122
|
+
const reader = body.getReader();
|
|
123
|
+
|
|
124
|
+
await reader.cancel('consumer canceled');
|
|
125
|
+
expect(() => (response as any).emit('didFailWithError', 'late error')).not.toThrow();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('does not throw when native emits didComplete twice', async () => {
|
|
129
|
+
const response = makeResponse();
|
|
130
|
+
const body = response.body!;
|
|
131
|
+
const reader = body.getReader();
|
|
132
|
+
const readPromise = reader.read();
|
|
133
|
+
|
|
134
|
+
(response as any).emit('didComplete');
|
|
135
|
+
expect(() => (response as any).emit('didComplete')).not.toThrow();
|
|
136
|
+
|
|
137
|
+
await readPromise;
|
|
138
|
+
});
|
|
139
|
+
|
|
82
140
|
describe('clone()', () => {
|
|
83
141
|
it('returns a Response that exposes the same metadata', () => {
|
|
84
142
|
const response = makeResponse();
|
|
@@ -116,6 +174,25 @@ describe('FetchResponse', () => {
|
|
|
116
174
|
expect(bytes.byteLength).toBe(11);
|
|
117
175
|
});
|
|
118
176
|
|
|
177
|
+
it('does not flip bodyUsed on siblings when a second clone is read', async () => {
|
|
178
|
+
const response = makeResponse();
|
|
179
|
+
const second = response.clone();
|
|
180
|
+
const third = response.clone();
|
|
181
|
+
|
|
182
|
+
await third.json().catch(() => {});
|
|
183
|
+
|
|
184
|
+
expect(response.bodyUsed).toBe(false);
|
|
185
|
+
expect(second.bodyUsed).toBe(false);
|
|
186
|
+
expect(third.bodyUsed).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('lets the original be read after being cloned twice', async () => {
|
|
190
|
+
const response = makeResponse();
|
|
191
|
+
response.clone();
|
|
192
|
+
response.clone();
|
|
193
|
+
expect((await response.arrayBuffer()).byteLength).toBe(11);
|
|
194
|
+
});
|
|
195
|
+
|
|
119
196
|
it('throws a TypeError if the body has already been read', async () => {
|
|
120
197
|
const response = makeResponse();
|
|
121
198
|
await response.arrayBuffer();
|
package/template.tgz
CHANGED
|
Binary file
|