expo 52.0.39 → 52.0.41

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 (29) 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 +12 -4
  5. package/build/devtools/DevToolsPluginClient.d.ts.map +1 -1
  6. package/build/devtools/DevToolsPluginClientImplApp.d.ts +1 -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 +7 -7
  18. package/package.json +10 -6
  19. package/src/devtools/DevToolsPluginClient.ts +71 -18
  20. package/src/devtools/DevToolsPluginClientImplApp.ts +32 -9
  21. package/src/devtools/DevToolsPluginClientImplBrowser.ts +13 -7
  22. package/src/devtools/MessageFramePacker.ts +100 -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
@@ -33,7 +33,7 @@ def getRNVersion() {
33
33
  ensureDependeciesWereEvaluated(project)
34
34
 
35
35
  group = 'host.exp.exponent'
36
- version = '52.0.39'
36
+ version = '52.0.41'
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.39"
49
+ versionName "52.0.41"
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,7 +30,6 @@ export declare abstract class DevToolsPluginClient {
30
30
  * @param params any extra payload.
31
31
  */
32
32
  sendMessage(method: string, params: any): void;
33
- private sendMessageImpl;
34
33
  /**
35
34
  * Subscribe to a message from the other end of DevTools.
36
35
  * @param method Subscribe to a message with a method name.
@@ -43,6 +42,16 @@ export declare abstract class DevToolsPluginClient {
43
42
  * @param listener Listener to be called when a message is received.
44
43
  */
45
44
  addMessageListenerOnce(method: string, listener: (params: any) => void): void;
45
+ /**
46
+ * Internal handshake message sender.
47
+ * @hidden
48
+ */
49
+ protected sendHandshakeMessage(params: HandshakeMessageParams): void;
50
+ /**
51
+ * Internal handshake message listener.
52
+ * @hidden
53
+ */
54
+ protected addHandskakeMessageListener(listener: (params: HandshakeMessageParams) => void): Partial<EventSubscription>;
46
55
  /**
47
56
  * Returns whether the client is connected to the server.
48
57
  */
@@ -51,8 +60,7 @@ export declare abstract class DevToolsPluginClient {
51
60
  * The method to create the WebSocket connection.
52
61
  */
53
62
  protected connectAsync(): Promise<WebSocket>;
54
- protected handleMessage: (event: WebSocketMessageEvent) => void;
55
- private handleMessageImpl;
63
+ protected handleMessage: (event: WebSocketMessageEvent) => Promise<void>;
56
64
  /**
57
65
  * Get the WebSocket backing store. Exposed for testing.
58
66
  * @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;IAmB9C;;;;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"}
@@ -10,5 +10,6 @@ export declare class DevToolsPluginClientImplApp extends DevToolsPluginClient {
10
10
  */
11
11
  initAsync(): Promise<void>;
12
12
  private addHandshakeHandler;
13
+ private terminateBrowserClient;
13
14
  }
14
15
  //# 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;AAG9D;;GAEG;AACH,qBAAa,2BAA4B,SAAQ,oBAAoB;IAEnE,OAAO,CAAC,gBAAgB,CAA8B;IAEtD;;;OAGG;IACY,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAKzC,OAAO,CAAC,mBAAmB;IA4B3B,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>): 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,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IA2BzF,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"}
@@ -16,7 +16,7 @@
16
16
  "expo-app-loader-provider": "~8.0.0",
17
17
  "expo-apple-authentication": "~7.1.3",
18
18
  "expo-application": "~6.0.2",
19
- "expo-asset": "~11.0.4",
19
+ "expo-asset": "~11.0.5",
20
20
  "expo-audio": "~0.3.5",
21
21
  "expo-auth-session": "~6.0.3",
22
22
  "expo-av": "~15.0.2",
@@ -34,16 +34,16 @@
34
34
  "expo-constants": "~17.0.8",
35
35
  "expo-contacts": "~14.0.5",
36
36
  "expo-crypto": "~14.0.2",
37
- "expo-dev-client": "~5.0.14",
37
+ "expo-dev-client": "~5.0.15",
38
38
  "expo-device": "~7.0.2",
39
39
  "expo-document-picker": "~13.0.3",
40
40
  "expo-face-detector": "~13.0.2",
41
- "expo-file-system": "~18.0.11",
41
+ "expo-file-system": "~18.0.12",
42
42
  "expo-font": "~13.0.4",
43
43
  "expo-gl": "~15.0.4",
44
44
  "expo-google-app-auth": "~8.3.0",
45
45
  "expo-haptics": "~14.0.1",
46
- "expo-image": "~2.0.6",
46
+ "expo-image": "~2.0.7",
47
47
  "expo-image-loader": "~5.0.0",
48
48
  "expo-image-manipulator": "~13.0.6",
49
49
  "expo-image-picker": "~16.0.6",
@@ -63,7 +63,7 @@
63
63
  "expo-network": "~7.0.5",
64
64
  "expo-notifications": "~0.29.14",
65
65
  "expo-print": "~14.0.3",
66
- "expo-router": "~4.0.18",
66
+ "expo-router": "~4.0.19",
67
67
  "expo-screen-capture": "~7.0.1",
68
68
  "expo-screen-orientation": "~8.0.4",
69
69
  "expo-secure-store": "~14.0.1",
@@ -72,14 +72,14 @@
72
72
  "expo-sms": "~13.0.1",
73
73
  "expo-speech": "~13.0.1",
74
74
  "expo-splash-screen": "~0.29.22",
75
- "expo-sqlite": "~15.1.2",
75
+ "expo-sqlite": "~15.1.3",
76
76
  "expo-status-bar": "~2.0.1",
77
77
  "expo-store-review": "~8.0.1",
78
78
  "expo-symbols": "~0.2.2",
79
79
  "expo-system-ui": "~4.0.8",
80
80
  "expo-task-manager": "~12.0.5",
81
81
  "expo-tracking-transparency": "~5.1.1",
82
- "expo-updates": "~0.27.3",
82
+ "expo-updates": "~0.27.4",
83
83
  "expo-video-thumbnails": "~9.0.3",
84
84
  "expo-video": "~2.0.6",
85
85
  "expo-web-browser": "~14.0.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo",
3
- "version": "52.0.39",
3
+ "version": "52.0.41",
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,16 +72,16 @@
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.20",
75
+ "@expo/cli": "0.22.22",
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
81
  "babel-preset-expo": "~12.0.9",
78
- "expo-asset": "~11.0.4",
82
+ "expo-asset": "~11.0.5",
79
83
  "expo-constants": "~17.0.8",
80
- "expo-file-system": "~18.0.11",
84
+ "expo-file-system": "~18.0.12",
81
85
  "expo-font": "~13.0.4",
82
86
  "expo-keep-awake": "~14.0.3",
83
87
  "expo-modules-autolinking": "2.0.8",
@@ -112,5 +116,5 @@
112
116
  "optional": true
113
117
  }
114
118
  },
115
- "gitHead": "ee33df50fbe6bf9bb5d772e7f341f85a27cf9993"
119
+ "gitHead": "5f0263343cdcddf406eb01fe2fb68182be041fd9"
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
 
@@ -70,16 +74,18 @@ export abstract class DevToolsPluginClient {
70
74
  logger.warn('Unable to send message in a disconnected state.');
71
75
  return;
72
76
  }
73
- this.sendMessageImpl(method, params);
74
- }
75
-
76
- private async sendMessageImpl(method: string, params: any) {
77
77
  const messageKey: MessageFramePackerMessageKey = {
78
78
  pluginName: this.connectionInfo.pluginName,
79
79
  method,
80
80
  };
81
- const packedData = await this.messageFramePacker.pack({ messageKey, payload: params });
82
- this.wsStore.ws?.send(packedData);
81
+ const packedData = this.messageFramePacker.pack({ messageKey, payload: params });
82
+ if (!(packedData instanceof Promise)) {
83
+ this.wsStore.ws?.send(packedData);
84
+ return;
85
+ }
86
+ packedData.then((data) => {
87
+ this.wsStore.ws?.send(data);
88
+ });
83
89
  }
84
90
 
85
91
  /**
@@ -100,6 +106,51 @@ export abstract class DevToolsPluginClient {
100
106
  this.eventEmitter.once(method, listener);
101
107
  }
102
108
 
109
+ /**
110
+ * Internal handshake message sender.
111
+ * @hidden
112
+ */
113
+ protected sendHandshakeMessage(params: HandshakeMessageParams) {
114
+ if (this.wsStore.ws?.readyState === WebSocket.CLOSED) {
115
+ logger.warn('Unable to send message in a disconnected state.');
116
+ return;
117
+ }
118
+ this.wsStore.ws?.send(JSON.stringify({ ...params, __isHandshakeMessages: true }));
119
+ }
120
+
121
+ /**
122
+ * Internal handshake message listener.
123
+ * @hidden
124
+ */
125
+ protected addHandskakeMessageListener(
126
+ listener: (params: HandshakeMessageParams) => void
127
+ ): Partial<EventSubscription> {
128
+ const messageListener = (event: MessageEvent) => {
129
+ if (typeof event.data !== 'string') {
130
+ // binary data is not coming from the handshake messages.
131
+ return;
132
+ }
133
+
134
+ const data = JSON.parse(event.data);
135
+ if (!data.__isHandshakeMessages) {
136
+ return;
137
+ }
138
+ delete data.__isHandshakeMessages;
139
+ const params = data as HandshakeMessageParams;
140
+ if (params.pluginName && params.pluginName !== this.connectionInfo.pluginName) {
141
+ return;
142
+ }
143
+ listener(params);
144
+ };
145
+
146
+ this.wsStore.ws?.addEventListener('message', messageListener);
147
+ return {
148
+ remove: () => {
149
+ this.wsStore.ws?.removeEventListener('message', messageListener);
150
+ },
151
+ };
152
+ }
153
+
103
154
  /**
104
155
  * Returns whether the client is connected to the server.
105
156
  */
@@ -135,23 +186,25 @@ export abstract class DevToolsPluginClient {
135
186
  });
136
187
  }
137
188
 
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;
189
+ protected handleMessage = async (event: WebSocketMessageEvent) => {
190
+ let data: ArrayBuffer | string;
191
+ if (typeof event.data === 'string') {
192
+ data = event.data;
193
+ } else if (event.data instanceof ArrayBuffer) {
194
+ data = event.data;
146
195
  } else if (ArrayBuffer.isView(event.data)) {
147
- buffer = event.data.buffer;
196
+ data = event.data.buffer;
148
197
  } else if (event.data instanceof Blob) {
149
- buffer = await blobToArrayBufferAsync(event.data);
198
+ data = await blobToArrayBufferAsync(event.data);
150
199
  } else {
151
200
  logger.warn('Unsupported received data type in handleMessageImpl');
152
201
  return;
153
202
  }
154
- const { messageKey, payload } = await this.messageFramePacker.unpack(buffer);
203
+ const { messageKey, payload, ...rest } = this.messageFramePacker.unpack(data);
204
+ // @ts-expect-error: `__isHandshakeMessages` is a private field that is not part of the MessageFramePacker type.
205
+ if (rest?.__isHandshakeMessages === true) {
206
+ return;
207
+ }
155
208
  if (messageKey.pluginName && messageKey.pluginName !== this.connectionInfo.pluginName) {
156
209
  return;
157
210
  }
@@ -1,5 +1,4 @@
1
1
  import { DevToolsPluginClient } from './DevToolsPluginClient';
2
- import type { HandshakeMessageParams } from './devtools.types';
3
2
  import * as logger from './logger';
4
3
 
5
4
  /**
@@ -19,15 +18,39 @@ export class DevToolsPluginClientImplApp extends DevToolsPluginClient {
19
18
  }
20
19
 
21
20
  private addHandshakeHandler() {
22
- this.addMessageListener('handshake', (params: HandshakeMessageParams) => {
23
- const previousBrowserClientId = this.browserClientMap[params.pluginName];
24
- if (previousBrowserClientId != null && previousBrowserClientId !== params.browserClientId) {
25
- logger.info(
26
- `Terminate the previous browser client connection - previousBrowserClientId[${previousBrowserClientId}]`
27
- );
28
- this.sendMessage('terminateBrowserClient', { browserClientId: previousBrowserClientId });
21
+ this.addHandskakeMessageListener((params) => {
22
+ if (params.method === 'handshake') {
23
+ const { pluginName, protocolVersion } = params;
24
+
25
+ // [0] Check protocol version
26
+ if (protocolVersion !== this.connectionInfo.protocolVersion) {
27
+ // Use console.warn than logger because we want to show the warning even logging is disabled.
28
+ console.warn(
29
+ `Received an incompatible devtools plugin handshake message - pluginName[${pluginName}]`
30
+ );
31
+ this.terminateBrowserClient(pluginName, params.browserClientId);
32
+ return;
33
+ }
34
+
35
+ // [1] Terminate duplicated browser clients for the same plugin
36
+ const previousBrowserClientId = this.browserClientMap[pluginName];
37
+ if (previousBrowserClientId != null && previousBrowserClientId !== params.browserClientId) {
38
+ logger.info(
39
+ `Terminate the previous browser client connection - previousBrowserClientId[${previousBrowserClientId}]`
40
+ );
41
+ this.terminateBrowserClient(pluginName, previousBrowserClientId);
42
+ }
43
+ this.browserClientMap[pluginName] = params.browserClientId;
29
44
  }
30
- this.browserClientMap[params.pluginName] = params.browserClientId;
45
+ });
46
+ }
47
+
48
+ private terminateBrowserClient(pluginName: string, browserClientId: string) {
49
+ this.sendHandshakeMessage({
50
+ protocolVersion: this.connectionInfo.protocolVersion,
51
+ method: 'terminateBrowserClient',
52
+ browserClientId,
53
+ pluginName,
31
54
  });
32
55
  }
33
56
  }
@@ -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,39 @@ 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({ messageKey, payload }: MessageFrame<T>): string | Uint8Array | Promise<Uint8Array> {
59
+ // Fast path to pack as string given `JSON.stringify` is fast.
60
+ if (this.isFastPathPayload(payload)) {
61
+ return JSON.stringify({ messageKey, payload });
62
+ }
70
63
 
71
- // [3] payload (variable length)
72
- packedArray.set(payloadBinary, 4 + messageKeyLength + 1);
64
+ // Slowest path for Blob returns a promise.
65
+ if (payload instanceof Blob) {
66
+ return new Promise(async (resolve, reject) => {
67
+ try {
68
+ const arrayBuffer = await blobToArrayBufferAsync(payload);
69
+ resolve(
70
+ this.packImpl(
71
+ { messageKey, payload: new Uint8Array(arrayBuffer) },
72
+ PayloadTypeIndicator.Blob
73
+ )
74
+ );
75
+ } catch (error) {
76
+ reject(error);
77
+ }
78
+ });
79
+ }
73
80
 
74
- return packedArray;
81
+ // Slow path for other types returns a Uint8Array.
82
+ return this.packImpl({ messageKey, payload }, undefined);
75
83
  }
76
84
 
77
- public async unpack(packedData: ArrayBuffer): Promise<MessageFrame<T>> {
85
+ public unpack(packedData: string | ArrayBuffer): MessageFrame<T> {
86
+ // Fast path to unpack as string given `JSON.parse` is fast.
87
+ if (typeof packedData === 'string') {
88
+ return JSON.parse(packedData);
89
+ }
90
+
78
91
  // [0] messageKeyLength (4 bytes)
79
92
  const messageKeyLengthView = new DataView(packedData, 0, 4);
80
93
  const messageKeyLength = messageKeyLengthView.getUint32(0, false);
@@ -90,12 +103,26 @@ export class MessageFramePacker<T extends MessageKeyTypeBase> {
90
103
 
91
104
  // [3] payload (variable length)
92
105
  const payloadBinary = packedData.slice(4 + messageKeyLength + 1);
93
- const payload = await this.deserializePayload(payloadBinary, payloadType);
106
+ const payload = this.deserializePayload(payloadBinary, payloadType);
94
107
 
95
108
  return { messageKey, payload };
96
109
  }
97
110
 
98
- private async payloadToUint8Array(payload: PayloadType): Promise<Uint8Array> {
111
+ private isFastPathPayload(payload: PayloadType): boolean {
112
+ if (payload == null) {
113
+ return true;
114
+ }
115
+ const payloadType = typeof payload;
116
+ if (payloadType === 'string' || payloadType === 'number') {
117
+ return true;
118
+ }
119
+ if (payloadType === 'object' && payload.constructor === Object) {
120
+ return true;
121
+ }
122
+ return false;
123
+ }
124
+
125
+ private payloadToUint8Array(payload: PayloadType): Uint8Array {
99
126
  if (payload instanceof Uint8Array) {
100
127
  return payload;
101
128
  } else if (typeof payload === 'string') {
@@ -112,16 +139,46 @@ export class MessageFramePacker<T extends MessageKeyTypeBase> {
112
139
  } else if (payload instanceof ArrayBuffer) {
113
140
  return new Uint8Array(payload);
114
141
  } else if (payload instanceof Blob) {
115
- return new Uint8Array(await blobToArrayBufferAsync(payload));
142
+ throw new Error('Blob is not supported in this callsite.');
116
143
  } else {
117
144
  return this.textEncoder.encode(JSON.stringify(payload));
118
145
  }
119
146
  }
120
147
 
121
- private async deserializePayload(
148
+ private packImpl(
149
+ { messageKey, payload }: MessageFrame<T>,
150
+ payloadType: PayloadTypeIndicator | undefined
151
+ ): Promise<Uint8Array> | Uint8Array {
152
+ const messageKeyString = JSON.stringify(messageKey);
153
+ const messageKeyBytes = this.textEncoder.encode(messageKeyString);
154
+ const messageKeyLength = messageKeyBytes.length;
155
+ const payloadBinary = this.payloadToUint8Array(payload);
156
+
157
+ const totalLength = 4 + messageKeyLength + 1 + payloadBinary.byteLength;
158
+ const buffer = new ArrayBuffer(totalLength);
159
+ const packedArray = new Uint8Array(buffer);
160
+
161
+ // [0] messageKeyLength (4 bytes)
162
+ const messageKeyLengthView = new DataView(buffer, 0, 4);
163
+ messageKeyLengthView.setUint32(0, messageKeyLength, false);
164
+
165
+ // [1] messageKey (variable length)
166
+ packedArray.set(messageKeyBytes, 4);
167
+
168
+ // [2] payloadTypeIndicator (1 byte)
169
+ const payloadTypeView = new DataView(buffer, 4 + messageKeyLength, 1);
170
+ payloadTypeView.setUint8(0, payloadType ?? MessageFramePacker.getPayloadTypeIndicator(payload));
171
+
172
+ // [3] payload (variable length)
173
+ packedArray.set(payloadBinary, 4 + messageKeyLength + 1);
174
+
175
+ return packedArray;
176
+ }
177
+
178
+ private deserializePayload(
122
179
  payloadBinary: ArrayBuffer,
123
180
  payloadTypeIndicator: PayloadTypeIndicator
124
- ): Promise<PayloadType> {
181
+ ): PayloadType {
125
182
  switch (payloadTypeIndicator) {
126
183
  case PayloadTypeIndicator.Uint8Array: {
127
184
  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
  };