expo 52.0.18 → 52.0.19

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.
Files changed (32) hide show
  1. package/android/build.gradle +2 -2
  2. package/android/src/main/java/expo/modules/fetch/NativeResponse.kt +30 -15
  3. package/build/devtools/DevToolsPluginClient.d.ts.map +1 -1
  4. package/build/utils/blobUtils.d.ts.map +1 -0
  5. package/build/winter/FormData.d.ts +47 -3
  6. package/build/winter/FormData.d.ts.map +1 -1
  7. package/build/winter/fetch/FetchResponse.d.ts +4 -1
  8. package/build/winter/fetch/FetchResponse.d.ts.map +1 -1
  9. package/build/winter/fetch/NativeRequest.d.ts +1 -0
  10. package/build/winter/fetch/NativeRequest.d.ts.map +1 -1
  11. package/build/winter/fetch/RequestUtils.d.ts +0 -14
  12. package/build/winter/fetch/RequestUtils.d.ts.map +1 -1
  13. package/build/winter/fetch/convertFormData.d.ts +19 -0
  14. package/build/winter/fetch/convertFormData.d.ts.map +1 -0
  15. package/build/winter/fetch/fetch.d.ts.map +1 -1
  16. package/bundledNativeModules.json +6 -6
  17. package/ios/Fetch/NativeResponse.swift +8 -1
  18. package/package.json +7 -7
  19. package/src/devtools/DevToolsPluginClient.ts +1 -1
  20. package/src/devtools/MessageFramePacker.ts +1 -1
  21. package/src/winter/FormData.ts +66 -15
  22. package/src/winter/__tests__/FormData.test.ios.ts +11 -1
  23. package/src/winter/fetch/FetchResponse.ts +13 -5
  24. package/src/winter/fetch/NativeRequest.ts +1 -0
  25. package/src/winter/fetch/RequestUtils.ts +9 -54
  26. package/src/winter/fetch/__tests__/RequestUtils-test.ts +0 -52
  27. package/src/winter/fetch/__tests__/convertFormData-test.native.ts +111 -0
  28. package/src/winter/fetch/convertFormData.ts +78 -0
  29. package/src/winter/fetch/fetch.ts +21 -7
  30. package/build/devtools/blobUtils.d.ts.map +0 -1
  31. /package/build/{devtools → utils}/blobUtils.d.ts +0 -0
  32. /package/src/{devtools → utils}/blobUtils.ts +0 -0
@@ -33,7 +33,7 @@ def getRNVersion() {
33
33
  ensureDependeciesWereEvaluated(project)
34
34
 
35
35
  group = 'host.exp.exponent'
36
- version = '52.0.18'
36
+ version = '52.0.19'
37
37
 
38
38
  buildscript {
39
39
  // Simple helper that allows the root project to override versions declared by this library.
@@ -46,7 +46,7 @@ android {
46
46
  namespace "expo.core"
47
47
  defaultConfig {
48
48
  versionCode 1
49
- versionName "52.0.18"
49
+ versionName "52.0.19"
50
50
  consumerProguardFiles("proguard-rules.pro")
51
51
  }
52
52
  testOptions {
@@ -72,8 +72,13 @@ internal class NativeResponse(appContext: AppContext, private val coroutineScope
72
72
  }
73
73
 
74
74
  fun emitRequestCancelled() {
75
- error = FetchRequestCancelledException()
75
+ val error = FetchRequestCancelledException()
76
+ this.error = error
77
+ if (state == ResponseState.BODY_STREAMING_STARTED) {
78
+ emit("didFailWithError", error)
79
+ }
76
80
  state = ResponseState.ERROR_RECEIVED
81
+ emit("readyForJSFinalization")
77
82
  }
78
83
 
79
84
  fun waitForStates(states: List<ResponseState>, callback: (ResponseState) -> Unit) {
@@ -108,6 +113,7 @@ internal class NativeResponse(appContext: AppContext, private val coroutineScope
108
113
  }
109
114
  error = e
110
115
  state = ResponseState.ERROR_RECEIVED
116
+ emit("readyForJSFinalization")
111
117
  }
112
118
 
113
119
  override fun onResponse(call: Call, response: Response) {
@@ -123,6 +129,7 @@ internal class NativeResponse(appContext: AppContext, private val coroutineScope
123
129
  emit("didComplete")
124
130
  }
125
131
  this@NativeResponse.state = ResponseState.BODY_COMPLETED
132
+ emit("readyForJSFinalization")
126
133
  }
127
134
  }
128
135
 
@@ -158,22 +165,30 @@ internal class NativeResponse(appContext: AppContext, private val coroutineScope
158
165
  }
159
166
 
160
167
  private fun pumpResponseBodyStream(stream: BufferedSource) {
161
- while (!stream.exhausted()) {
162
- if (isInvalidState(
163
- ResponseState.RESPONSE_RECEIVED,
164
- ResponseState.BODY_STREAMING_STARTED,
165
- ResponseState.BODY_STREAMING_CANCELLED
166
- )
167
- ) {
168
- break
168
+ try {
169
+ while (!stream.exhausted()) {
170
+ if (isInvalidState(
171
+ ResponseState.RESPONSE_RECEIVED,
172
+ ResponseState.BODY_STREAMING_STARTED,
173
+ ResponseState.BODY_STREAMING_CANCELLED
174
+ )
175
+ ) {
176
+ break
177
+ }
178
+ if (state == ResponseState.RESPONSE_RECEIVED) {
179
+ sink.appendBufferBody(stream.buffer.readByteArray())
180
+ } else if (state == ResponseState.BODY_STREAMING_STARTED) {
181
+ emit("didReceiveResponseData", stream.buffer.readByteArray())
182
+ } else {
183
+ break
184
+ }
169
185
  }
170
- if (state == ResponseState.RESPONSE_RECEIVED) {
171
- sink.appendBufferBody(stream.buffer.readByteArray())
172
- } else if (state == ResponseState.BODY_STREAMING_STARTED) {
173
- emit("didReceiveResponseData", stream.buffer.readByteArray())
174
- } else {
175
- break
186
+ } catch (e: IOException) {
187
+ this.error = e
188
+ if (state == ResponseState.BODY_STREAMING_STARTED) {
189
+ emit("didFailWithError", e)
176
190
  }
191
+ state = ResponseState.ERROR_RECEIVED
177
192
  }
178
193
  }
179
194
 
@@ -1 +1 @@
1
- {"version":3,"file":"DevToolsPluginClient.d.ts","sourceRoot":"","sources":["../../src/devtools/DevToolsPluginClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAG5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,OAAO,KAAK,EAAE,cAAc,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAQpF;;;GAGG;AACH,8BAAsB,oBAAoB;aAYtB,cAAc,EAAE,cAAc;IAC9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IAZ3B,SAAS,CAAC,YAAY,EAAE,YAAY,CAAsB;IAE1D,OAAO,CAAC,MAAM,CAAC,cAAc,CAAsD;IACnF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8D;IAEtF,SAAS,CAAC,QAAQ,UAAS;IAC3B,SAAS,CAAC,OAAO,SAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CACR;gBAGT,cAAc,EAAE,cAAc,EAC7B,OAAO,CAAC,yCAA6B;IAKxD;;;OAGG;IACU,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAQvC;;OAEG;IACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAWxC;;;;OAIG;IACI,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG;YAQhC,eAAe;IAS7B;;;;OAIG;IACI,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,GAAG,iBAAiB;IAI7F;;;;OAIG;IACI,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAIpF;;OAEG;IACI,WAAW,IAAI,OAAO;IAI7B;;OAEG;IACH,SAAS,CAAC,YAAY,IAAI,OAAO,CAAC,SAAS,CAAC;IAyB5C,SAAS,CAAC,aAAa,UAAW,qBAAqB,UAErD;IAEF,OAAO,CAAC,iBAAiB,CAiBvB;IAEF;;;OAGG;IACI,wBAAwB,IAAI,qBAAqB;CAGzD"}
1
+ {"version":3,"file":"DevToolsPluginClient.d.ts","sourceRoot":"","sources":["../../src/devtools/DevToolsPluginClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAG5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,OAAO,KAAK,EAAE,cAAc,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AASpF;;;GAGG;AACH,8BAAsB,oBAAoB;aAYtB,cAAc,EAAE,cAAc;IAC9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IAZ3B,SAAS,CAAC,YAAY,EAAE,YAAY,CAAsB;IAE1D,OAAO,CAAC,MAAM,CAAC,cAAc,CAAsD;IACnF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8D;IAEtF,SAAS,CAAC,QAAQ,UAAS;IAC3B,SAAS,CAAC,OAAO,SAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CACR;gBAGT,cAAc,EAAE,cAAc,EAC7B,OAAO,CAAC,yCAA6B;IAKxD;;;OAGG;IACU,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAQvC;;OAEG;IACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAWxC;;;;OAIG;IACI,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG;YAQhC,eAAe;IAS7B;;;;OAIG;IACI,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,GAAG,iBAAiB;IAI7F;;;;OAIG;IACI,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAIpF;;OAEG;IACI,WAAW,IAAI,OAAO;IAI7B;;OAEG;IACH,SAAS,CAAC,YAAY,IAAI,OAAO,CAAC,SAAS,CAAC;IAyB5C,SAAS,CAAC,aAAa,UAAW,qBAAqB,UAErD;IAEF,OAAO,CAAC,iBAAiB,CAiBvB;IAEF;;;OAGG;IACI,wBAAwB,IAAI,qBAAqB;CAGzD"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blobUtils.d.ts","sourceRoot":"","sources":["../../src/utils/blobUtils.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,WAAW,CAAC,CAKvE;AAED;;GAEG;AACH,wBAAsB,4BAA4B,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,WAAW,CAAC,CASnF"}
@@ -1,5 +1,49 @@
1
- export declare function installFormDataPatch(formData: typeof FormData): {
2
- new (form?: HTMLFormElement | undefined, submitter?: HTMLElement | null | undefined): FormData;
3
- prototype: FormData;
1
+ export type ExpoFormDataValue = string | Blob;
2
+ export type ExpoFormDataPart = {
3
+ string: string;
4
+ headers: {
5
+ [name: string]: string;
6
+ };
7
+ } | {
8
+ blob: Blob;
9
+ headers: {
10
+ [name: string]: string;
11
+ };
12
+ name?: string | undefined;
13
+ type?: string | undefined;
14
+ } | {
15
+ uri: string;
16
+ headers: {
17
+ [name: string]: string;
18
+ };
19
+ name?: string | undefined;
20
+ type?: string | undefined;
4
21
  };
22
+ export declare class ExpoFormData {
23
+ constructor();
24
+ append(name: string, value: string): void;
25
+ append(name: string, value: Blob, filename?: string): void;
26
+ delete(name: string): void;
27
+ get(name: string): FormDataEntryValue | null;
28
+ getAll(name: string): FormDataEntryValue[];
29
+ has(name: string): boolean;
30
+ set(name: string, value: string): void;
31
+ set(name: string, value: Blob, filename?: string): void;
32
+ append(name: string, value: {
33
+ uri: string;
34
+ name?: string;
35
+ type?: string;
36
+ }): void;
37
+ set(name: string, value: {
38
+ uri: string;
39
+ name?: string;
40
+ type?: string;
41
+ }): void;
42
+ forEach(callback: (value: FormDataEntryValue, key: string, iterable: FormData) => void, thisArg?: unknown): void;
43
+ keys(): IterableIterator<string>;
44
+ values(): IterableIterator<FormDataEntryValue>;
45
+ entries(): IterableIterator<[string, FormDataEntryValue]>;
46
+ [Symbol.iterator](): IterableIterator<[string, FormDataEntryValue]>;
47
+ }
48
+ export declare function installFormDataPatch(formData: typeof FormData): typeof ExpoFormData;
5
49
  //# sourceMappingURL=FormData.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FormData.d.ts","sourceRoot":"","sources":["../../src/winter/FormData.ts"],"names":[],"mappings":"AA6BA,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,OAAO,QAAQ;;;EA8H7D"}
1
+ {"version":3,"file":"FormData.d.ts","sourceRoot":"","sources":["../../src/winter/FormData.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAAC;AAC9C,MAAM,MAAM,gBAAgB,GACxB;IAEE,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACrC,GACD;IAEE,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACpC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B,GACD;IAEE,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACpC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B,CAAC;AAEN,MAAM,CAAC,OAAO,OAAO,YAAY;;IAE/B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IACzC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAC1D,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAC1B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI;IAC5C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAC1C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAC1B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IACtC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAGvD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAChF,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAG7E,OAAO,CACL,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,KAAK,IAAI,EAC9E,OAAO,CAAC,EAAE,OAAO,GAChB,IAAI;IACP,IAAI,IAAI,gBAAgB,CAAC,MAAM,CAAC;IAChC,MAAM,IAAI,gBAAgB,CAAC,kBAAkB,CAAC;IAC9C,OAAO,IAAI,gBAAgB,CAAC,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IACzD,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CACpE;AAyBD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,OAAO,QAAQ,GAAG,OAAO,YAAY,CA+HnF"}
@@ -1,11 +1,14 @@
1
1
  import { ReadableStream } from 'web-streams-polyfill';
2
2
  import type { NativeResponse } from './NativeRequest';
3
3
  declare const ConcreteNativeResponse: typeof NativeResponse;
4
+ export type AbortSubscriptionCleanupFunction = () => void;
4
5
  /**
5
6
  * A response implementation for the `fetch.Response` API.
6
7
  */
7
8
  export declare class FetchResponse extends ConcreteNativeResponse implements Response {
9
+ private readonly abortCleanupFunction;
8
10
  private streamingStarted;
11
+ constructor(abortCleanupFunction: AbortSubscriptionCleanupFunction);
9
12
  get body(): ReadableStream<Uint8Array> | null;
10
13
  get headers(): Headers;
11
14
  get ok(): boolean;
@@ -16,7 +19,7 @@ export declare class FetchResponse extends ConcreteNativeResponse implements Res
16
19
  toString(): string;
17
20
  toJSON(): object;
18
21
  clone(): FetchResponse;
19
- private removeAllRegisteredListeners;
22
+ private finalize;
20
23
  }
21
24
  export {};
22
25
  //# sourceMappingURL=FetchResponse.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FetchResponse.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/FetchResponse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,QAAA,MAAM,sBAAsB,uBAA0D,CAAC;AAEvF;;GAEG;AACH,qBAAa,aAAc,SAAQ,sBAAuB,YAAW,QAAQ;IAC3E,OAAO,CAAC,gBAAgB,CAAS;IAEjC,IAAI,IAAI,IAAI,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CA6B5C;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,EAAE,IAAI,OAAO,CAEhB;IAED,SAAgB,IAAI,aAAa;IAE3B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAKrB,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IAY7B,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC;IAK1B,QAAQ,IAAI,MAAM;IAIlB,MAAM,IAAI,MAAM;IAShB,KAAK,IAAI,aAAa;IAItB,OAAO,CAAC,4BAA4B;CAKrC"}
1
+ {"version":3,"file":"FetchResponse.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/FetchResponse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,QAAA,MAAM,sBAAsB,uBAA0D,CAAC;AAEvF,MAAM,MAAM,gCAAgC,GAAG,MAAM,IAAI,CAAC;AAE1D;;GAEG;AACH,qBAAa,aAAc,SAAQ,sBAAuB,YAAW,QAAQ;IAG/D,OAAO,CAAC,QAAQ,CAAC,oBAAoB;IAFjD,OAAO,CAAC,gBAAgB,CAAS;gBAEJ,oBAAoB,EAAE,gCAAgC;IAKnF,IAAI,IAAI,IAAI,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CA0B5C;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,EAAE,IAAI,OAAO,CAEhB;IAED,SAAgB,IAAI,aAAa;IAE3B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAKrB,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IAY7B,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC;IAK1B,QAAQ,IAAI,MAAM;IAIlB,MAAM,IAAI,MAAM;IAShB,KAAK,IAAI,aAAa;IAItB,OAAO,CAAC,QAAQ,CAQd;CACH"}
@@ -13,6 +13,7 @@ export type NativeResponseEvents = {
13
13
  didReceiveResponseData(data: Uint8Array): void;
14
14
  didComplete(): void;
15
15
  didFailWithError(error: string): void;
16
+ readyForJSFinalization(): void;
16
17
  };
17
18
  export declare class NativeResponse extends SharedObject<NativeResponseEvents> {
18
19
  readonly bodyUsed: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"NativeRequest.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/NativeRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,MAAM,iBAAiB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;AAEnD,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,YAAY;IAC9C,KAAK,CACV,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,iBAAiB,EAC9B,WAAW,EAAE,UAAU,GAAG,IAAI,GAC7B,OAAO,CAAC,cAAc,CAAC;IACnB,MAAM,IAAI,IAAI;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/C,WAAW,IAAI,IAAI,CAAC;IACpB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC,CAAC;AAEF,MAAM,CAAC,OAAO,OAAO,cAAe,SAAQ,YAAY,CAAC,oBAAoB,CAAC;IAC5E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,cAAc,IAAI,IAAI;IACtB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IACrC,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC;IACnC,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CACxB"}
1
+ {"version":3,"file":"NativeRequest.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/NativeRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,MAAM,iBAAiB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;AAEnD,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,YAAY;IAC9C,KAAK,CACV,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,iBAAiB,EAC9B,WAAW,EAAE,UAAU,GAAG,IAAI,GAC7B,OAAO,CAAC,cAAc,CAAC;IACnB,MAAM,IAAI,IAAI;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/C,WAAW,IAAI,IAAI,CAAC;IACpB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,sBAAsB,IAAI,IAAI,CAAC;CAChC,CAAC;AAEF,MAAM,CAAC,OAAO,OAAO,cAAe,SAAQ,YAAY,CAAC,oBAAoB,CAAC;IAC5E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,cAAc,IAAI,IAAI;IACtB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IACrC,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC;IACnC,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CACxB"}
@@ -4,20 +4,6 @@ import { type NativeHeadersType } from './NativeRequest';
4
4
  * convert a ReadableStream to a Uint8Array
5
5
  */
6
6
  export declare function convertReadableStreamToUint8ArrayAsync(stream: ReadableStream<Uint8Array>): Promise<Uint8Array>;
7
- /**
8
- * Convert FormData to string
9
- *
10
- * `uri` is not supported for React Native's FormData.
11
- * `blob` is not supported for standard FormData.
12
- */
13
- export declare function convertFormData(formData: FormData, boundary?: string): {
14
- body: string;
15
- boundary: string;
16
- };
17
- /**
18
- * Create mutipart boundary
19
- */
20
- export declare function createBoundary(): string;
21
7
  /**
22
8
  * Normalize a BodyInit object to a Uint8Array for NativeRequest
23
9
  */
@@ -1 +1 @@
1
- {"version":3,"file":"RequestUtils.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/RequestUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD;;GAEG;AACH,wBAAsB,sCAAsC,CAC1D,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GACjC,OAAO,CAAC,UAAU,CAAC,CAsBrB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,GAAE,MAAyB,GAClC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CA2BpC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAOvC;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,GAChC,OAAO,CAAC;IAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;CAAE,CAAC,CA0C7E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,iBAAiB,CAe/F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,iBAAiB,GAC5B,iBAAiB,CAYnB"}
1
+ {"version":3,"file":"RequestUtils.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/RequestUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAIzD;;GAEG;AACH,wBAAsB,sCAAsC,CAC1D,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GACjC,OAAO,CAAC,UAAU,CAAC,CAsBrB;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,GAChC,OAAO,CAAC;IAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;CAAE,CAAC,CA6C7E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,iBAAiB,CAe/F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,iBAAiB,GAC5B,iBAAiB,CAYnB"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Convert FormData to Uint8Array with a boundary
3
+ *
4
+ * `uri` is not supported for React Native's FormData.
5
+ * `blob` is not supported for standard FormData.
6
+ */
7
+ export declare function convertFormDataAsync(formData: FormData, boundary?: string): Promise<{
8
+ body: Uint8Array;
9
+ boundary: string;
10
+ }>;
11
+ /**
12
+ * Create mutipart boundary
13
+ */
14
+ export declare function createBoundary(): string;
15
+ /**
16
+ * Merge Uint8Arrays into a single Uint8Array
17
+ */
18
+ export declare function joinUint8Arrays(arrays: Uint8Array[]): Uint8Array;
19
+ //# sourceMappingURL=convertFormData.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convertFormData.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/convertFormData.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,GAAE,MAAyB,GAClC,OAAO,CAAC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAsCjD;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAOvC;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,CAWhE"}
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/fetch.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,wBAAsB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAoCxF"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/fetch.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAyC,MAAM,iBAAiB,CAAC;AAGvF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,wBAAsB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAqCxF"}
@@ -17,7 +17,7 @@
17
17
  "expo-apple-authentication": "~7.1.2",
18
18
  "expo-application": "~6.0.1",
19
19
  "expo-asset": "~11.0.1",
20
- "expo-audio": "~0.3.0",
20
+ "expo-audio": "~0.3.1",
21
21
  "expo-auth-session": "~6.0.1",
22
22
  "expo-av": "~15.0.1",
23
23
  "expo-background-fetch": "~13.0.3",
@@ -26,7 +26,7 @@
26
26
  "expo-brightness": "~13.0.2",
27
27
  "expo-build-properties": "~0.13.1",
28
28
  "expo-calendar": "~14.0.5",
29
- "expo-camera": "~16.0.9",
29
+ "expo-camera": "~16.0.10",
30
30
  "expo-cellular": "~7.0.1",
31
31
  "expo-checkbox": "~4.0.0",
32
32
  "expo-clipboard": "~7.0.0",
@@ -37,7 +37,7 @@
37
37
  "expo-device": "~7.0.1",
38
38
  "expo-document-picker": "~13.0.1",
39
39
  "expo-face-detector": "~13.0.1",
40
- "expo-file-system": "~18.0.5",
40
+ "expo-file-system": "~18.0.6",
41
41
  "expo-font": "~13.0.1",
42
42
  "expo-gl": "~15.0.2",
43
43
  "expo-google-app-auth": "~8.3.0",
@@ -57,12 +57,12 @@
57
57
  "expo-mail-composer": "~14.0.1",
58
58
  "expo-media-library": "~17.0.3",
59
59
  "expo-module-template": "~10.15.12",
60
- "expo-modules-core": "~2.1.1",
60
+ "expo-modules-core": "~2.1.2",
61
61
  "expo-navigation-bar": "~4.0.6",
62
- "expo-network": "~7.0.3",
62
+ "expo-network": "~7.0.4",
63
63
  "expo-notifications": "~0.29.11",
64
64
  "expo-print": "~14.0.2",
65
- "expo-router": "~4.0.12",
65
+ "expo-router": "~4.0.13",
66
66
  "expo-screen-capture": "~7.0.0",
67
67
  "expo-screen-orientation": "~8.0.1",
68
68
  "expo-secure-store": "~14.0.0",
@@ -59,8 +59,13 @@ internal final class NativeResponse: SharedObject, ExpoURLSessionTaskDelegate {
59
59
  }
60
60
 
61
61
  func emitRequestCanceled() {
62
- error = FetchRequestCanceledException()
62
+ let error = FetchRequestCanceledException()
63
+ self.error = error
64
+ if state == .bodyStreamingStarted {
65
+ emit(event: "didFailWithError", arguments: error.localizedDescription)
66
+ }
63
67
  state = .errorReceived
68
+ emit(event: "readyForJSFinalization")
64
69
  }
65
70
 
66
71
  /**
@@ -197,5 +202,7 @@ internal final class NativeResponse: SharedObject, ExpoURLSessionTaskDelegate {
197
202
  } else {
198
203
  state = .bodyCompleted
199
204
  }
205
+
206
+ emit(event: "readyForJSFinalization")
200
207
  }
201
208
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo",
3
- "version": "52.0.18",
3
+ "version": "52.0.19",
4
4
  "description": "The Expo SDK",
5
5
  "main": "src/Expo.ts",
6
6
  "module": "src/Expo.ts",
@@ -68,20 +68,20 @@
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": "0.22.5",
71
+ "@expo/cli": "0.22.6",
72
72
  "@expo/config": "~10.0.6",
73
73
  "@expo/config-plugins": "~9.0.12",
74
- "@expo/fingerprint": "0.11.3",
75
- "@expo/metro-config": "0.19.7",
74
+ "@expo/fingerprint": "0.11.4",
75
+ "@expo/metro-config": "0.19.8",
76
76
  "@expo/vector-icons": "^14.0.0",
77
77
  "babel-preset-expo": "~12.0.4",
78
78
  "expo-asset": "~11.0.1",
79
79
  "expo-constants": "~17.0.3",
80
- "expo-file-system": "~18.0.5",
80
+ "expo-file-system": "~18.0.6",
81
81
  "expo-font": "~13.0.1",
82
82
  "expo-keep-awake": "~14.0.1",
83
83
  "expo-modules-autolinking": "2.0.4",
84
- "expo-modules-core": "2.1.1",
84
+ "expo-modules-core": "2.1.2",
85
85
  "fbemitter": "^3.0.0",
86
86
  "web-streams-polyfill": "^3.3.2",
87
87
  "whatwg-url-without-unicode": "8.0.0-3"
@@ -112,5 +112,5 @@
112
112
  "optional": true
113
113
  }
114
114
  },
115
- "gitHead": "02366411c0bb2cb0055d7f4c0813bdaed1fa9bf8"
115
+ "gitHead": "637acf537a92ac6209bfced71e097ba736b43fef"
116
116
  }
@@ -3,9 +3,9 @@ import { EventEmitter, EventSubscription } from 'fbemitter';
3
3
  import { MessageFramePacker } from './MessageFramePacker';
4
4
  import { WebSocketBackingStore } from './WebSocketBackingStore';
5
5
  import { WebSocketWithReconnect } from './WebSocketWithReconnect';
6
- import { blobToArrayBufferAsync } from './blobUtils';
7
6
  import type { ConnectionInfo, DevToolsPluginClientOptions } from './devtools.types';
8
7
  import * as logger from './logger';
8
+ import { blobToArrayBufferAsync } from '../utils/blobUtils';
9
9
 
10
10
  interface MessageFramePackerMessageKey {
11
11
  pluginName: string;
@@ -22,7 +22,7 @@
22
22
  *
23
23
  */
24
24
 
25
- import { blobToArrayBufferAsync } from './blobUtils';
25
+ import { blobToArrayBufferAsync } from '../utils/blobUtils';
26
26
 
27
27
  type MessageKeyTypeBase = string | object;
28
28
  type PayloadType = Uint8Array | string | number | null | undefined | object | ArrayBuffer | Blob;
@@ -6,6 +6,54 @@ type ReactNativeFormDataInternal = FormData & {
6
6
  _parts: [string, string | Blob][];
7
7
  };
8
8
 
9
+ export type ExpoFormDataValue = string | Blob;
10
+ export type ExpoFormDataPart =
11
+ | {
12
+ // WinterCG FormData string
13
+ string: string;
14
+ headers: { [name: string]: string };
15
+ }
16
+ | {
17
+ // WinterCG FormData blob in our wrapped form
18
+ blob: Blob;
19
+ headers: { [name: string]: string };
20
+ name?: string | undefined;
21
+ type?: string | undefined;
22
+ }
23
+ | {
24
+ // React Native proprietary local file
25
+ uri: string;
26
+ headers: { [name: string]: string };
27
+ name?: string | undefined;
28
+ type?: string | undefined;
29
+ };
30
+
31
+ export declare class ExpoFormData {
32
+ constructor();
33
+ append(name: string, value: string): void;
34
+ append(name: string, value: Blob, filename?: string): void;
35
+ delete(name: string): void;
36
+ get(name: string): FormDataEntryValue | null;
37
+ getAll(name: string): FormDataEntryValue[];
38
+ has(name: string): boolean;
39
+ set(name: string, value: string): void;
40
+ set(name: string, value: Blob, filename?: string): void;
41
+
42
+ // React Native proprietary local file
43
+ append(name: string, value: { uri: string; name?: string; type?: string }): void;
44
+ set(name: string, value: { uri: string; name?: string; type?: string }): void;
45
+
46
+ // iterable
47
+ forEach(
48
+ callback: (value: FormDataEntryValue, key: string, iterable: FormData) => void,
49
+ thisArg?: unknown
50
+ ): void;
51
+ keys(): IterableIterator<string>;
52
+ values(): IterableIterator<FormDataEntryValue>;
53
+ entries(): IterableIterator<[string, FormDataEntryValue]>;
54
+ [Symbol.iterator](): IterableIterator<[string, FormDataEntryValue]>;
55
+ }
56
+
9
57
  function ensureMinArgCount(name: string, args: any[], expected: number) {
10
58
  if (args.length < expected) {
11
59
  const argName = expected === 2 ? 'arguments' : 'argument';
@@ -16,29 +64,32 @@ function ensureMinArgCount(name: string, args: any[], expected: number) {
16
64
  }
17
65
  }
18
66
 
19
- function normalizeArgs(name: string, value: any): [string, File | string] {
20
- if (typeof value !== 'object') {
67
+ function normalizeArgs(
68
+ name: string,
69
+ value: any,
70
+ blobFilename: string | undefined
71
+ ): [string, File | string] {
72
+ if (value instanceof Blob) {
73
+ value = { type: value.type, name: blobFilename || 'blob', blob: value };
74
+ } else if (typeof value !== 'object') {
21
75
  value = String(value);
22
76
  }
23
- // TODO: Add Blob normalization in the future, right now this isn't supported to ensure parity with the rest of the FormData
24
- // implementation in React Native.
25
- // https://github.com/facebook/react-native/blob/42dcfdd2cdb59fe545523cb57db6ee32a96b9298/packages/react-native/Libraries/Network/FormData.js#L64
26
-
27
77
  return [String(name), value];
28
78
  }
29
79
 
30
- export function installFormDataPatch(formData: typeof FormData) {
31
- formData.prototype.append ??= function append(this: ReactNativeFormDataInternal, ...props) {
80
+ export function installFormDataPatch(formData: typeof FormData): typeof ExpoFormData {
81
+ formData.prototype.append = function append(this: ReactNativeFormDataInternal, ...props) {
32
82
  ensureMinArgCount('append', props, 2);
33
- const [name, value] = props;
34
- this._parts.push(normalizeArgs(name, value));
83
+ // @ts-ignore: When inferred FormData.append from React Native types, it does not support the 3rd blobFilename argument.
84
+ const [name, value, blobFilename] = props;
85
+ this._parts.push(normalizeArgs(name, value, blobFilename));
35
86
  };
36
87
 
37
88
  // @ts-ignore: DOM.iterable is disabled for jest compat
38
- formData.prototype.set ??= function set(this: ReactNativeFormDataInternal, ...props) {
89
+ formData.prototype.set = function set(this: ReactNativeFormDataInternal, ...props) {
39
90
  ensureMinArgCount('set', props, 2);
40
- const [name, value] = props;
41
- const args = normalizeArgs(name, value);
91
+ const [name, value, blobFilename] = props;
92
+ const args = normalizeArgs(name, value, blobFilename);
42
93
  let replaced = false;
43
94
 
44
95
  for (let i = 0; i < this._parts.length; i++) {
@@ -121,7 +172,7 @@ export function installFormDataPatch(formData: typeof FormData) {
121
172
 
122
173
  // Required for RSC: https://github.com/facebook/react/blob/985747f81033833dca22f30b0c04704dd4bd3714/packages/react-server/src/ReactFlightServer.js#L2117
123
174
  // @ts-ignore: DOM.iterable is disabled for jest compat
124
- formData.prototype.entries = function* entries(
175
+ formData.prototype.entries ??= function* entries(
125
176
  this: ReactNativeFormDataInternal
126
177
  ): IterableIterator<[string, FormDataEntryValue]> {
127
178
  for (const part of this._parts) {
@@ -152,5 +203,5 @@ export function installFormDataPatch(formData: typeof FormData) {
152
203
  // @ts-ignore: DOM.iterable is disabled for jest compat
153
204
  formData.prototype[Symbol.iterator] = formData.prototype.entries;
154
205
 
155
- return formData;
206
+ return formData as unknown as typeof ExpoFormData;
156
207
  }
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable no-global-assign */
2
- import { installFormDataPatch } from '../FormData';
2
+ import { installFormDataPatch, type ExpoFormData } from '../FormData';
3
3
 
4
4
  const jestFormDataPolyfill = FormData;
5
5
 
@@ -48,6 +48,16 @@ describe('FormData', () => {
48
48
  a.delete('a');
49
49
  expect(a.get('a')).toBe(null);
50
50
  });
51
+
52
+ it(`supports react-native local uri`, () => {
53
+ const a = new FormData() as ExpoFormData;
54
+ a.append('a', { uri: 'file:///path/to/test.jpg', type: 'image/jpeg', name: 'test.jpg' });
55
+ expect(a.get('a')).toEqual({
56
+ uri: 'file:///path/to/test.jpg',
57
+ type: 'image/jpeg',
58
+ name: 'test.jpg',
59
+ });
60
+ });
51
61
  });
52
62
 
53
63
  describe('getters', () => {
@@ -5,12 +5,19 @@ import type { NativeResponse } from './NativeRequest';
5
5
 
6
6
  const ConcreteNativeResponse = ExpoFetchModule.NativeResponse as typeof NativeResponse;
7
7
 
8
+ export type AbortSubscriptionCleanupFunction = () => void;
9
+
8
10
  /**
9
11
  * A response implementation for the `fetch.Response` API.
10
12
  */
11
13
  export class FetchResponse extends ConcreteNativeResponse implements Response {
12
14
  private streamingStarted = false;
13
15
 
16
+ constructor(private readonly abortCleanupFunction: AbortSubscriptionCleanupFunction) {
17
+ super();
18
+ this.addListener('readyForJSFinalization', this.finalize);
19
+ }
20
+
14
21
  get body(): ReadableStream<Uint8Array> | null {
15
22
  const response = this;
16
23
  return new ReadableStream({
@@ -20,12 +27,10 @@ export class FetchResponse extends ConcreteNativeResponse implements Response {
20
27
  });
21
28
 
22
29
  response.addListener('didComplete', () => {
23
- response.removeAllRegisteredListeners();
24
30
  controller.close();
25
31
  });
26
32
 
27
33
  response.addListener('didFailWithError', (error: string) => {
28
- response.removeAllRegisteredListeners();
29
34
  controller.error(new Error(error));
30
35
  });
31
36
  },
@@ -36,7 +41,6 @@ export class FetchResponse extends ConcreteNativeResponse implements Response {
36
41
  }
37
42
  },
38
43
  cancel(reason) {
39
- response.removeAllRegisteredListeners();
40
44
  response.cancelStreaming(String(reason));
41
45
  },
42
46
  });
@@ -91,9 +95,13 @@ export class FetchResponse extends ConcreteNativeResponse implements Response {
91
95
  throw new Error('Not implemented');
92
96
  }
93
97
 
94
- private removeAllRegisteredListeners() {
98
+ private finalize = (): void => {
99
+ this.removeListener('readyForJSFinalization', this.finalize);
100
+
101
+ this.abortCleanupFunction();
102
+
95
103
  this.removeAllListeners('didReceiveResponseData');
96
104
  this.removeAllListeners('didComplete');
97
105
  this.removeAllListeners('didFailWithError');
98
- }
106
+ };
99
107
  }
@@ -21,6 +21,7 @@ export type NativeResponseEvents = {
21
21
  didReceiveResponseData(data: Uint8Array): void;
22
22
  didComplete(): void;
23
23
  didFailWithError(error: string): void;
24
+ readyForJSFinalization(): void;
24
25
  };
25
26
 
26
27
  export declare class NativeResponse extends SharedObject<NativeResponseEvents> {
@@ -1,6 +1,8 @@
1
1
  import { ReadableStream } from 'web-streams-polyfill';
2
2
 
3
3
  import { type NativeHeadersType } from './NativeRequest';
4
+ import { convertFormDataAsync } from './convertFormData';
5
+ import { blobToArrayBufferAsync } from '../../utils/blobUtils';
4
6
 
5
7
  /**
6
8
  * convert a ReadableStream to a Uint8Array
@@ -31,56 +33,6 @@ export async function convertReadableStreamToUint8ArrayAsync(
31
33
  return result;
32
34
  }
33
35
 
34
- /**
35
- * Convert FormData to string
36
- *
37
- * `uri` is not supported for React Native's FormData.
38
- * `blob` is not supported for standard FormData.
39
- */
40
- export function convertFormData(
41
- formData: FormData,
42
- boundary: string = createBoundary()
43
- ): { body: string; boundary: string } {
44
- // @ts-expect-error: React Native's FormData is not compatible with the web's FormData
45
- if (typeof formData.getParts !== 'function') {
46
- throw new Error('Unsupported FormData implementation');
47
- }
48
- // @ts-expect-error: React Native's FormData is not compatible with the web's FormData
49
- const parts: FormDataPart[] = formData.getParts();
50
-
51
- const results: string[] = [];
52
- for (const entry of parts) {
53
- results.push(`--${boundary}\r\n`);
54
- for (const [headerKey, headerValue] of Object.entries(entry.headers)) {
55
- results.push(`${headerKey}: ${headerValue}\r\n`);
56
- }
57
- results.push(`\r\n`);
58
- // @ts-expect-error: TypeScript doesn't know about the `string` property
59
- if (entry.string != null) {
60
- // @ts-expect-error: TypeScript doesn't know about the `string` property
61
- results.push(entry.string);
62
- } else {
63
- throw new Error('Unsupported FormDataPart implementation');
64
- }
65
- results.push(`\r\n`);
66
- }
67
-
68
- results.push(`--${boundary}--\r\n`);
69
- return { body: results.join(''), boundary };
70
- }
71
-
72
- /**
73
- * Create mutipart boundary
74
- */
75
- export function createBoundary(): string {
76
- const boundaryChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
77
- let boundary = '----ExpoFetchFormBoundary';
78
- for (let i = 0; i < 16; i++) {
79
- boundary += boundaryChars.charAt(Math.floor(Math.random() * boundaryChars.length));
80
- }
81
- return boundary;
82
- }
83
-
84
36
  /**
85
37
  * Normalize a BodyInit object to a Uint8Array for NativeRequest
86
38
  */
@@ -105,7 +57,10 @@ export async function normalizeBodyInitAsync(
105
57
  }
106
58
 
107
59
  if (body instanceof Blob) {
108
- return { body: new Uint8Array(await body.arrayBuffer()) };
60
+ return {
61
+ body: new Uint8Array(await blobToArrayBufferAsync(body)),
62
+ overriddenHeaders: [['Content-Type', body.type]],
63
+ };
109
64
  }
110
65
 
111
66
  if (body instanceof URLSearchParams) {
@@ -119,10 +74,10 @@ export async function normalizeBodyInitAsync(
119
74
  }
120
75
 
121
76
  if (body instanceof FormData) {
122
- const { body: result, boundary } = convertFormData(body);
123
- const encoder = new TextEncoder();
77
+ const { body: result, boundary } = await convertFormDataAsync(body);
78
+
124
79
  return {
125
- body: encoder.encode(result),
80
+ body: result,
126
81
  overriddenHeaders: [['Content-Type', `multipart/form-data; boundary=${boundary}`]],
127
82
  };
128
83
  }
@@ -7,58 +7,12 @@ import { ReadableStream } from 'web-streams-polyfill';
7
7
 
8
8
  import { type NativeHeadersType } from '../NativeRequest';
9
9
  import {
10
- convertFormData,
11
10
  convertReadableStreamToUint8ArrayAsync,
12
- createBoundary,
13
11
  normalizeBodyInitAsync,
14
12
  normalizeHeadersInit,
15
13
  overrideHeaders,
16
14
  } from '../RequestUtils';
17
15
 
18
- describe(convertFormData, () => {
19
- it('should convert react-native FormData to a string with boundary', () => {
20
- const formData = new RNFormData();
21
- formData.append('foo', 'foo');
22
- formData.append('bar', 'bar');
23
- const boundary = '----ExpoFetchFormBoundary0000000000000000';
24
- const { body, boundary: resultBoundary } = convertFormData(formData, boundary);
25
- expect(body).toMatchInlineSnapshot(`
26
- "------ExpoFetchFormBoundary0000000000000000
27
- content-disposition: form-data; name="foo"
28
-
29
- foo
30
- ------ExpoFetchFormBoundary0000000000000000
31
- content-disposition: form-data; name="bar"
32
-
33
- bar
34
- ------ExpoFetchFormBoundary0000000000000000--
35
- "
36
- `);
37
- expect(resultBoundary).toBe(boundary);
38
- });
39
-
40
- it('should throw an error if the react-native FormData passing an uri', () => {
41
- const formData = new RNFormData();
42
- formData.append('foo', {
43
- uri: 'file:/path/to/test.jpg',
44
- type: 'image/jpeg',
45
- name: 'test.jpg',
46
- });
47
- expect(() => {
48
- convertFormData(formData);
49
- }).toThrow(/Unsupported FormDataPart implementation/);
50
- });
51
-
52
- it('should throw an error if passing brower compatible FormData', () => {
53
- const formData = new globalThis.FormData();
54
- formData.append('foo', 'foo');
55
- formData.append('blob', new Blob());
56
- expect(() => {
57
- convertFormData(formData);
58
- }).toThrow(/Unsupported FormData implementation/);
59
- });
60
- });
61
-
62
16
  describe(convertReadableStreamToUint8ArrayAsync, () => {
63
17
  it('should convert a readable stream to a Uint8Array', async () => {
64
18
  const stream = new ReadableStream<Uint8Array>({
@@ -95,12 +49,6 @@ describe(convertReadableStreamToUint8ArrayAsync, () => {
95
49
  });
96
50
  });
97
51
 
98
- describe(createBoundary, () => {
99
- it('should return a boundary string with ExpoFetchFormBoundary prefix plus 16 random chars', () => {
100
- expect(createBoundary()).toMatch(/^----ExpoFetchFormBoundary[\w]{16}$/);
101
- });
102
- });
103
-
104
52
  describe(normalizeBodyInitAsync, () => {
105
53
  let originalFormData;
106
54
  beforeAll(() => {
@@ -0,0 +1,111 @@
1
+ import RNFormData from 'react-native/Libraries/Network/FormData';
2
+ import { TextDecoder, TextEncoder } from 'util';
3
+
4
+ import { installFormDataPatch } from '../../FormData';
5
+ import { createBoundary, convertFormDataAsync, joinUint8Arrays } from '../convertFormData';
6
+
7
+ // @ts-ignore - TextDecoder and TextEncoder are not defined in native jest environments.
8
+ globalThis.TextDecoder ??= TextDecoder;
9
+ globalThis.TextEncoder ??= TextEncoder;
10
+
11
+ const ExpoFormData = installFormDataPatch(RNFormData);
12
+
13
+ describe(convertFormDataAsync, () => {
14
+ it('should convert string', async () => {
15
+ const formData = new ExpoFormData();
16
+ formData.append('foo', 'foo');
17
+ formData.append('bar', 'bar');
18
+ const boundary = '----ExpoFetchFormBoundary0000000000000000';
19
+ const { body, boundary: resultBoundary } = await convertFormDataAsync(formData, boundary);
20
+ expect(new TextDecoder().decode(body)).toMatchInlineSnapshot(`
21
+ "------ExpoFetchFormBoundary0000000000000000
22
+ content-disposition: form-data; name="foo"
23
+
24
+ foo
25
+ ------ExpoFetchFormBoundary0000000000000000
26
+ content-disposition: form-data; name="bar"
27
+
28
+ bar
29
+ ------ExpoFetchFormBoundary0000000000000000--
30
+ "
31
+ `);
32
+ expect(resultBoundary).toBe(boundary);
33
+ });
34
+
35
+ it(`should convert blob`, async () => {
36
+ const formData = new ExpoFormData();
37
+ const blob = new Blob(['hello blob'], { type: 'text/plain' });
38
+ formData.append('blob', blob, 'blobFile');
39
+ const boundary = '----ExpoFetchFormBoundary0000000000000000';
40
+ const { body, boundary: resultBoundary } = await convertFormDataAsync(formData, boundary);
41
+ expect(new TextDecoder().decode(body)).toMatchInlineSnapshot(`
42
+ "------ExpoFetchFormBoundary0000000000000000
43
+ content-disposition: form-data; name="blob"; filename="blobFile"; filename*=utf-8''blobFile
44
+ content-type: text/plain
45
+
46
+ hello blob
47
+ ------ExpoFetchFormBoundary0000000000000000--
48
+ "
49
+ `);
50
+ expect(resultBoundary).toBe(boundary);
51
+ });
52
+
53
+ it(`should convert expo-file-system FileBlob`, async () => {
54
+ const formData = new ExpoFormData();
55
+ const mockFileBlob = {
56
+ file: {
57
+ bytes: () => new Uint8Array([65, 66, 67]),
58
+ },
59
+ };
60
+ // @ts-ignore
61
+ formData.append('blob', mockFileBlob);
62
+ const boundary = '----ExpoFetchFormBoundary0000000000000000';
63
+ const { body, boundary: resultBoundary } = await convertFormDataAsync(formData, boundary);
64
+ expect(new TextDecoder().decode(body)).toMatchInlineSnapshot(`
65
+ "------ExpoFetchFormBoundary0000000000000000
66
+ content-disposition: form-data; name="blob"
67
+
68
+ ABC
69
+ ------ExpoFetchFormBoundary0000000000000000--
70
+ "
71
+ `);
72
+ expect(resultBoundary).toBe(boundary);
73
+ });
74
+
75
+ it('should throw an error if the react-native FormData passing an uri', async () => {
76
+ const formData = new ExpoFormData();
77
+ formData.append('foo', {
78
+ uri: 'file:/path/to/test.jpg',
79
+ type: 'image/jpeg',
80
+ name: 'test.jpg',
81
+ });
82
+ expect(convertFormDataAsync(formData)).rejects.toThrow(
83
+ /Unsupported FormDataPart implementation/
84
+ );
85
+ });
86
+ });
87
+
88
+ describe(createBoundary, () => {
89
+ it('should return a boundary string with ExpoFetchFormBoundary prefix plus 16 random chars', () => {
90
+ expect(createBoundary()).toMatch(/^----ExpoFetchFormBoundary[\w]{16}$/);
91
+ });
92
+ });
93
+
94
+ describe(joinUint8Arrays, () => {
95
+ it(`should join multiple uint8 arrays`, () => {
96
+ const array1 = new Uint8Array([1, 2]);
97
+ const array2 = new Uint8Array([3, 4]);
98
+ const result = joinUint8Arrays([array1, array2]);
99
+ const expected = new Uint8Array([1, 2, 3, 4]);
100
+ expect(result).toEqual(expected);
101
+ });
102
+
103
+ it(`should join 0 size arrays correctly`, () => {
104
+ const array1 = new Uint8Array([1, 2]);
105
+ const array2 = new Uint8Array([]);
106
+ const array3 = new Uint8Array([3, 4]);
107
+ const result = joinUint8Arrays([array1, array2, array3]);
108
+ const expected = new Uint8Array([1, 2, 3, 4]);
109
+ expect(result).toEqual(expected);
110
+ });
111
+ });
@@ -0,0 +1,78 @@
1
+ import { blobToArrayBufferAsync } from '../../utils/blobUtils';
2
+
3
+ /**
4
+ * Convert FormData to Uint8Array with a boundary
5
+ *
6
+ * `uri` is not supported for React Native's FormData.
7
+ * `blob` is not supported for standard FormData.
8
+ */
9
+ export async function convertFormDataAsync(
10
+ formData: FormData,
11
+ boundary: string = createBoundary()
12
+ ): Promise<{ body: Uint8Array; boundary: string }> {
13
+ // @ts-expect-error: React Native's FormData is not compatible with the web's FormData
14
+ if (typeof formData.getParts !== 'function') {
15
+ throw new Error('Unsupported FormData implementation');
16
+ }
17
+ // @ts-expect-error: React Native's FormData is not 100% compatible with ours
18
+ const parts: ExpoFormDataPart[] = formData.getParts();
19
+
20
+ const results: (Uint8Array | string)[] = [];
21
+ for (const entry of parts) {
22
+ results.push(`--${boundary}\r\n`);
23
+ for (const [headerKey, headerValue] of Object.entries(entry.headers)) {
24
+ results.push(`${headerKey}: ${headerValue}\r\n`);
25
+ }
26
+ results.push(`\r\n`);
27
+ if ('string' in entry) {
28
+ results.push(entry.string);
29
+ } else if ('file' in entry) {
30
+ results.push(entry.file.bytes());
31
+ } else if ('blob' in entry) {
32
+ results.push(new Uint8Array(await blobToArrayBufferAsync(entry.blob)));
33
+ } else {
34
+ throw new Error('Unsupported FormDataPart implementation');
35
+ }
36
+ results.push(`\r\n`);
37
+ }
38
+
39
+ results.push(`--${boundary}--\r\n`);
40
+ const arrays = results.map((result) => {
41
+ if (typeof result === 'string') {
42
+ // @ts-ignore: TextEncoder is not available in all environments
43
+ return new TextEncoder().encode(result);
44
+ } else {
45
+ return result;
46
+ }
47
+ }) as Uint8Array[];
48
+
49
+ return { body: joinUint8Arrays(arrays), boundary };
50
+ }
51
+
52
+ /**
53
+ * Create mutipart boundary
54
+ */
55
+ export function createBoundary(): string {
56
+ const boundaryChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
57
+ let boundary = '----ExpoFetchFormBoundary';
58
+ for (let i = 0; i < 16; i++) {
59
+ boundary += boundaryChars.charAt(Math.floor(Math.random() * boundaryChars.length));
60
+ }
61
+ return boundary;
62
+ }
63
+
64
+ /**
65
+ * Merge Uint8Arrays into a single Uint8Array
66
+ */
67
+ export function joinUint8Arrays(arrays: Uint8Array[]): Uint8Array {
68
+ const totalLength: number = arrays.reduce((acc: number, arr: Uint8Array) => acc + arr.length, 0);
69
+ const result: Uint8Array = new Uint8Array(totalLength);
70
+
71
+ let offset: number = 0;
72
+ arrays.forEach((array: Uint8Array) => {
73
+ result.set(array, offset);
74
+ offset += array.length;
75
+ });
76
+
77
+ return result;
78
+ }
@@ -1,12 +1,16 @@
1
1
  import { ExpoFetchModule } from './ExpoFetchModule';
2
2
  import { FetchError } from './FetchErrors';
3
- import { FetchResponse } from './FetchResponse';
3
+ import { FetchResponse, type AbortSubscriptionCleanupFunction } from './FetchResponse';
4
4
  import { NativeRequest, NativeRequestInit } from './NativeRequest';
5
5
  import { normalizeBodyInitAsync, normalizeHeadersInit, overrideHeaders } from './RequestUtils';
6
6
  import type { FetchRequestInit } from './fetch.types';
7
7
 
8
8
  export async function fetch(url: string, init?: FetchRequestInit): Promise<FetchResponse> {
9
- const response = new FetchResponse();
9
+ let abortSubscription: AbortSubscriptionCleanupFunction | null = null;
10
+
11
+ const response = new FetchResponse(() => {
12
+ abortSubscription?.();
13
+ });
10
14
  const request = new ExpoFetchModule.NativeRequest(response) as NativeRequest;
11
15
 
12
16
  let headers = normalizeHeadersInit(init?.headers);
@@ -25,10 +29,9 @@ export async function fetch(url: string, init?: FetchRequestInit): Promise<Fetch
25
29
  if (init?.signal && init.signal.aborted) {
26
30
  throw new FetchError('The operation was aborted.');
27
31
  }
28
- const abortHandler = () => {
32
+ abortSubscription = addAbortSignalListener(init?.signal, () => {
29
33
  request.cancel();
30
- };
31
- init?.signal?.addEventListener('abort', abortHandler);
34
+ });
32
35
  try {
33
36
  await request.start(url, nativeRequestInit, requestBody);
34
37
  } catch (e: unknown) {
@@ -37,8 +40,19 @@ export async function fetch(url: string, init?: FetchRequestInit): Promise<Fetch
37
40
  } else {
38
41
  throw new FetchError(String(e));
39
42
  }
40
- } finally {
41
- init?.signal?.removeEventListener('abort', abortHandler);
42
43
  }
43
44
  return response;
44
45
  }
46
+
47
+ /**
48
+ * A wrapper of `AbortSignal.addEventListener` that returns a cleanup function.
49
+ */
50
+ function addAbortSignalListener(
51
+ signal: AbortSignal | undefined,
52
+ listener: Parameters<AbortSignal['addEventListener']>[1]
53
+ ): AbortSubscriptionCleanupFunction {
54
+ signal?.addEventListener('abort', listener);
55
+ return () => {
56
+ signal?.removeEventListener('abort', listener);
57
+ };
58
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"blobUtils.d.ts","sourceRoot":"","sources":["../../src/devtools/blobUtils.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,WAAW,CAAC,CAKvE;AAED;;GAEG;AACH,wBAAsB,4BAA4B,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,WAAW,CAAC,CASnF"}
File without changes
File without changes