expo 57.0.9 → 57.0.11

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.
@@ -10,7 +10,7 @@ buildscript {
10
10
  }
11
11
 
12
12
  group = 'host.exp.exponent'
13
- version = '57.0.9'
13
+ version = '57.0.11'
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 "57.0.9"
24
+ versionName "57.0.11"
25
25
  consumerProguardFiles("proguard-rules.pro")
26
26
  }
27
27
  testOptions {
@@ -10,7 +10,24 @@ declare const stateKey: unique symbol;
10
10
  export declare class FetchResponse extends ConcreteNativeResponse implements Response {
11
11
  private readonly abortCleanupFunction;
12
12
  private [stateKey];
13
+ private bodyStreamClosed;
14
+ private bodyStreamController;
15
+ private abortReason;
13
16
  constructor(abortCleanupFunction: AbortSubscriptionCleanupFunction);
17
+ /**
18
+ * Rejects the pending body read with the abort reason and closes the
19
+ * teardown guard so late native events are dropped (#34804). Idempotent,
20
+ * and safe to call before the body stream exists.
21
+ */
22
+ abort(reason?: unknown): void;
23
+ /**
24
+ * These helpers run on the native emitter's stack, where a throw reaches the
25
+ * global handler. The guard can be stale (internal stream errors bypass
26
+ * `controller.error()`), so check `desiredSize` (null = errored) and catch.
27
+ */
28
+ private closeBodyStream;
29
+ private errorBodyStream;
30
+ private enqueueBodyChunk;
14
31
  get _rawHeaders(): NativeHeadersType;
15
32
  get status(): number;
16
33
  get statusText(): string;
@@ -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;AAGzE,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;AAuI/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;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBrB,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"}
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;AAGzE,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;AAuI/C;;GAEG;AACH,qBAAa,aAAc,SAAQ,sBAAuB,YAAW,QAAQ;IAa/D,OAAO,CAAC,QAAQ,CAAC,oBAAoB;IAZjD,OAAO,CAAC,CAAC,QAAQ,CAAC,CAGhB;IAIF,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,oBAAoB,CACrB;IACP,OAAO,CAAC,WAAW,CAAsB;gBAEZ,oBAAoB,EAAE,gCAAgC;IAKnF;;;;OAIG;IACH,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI;IAmB7B;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,eAAe;IAevB,OAAO,CAAC,gBAAgB;IAkBxB,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,CAoEzD;IAED,IAAa,QAAQ,IAAI,OAAO,CAE/B;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,EAAE,IAAI,OAAO,CAEhB;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBrB,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"}
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/fetch.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAyC,MAAM,iBAAiB,CAAC;AAQvF,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAwBxE,wBAAsB,KAAK,CACzB,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,gBAAgB,EACtC,IAAI,CAAC,EAAE,gBAAgB,GACtB,OAAO,CAAC,aAAa,CAAC,CAuDxB"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/fetch.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAyC,MAAM,iBAAiB,CAAC;AAQvF,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAwBxE,wBAAsB,KAAK,CACzB,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,gBAAgB,EACtC,IAAI,CAAC,EAAE,gBAAgB,GACtB,OAAO,CAAC,aAAa,CAAC,CA0DxB"}
@@ -3,7 +3,7 @@
3
3
  "@expo/fingerprint": "~0.20.6",
4
4
  "@expo/metro-runtime": "~57.0.8",
5
5
  "@expo/vector-icons": "^15.0.2",
6
- "@expo/ui": "~57.0.8",
6
+ "@expo/ui": "~57.0.9",
7
7
  "@react-native-async-storage/async-storage": "2.2.0",
8
8
  "@react-native-community/datetimepicker": "9.1.0",
9
9
  "@react-native-masked-view/masked-view": "0.3.2",
@@ -17,85 +17,85 @@
17
17
  "expo-analytics-amplitude": "~11.3.0",
18
18
  "expo-app-auth": "~11.1.0",
19
19
  "expo-app-loader-provider": "~8.0.0",
20
- "expo-app-metrics": "~57.0.7",
20
+ "expo-app-metrics": "~57.0.9",
21
21
  "expo-apple-authentication": "~57.0.1",
22
22
  "expo-application": "~57.0.2",
23
- "expo-asset": "~57.0.8",
23
+ "expo-asset": "~57.0.9",
24
24
  "expo-audio": "~57.0.3",
25
- "expo-auth-session": "~57.0.5",
26
- "expo-background-fetch": "~57.0.7",
27
- "expo-background-task": "~57.0.7",
25
+ "expo-auth-session": "~57.0.6",
26
+ "expo-background-fetch": "~57.0.8",
27
+ "expo-background-task": "~57.0.8",
28
28
  "expo-battery": "~57.0.1",
29
29
  "expo-blur": "~57.0.2",
30
30
  "expo-brightness": "~57.0.1",
31
- "expo-brownfield": "~57.0.8",
32
- "expo-build-properties": "~57.0.8",
31
+ "expo-brownfield": "~57.0.9",
32
+ "expo-build-properties": "~57.0.9",
33
33
  "expo-calendar": "~57.0.1",
34
34
  "expo-camera": "~57.0.3",
35
35
  "expo-cellular": "~57.0.1",
36
36
  "expo-checkbox": "~57.0.0",
37
37
  "expo-clipboard": "~57.0.1",
38
- "expo-constants": "~57.0.8",
38
+ "expo-constants": "~57.0.9",
39
39
  "expo-contacts": "~57.0.3",
40
40
  "expo-crypto": "~57.0.1",
41
41
  "expo-dev-client": "~57.0.10",
42
42
  "expo-device": "~57.0.1",
43
43
  "expo-document-picker": "~57.0.1",
44
44
  "expo-eas-client": "~57.0.1",
45
- "expo-file-system": "~57.0.1",
45
+ "expo-file-system": "~57.0.2",
46
46
  "expo-font": "~57.0.1",
47
47
  "expo-gl": "~57.0.2",
48
48
  "expo-glass-effect": "~57.0.1",
49
49
  "expo-google-app-auth": "~8.3.0",
50
50
  "expo-haptics": "~57.0.1",
51
- "expo-image": "~57.0.1",
51
+ "expo-image": "~57.0.2",
52
52
  "expo-image-loader": "~57.0.1",
53
- "expo-image-manipulator": "~57.0.7",
54
- "expo-image-picker": "~57.0.7",
53
+ "expo-image-manipulator": "~57.0.8",
54
+ "expo-image-picker": "~57.0.8",
55
55
  "expo-intent-launcher": "~57.0.1",
56
- "expo-insights": "~57.0.7",
56
+ "expo-insights": "~57.0.8",
57
57
  "expo-keep-awake": "~57.0.1",
58
58
  "expo-linear-gradient": "~57.0.1",
59
- "expo-linking": "~57.0.4",
59
+ "expo-linking": "~57.0.5",
60
60
  "expo-local-authentication": "~57.0.2",
61
61
  "expo-localization": "~57.0.1",
62
- "expo-location": "~57.0.7",
62
+ "expo-location": "~57.0.8",
63
63
  "expo-mail-composer": "~57.0.1",
64
64
  "expo-manifests": "~57.0.1",
65
65
  "expo-maps": "~57.0.1",
66
66
  "expo-mcp": "~0.2.1",
67
67
  "expo-media-library": "~57.0.3",
68
68
  "expo-mesh-gradient": "~57.0.1",
69
- "expo-module-template": "~57.0.5",
70
- "expo-modules-core": "~57.0.8",
69
+ "expo-module-template": "~57.0.6",
70
+ "expo-modules-core": "~57.0.10",
71
71
  "expo-navigation-bar": "~57.0.2",
72
72
  "expo-network": "~57.0.1",
73
- "expo-notifications": "~57.0.8",
74
- "expo-observe": "~57.0.9",
73
+ "expo-notifications": "~57.0.9",
74
+ "expo-observe": "~57.0.11",
75
75
  "expo-print": "~57.0.1",
76
76
  "expo-live-photo": "~57.0.1",
77
- "expo-router": "~57.0.9",
77
+ "expo-router": "~57.0.11",
78
78
  "expo-screen-capture": "~57.0.1",
79
79
  "expo-screen-orientation": "~57.0.1",
80
80
  "expo-secure-store": "~57.0.1",
81
81
  "expo-sensors": "~57.0.2",
82
82
  "expo-server": "~57.0.1",
83
- "expo-sharing": "~57.0.8",
83
+ "expo-sharing": "~57.0.9",
84
84
  "expo-sms": "~57.0.1",
85
85
  "expo-speech": "~57.0.1",
86
86
  "expo-splash-screen": "~57.0.5",
87
87
  "expo-sqlite": "~57.0.1",
88
88
  "expo-status-bar": "~57.0.1",
89
89
  "expo-store-review": "~57.0.1",
90
- "expo-symbols": "~57.0.1",
90
+ "expo-symbols": "~57.0.2",
91
91
  "expo-system-ui": "~57.0.2",
92
- "expo-task-manager": "~57.0.7",
92
+ "expo-task-manager": "~57.0.8",
93
93
  "expo-tracking-transparency": "~57.0.1",
94
- "expo-updates": "~57.0.11",
94
+ "expo-updates": "~57.0.12",
95
95
  "expo-video-thumbnails": "~57.0.1",
96
96
  "expo-video": "~57.0.2",
97
97
  "expo-web-browser": "~57.0.2",
98
- "expo-widgets": "~57.0.7",
98
+ "expo-widgets": "~57.0.8",
99
99
  "jest-expo": "~57.0.3",
100
100
  "lottie-react-native": "~7.3.8",
101
101
  "react": "19.2.3",
@@ -129,32 +129,50 @@ internal final class NativeResponse: SharedObject, ExpoURLSessionTaskDelegate, @
129
129
 
130
130
  // MARK: - ExpoURLSessionTaskDelegate implementations
131
131
 
132
+ // URLSession runs these callbacks on its own queue, racing `state` / sink access on `dispatchQueue`.
133
+ // Hop every callback onto `dispatchQueue` to serialize the whole state machine on one queue.
134
+
132
135
  func urlSessionDidStart(_ session: ExpoURLSessionTask) {
133
- if isInvalidState(.intialized) {
134
- return
136
+ dispatchQueue.async { [weak self] in
137
+ guard let self else {
138
+ return
139
+ }
140
+ if self.isInvalidState(.intialized) {
141
+ return
142
+ }
143
+ self.state = .started
135
144
  }
136
- state = .started
137
145
  }
138
146
 
139
147
  func urlSession(_ session: ExpoURLSessionTask, didReceive response: URLResponse) {
140
- if isInvalidState(.started) {
141
- return
148
+ dispatchQueue.async { [weak self] in
149
+ guard let self else {
150
+ return
151
+ }
152
+ if self.isInvalidState(.started) {
153
+ return
154
+ }
155
+ self.responseInit = Self.createResponseInit(response: response)
156
+ self.state = .responseReceived
142
157
  }
143
- responseInit = Self.createResponseInit(response: response)
144
- state = .responseReceived
145
158
  }
146
159
 
147
160
  func urlSession(_ session: ExpoURLSessionTask, didReceive data: Data) {
148
- if isInvalidState(.responseReceived, .bodyStreamingStarted, .bodyStreamingCanceled) {
149
- return
150
- }
161
+ dispatchQueue.async { [weak self] in
162
+ guard let self else {
163
+ return
164
+ }
165
+ if self.isInvalidState(.responseReceived, .bodyStreamingStarted, .bodyStreamingCanceled) {
166
+ return
167
+ }
151
168
 
152
- if state == .responseReceived {
153
- self.sink.appendBufferBody(data: data)
154
- } else if state == .bodyStreamingStarted {
155
- emit(event: "didReceiveResponseData", payload: data)
169
+ if self.state == .responseReceived {
170
+ self.sink.appendBufferBody(data: data)
171
+ } else if self.state == .bodyStreamingStarted {
172
+ self.emit(event: "didReceiveResponseData", payload: data)
173
+ }
174
+ // no-op in .bodyStreamingCanceled state
156
175
  }
157
- // no-op in .bodyStreamingCanceled state
158
176
  }
159
177
 
160
178
  func urlSession(
@@ -164,22 +182,37 @@ internal final class NativeResponse: SharedObject, ExpoURLSessionTaskDelegate, @
164
182
  newRequest request: URLRequest,
165
183
  completionHandler: @escaping (URLRequest?) -> Void
166
184
  ) {
167
- let shouldFollowRedirects = self.redirectMode == .follow
168
- completionHandler(shouldFollowRedirects ? request : nil)
169
- self.redirected = shouldFollowRedirects
185
+ dispatchQueue.async { [weak self] in
186
+ guard let self else {
187
+ completionHandler(nil)
188
+ return
189
+ }
190
+ let shouldFollowRedirects = self.redirectMode == .follow
191
+ completionHandler(shouldFollowRedirects ? request : nil)
192
+ self.redirected = shouldFollowRedirects
170
193
 
171
- if self.redirectMode == .error {
172
- let error = FetchRedirectException()
173
- self.error = error
174
- if state == .bodyStreamingStarted {
175
- emit(event: "didFailWithError", payload: error.localizedDescription)
194
+ if self.redirectMode == .error {
195
+ let error = FetchRedirectException()
196
+ self.error = error
197
+ if self.state == .bodyStreamingStarted {
198
+ self.emit(event: "didFailWithError", payload: error.localizedDescription)
199
+ }
200
+ self.state = .errorReceived
201
+ self.emit(event: "readyForJSFinalization")
176
202
  }
177
- state = .errorReceived
178
- emit(event: "readyForJSFinalization")
179
203
  }
180
204
  }
181
205
 
182
206
  func urlSession(_ session: ExpoURLSessionTask, task: URLSessionTask, didCompleteWithError error: (any Error)?) {
207
+ dispatchQueue.async { [weak self] in
208
+ guard let self else {
209
+ return
210
+ }
211
+ self.handleDidCompleteWithError(session: session, task: task, error: error)
212
+ }
213
+ }
214
+
215
+ private func handleDidCompleteWithError(session: ExpoURLSessionTask, task: URLSessionTask, error: (any Error)?) {
183
216
  if isInvalidState(.started, .responseReceived, .bodyStreamingStarted, .bodyStreamingCanceled) {
184
217
  return
185
218
  }
@@ -202,7 +235,7 @@ internal final class NativeResponse: SharedObject, ExpoURLSessionTaskDelegate, @
202
235
  guard let self else {
203
236
  return
204
237
  }
205
- self.urlSession(session, task: task, didCompleteWithError: error)
238
+ self.handleDidCompleteWithError(session: session, task: task, error: error)
206
239
  }
207
240
  return
208
241
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo",
3
- "version": "57.0.9",
3
+ "version": "57.0.11",
4
4
  "description": "The Expo SDK",
5
5
  "main": "src/Expo.ts",
6
6
  "module": "src/Expo.ts",
@@ -68,9 +68,9 @@
68
68
  "homepage": "https://github.com/expo/expo/tree/main/packages/expo",
69
69
  "dependencies": {
70
70
  "@babel/runtime": "^7.20.0",
71
- "@expo/cli": "^57.0.11",
71
+ "@expo/cli": "^57.0.13",
72
72
  "@expo/config": "~57.0.6",
73
- "@expo/config-plugins": "~57.0.6",
73
+ "@expo/config-plugins": "~57.0.7",
74
74
  "@expo/devtools": "~57.0.1",
75
75
  "@expo/dom-webview": "~57.0.1",
76
76
  "@expo/fingerprint": "^0.20.6",
@@ -79,14 +79,14 @@
79
79
  "@expo/metro": "~56.0.0",
80
80
  "@expo/metro-config": "~57.0.7",
81
81
  "@ungap/structured-clone": "^1.3.0",
82
- "babel-preset-expo": "~57.0.5",
83
- "expo-asset": "~57.0.8",
84
- "expo-constants": "~57.0.8",
85
- "expo-file-system": "~57.0.1",
82
+ "babel-preset-expo": "~57.0.6",
83
+ "expo-asset": "~57.0.9",
84
+ "expo-constants": "~57.0.9",
85
+ "expo-file-system": "~57.0.2",
86
86
  "expo-font": "~57.0.1",
87
87
  "expo-keep-awake": "~57.0.1",
88
88
  "expo-modules-autolinking": "~57.0.9",
89
- "expo-modules-core": "~57.0.8",
89
+ "expo-modules-core": "~57.0.10",
90
90
  "pretty-format": "^29.7.0",
91
91
  "react-refresh": "^0.14.2",
92
92
  "whatwg-url-minimum": "^0.1.2"
@@ -102,7 +102,7 @@
102
102
  "web-streams-polyfill": "^3.3.2",
103
103
  "@expo/dom-webview": "57.0.1",
104
104
  "@expo/metro-runtime": "57.0.8",
105
- "expo-updates": "57.0.11"
105
+ "expo-updates": "57.0.12"
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": "ff6a677893d78b02c3adf63f67ba159b926395b0",
142
+ "gitHead": "e322c877a4d185fb18b27d68945a190cb7f15ce6",
143
143
  "scripts": {
144
144
  "build": "tsc",
145
145
  "clean": "rimraf build",
@@ -90,20 +90,9 @@ describe('asyncRequireModule', () => {
90
90
 
91
91
  function asyncRequire(moduleID, paths, moduleName) {
92
92
  var ret = asyncRequireImpl(moduleID, paths, moduleName);
93
- if (!(ret instanceof Promise)) {
94
- return {
95
- _result: ret,
96
- then: function(resolve, reject) {
97
- return Promise.resolve(ret).then(resolve, reject);
98
- },
99
- };
100
- }
101
- return {
102
- _result: ret,
103
- then: function(resolve, reject) {
104
- return ret.then(resolve, reject);
105
- },
106
- };
93
+ var promise = Promise.resolve(ret);
94
+ promise._result = ret;
95
+ return promise;
107
96
  }
108
97
 
109
98
  asyncRequire.unstable_importMaybeSync = function(moduleID, paths) {
@@ -225,14 +214,20 @@ describe('asyncRequireModule', () => {
225
214
  });
226
215
 
227
216
  describe('thenable return value', () => {
228
- it('exposes a synchronous _result when no bundle load was needed', () => {
217
+ it('returns a Promise with a synchronous _result when no bundle load was needed', async () => {
229
218
  const ret = asyncRequire(42, null, 'my-module');
230
219
 
231
- expect(typeof ret.then).toBe('function');
220
+ const onFinally = jest.fn();
221
+
222
+ expect(ret).toBeInstanceOf(Promise);
223
+ expect(typeof ret.catch).toBe('function');
224
+ expect(typeof ret.finally).toBe('function');
232
225
  expect(ret._result).toEqual({ default: 'module-42' });
226
+ await expect(ret.finally(onFinally)).resolves.toEqual({ default: 'module-42' });
227
+ expect(onFinally).toHaveBeenCalledTimes(1);
233
228
  });
234
229
 
235
- it('exposes a Promise _result when a bundle load was needed', async () => {
230
+ it('returns a Promise with a Promise _result when a bundle load was needed', async () => {
236
231
  process.env.EXPO_OS = 'web';
237
232
  mockImportAll
238
233
  .mockImplementationOnce(() => {
@@ -244,10 +239,22 @@ describe('asyncRequireModule', () => {
244
239
 
245
240
  const ret = asyncRequire(42, { '42': '/bundles/my-module.bundle' }, 'my-module');
246
241
 
247
- expect(typeof ret.then).toBe('function');
242
+ expect(ret).toBeInstanceOf(Promise);
243
+ expect(typeof ret.catch).toBe('function');
244
+ expect(typeof ret.finally).toBe('function');
248
245
  expect(ret._result).toBeInstanceOf(Promise);
249
246
  await expect(ret._result).resolves.toEqual({ default: 'module-42' });
250
247
  });
248
+
249
+ it('supports catching a rejected bundle load', async () => {
250
+ process.env.EXPO_OS = 'ios';
251
+ const error = new Error('Bundle load failed');
252
+ (globalThis as any).__loadBundleAsync = jest.fn(() => Promise.reject(error));
253
+
254
+ const ret = asyncRequire(42, { '42': '/bundles/my-module.bundle' }, 'my-module');
255
+
256
+ await expect(ret.catch((caughtError: Error) => caughtError)).resolves.toBe(error);
257
+ });
251
258
  });
252
259
 
253
260
  describe('unstable_importMaybeSync', () => {
@@ -94,30 +94,22 @@ function asyncRequireImpl<T>(
94
94
  return importAll();
95
95
  }
96
96
 
97
+ type PromiseWithResult<T> = Promise<T> & {
98
+ _result?: T | Promise<T>;
99
+ };
100
+
97
101
  function asyncRequire<T>(
98
102
  moduleID: number,
99
103
  paths: DependencyMapPaths,
100
104
  moduleName?: string
101
- ): PromiseLike<T> & { _result?: T | Promise<T> } {
105
+ ): PromiseWithResult<T> {
102
106
  const ret = asyncRequireImpl<T>(moduleID, paths, moduleName);
103
- if (!(ret instanceof Promise)) {
104
- // We return a thenable with an added `unstable_importMaybeSync`-like
105
- // `_result` property to bypass this being force-converted to a promise
106
- // for rehydration
107
- return {
108
- _result: ret,
109
- then(resolve, reject) {
110
- return Promise.resolve(ret).then(resolve, reject);
111
- },
112
- };
113
- } else {
114
- return {
115
- _result: ret,
116
- then(resolve, reject) {
117
- return ret.then(resolve, reject);
118
- },
119
- };
120
- }
107
+ // We return a Promise with an added `unstable_importMaybeSync`-like
108
+ // `_result` property to bypass this being force-converted to a promise
109
+ // for rehydration
110
+ const promise: PromiseWithResult<T> = Promise.resolve(ret);
111
+ promise._result = ret;
112
+ return promise;
121
113
  }
122
114
 
123
115
  // Synchronous version of asyncRequire, which can still return a promise
@@ -163,11 +163,94 @@ export class FetchResponse extends ConcreteNativeResponse implements Response {
163
163
  metadata: null,
164
164
  };
165
165
 
166
+ // `bodyStreamClosed` is only a fast path — the stream can leave `readable`
167
+ // without it being set, so every controller call must also ask the controller.
168
+ private bodyStreamClosed = false;
169
+ private bodyStreamController: ReadableStreamDefaultController<Uint8Array<ArrayBuffer>> | null =
170
+ null;
171
+ private abortReason: unknown = undefined;
172
+
166
173
  constructor(private readonly abortCleanupFunction: AbortSubscriptionCleanupFunction) {
167
174
  super();
168
175
  this.addListener('readyForJSFinalization', this.finalize);
169
176
  }
170
177
 
178
+ /**
179
+ * Rejects the pending body read with the abort reason and closes the
180
+ * teardown guard so late native events are dropped (#34804). Idempotent,
181
+ * and safe to call before the body stream exists.
182
+ */
183
+ abort(reason?: unknown): void {
184
+ const abortError =
185
+ reason != null
186
+ ? reason
187
+ : typeof DOMException !== 'undefined'
188
+ ? new DOMException('The operation was aborted.', 'AbortError')
189
+ : Object.assign(new Error('The operation was aborted.'), { name: 'AbortError' });
190
+ this.abortReason = abortError;
191
+ if (this.bodyStreamClosed) {
192
+ return;
193
+ }
194
+ this.bodyStreamClosed = true;
195
+ try {
196
+ this.bodyStreamController?.error(abortError);
197
+ } catch {
198
+ // Controller already closed/errored — nothing to propagate.
199
+ }
200
+ }
201
+
202
+ /**
203
+ * These helpers run on the native emitter's stack, where a throw reaches the
204
+ * global handler. The guard can be stale (internal stream errors bypass
205
+ * `controller.error()`), so check `desiredSize` (null = errored) and catch.
206
+ */
207
+ private closeBodyStream(
208
+ controller: ReadableStreamDefaultController<Uint8Array<ArrayBuffer>>
209
+ ): void {
210
+ if (this.bodyStreamClosed) {
211
+ return;
212
+ }
213
+ this.bodyStreamClosed = true;
214
+ if (controller.desiredSize == null) {
215
+ return;
216
+ }
217
+ try {
218
+ controller.close();
219
+ } catch {
220
+ // Closed by another path.
221
+ }
222
+ }
223
+
224
+ private errorBodyStream(
225
+ controller: ReadableStreamDefaultController<Uint8Array<ArrayBuffer>>,
226
+ error: unknown
227
+ ): void {
228
+ if (this.bodyStreamClosed) {
229
+ return;
230
+ }
231
+ this.bodyStreamClosed = true;
232
+ try {
233
+ controller.error(error);
234
+ } catch {
235
+ // Already closed or errored.
236
+ }
237
+ }
238
+
239
+ private enqueueBodyChunk(
240
+ controller: ReadableStreamDefaultController<Uint8Array<ArrayBuffer>>,
241
+ data: Uint8Array<ArrayBuffer>
242
+ ): void {
243
+ if (this.bodyStreamClosed || controller.desiredSize == null) {
244
+ return;
245
+ }
246
+ try {
247
+ controller.enqueue(data);
248
+ } catch {
249
+ // The stream left `readable` between the check and the enqueue — drop
250
+ // the chunk; throwing would error the stream behind the guard's back.
251
+ }
252
+ }
253
+
171
254
  // Originals have metadata=null and fall through to the native getter via super.
172
255
  // Clones carry a snapshot and stand alone.
173
256
 
@@ -199,38 +282,35 @@ export class FetchResponse extends ConcreteNativeResponse implements Response {
199
282
  const body = this[stateKey].body;
200
283
 
201
284
  if (body.stream == null) {
202
- // This flag prevents enqueuing data after the stream is closed or canceled.
203
- // Because it might be too late for the multithreaded native code to stop enqueuing data,
204
- // we cannot simply rely on the native code to stop sending `didReceiveResponseData`.
205
- let isControllerClosed = false;
206
-
285
+ // `bodyStreamClosed` prevents enqueuing/closing after the stream is
286
+ // closed, canceled, or aborted native code may keep sending
287
+ // `didReceiveResponseData` past that point.
207
288
  body.stream = new ReadableStream(
208
289
  {
209
290
  start: (controller) => {
291
+ this.bodyStreamController = controller;
292
+
293
+ // Aborted before the first read — reject immediately.
294
+ if (this.abortReason !== undefined) {
295
+ this.bodyStreamClosed = true;
296
+ controller.error(this.abortReason);
297
+ return;
298
+ }
299
+
210
300
  if (body.streamingState === 'completed') {
211
301
  return;
212
302
  }
213
303
 
214
304
  this.addListener('didReceiveResponseData', (data: Uint8Array<ArrayBuffer>) => {
215
- if (!isControllerClosed) {
216
- controller.enqueue(data);
217
- }
305
+ this.enqueueBodyChunk(controller, data);
218
306
  });
219
307
 
220
308
  this.addListener('didComplete', () => {
221
- if (isControllerClosed) {
222
- return;
223
- }
224
- isControllerClosed = true;
225
- controller.close();
309
+ this.closeBodyStream(controller);
226
310
  });
227
311
 
228
312
  this.addListener('didFailWithError', (error: string) => {
229
- if (isControllerClosed) {
230
- return;
231
- }
232
- isControllerClosed = true;
233
- controller.error(new Error(error));
313
+ this.errorBodyStream(controller, new Error(error));
234
314
  });
235
315
  },
236
316
 
@@ -239,25 +319,22 @@ export class FetchResponse extends ConcreteNativeResponse implements Response {
239
319
  const completedData = await this.startStreaming();
240
320
 
241
321
  if (completedData != null) {
242
- if (!isControllerClosed) {
243
- controller.enqueue(completedData);
244
- controller.close();
245
- isControllerClosed = true;
246
- }
247
-
322
+ this.enqueueBodyChunk(controller, completedData);
323
+ this.closeBodyStream(controller);
248
324
  body.streamingState = 'completed';
249
325
  } else {
250
326
  body.streamingState = 'started';
251
327
  }
252
328
  } else if (body.streamingState === 'completed') {
253
- controller.close();
254
- isControllerClosed = true;
329
+ this.closeBodyStream(controller);
255
330
  }
256
331
  },
257
332
 
258
333
  cancel: (reason) => {
334
+ // Flag first — the stream is already `closed` when this callback
335
+ // runs, so the guard must not go stale if cancelStreaming throws.
336
+ this.bodyStreamClosed = true;
259
337
  this.cancelStreaming(String(reason));
260
- isControllerClosed = true;
261
338
  },
262
339
  },
263
340
  {
@@ -138,6 +138,96 @@ describe('FetchResponse', () => {
138
138
  await readPromise;
139
139
  });
140
140
 
141
+ describe('abort()', () => {
142
+ // abort() covers cancellation via an AbortSignal: fetch() routes it to a native
143
+ // request.cancel() that never reaches the JS controller, leaving the in-flight
144
+ // read hung (expo/expo#34804 / #33549 / #33553).
145
+
146
+ it('rejects the in-flight body read with an AbortError', async () => {
147
+ const response = makeResponse();
148
+ const reader = response.body!.getReader();
149
+ const pending = reader.read();
150
+
151
+ response.abort();
152
+
153
+ await expect(pending).rejects.toMatchObject({ name: 'AbortError' });
154
+ });
155
+
156
+ it('rejects a read started after abort with an AbortError', async () => {
157
+ const response = makeResponse();
158
+ const body = response.body!; // create the stream, then abort before reading
159
+ response.abort();
160
+
161
+ await expect(body.getReader().read()).rejects.toMatchObject({ name: 'AbortError' });
162
+ });
163
+
164
+ it('does not deliver or throw on native data that arrives after abort', async () => {
165
+ const response = makeResponse();
166
+ const reader = response.body!.getReader();
167
+ const pending = reader.read();
168
+
169
+ response.abort();
170
+ // The teardown race: native delivers one more data event after cancel.
171
+ const chunk = new TextEncoder().encode('late');
172
+ expect(() => (response as any).emit('didReceiveResponseData', chunk)).not.toThrow();
173
+ expect(() => (response as any).emit('didComplete')).not.toThrow();
174
+
175
+ await expect(pending).rejects.toMatchObject({ name: 'AbortError' });
176
+ });
177
+
178
+ it('preserves a provided abort reason', async () => {
179
+ const response = makeResponse();
180
+ const reader = response.body!.getReader();
181
+ const pending = reader.read();
182
+
183
+ const reason = new DOMException('user navigated away', 'AbortError');
184
+ response.abort(reason);
185
+
186
+ await expect(pending).rejects.toBe(reason);
187
+ });
188
+
189
+ it('is idempotent and safe to call twice', async () => {
190
+ const response = makeResponse();
191
+ response.body!.getReader();
192
+ response.abort();
193
+ expect(() => response.abort()).not.toThrow();
194
+ });
195
+ });
196
+
197
+ describe('internally errored stream (rejected pull)', () => {
198
+ // When pull() rejects, the streams implementation errors the stream internally
199
+ // without calling controller.error(), so the bodyStreamClosed guard goes stale.
200
+ // Native events landing after that used to throw on the errored stream.
201
+
202
+ it('surfaces the underlying error to the reader', async () => {
203
+ const response = makeResponse();
204
+
205
+ (response as any).startStreaming = async () => {
206
+ throw new Error('native module call failed');
207
+ };
208
+
209
+ const reader = response.body!.getReader();
210
+ await expect(reader.read()).rejects.toThrow('native module call failed');
211
+ });
212
+
213
+ it('does not throw on native events that arrive after pull() rejected', async () => {
214
+ const response = makeResponse();
215
+
216
+ (response as any).startStreaming = async () => {
217
+ throw new Error('native module call failed');
218
+ };
219
+
220
+ const reader = response.body!.getReader();
221
+ await expect(reader.read()).rejects.toThrow('native module call failed');
222
+
223
+ // The teardown race: native still delivers a chunk, then completion.
224
+ const chunk = new TextEncoder().encode('late');
225
+ expect(() => (response as any).emit('didReceiveResponseData', chunk)).not.toThrow();
226
+ expect(() => (response as any).emit('didComplete')).not.toThrow();
227
+ expect(() => (response as any).emit('didFailWithError', 'late error')).not.toThrow();
228
+ });
229
+ });
230
+
141
231
  describe('clone()', () => {
142
232
  it('returns a Response that exposes the same metadata', () => {
143
233
  const response = makeResponse();
@@ -75,9 +75,12 @@ export async function fetch(
75
75
  };
76
76
 
77
77
  if (signal && signal.aborted) {
78
- throw new FetchError('The operation was aborted.');
78
+ throw new FetchError('The operation was aborted.', { cause: signal.reason });
79
79
  }
80
80
  abortSubscription = addAbortSignalListener(signal, () => {
81
+ // Abort the body stream before canceling the native request, so late
82
+ // native events can't reach an abandoned controller.
83
+ response.abort(signal?.reason);
81
84
  request.cancel();
82
85
  });
83
86
  try {