expo 52.0.40 → 52.0.42

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 (30) hide show
  1. package/android/build.gradle +2 -2
  2. package/bin/autolinking +3 -0
  3. package/bin/fingerprint +3 -0
  4. package/build/devtools/DevToolsPluginClient.d.ts +13 -3
  5. package/build/devtools/DevToolsPluginClient.d.ts.map +1 -1
  6. package/build/devtools/DevToolsPluginClientImplApp.d.ts +2 -0
  7. package/build/devtools/DevToolsPluginClientImplApp.d.ts.map +1 -1
  8. package/build/devtools/DevToolsPluginClientImplBrowser.d.ts.map +1 -1
  9. package/build/devtools/MessageFramePacker.d.ts +26 -16
  10. package/build/devtools/MessageFramePacker.d.ts.map +1 -1
  11. package/build/devtools/ProtocolVersion.d.ts +7 -0
  12. package/build/devtools/ProtocolVersion.d.ts.map +1 -0
  13. package/build/devtools/devtools.types.d.ts +14 -8
  14. package/build/devtools/devtools.types.d.ts.map +1 -1
  15. package/build/devtools/getConnectionInfo.d.ts.map +1 -1
  16. package/build/devtools/getConnectionInfo.native.d.ts.map +1 -1
  17. package/bundledNativeModules.json +15 -7
  18. package/package.json +10 -6
  19. package/src/devtools/DevToolsPluginClient.ts +82 -18
  20. package/src/devtools/DevToolsPluginClientImplApp.ts +63 -4
  21. package/src/devtools/DevToolsPluginClientImplBrowser.ts +13 -7
  22. package/src/devtools/MessageFramePacker.ts +103 -43
  23. package/src/devtools/ProtocolVersion.ts +6 -0
  24. package/src/devtools/__tests__/DevToolsPluginClient-test.ts +42 -0
  25. package/src/devtools/__tests__/DevToolsPluginClientFactory-test.ts +9 -0
  26. package/src/devtools/__tests__/MessageFramePack-test.node.ts +49 -19
  27. package/src/devtools/devtools.types.ts +15 -8
  28. package/src/devtools/getConnectionInfo.native.ts +2 -0
  29. package/src/devtools/getConnectionInfo.ts +2 -0
  30. package/src/winter/fetch/__tests__/convertFormData-test.native.ts +1 -1
@@ -33,7 +33,7 @@ def getRNVersion() {
33
33
  ensureDependeciesWereEvaluated(project)
34
34
 
35
35
  group = 'host.exp.exponent'
36
- version = '52.0.40'
36
+ version = '52.0.42'
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.40"
49
+ versionName "52.0.42"
50
50
  consumerProguardFiles("proguard-rules.pro")
51
51
  }
52
52
  testOptions {
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ require('expo-modules-autolinking/bin/expo-modules-autolinking');
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ require('@expo/fingerprint/bin/cli');
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter, EventSubscription } from 'fbemitter';
2
2
  import { WebSocketBackingStore } from './WebSocketBackingStore';
3
- import type { ConnectionInfo, DevToolsPluginClientOptions } from './devtools.types';
3
+ import type { ConnectionInfo, DevToolsPluginClientOptions, HandshakeMessageParams } from './devtools.types';
4
4
  /**
5
5
  * This client is for the Expo DevTools Plugins to communicate between the app and the DevTools webpage hosted in a browser.
6
6
  * All the code should be both compatible with browsers and React Native.
@@ -30,6 +30,7 @@ export declare abstract class DevToolsPluginClient {
30
30
  * @param params any extra payload.
31
31
  */
32
32
  sendMessage(method: string, params: any): void;
33
+ protected sendMessageLegacy(method: string, params: any): void;
33
34
  private sendMessageImpl;
34
35
  /**
35
36
  * Subscribe to a message from the other end of DevTools.
@@ -43,6 +44,16 @@ export declare abstract class DevToolsPluginClient {
43
44
  * @param listener Listener to be called when a message is received.
44
45
  */
45
46
  addMessageListenerOnce(method: string, listener: (params: any) => void): void;
47
+ /**
48
+ * Internal handshake message sender.
49
+ * @hidden
50
+ */
51
+ protected sendHandshakeMessage(params: HandshakeMessageParams): void;
52
+ /**
53
+ * Internal handshake message listener.
54
+ * @hidden
55
+ */
56
+ protected addHandskakeMessageListener(listener: (params: HandshakeMessageParams) => void): Partial<EventSubscription>;
46
57
  /**
47
58
  * Returns whether the client is connected to the server.
48
59
  */
@@ -51,8 +62,7 @@ export declare abstract class DevToolsPluginClient {
51
62
  * The method to create the WebSocket connection.
52
63
  */
53
64
  protected connectAsync(): Promise<WebSocket>;
54
- protected handleMessage: (event: WebSocketMessageEvent) => void;
55
- private handleMessageImpl;
65
+ protected handleMessage: (event: WebSocketMessageEvent) => Promise<void>;
56
66
  /**
57
67
  * Get the WebSocket backing store. Exposed for testing.
58
68
  * @hidden
@@ -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;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"}
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,EACV,cAAc,EACd,2BAA2B,EAC3B,sBAAsB,EACvB,MAAM,kBAAkB,CAAC;AAS1B;;;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;IAI9C,SAAS,CAAC,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG;IAIvD,OAAO,CAAC,eAAe;IAsBvB;;;;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;;;OAGG;IACH,SAAS,CAAC,oBAAoB,CAAC,MAAM,EAAE,sBAAsB;IAQ7D;;;OAGG;IACH,SAAS,CAAC,2BAA2B,CACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,GACjD,OAAO,CAAC,iBAAiB,CAAC;IA2B7B;;OAEG;IACI,WAAW,IAAI,OAAO;IAI7B;;OAEG;IACH,SAAS,CAAC,YAAY,IAAI,OAAO,CAAC,SAAS,CAAC;IAyB5C,SAAS,CAAC,aAAa,UAAiB,qBAAqB,mBAuB3D;IAEF;;;OAGG;IACI,wBAAwB,IAAI,qBAAqB;CAGzD"}
@@ -9,6 +9,8 @@ export declare class DevToolsPluginClientImplApp extends DevToolsPluginClient {
9
9
  * @hidden
10
10
  */
11
11
  initAsync(): Promise<void>;
12
+ sendMessage(method: string, params: any): void;
12
13
  private addHandshakeHandler;
14
+ private terminateBrowserClient;
13
15
  }
14
16
  //# sourceMappingURL=DevToolsPluginClientImplApp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DevToolsPluginClientImplApp.d.ts","sourceRoot":"","sources":["../../src/devtools/DevToolsPluginClientImplApp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAI9D;;GAEG;AACH,qBAAa,2BAA4B,SAAQ,oBAAoB;IAEnE,OAAO,CAAC,gBAAgB,CAA8B;IAEtD;;;OAGG;IACY,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAKzC,OAAO,CAAC,mBAAmB;CAY5B"}
1
+ {"version":3,"file":"DevToolsPluginClientImplApp.d.ts","sourceRoot":"","sources":["../../src/devtools/DevToolsPluginClientImplApp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAS9D;;GAEG;AACH,qBAAa,2BAA4B,SAAQ,oBAAoB;IACnE,OAAO,CAAC,gBAAgB,CAA6C;IAErE;;;OAGG;IACY,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAKhC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG;IAUhD,OAAO,CAAC,mBAAmB;IAiD3B,OAAO,CAAC,sBAAsB;CAQ/B"}
@@ -1 +1 @@
1
- {"version":3,"file":"DevToolsPluginClientImplBrowser.d.ts","sourceRoot":"","sources":["../../src/devtools/DevToolsPluginClientImplBrowser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAG9D;;GAEG;AACH,qBAAa,+BAAgC,SAAQ,oBAAoB;IACvE,OAAO,CAAC,eAAe,CAAiC;IAExD;;;OAGG;IACY,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAKzC,OAAO,CAAC,cAAc;CAavB"}
1
+ {"version":3,"file":"DevToolsPluginClientImplBrowser.d.ts","sourceRoot":"","sources":["../../src/devtools/DevToolsPluginClientImplBrowser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAG9D;;GAEG;AACH,qBAAa,+BAAgC,SAAQ,oBAAoB;IACvE,OAAO,CAAC,eAAe,CAAiC;IAExD;;;OAGG;IACY,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAKzC,OAAO,CAAC,cAAc;CAmBvB"}
@@ -1,25 +1,33 @@
1
1
  /**
2
- * A message frame packer that serializes a messageKey and a payload into a binary format.
2
+ * A message frame packer that serializes a messageKey and a payload into either a JSON string
3
+ * (fast path) or a binary format (for complex payloads).
3
4
  *
4
- * +------------------+-------------------+----------------------------+--------------------------+
5
- * | 4 bytes (Uint32) | Variable length | 1 byte (Uint8) | Variable length |
6
- * | MessageKeyLength | MessageKey (JSON) | PayloadTypeIndicator (enum)| Payload (binary data) |
7
- * +------------------+-------------------+----------------------------+--------------------------+
5
+ * Fast Path (JSON.stringify/JSON.parse):
6
+ * - For simple payloads (e.g., strings, numbers, null, undefined, or plain objects), the packer
7
+ * uses `JSON.stringify` for serialization and `JSON.parse` for deserialization, ensuring
8
+ * optimal performance.
8
9
  *
9
- * MessageFrame Format:
10
+ * Binary Format:
11
+ * - For more complex payloads (e.g., Uint8Array, ArrayBuffer, Blob), the packer uses a binary
12
+ * format with the following structure:
10
13
  *
11
- * 1. MessageKeyLength (4 bytes):
12
- * - A 4-byte unsigned integer indicating the length of the MessageKey JSON string.
14
+ * +------------------+-------------------+----------------------------+--------------------------+
15
+ * | 4 bytes (Uint32) | Variable length | 1 byte (Uint8) | Variable length |
16
+ * | MessageKeyLength | MessageKey (JSON) | PayloadTypeIndicator (enum)| Payload (binary data) |
17
+ * +------------------+-------------------+----------------------------+--------------------------+
13
18
  *
14
- * 2. MessageKey (Variable length):
15
- * - The JSON string representing the message key, encoded as UTF-8.
19
+ * 1. MessageKeyLength (4 bytes):
20
+ * - A 4-byte unsigned integer indicating the length of the MessageKey JSON string.
16
21
  *
17
- * 3. PayloadTypeIndicator (1 byte):
18
- * - A single byte enum value representing the type of the payload (e.g., Uint8Array, String, Object, ArrayBuffer, Blob).
22
+ * 2. MessageKey (Variable length):
23
+ * - The JSON string representing the message key, encoded as UTF-8.
19
24
  *
20
- * 4. Payload (Variable length):
21
- * - The actual payload data, which can vary in type and length depending on the PayloadType.
25
+ * 3. PayloadTypeIndicator (1 byte):
26
+ * - A single byte enum value representing the type of the payload (e.g., Uint8Array, String,
27
+ * Object, ArrayBuffer, Blob).
22
28
  *
29
+ * 4. Payload (Variable length):
30
+ * - The actual payload data, which can vary in type and length depending on the PayloadType.
23
31
  */
24
32
  type MessageKeyTypeBase = string | object;
25
33
  type PayloadType = Uint8Array | string | number | null | undefined | object | ArrayBuffer | Blob;
@@ -30,9 +38,11 @@ interface MessageFrame<T extends MessageKeyTypeBase> {
30
38
  export declare class MessageFramePacker<T extends MessageKeyTypeBase> {
31
39
  private textEncoder;
32
40
  private textDecoder;
33
- pack({ messageKey, payload }: MessageFrame<T>): Promise<Uint8Array>;
34
- unpack(packedData: ArrayBuffer): Promise<MessageFrame<T>>;
41
+ pack({ messageKey, payload }: MessageFrame<T>, useLegacyTransport?: boolean): string | Uint8Array | Promise<Uint8Array>;
42
+ unpack(packedData: string | ArrayBuffer): MessageFrame<T>;
43
+ private isFastPathPayload;
35
44
  private payloadToUint8Array;
45
+ private packImpl;
36
46
  private deserializePayload;
37
47
  private static getPayloadTypeIndicator;
38
48
  }
@@ -1 +1 @@
1
- {"version":3,"file":"MessageFramePacker.d.ts","sourceRoot":"","sources":["../../src/devtools/MessageFramePacker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,KAAK,kBAAkB,GAAG,MAAM,GAAG,MAAM,CAAC;AAC1C,KAAK,WAAW,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;AAajG,UAAU,YAAY,CAAC,CAAC,SAAS,kBAAkB;IACjD,UAAU,EAAE,CAAC,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,qBAAa,kBAAkB,CAAC,CAAC,SAAS,kBAAkB;IAC1D,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,WAAW,CAAqB;IAE3B,IAAI,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;IA2BnE,MAAM,CAAC,UAAU,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YAqBxD,mBAAmB;YAuBnB,kBAAkB;IAoChC,OAAO,CAAC,MAAM,CAAC,uBAAuB;CAqBvC"}
1
+ {"version":3,"file":"MessageFramePacker.d.ts","sourceRoot":"","sources":["../../src/devtools/MessageFramePacker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,KAAK,kBAAkB,GAAG,MAAM,GAAG,MAAM,CAAC;AAC1C,KAAK,WAAW,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;AAajG,UAAU,YAAY,CAAC,CAAC,SAAS,kBAAkB;IACjD,UAAU,EAAE,CAAC,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,qBAAa,kBAAkB,CAAC,CAAC,SAAS,kBAAkB;IAC1D,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,WAAW,CAAqB;IAEjC,IAAI,CACT,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC,EACxC,kBAAkB,CAAC,EAAE,OAAO,GAC3B,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IA2BrC,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,WAAW,GAAG,YAAY,CAAC,CAAC,CAAC;IA0BhE,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,mBAAmB;IAuB3B,OAAO,CAAC,QAAQ;IA8BhB,OAAO,CAAC,kBAAkB;IAoC1B,OAAO,CAAC,MAAM,CAAC,uBAAuB;CAqBvC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A transport protocol version between the app and the webui.
3
+ * It shows a warning in handshaking stage if the version is different between the app and the webui.
4
+ * The value should be increased when the protocol is changed.
5
+ */
6
+ export declare const PROTOCOL_VERSION = 1;
7
+ //# sourceMappingURL=ProtocolVersion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProtocolVersion.d.ts","sourceRoot":"","sources":["../../src/devtools/ProtocolVersion.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,IAAI,CAAC"}
@@ -15,14 +15,10 @@ export interface ConnectionInfo {
15
15
  * @hidden
16
16
  */
17
17
  wsStore?: WebSocketBackingStore;
18
- }
19
- /**
20
- * Parameters for the `handshake` message.
21
- * @hidden
22
- */
23
- export interface HandshakeMessageParams {
24
- browserClientId: string;
25
- pluginName: string;
18
+ /**
19
+ * The transport protocol version between the app and the webui.
20
+ */
21
+ protocolVersion: number;
26
22
  }
27
23
  /**
28
24
  * Options for the devtools plugin client.
@@ -33,4 +29,14 @@ export interface DevToolsPluginClientOptions {
33
29
  */
34
30
  websocketBinaryType?: 'arraybuffer' | 'blob';
35
31
  }
32
+ /**
33
+ * The handshake messages for the devtools plugin client.
34
+ * @hidden
35
+ */
36
+ export interface HandshakeMessageParams {
37
+ protocolVersion: number;
38
+ pluginName: string;
39
+ method: 'handshake' | 'terminateBrowserClient';
40
+ browserClientId: string;
41
+ }
36
42
  //# sourceMappingURL=devtools.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"devtools.types.d.ts","sourceRoot":"","sources":["../../src/devtools/devtools.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,MAAM,EACF,KAAK,GACL,SAAS,CAAC;IAEd,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAElB,uBAAuB;IACvB,UAAU,EAAE,MAAM,CAAC;IAEnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C;;OAEG;IACH,mBAAmB,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC;CAC9C"}
1
+ {"version":3,"file":"devtools.types.d.ts","sourceRoot":"","sources":["../../src/devtools/devtools.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,MAAM,EACF,KAAK,GACL,SAAS,CAAC;IAEd,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAElB,uBAAuB;IACvB,UAAU,EAAE,MAAM,CAAC;IAEnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAEhC;;OAEG;IACH,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C;;OAEG;IACH,mBAAmB,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC;CAC9C;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,WAAW,GAAG,wBAAwB,CAAC;IAC/C,eAAe,EAAE,MAAM,CAAC;CACzB"}
@@ -1 +1 @@
1
- {"version":3,"file":"getConnectionInfo.d.ts","sourceRoot":"","sources":["../../src/devtools/getConnectionInfo.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,wBAAgB,iBAAiB,IAAI,IAAI,CAAC,cAAc,EAAE,YAAY,CAAC,CAOtE"}
1
+ {"version":3,"file":"getConnectionInfo.d.ts","sourceRoot":"","sources":["../../src/devtools/getConnectionInfo.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,wBAAgB,iBAAiB,IAAI,IAAI,CAAC,cAAc,EAAE,YAAY,CAAC,CAQtE"}
@@ -1 +1 @@
1
- {"version":3,"file":"getConnectionInfo.native.d.ts","sourceRoot":"","sources":["../../src/devtools/getConnectionInfo.native.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,wBAAgB,iBAAiB,IAAI,IAAI,CAAC,cAAc,EAAE,YAAY,CAAC,CAStE"}
1
+ {"version":3,"file":"getConnectionInfo.native.d.ts","sourceRoot":"","sources":["../../src/devtools/getConnectionInfo.native.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,wBAAgB,iBAAiB,IAAI,IAAI,CAAC,cAAc,EAAE,YAAY,CAAC,CAUtE"}
@@ -1,4 +1,12 @@
1
1
  {
2
+ "@dev-plugins/apollo-client": "~0.2.0",
3
+ "@dev-plugins/async-storage": "~0.2.0",
4
+ "@dev-plugins/react-native-mmkv": "~0.2.0",
5
+ "@dev-plugins/react-navigation": "~0.2.0",
6
+ "@dev-plugins/react-query": "~0.2.0",
7
+ "@dev-plugins/redux": "~0.2.0",
8
+ "@dev-plugins/tinybase": "~0.2.0",
9
+ "@dev-plugins/vanilla-log-viewer": "~0.2.0",
2
10
  "@expo/metro-runtime": "~4.0.1",
3
11
  "@expo/vector-icons": "^14.0.2",
4
12
  "@react-native-async-storage/async-storage": "1.23.1",
@@ -43,7 +51,7 @@
43
51
  "expo-gl": "~15.0.4",
44
52
  "expo-google-app-auth": "~8.3.0",
45
53
  "expo-haptics": "~14.0.1",
46
- "expo-image": "~2.0.6",
54
+ "expo-image": "~2.0.7",
47
55
  "expo-image-loader": "~5.0.0",
48
56
  "expo-image-manipulator": "~13.0.6",
49
57
  "expo-image-picker": "~16.0.6",
@@ -54,12 +62,12 @@
54
62
  "expo-linking": "~7.0.5",
55
63
  "expo-local-authentication": "~15.0.2",
56
64
  "expo-localization": "~16.0.1",
57
- "expo-location": "~18.0.8",
65
+ "expo-location": "~18.0.9",
58
66
  "expo-mail-composer": "~14.0.2",
59
67
  "expo-media-library": "~17.0.6",
60
- "expo-module-template": "~10.15.13",
68
+ "expo-module-template": "~10.15.14",
61
69
  "expo-modules-core": "~2.2.3",
62
- "expo-navigation-bar": "~4.0.8",
70
+ "expo-navigation-bar": "~4.0.9",
63
71
  "expo-network": "~7.0.5",
64
72
  "expo-notifications": "~0.29.14",
65
73
  "expo-print": "~14.0.3",
@@ -72,11 +80,11 @@
72
80
  "expo-sms": "~13.0.1",
73
81
  "expo-speech": "~13.0.1",
74
82
  "expo-splash-screen": "~0.29.22",
75
- "expo-sqlite": "~15.1.2",
83
+ "expo-sqlite": "~15.1.3",
76
84
  "expo-status-bar": "~2.0.1",
77
85
  "expo-store-review": "~8.0.1",
78
86
  "expo-symbols": "~0.2.2",
79
- "expo-system-ui": "~4.0.8",
87
+ "expo-system-ui": "~4.0.9",
80
88
  "expo-task-manager": "~12.0.5",
81
89
  "expo-tracking-transparency": "~5.1.1",
82
90
  "expo-updates": "~0.27.4",
@@ -87,7 +95,7 @@
87
95
  "lottie-react-native": "7.1.0",
88
96
  "react": "18.3.1",
89
97
  "react-dom": "18.3.1",
90
- "react-native": "0.76.7",
98
+ "react-native": "0.76.8",
91
99
  "react-native-web": "~0.19.13",
92
100
  "react-native-gesture-handler": "~2.20.2",
93
101
  "react-native-get-random-values": "~1.11.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo",
3
- "version": "52.0.40",
3
+ "version": "52.0.42",
4
4
  "description": "The Expo SDK",
5
5
  "main": "src/Expo.ts",
6
6
  "module": "src/Expo.ts",
@@ -10,7 +10,11 @@
10
10
  "*.fx.web.tsx",
11
11
  "./src/winter/*.ts"
12
12
  ],
13
- "bin": "bin/cli",
13
+ "bin": {
14
+ "expo": "bin/cli",
15
+ "expo-modules-autolinking": "bin/autolinking",
16
+ "fingerprint": "bin/fingerprint"
17
+ },
14
18
  "files": [
15
19
  "android",
16
20
  "bin",
@@ -68,13 +72,13 @@
68
72
  "homepage": "https://github.com/expo/expo/tree/main/packages/expo",
69
73
  "dependencies": {
70
74
  "@babel/runtime": "^7.20.0",
71
- "@expo/cli": "0.22.21",
75
+ "@expo/cli": "0.22.23",
72
76
  "@expo/config": "~10.0.11",
73
77
  "@expo/config-plugins": "~9.0.17",
74
78
  "@expo/fingerprint": "0.11.11",
75
79
  "@expo/metro-config": "0.19.12",
76
80
  "@expo/vector-icons": "^14.0.0",
77
- "babel-preset-expo": "~12.0.9",
81
+ "babel-preset-expo": "~12.0.10",
78
82
  "expo-asset": "~11.0.5",
79
83
  "expo-constants": "~17.0.8",
80
84
  "expo-file-system": "~18.0.12",
@@ -92,7 +96,7 @@
92
96
  "expo-module-scripts": "^4.0.4",
93
97
  "react": "18.3.1",
94
98
  "react-dom": "18.3.1",
95
- "react-native": "0.76.7"
99
+ "react-native": "0.76.8"
96
100
  },
97
101
  "peerDependencies": {
98
102
  "@expo/dom-webview": "*",
@@ -112,5 +116,5 @@
112
116
  "optional": true
113
117
  }
114
118
  },
115
- "gitHead": "0d83355bc0ec72f115de85eb418aaf9500c68528"
119
+ "gitHead": "efc676ef6dbe6c2eaed19c1a31abb79a935f583a"
116
120
  }
@@ -3,7 +3,11 @@ 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 type { ConnectionInfo, DevToolsPluginClientOptions } from './devtools.types';
6
+ import type {
7
+ ConnectionInfo,
8
+ DevToolsPluginClientOptions,
9
+ HandshakeMessageParams,
10
+ } from './devtools.types';
7
11
  import * as logger from './logger';
8
12
  import { blobToArrayBufferAsync } from '../utils/blobUtils';
9
13
 
@@ -66,20 +70,33 @@ export abstract class DevToolsPluginClient {
66
70
  * @param params any extra payload.
67
71
  */
68
72
  public sendMessage(method: string, params: any) {
73
+ this.sendMessageImpl(method, params, false);
74
+ }
75
+
76
+ protected sendMessageLegacy(method: string, params: any) {
77
+ this.sendMessageImpl(method, params, true);
78
+ }
79
+
80
+ private sendMessageImpl(method: string, params: any, useLegacyTransport: boolean) {
69
81
  if (this.wsStore.ws?.readyState === WebSocket.CLOSED) {
70
82
  logger.warn('Unable to send message in a disconnected state.');
71
83
  return;
72
84
  }
73
- this.sendMessageImpl(method, params);
74
- }
75
-
76
- private async sendMessageImpl(method: string, params: any) {
77
85
  const messageKey: MessageFramePackerMessageKey = {
78
86
  pluginName: this.connectionInfo.pluginName,
79
87
  method,
80
88
  };
81
- const packedData = await this.messageFramePacker.pack({ messageKey, payload: params });
82
- this.wsStore.ws?.send(packedData);
89
+ const packedData = this.messageFramePacker.pack(
90
+ { messageKey, payload: params },
91
+ useLegacyTransport
92
+ );
93
+ if (!(packedData instanceof Promise)) {
94
+ this.wsStore.ws?.send(packedData);
95
+ return;
96
+ }
97
+ packedData.then((data) => {
98
+ this.wsStore.ws?.send(data);
99
+ });
83
100
  }
84
101
 
85
102
  /**
@@ -100,6 +117,51 @@ export abstract class DevToolsPluginClient {
100
117
  this.eventEmitter.once(method, listener);
101
118
  }
102
119
 
120
+ /**
121
+ * Internal handshake message sender.
122
+ * @hidden
123
+ */
124
+ protected sendHandshakeMessage(params: HandshakeMessageParams) {
125
+ if (this.wsStore.ws?.readyState === WebSocket.CLOSED) {
126
+ logger.warn('Unable to send message in a disconnected state.');
127
+ return;
128
+ }
129
+ this.wsStore.ws?.send(JSON.stringify({ ...params, __isHandshakeMessages: true }));
130
+ }
131
+
132
+ /**
133
+ * Internal handshake message listener.
134
+ * @hidden
135
+ */
136
+ protected addHandskakeMessageListener(
137
+ listener: (params: HandshakeMessageParams) => void
138
+ ): Partial<EventSubscription> {
139
+ const messageListener = (event: MessageEvent) => {
140
+ if (typeof event.data !== 'string') {
141
+ // binary data is not coming from the handshake messages.
142
+ return;
143
+ }
144
+
145
+ const data = JSON.parse(event.data);
146
+ if (!data.__isHandshakeMessages) {
147
+ return;
148
+ }
149
+ delete data.__isHandshakeMessages;
150
+ const params = data as HandshakeMessageParams;
151
+ if (params.pluginName && params.pluginName !== this.connectionInfo.pluginName) {
152
+ return;
153
+ }
154
+ listener(params);
155
+ };
156
+
157
+ this.wsStore.ws?.addEventListener('message', messageListener);
158
+ return {
159
+ remove: () => {
160
+ this.wsStore.ws?.removeEventListener('message', messageListener);
161
+ },
162
+ };
163
+ }
164
+
103
165
  /**
104
166
  * Returns whether the client is connected to the server.
105
167
  */
@@ -135,23 +197,25 @@ export abstract class DevToolsPluginClient {
135
197
  });
136
198
  }
137
199
 
138
- protected handleMessage = (event: WebSocketMessageEvent) => {
139
- this.handleMessageImpl(event);
140
- };
141
-
142
- private handleMessageImpl = async (event: WebSocketMessageEvent) => {
143
- let buffer: ArrayBuffer;
144
- if (event.data instanceof ArrayBuffer) {
145
- buffer = event.data;
200
+ protected handleMessage = async (event: WebSocketMessageEvent) => {
201
+ let data: ArrayBuffer | string;
202
+ if (typeof event.data === 'string') {
203
+ data = event.data;
204
+ } else if (event.data instanceof ArrayBuffer) {
205
+ data = event.data;
146
206
  } else if (ArrayBuffer.isView(event.data)) {
147
- buffer = event.data.buffer;
207
+ data = event.data.buffer;
148
208
  } else if (event.data instanceof Blob) {
149
- buffer = await blobToArrayBufferAsync(event.data);
209
+ data = await blobToArrayBufferAsync(event.data);
150
210
  } else {
151
211
  logger.warn('Unsupported received data type in handleMessageImpl');
152
212
  return;
153
213
  }
154
- const { messageKey, payload } = await this.messageFramePacker.unpack(buffer);
214
+ const { messageKey, payload, ...rest } = this.messageFramePacker.unpack(data);
215
+ // @ts-expect-error: `__isHandshakeMessages` is a private field that is not part of the MessageFramePacker type.
216
+ if (rest?.__isHandshakeMessages === true) {
217
+ return;
218
+ }
155
219
  if (messageKey.pluginName && messageKey.pluginName !== this.connectionInfo.pluginName) {
156
220
  return;
157
221
  }
@@ -2,12 +2,16 @@ import { DevToolsPluginClient } from './DevToolsPluginClient';
2
2
  import type { HandshakeMessageParams } from './devtools.types';
3
3
  import * as logger from './logger';
4
4
 
5
+ interface BrowserClientMetadata {
6
+ browserClientId: string;
7
+ useLegacyTransport?: boolean;
8
+ }
9
+
5
10
  /**
6
11
  * The DevToolsPluginClient for the app -> browser communication.
7
12
  */
8
13
  export class DevToolsPluginClientImplApp extends DevToolsPluginClient {
9
- // Map of pluginName -> browserClientId
10
- private browserClientMap: Record<string, string> = {};
14
+ private browserClientMap: Record<string, BrowserClientMetadata> = {};
11
15
 
12
16
  /**
13
17
  * Initialize the connection.
@@ -18,16 +22,71 @@ export class DevToolsPluginClientImplApp extends DevToolsPluginClient {
18
22
  this.addHandshakeHandler();
19
23
  }
20
24
 
25
+ override sendMessage(method: string, params: any) {
26
+ const pluginName = this.connectionInfo.pluginName;
27
+ const useLegacyTransport = this.browserClientMap[pluginName]?.useLegacyTransport ?? false;
28
+ if (useLegacyTransport) {
29
+ this.sendMessageLegacy(method, params);
30
+ } else {
31
+ super.sendMessage(method, params);
32
+ }
33
+ }
34
+
21
35
  private addHandshakeHandler() {
36
+ this.addHandskakeMessageListener((params) => {
37
+ if (params.method === 'handshake') {
38
+ const { pluginName, protocolVersion } = params;
39
+
40
+ // [0] Check protocol version
41
+ if (protocolVersion !== this.connectionInfo.protocolVersion) {
42
+ // Use console.warn than logger because we want to show the warning even logging is disabled.
43
+ console.warn(
44
+ `Received an incompatible devtools plugin handshake message - pluginName[${pluginName}]`
45
+ );
46
+ this.terminateBrowserClient(pluginName, params.browserClientId);
47
+ return;
48
+ }
49
+
50
+ // [1] Terminate duplicated browser clients for the same plugin
51
+ const previousBrowserClientMetadata = this.browserClientMap[pluginName];
52
+ const previousBrowserClientId = previousBrowserClientMetadata?.browserClientId;
53
+ if (previousBrowserClientId != null && previousBrowserClientId !== params.browserClientId) {
54
+ logger.info(
55
+ `Terminate the previous browser client connection - previousBrowserClientId[${previousBrowserClientId}]`
56
+ );
57
+ this.terminateBrowserClient(pluginName, previousBrowserClientId);
58
+ }
59
+ this.browserClientMap[pluginName] = {
60
+ browserClientId: params.browserClientId,
61
+ };
62
+ }
63
+ });
64
+
65
+ // backward compatible handshaking
22
66
  this.addMessageListener('handshake', (params: HandshakeMessageParams) => {
23
- const previousBrowserClientId = this.browserClientMap[params.pluginName];
67
+ console.warn(`Received a legacy plugin handshake message - pluginName[${params.pluginName}]`);
68
+ const { pluginName } = params;
69
+ const previousBrowserClientMetadata = this.browserClientMap[pluginName];
70
+ const previousBrowserClientId = previousBrowserClientMetadata?.browserClientId;
24
71
  if (previousBrowserClientId != null && previousBrowserClientId !== params.browserClientId) {
25
72
  logger.info(
26
73
  `Terminate the previous browser client connection - previousBrowserClientId[${previousBrowserClientId}]`
27
74
  );
28
75
  this.sendMessage('terminateBrowserClient', { browserClientId: previousBrowserClientId });
29
76
  }
30
- this.browserClientMap[params.pluginName] = params.browserClientId;
77
+ this.browserClientMap[pluginName] = {
78
+ browserClientId: params.browserClientId,
79
+ useLegacyTransport: true,
80
+ };
81
+ });
82
+ }
83
+
84
+ private terminateBrowserClient(pluginName: string, browserClientId: string) {
85
+ this.sendHandshakeMessage({
86
+ protocolVersion: this.connectionInfo.protocolVersion,
87
+ method: 'terminateBrowserClient',
88
+ browserClientId,
89
+ pluginName,
31
90
  });
32
91
  }
33
92
  }
@@ -17,16 +17,22 @@ export class DevToolsPluginClientImplBrowser extends DevToolsPluginClient {
17
17
  }
18
18
 
19
19
  private startHandshake() {
20
- this.addMessageListener('terminateBrowserClient', (params) => {
21
- if (this.browserClientId !== params.browserClientId) {
22
- return;
20
+ this.addHandskakeMessageListener((params) => {
21
+ if (
22
+ params.method === 'terminateBrowserClient' &&
23
+ this.browserClientId === params.browserClientId
24
+ ) {
25
+ logger.info(
26
+ 'Received terminateBrowserClient messages and terminate the current connection'
27
+ );
28
+ this.closeAsync();
23
29
  }
24
- logger.info('Received terminateBrowserClient messages and terminate the current connection');
25
- this.closeAsync();
26
30
  });
27
- this.sendMessage('handshake', {
28
- browserClientId: this.browserClientId,
31
+ this.sendHandshakeMessage({
32
+ protocolVersion: this.connectionInfo.protocolVersion,
29
33
  pluginName: this.connectionInfo.pluginName,
34
+ method: 'handshake',
35
+ browserClientId: this.browserClientId,
30
36
  });
31
37
  }
32
38
  }
@@ -1,25 +1,33 @@
1
1
  /**
2
- * A message frame packer that serializes a messageKey and a payload into a binary format.
2
+ * A message frame packer that serializes a messageKey and a payload into either a JSON string
3
+ * (fast path) or a binary format (for complex payloads).
3
4
  *
4
- * +------------------+-------------------+----------------------------+--------------------------+
5
- * | 4 bytes (Uint32) | Variable length | 1 byte (Uint8) | Variable length |
6
- * | MessageKeyLength | MessageKey (JSON) | PayloadTypeIndicator (enum)| Payload (binary data) |
7
- * +------------------+-------------------+----------------------------+--------------------------+
5
+ * Fast Path (JSON.stringify/JSON.parse):
6
+ * - For simple payloads (e.g., strings, numbers, null, undefined, or plain objects), the packer
7
+ * uses `JSON.stringify` for serialization and `JSON.parse` for deserialization, ensuring
8
+ * optimal performance.
8
9
  *
9
- * MessageFrame Format:
10
+ * Binary Format:
11
+ * - For more complex payloads (e.g., Uint8Array, ArrayBuffer, Blob), the packer uses a binary
12
+ * format with the following structure:
10
13
  *
11
- * 1. MessageKeyLength (4 bytes):
12
- * - A 4-byte unsigned integer indicating the length of the MessageKey JSON string.
14
+ * +------------------+-------------------+----------------------------+--------------------------+
15
+ * | 4 bytes (Uint32) | Variable length | 1 byte (Uint8) | Variable length |
16
+ * | MessageKeyLength | MessageKey (JSON) | PayloadTypeIndicator (enum)| Payload (binary data) |
17
+ * +------------------+-------------------+----------------------------+--------------------------+
13
18
  *
14
- * 2. MessageKey (Variable length):
15
- * - The JSON string representing the message key, encoded as UTF-8.
19
+ * 1. MessageKeyLength (4 bytes):
20
+ * - A 4-byte unsigned integer indicating the length of the MessageKey JSON string.
16
21
  *
17
- * 3. PayloadTypeIndicator (1 byte):
18
- * - A single byte enum value representing the type of the payload (e.g., Uint8Array, String, Object, ArrayBuffer, Blob).
22
+ * 2. MessageKey (Variable length):
23
+ * - The JSON string representing the message key, encoded as UTF-8.
19
24
  *
20
- * 4. Payload (Variable length):
21
- * - The actual payload data, which can vary in type and length depending on the PayloadType.
25
+ * 3. PayloadTypeIndicator (1 byte):
26
+ * - A single byte enum value representing the type of the payload (e.g., Uint8Array, String,
27
+ * Object, ArrayBuffer, Blob).
22
28
  *
29
+ * 4. Payload (Variable length):
30
+ * - The actual payload data, which can vary in type and length depending on the PayloadType.
23
31
  */
24
32
 
25
33
  import { blobToArrayBufferAsync } from '../utils/blobUtils';
@@ -47,34 +55,42 @@ export class MessageFramePacker<T extends MessageKeyTypeBase> {
47
55
  private textEncoder = new TextEncoder();
48
56
  private textDecoder = new TextDecoder();
49
57
 
50
- public async pack({ messageKey, payload }: MessageFrame<T>): Promise<Uint8Array> {
51
- const messageKeyString = JSON.stringify(messageKey);
52
- const messageKeyBytes = this.textEncoder.encode(messageKeyString);
53
- const messageKeyLength = messageKeyBytes.length;
54
- const payloadBinary = await this.payloadToUint8Array(payload);
55
-
56
- const totalLength = 4 + messageKeyLength + 1 + payloadBinary.byteLength;
57
- const buffer = new ArrayBuffer(totalLength);
58
- const packedArray = new Uint8Array(buffer);
59
-
60
- // [0] messageKeyLength (4 bytes)
61
- const messageKeyLengthView = new DataView(buffer, 0, 4);
62
- messageKeyLengthView.setUint32(0, messageKeyLength, false);
63
-
64
- // [1] messageKey (variable length)
65
- packedArray.set(messageKeyBytes, 4);
66
-
67
- // [2] payloadTypeIndicator (1 byte)
68
- const payloadTypeView = new DataView(buffer, 4 + messageKeyLength, 1);
69
- payloadTypeView.setUint8(0, MessageFramePacker.getPayloadTypeIndicator(payload));
58
+ public pack(
59
+ { messageKey, payload }: MessageFrame<T>,
60
+ useLegacyTransport?: boolean
61
+ ): string | Uint8Array | Promise<Uint8Array> {
62
+ // Fast path to pack as string given `JSON.stringify` is fast.
63
+ if (this.isFastPathPayload(payload) && !useLegacyTransport) {
64
+ return JSON.stringify({ messageKey, payload });
65
+ }
70
66
 
71
- // [3] payload (variable length)
72
- packedArray.set(payloadBinary, 4 + messageKeyLength + 1);
67
+ // Slowest path for Blob returns a promise.
68
+ if (payload instanceof Blob) {
69
+ return new Promise(async (resolve, reject) => {
70
+ try {
71
+ const arrayBuffer = await blobToArrayBufferAsync(payload);
72
+ resolve(
73
+ this.packImpl(
74
+ { messageKey, payload: new Uint8Array(arrayBuffer) },
75
+ PayloadTypeIndicator.Blob
76
+ )
77
+ );
78
+ } catch (error) {
79
+ reject(error);
80
+ }
81
+ });
82
+ }
73
83
 
74
- return packedArray;
84
+ // Slow path for other types returns a Uint8Array.
85
+ return this.packImpl({ messageKey, payload }, undefined);
75
86
  }
76
87
 
77
- public async unpack(packedData: ArrayBuffer): Promise<MessageFrame<T>> {
88
+ public unpack(packedData: string | ArrayBuffer): MessageFrame<T> {
89
+ // Fast path to unpack as string given `JSON.parse` is fast.
90
+ if (typeof packedData === 'string') {
91
+ return JSON.parse(packedData);
92
+ }
93
+
78
94
  // [0] messageKeyLength (4 bytes)
79
95
  const messageKeyLengthView = new DataView(packedData, 0, 4);
80
96
  const messageKeyLength = messageKeyLengthView.getUint32(0, false);
@@ -90,12 +106,26 @@ export class MessageFramePacker<T extends MessageKeyTypeBase> {
90
106
 
91
107
  // [3] payload (variable length)
92
108
  const payloadBinary = packedData.slice(4 + messageKeyLength + 1);
93
- const payload = await this.deserializePayload(payloadBinary, payloadType);
109
+ const payload = this.deserializePayload(payloadBinary, payloadType);
94
110
 
95
111
  return { messageKey, payload };
96
112
  }
97
113
 
98
- private async payloadToUint8Array(payload: PayloadType): Promise<Uint8Array> {
114
+ private isFastPathPayload(payload: PayloadType): boolean {
115
+ if (payload == null) {
116
+ return true;
117
+ }
118
+ const payloadType = typeof payload;
119
+ if (payloadType === 'string' || payloadType === 'number') {
120
+ return true;
121
+ }
122
+ if (payloadType === 'object' && payload.constructor === Object) {
123
+ return true;
124
+ }
125
+ return false;
126
+ }
127
+
128
+ private payloadToUint8Array(payload: PayloadType): Uint8Array {
99
129
  if (payload instanceof Uint8Array) {
100
130
  return payload;
101
131
  } else if (typeof payload === 'string') {
@@ -112,16 +142,46 @@ export class MessageFramePacker<T extends MessageKeyTypeBase> {
112
142
  } else if (payload instanceof ArrayBuffer) {
113
143
  return new Uint8Array(payload);
114
144
  } else if (payload instanceof Blob) {
115
- return new Uint8Array(await blobToArrayBufferAsync(payload));
145
+ throw new Error('Blob is not supported in this callsite.');
116
146
  } else {
117
147
  return this.textEncoder.encode(JSON.stringify(payload));
118
148
  }
119
149
  }
120
150
 
121
- private async deserializePayload(
151
+ private packImpl(
152
+ { messageKey, payload }: MessageFrame<T>,
153
+ payloadType: PayloadTypeIndicator | undefined
154
+ ): Promise<Uint8Array> | Uint8Array {
155
+ const messageKeyString = JSON.stringify(messageKey);
156
+ const messageKeyBytes = this.textEncoder.encode(messageKeyString);
157
+ const messageKeyLength = messageKeyBytes.length;
158
+ const payloadBinary = this.payloadToUint8Array(payload);
159
+
160
+ const totalLength = 4 + messageKeyLength + 1 + payloadBinary.byteLength;
161
+ const buffer = new ArrayBuffer(totalLength);
162
+ const packedArray = new Uint8Array(buffer);
163
+
164
+ // [0] messageKeyLength (4 bytes)
165
+ const messageKeyLengthView = new DataView(buffer, 0, 4);
166
+ messageKeyLengthView.setUint32(0, messageKeyLength, false);
167
+
168
+ // [1] messageKey (variable length)
169
+ packedArray.set(messageKeyBytes, 4);
170
+
171
+ // [2] payloadTypeIndicator (1 byte)
172
+ const payloadTypeView = new DataView(buffer, 4 + messageKeyLength, 1);
173
+ payloadTypeView.setUint8(0, payloadType ?? MessageFramePacker.getPayloadTypeIndicator(payload));
174
+
175
+ // [3] payload (variable length)
176
+ packedArray.set(payloadBinary, 4 + messageKeyLength + 1);
177
+
178
+ return packedArray;
179
+ }
180
+
181
+ private deserializePayload(
122
182
  payloadBinary: ArrayBuffer,
123
183
  payloadTypeIndicator: PayloadTypeIndicator
124
- ): Promise<PayloadType> {
184
+ ): PayloadType {
125
185
  switch (payloadTypeIndicator) {
126
186
  case PayloadTypeIndicator.Uint8Array: {
127
187
  return new Uint8Array(payloadBinary);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * A transport protocol version between the app and the webui.
3
+ * It shows a warning in handshaking stage if the version is different between the app and the webui.
4
+ * The value should be increased when the protocol is changed.
5
+ */
6
+ export const PROTOCOL_VERSION = 1;
@@ -12,6 +12,8 @@ globalThis.WebSocket = MockWebSocket;
12
12
  globalThis.TextDecoder ??= TextDecoder;
13
13
  globalThis.TextEncoder ??= TextEncoder;
14
14
 
15
+ const TEST_PROTOCOL_VERSION = 1;
16
+
15
17
  describe(`DevToolsPluginClient`, () => {
16
18
  let appClient: DevToolsPluginClient;
17
19
  let testCaseCounter = 0;
@@ -23,6 +25,7 @@ describe(`DevToolsPluginClient`, () => {
23
25
  testCaseCounter += 1;
24
26
  devServer = `localhost:${8000 + testCaseCounter}`;
25
27
  appClient = await createDevToolsPluginClient({
28
+ protocolVersion: TEST_PROTOCOL_VERSION,
26
29
  devServer,
27
30
  sender: 'app',
28
31
  pluginName,
@@ -62,12 +65,14 @@ describe(`DevToolsPluginClient (browser <> app)`, () => {
62
65
  const message = { foo: 'bar' };
63
66
 
64
67
  appClient = await createDevToolsPluginClient({
68
+ protocolVersion: TEST_PROTOCOL_VERSION,
65
69
  devServer,
66
70
  sender: 'app',
67
71
  pluginName,
68
72
  wsStore: new WebSocketBackingStore(),
69
73
  });
70
74
  browserClient = await createDevToolsPluginClient({
75
+ protocolVersion: TEST_PROTOCOL_VERSION,
71
76
  devServer,
72
77
  sender: 'browser',
73
78
  pluginName,
@@ -87,12 +92,14 @@ describe(`DevToolsPluginClient (browser <> app)`, () => {
87
92
 
88
93
  it('should support ping-pong messages', async () => {
89
94
  appClient = await createDevToolsPluginClient({
95
+ protocolVersion: TEST_PROTOCOL_VERSION,
90
96
  devServer,
91
97
  sender: 'app',
92
98
  pluginName,
93
99
  wsStore: new WebSocketBackingStore(),
94
100
  });
95
101
  browserClient = await createDevToolsPluginClient({
102
+ protocolVersion: TEST_PROTOCOL_VERSION,
96
103
  devServer,
97
104
  sender: 'browser',
98
105
  pluginName,
@@ -123,12 +130,14 @@ describe(`DevToolsPluginClient (browser <> app)`, () => {
123
130
  const message = { foo: 'bar' };
124
131
 
125
132
  appClient = await createDevToolsPluginClient({
133
+ protocolVersion: TEST_PROTOCOL_VERSION,
126
134
  devServer,
127
135
  sender: 'app',
128
136
  pluginName,
129
137
  wsStore: new WebSocketBackingStore(),
130
138
  });
131
139
  browserClient = await createDevToolsPluginClient({
140
+ protocolVersion: TEST_PROTOCOL_VERSION,
132
141
  devServer,
133
142
  sender: 'browser',
134
143
  pluginName: 'pluginB',
@@ -149,6 +158,7 @@ describe(`DevToolsPluginClient (browser <> app)`, () => {
149
158
  const method = 'testMethod';
150
159
 
151
160
  appClient = await createDevToolsPluginClient({
161
+ protocolVersion: TEST_PROTOCOL_VERSION,
152
162
  devServer,
153
163
  sender: 'app',
154
164
  pluginName,
@@ -160,6 +170,7 @@ describe(`DevToolsPluginClient (browser <> app)`, () => {
160
170
  });
161
171
 
162
172
  browserClient = await createDevToolsPluginClient({
173
+ protocolVersion: TEST_PROTOCOL_VERSION,
163
174
  devServer,
164
175
  sender: 'browser',
165
176
  pluginName,
@@ -168,6 +179,7 @@ describe(`DevToolsPluginClient (browser <> app)`, () => {
168
179
 
169
180
  await delayAsync(100);
170
181
  const browserClient2 = await createDevToolsPluginClient({
182
+ protocolVersion: TEST_PROTOCOL_VERSION,
171
183
  devServer,
172
184
  sender: 'browser',
173
185
  pluginName,
@@ -185,6 +197,34 @@ describe(`DevToolsPluginClient (browser <> app)`, () => {
185
197
  expect(receivedMessages[0]).toEqual({ from: 'browserClient2' });
186
198
  await browserClient2.closeAsync();
187
199
  });
200
+
201
+ it('should terminate client from incompatible protocol version', async () => {
202
+ const spyConsoleInfo = jest.spyOn(console, 'info').mockImplementation(() => {});
203
+ const spyConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {});
204
+ appClient = await createDevToolsPluginClient({
205
+ protocolVersion: TEST_PROTOCOL_VERSION,
206
+ devServer,
207
+ sender: 'app',
208
+ pluginName,
209
+ wsStore: new WebSocketBackingStore(),
210
+ });
211
+ browserClient = await createDevToolsPluginClient({
212
+ protocolVersion: -1,
213
+ devServer,
214
+ sender: 'browser',
215
+ pluginName,
216
+ wsStore: new WebSocketBackingStore(),
217
+ });
218
+
219
+ await delayAsync(100);
220
+ expect(spyConsoleWarn).toHaveBeenCalledWith(
221
+ `Received an incompatible devtools plugin handshake message - pluginName[${pluginName}]`
222
+ );
223
+ expect(browserClient.isConnected()).toBe(false);
224
+
225
+ spyConsoleInfo.mockRestore();
226
+ spyConsoleWarn.mockRestore();
227
+ });
188
228
  });
189
229
 
190
230
  describe(`DevToolsPluginClient - multiplexing`, () => {
@@ -200,12 +240,14 @@ describe(`DevToolsPluginClient - multiplexing`, () => {
200
240
  it('should not close the websocket connection while there are alive clients', async () => {
201
241
  const wsStore = new WebSocketBackingStore();
202
242
  const appClient1 = await createDevToolsPluginClient({
243
+ protocolVersion: TEST_PROTOCOL_VERSION,
203
244
  devServer,
204
245
  sender: 'app',
205
246
  pluginName: 'plugin1',
206
247
  wsStore,
207
248
  });
208
249
  const appClient2 = await createDevToolsPluginClient({
250
+ protocolVersion: TEST_PROTOCOL_VERSION,
209
251
  devServer,
210
252
  sender: 'app',
211
253
  pluginName: 'plugin2',
@@ -18,6 +18,8 @@ globalThis.WebSocket = MockWebSocket;
18
18
  globalThis.TextDecoder ??= TextDecoder;
19
19
  globalThis.TextEncoder ??= TextEncoder;
20
20
 
21
+ const TEST_PROTOCOL_VERSION = 1;
22
+
21
23
  describe(getDevToolsPluginClientAsync, () => {
22
24
  const mockGetConnectionInfo = getConnectionInfo as jest.MockedFunction<typeof getConnectionInfo>;
23
25
 
@@ -28,6 +30,7 @@ describe(getDevToolsPluginClientAsync, () => {
28
30
 
29
31
  it('should return a DevToolsPluginClientImplApp client when sender is from app', async () => {
30
32
  mockGetConnectionInfo.mockReturnValue({
33
+ protocolVersion: TEST_PROTOCOL_VERSION,
31
34
  sender: 'app',
32
35
  devServer: 'localhost:8081',
33
36
  });
@@ -37,6 +40,7 @@ describe(getDevToolsPluginClientAsync, () => {
37
40
 
38
41
  it('should return a DevToolsPluginClientImplApp client when sender is from browser', async () => {
39
42
  mockGetConnectionInfo.mockReturnValue({
43
+ protocolVersion: TEST_PROTOCOL_VERSION,
40
44
  sender: 'browser',
41
45
  devServer: 'localhost:8081',
42
46
  });
@@ -46,6 +50,7 @@ describe(getDevToolsPluginClientAsync, () => {
46
50
 
47
51
  it('should return the same client from the same plugin name when called multiple times', async () => {
48
52
  mockGetConnectionInfo.mockReturnValue({
53
+ protocolVersion: TEST_PROTOCOL_VERSION,
49
54
  sender: 'app',
50
55
  devServer: 'localhost:8081',
51
56
  });
@@ -58,6 +63,7 @@ describe(getDevToolsPluginClientAsync, () => {
58
63
 
59
64
  it('should return a new client from the same plugin name from disconnected', async () => {
60
65
  mockGetConnectionInfo.mockReturnValue({
66
+ protocolVersion: TEST_PROTOCOL_VERSION,
61
67
  sender: 'app',
62
68
  devServer: 'localhost:8081',
63
69
  });
@@ -73,12 +79,14 @@ describe(getDevToolsPluginClientAsync, () => {
73
79
  const pluginName = 'testPlugin';
74
80
 
75
81
  mockGetConnectionInfo.mockReturnValueOnce({
82
+ protocolVersion: TEST_PROTOCOL_VERSION,
76
83
  sender: 'app',
77
84
  devServer: 'localhost:8081',
78
85
  });
79
86
  const client = await getDevToolsPluginClientAsync(pluginName);
80
87
 
81
88
  mockGetConnectionInfo.mockReturnValueOnce({
89
+ protocolVersion: TEST_PROTOCOL_VERSION,
82
90
  sender: 'app',
83
91
  devServer: 'localhost:8082',
84
92
  });
@@ -89,6 +97,7 @@ describe(getDevToolsPluginClientAsync, () => {
89
97
 
90
98
  it('should have at most one client per plugin name', async () => {
91
99
  mockGetConnectionInfo.mockReturnValue({
100
+ protocolVersion: TEST_PROTOCOL_VERSION,
92
101
  sender: 'app',
93
102
  devServer: 'localhost:8081',
94
103
  });
@@ -13,8 +13,11 @@ describe(MessageFramePacker, () => {
13
13
  payload: 'testPayload',
14
14
  };
15
15
 
16
- const packedData = await packer.pack(messageFrame);
17
- const unpackedData = await packer.unpack(packedData.buffer);
16
+ const packedData = packer.pack(messageFrame);
17
+ if (packedData instanceof Promise || packedData instanceof Uint8Array) {
18
+ throw new Error('Unexpected packed data type');
19
+ }
20
+ const unpackedData = packer.unpack(packedData);
18
21
  expect(unpackedData).toEqual(messageFrame);
19
22
  });
20
23
 
@@ -24,8 +27,11 @@ describe(MessageFramePacker, () => {
24
27
  payload: 12345,
25
28
  };
26
29
 
27
- const packedData = await packer.pack(messageFrame);
28
- const unpackedData = await packer.unpack(packedData.buffer);
30
+ const packedData = packer.pack(messageFrame);
31
+ if (packedData instanceof Promise || packedData instanceof Uint8Array) {
32
+ throw new Error('Unexpected packed data type');
33
+ }
34
+ const unpackedData = packer.unpack(packedData);
29
35
  expect(unpackedData).toEqual(messageFrame);
30
36
  });
31
37
 
@@ -35,8 +41,11 @@ describe(MessageFramePacker, () => {
35
41
  payload: null,
36
42
  };
37
43
 
38
- const packedData = await packer.pack(messageFrame);
39
- const unpackedData = await packer.unpack(packedData.buffer);
44
+ const packedData = packer.pack(messageFrame);
45
+ if (packedData instanceof Promise || packedData instanceof Uint8Array) {
46
+ throw new Error('Unexpected packed data type');
47
+ }
48
+ const unpackedData = packer.unpack(packedData);
40
49
  expect(unpackedData).toEqual(messageFrame);
41
50
  });
42
51
 
@@ -45,8 +54,11 @@ describe(MessageFramePacker, () => {
45
54
  messageKey: 'testKey',
46
55
  };
47
56
 
48
- const packedData = await packer.pack(messageFrame);
49
- const unpackedData = await packer.unpack(packedData.buffer);
57
+ const packedData = packer.pack(messageFrame);
58
+ if (packedData instanceof Promise || packedData instanceof Uint8Array) {
59
+ throw new Error('Unexpected packed data type');
60
+ }
61
+ const unpackedData = packer.unpack(packedData);
50
62
  expect(unpackedData).toEqual(messageFrame);
51
63
  });
52
64
 
@@ -56,8 +68,11 @@ describe(MessageFramePacker, () => {
56
68
  payload: { key: 'value' },
57
69
  };
58
70
 
59
- const packedData = await packer.pack(messageFrame);
60
- const unpackedData = await packer.unpack(packedData.buffer);
71
+ const packedData = packer.pack(messageFrame);
72
+ if (packedData instanceof Promise || packedData instanceof Uint8Array) {
73
+ throw new Error('Unexpected packed data type');
74
+ }
75
+ const unpackedData = packer.unpack(packedData);
61
76
  expect(unpackedData).toEqual(messageFrame);
62
77
  });
63
78
 
@@ -67,8 +82,11 @@ describe(MessageFramePacker, () => {
67
82
  payload: new Uint8Array([1, 2, 3, 4]),
68
83
  };
69
84
 
70
- const packedData = await packer.pack(messageFrame);
71
- const unpackedData = await packer.unpack(packedData.buffer);
85
+ const packedData = packer.pack(messageFrame);
86
+ if (packedData instanceof Promise || typeof packedData === 'string') {
87
+ throw new Error('Unexpected packed data type');
88
+ }
89
+ const unpackedData = packer.unpack(packedData.buffer);
72
90
  expect(unpackedData.messageKey).toEqual(messageFrame.messageKey);
73
91
  expect(unpackedData.payload).toEqual(messageFrame.payload);
74
92
  });
@@ -83,8 +101,11 @@ describe(MessageFramePacker, () => {
83
101
  payload: buffer,
84
102
  };
85
103
 
86
- const packedData = await packer.pack(messageFrame);
87
- const unpackedData = await packer.unpack(packedData.buffer);
104
+ const packedData = packer.pack(messageFrame);
105
+ if (packedData instanceof Promise || typeof packedData === 'string') {
106
+ throw new Error('Unexpected packed data type');
107
+ }
108
+ const unpackedData = packer.unpack(packedData.buffer);
88
109
  expect(unpackedData.messageKey).toEqual(messageFrame.messageKey);
89
110
  expect(new Uint8Array(unpackedData.payload as ArrayBuffer)).toEqual(view);
90
111
  });
@@ -97,8 +118,12 @@ describe(MessageFramePacker, () => {
97
118
  payload: blob,
98
119
  };
99
120
 
100
- const packedData = await packer.pack(messageFrame);
101
- const unpackedData = await packer.unpack(packedData.buffer);
121
+ const packedDataPromise = packer.pack(messageFrame);
122
+ if (packedDataPromise instanceof Uint8Array || typeof packedDataPromise === 'string') {
123
+ throw new Error('Unexpected packed data type');
124
+ }
125
+ const packedData = await packedDataPromise;
126
+ const unpackedData = packer.unpack(packedData.buffer);
102
127
  expect(unpackedData.messageKey).toEqual(messageFrame.messageKey);
103
128
  const unpackedBlob = unpackedData.payload as Blob;
104
129
  expect(await unpackedBlob.text()).toEqual(await blob.text());
@@ -111,8 +136,11 @@ describe(MessageFramePacker, () => {
111
136
  payload: 'testPayload',
112
137
  };
113
138
 
114
- const packedData = await packerWithObjectKey.pack(messageFrame);
115
- const unpackedData = await packerWithObjectKey.unpack(packedData.buffer);
139
+ const packedData = packerWithObjectKey.pack(messageFrame);
140
+ if (packedData instanceof Promise || packedData instanceof Uint8Array) {
141
+ throw new Error('Unexpected packed data type');
142
+ }
143
+ const unpackedData = packerWithObjectKey.unpack(packedData);
116
144
  expect(unpackedData).toEqual(messageFrame);
117
145
  });
118
146
 
@@ -122,6 +150,8 @@ describe(MessageFramePacker, () => {
122
150
  payload: () => {},
123
151
  };
124
152
 
125
- await expect(packer.pack(messageFrame)).rejects.toThrow('Unsupported payload type');
153
+ expect(() => {
154
+ packer.pack(messageFrame);
155
+ }).toThrow('Unsupported payload type');
126
156
  });
127
157
  });
@@ -21,15 +21,11 @@ export interface ConnectionInfo {
21
21
  * @hidden
22
22
  */
23
23
  wsStore?: WebSocketBackingStore;
24
- }
25
24
 
26
- /**
27
- * Parameters for the `handshake` message.
28
- * @hidden
29
- */
30
- export interface HandshakeMessageParams {
31
- browserClientId: string;
32
- pluginName: string;
25
+ /**
26
+ * The transport protocol version between the app and the webui.
27
+ */
28
+ protocolVersion: number;
33
29
  }
34
30
 
35
31
  /**
@@ -41,3 +37,14 @@ export interface DevToolsPluginClientOptions {
41
37
  */
42
38
  websocketBinaryType?: 'arraybuffer' | 'blob';
43
39
  }
40
+
41
+ /**
42
+ * The handshake messages for the devtools plugin client.
43
+ * @hidden
44
+ */
45
+ export interface HandshakeMessageParams {
46
+ protocolVersion: number;
47
+ pluginName: string;
48
+ method: 'handshake' | 'terminateBrowserClient';
49
+ browserClientId: string;
50
+ }
@@ -2,6 +2,7 @@
2
2
  * Get the dev server address.
3
3
  */
4
4
 
5
+ import { PROTOCOL_VERSION } from './ProtocolVersion';
5
6
  import type { ConnectionInfo } from './devtools.types';
6
7
 
7
8
  export function getConnectionInfo(): Omit<ConnectionInfo, 'pluginName'> {
@@ -10,6 +11,7 @@ export function getConnectionInfo(): Omit<ConnectionInfo, 'pluginName'> {
10
11
  .url.replace(/^https?:\/\//, '')
11
12
  .replace(/\/?$/, '') as string;
12
13
  return {
14
+ protocolVersion: PROTOCOL_VERSION,
13
15
  sender: 'app',
14
16
  devServer,
15
17
  };
@@ -2,12 +2,14 @@
2
2
  * Get the dev server address.
3
3
  */
4
4
 
5
+ import { PROTOCOL_VERSION } from './ProtocolVersion';
5
6
  import type { ConnectionInfo } from './devtools.types';
6
7
 
7
8
  export function getConnectionInfo(): Omit<ConnectionInfo, 'pluginName'> {
8
9
  const devServerQuery = new URLSearchParams(window.location.search).get('devServer');
9
10
  const host = window.location.origin.replace(/^https?:\/\//, '');
10
11
  return {
12
+ protocolVersion: PROTOCOL_VERSION,
11
13
  sender: 'browser',
12
14
  devServer: devServerQuery || host,
13
15
  };
@@ -40,7 +40,7 @@ describe(convertFormDataAsync, () => {
40
40
  const { body, boundary: resultBoundary } = await convertFormDataAsync(formData, boundary);
41
41
  expect(new TextDecoder().decode(body)).toMatchInlineSnapshot(`
42
42
  "------ExpoFetchFormBoundary0000000000000000
43
- content-disposition: form-data; name="blob"; filename="blobFile"; filename*=utf-8''blobFile
43
+ content-disposition: form-data; name="blob"; filename="blobFile"
44
44
  content-type: text/plain
45
45
 
46
46
  hello blob