@react-native-harness/bridge 1.5.0 → 1.7.0

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 (104) hide show
  1. package/dist/app-liveness.d.ts +33 -0
  2. package/dist/app-liveness.d.ts.map +1 -0
  3. package/dist/app-liveness.js +57 -0
  4. package/dist/binary.d.ts +16 -0
  5. package/dist/binary.d.ts.map +1 -0
  6. package/dist/binary.js +47 -0
  7. package/dist/client.d.ts +6 -4
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +112 -105
  10. package/dist/connection-check.d.ts +22 -0
  11. package/dist/connection-check.d.ts.map +1 -0
  12. package/dist/connection-check.js +30 -0
  13. package/dist/errors.d.ts +6 -2
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +12 -4
  16. package/dist/image-snapshot.d.ts +2 -2
  17. package/dist/image-snapshot.d.ts.map +1 -1
  18. package/dist/image-snapshot.js +21 -8
  19. package/dist/index.d.ts +0 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +0 -2
  22. package/dist/js-block-budget-policy.d.ts +31 -0
  23. package/dist/js-block-budget-policy.d.ts.map +1 -0
  24. package/dist/js-block-budget-policy.js +60 -0
  25. package/dist/js-watchdog.d.ts +31 -0
  26. package/dist/js-watchdog.d.ts.map +1 -0
  27. package/dist/js-watchdog.js +46 -0
  28. package/dist/link/channel.d.ts +45 -0
  29. package/dist/link/channel.d.ts.map +1 -0
  30. package/dist/link/channel.js +414 -0
  31. package/dist/link/envelope.d.ts +38 -0
  32. package/dist/link/envelope.d.ts.map +1 -0
  33. package/dist/link/envelope.js +90 -0
  34. package/dist/link/index.d.ts +22 -0
  35. package/dist/link/index.d.ts.map +1 -0
  36. package/dist/link/index.js +271 -0
  37. package/dist/link/ports.d.ts +31 -0
  38. package/dist/link/ports.d.ts.map +1 -0
  39. package/dist/link/ports.js +7 -0
  40. package/dist/link/scheduler.d.ts +21 -0
  41. package/dist/link/scheduler.d.ts.map +1 -0
  42. package/dist/link/scheduler.js +116 -0
  43. package/dist/link/standard-websocket.d.ts +36 -0
  44. package/dist/link/standard-websocket.d.ts.map +1 -0
  45. package/dist/link/standard-websocket.js +214 -0
  46. package/dist/link/streams.d.ts +4 -0
  47. package/dist/link/streams.d.ts.map +1 -0
  48. package/dist/link/streams.js +15 -0
  49. package/dist/protocol.d.ts +10 -5
  50. package/dist/protocol.d.ts.map +1 -1
  51. package/dist/protocol.js +2 -2
  52. package/dist/rpc-peer.d.ts +3 -1
  53. package/dist/rpc-peer.d.ts.map +1 -1
  54. package/dist/rpc-peer.js +2 -2
  55. package/dist/server.d.ts +4 -0
  56. package/dist/server.d.ts.map +1 -1
  57. package/dist/server.js +71 -56
  58. package/dist/shared/test-runner.d.ts +11 -0
  59. package/dist/shared/test-runner.d.ts.map +1 -1
  60. package/dist/shared.d.ts +5 -1
  61. package/dist/shared.d.ts.map +1 -1
  62. package/package.json +4 -3
  63. package/src/app-liveness.ts +103 -0
  64. package/src/binary.ts +74 -0
  65. package/src/client.ts +172 -170
  66. package/src/connection-check.ts +60 -0
  67. package/src/errors.ts +16 -5
  68. package/src/image-snapshot.ts +31 -9
  69. package/src/index.ts +0 -2
  70. package/src/js-block-budget-policy.ts +90 -0
  71. package/src/js-watchdog.ts +78 -0
  72. package/src/link/README.md +43 -0
  73. package/src/link/channel.ts +473 -0
  74. package/src/link/envelope.ts +110 -0
  75. package/src/link/index.ts +321 -0
  76. package/src/link/ports.ts +42 -0
  77. package/src/link/scheduler.ts +132 -0
  78. package/src/link/standard-websocket.ts +251 -0
  79. package/src/link/streams.ts +34 -0
  80. package/src/protocol.ts +14 -9
  81. package/src/rpc-peer.ts +6 -3
  82. package/src/server.ts +99 -67
  83. package/src/shared/test-runner.ts +12 -0
  84. package/src/shared.ts +6 -1
  85. package/dist/binary-transfer.d.ts +0 -17
  86. package/dist/binary-transfer.d.ts.map +0 -1
  87. package/dist/binary-transfer.js +0 -60
  88. package/dist/heartbeat.d.ts +0 -13
  89. package/dist/heartbeat.d.ts.map +0 -1
  90. package/dist/heartbeat.js +0 -49
  91. package/dist/transport.d.ts +0 -22
  92. package/dist/transport.d.ts.map +0 -1
  93. package/dist/transport.js +0 -22
  94. package/dist/websocket-client-transport.d.ts +0 -3
  95. package/dist/websocket-client-transport.d.ts.map +0 -1
  96. package/dist/websocket-client-transport.js +0 -45
  97. package/dist/websocket-server-transport.d.ts +0 -4
  98. package/dist/websocket-server-transport.d.ts.map +0 -1
  99. package/dist/websocket-server-transport.js +0 -43
  100. package/src/binary-transfer.ts +0 -79
  101. package/src/heartbeat.ts +0 -67
  102. package/src/transport.ts +0 -47
  103. package/src/websocket-client-transport.ts +0 -85
  104. package/src/websocket-server-transport.ts +0 -54
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-native-harness/bridge",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -31,9 +31,10 @@
31
31
  "pngjs": "^7.0.0",
32
32
  "ssim.js": "^3.5.0",
33
33
  "tslib": "^2.3.0",
34
+ "web-streams-polyfill": "^4.3.0",
34
35
  "ws": "^8.18.2",
35
- "@react-native-harness/platforms": "1.5.0",
36
- "@react-native-harness/tools": "1.5.0"
36
+ "@react-native-harness/platforms": "1.7.0",
37
+ "@react-native-harness/tools": "1.7.0"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@types/pixelmatch": "^5.2.6",
@@ -0,0 +1,103 @@
1
+ import {
2
+ createConnectionCheck,
3
+ type ConnectionCheckOptions,
4
+ type PingableSocket,
5
+ } from './connection-check.js';
6
+ import { AppBridgeDisconnectedError } from './errors.js';
7
+ import {
8
+ createJsBlockBudgetPolicy,
9
+ getIdleBudget,
10
+ type JsBlockBudgetPolicyOptions,
11
+ } from './js-block-budget-policy.js';
12
+ import { createJsWatchdog } from './js-watchdog.js';
13
+ import type { BridgeEvents } from './shared.js';
14
+
15
+ export type AppLivenessOptions = {
16
+ connectionCheck: ConnectionCheckOptions;
17
+ jsBlockBudget: JsBlockBudgetPolicyOptions;
18
+ jsProbeIntervalMs?: number;
19
+ };
20
+
21
+ export type AppLivenessOverrides = {
22
+ connectionCheck?: Partial<ConnectionCheckOptions>;
23
+ jsBlockBudget?: Partial<JsBlockBudgetPolicyOptions>;
24
+ jsProbeIntervalMs?: number;
25
+ };
26
+
27
+ export const DEFAULT_APP_LIVENESS_OPTIONS: AppLivenessOptions = {
28
+ connectionCheck: { intervalMs: 5_000, timeoutMs: 20_000 },
29
+ jsBlockBudget: { idleMs: 20_000, fileLoadMs: 60_000, graceMs: 5_000 },
30
+ };
31
+
32
+ // An override left `undefined` keeps the default instead of replacing it,
33
+ // which would turn a budget into `undefined` and disable it.
34
+ const definedOnly = <T extends object>(values: Partial<T> = {}): Partial<T> =>
35
+ Object.fromEntries(
36
+ Object.entries(values).filter(([, value]) => value !== undefined),
37
+ ) as Partial<T>;
38
+
39
+ export const resolveAppLivenessOptions = (
40
+ overrides: AppLivenessOverrides = {},
41
+ ): AppLivenessOptions => ({
42
+ connectionCheck: {
43
+ ...DEFAULT_APP_LIVENESS_OPTIONS.connectionCheck,
44
+ ...definedOnly(overrides.connectionCheck),
45
+ },
46
+ jsBlockBudget: {
47
+ ...DEFAULT_APP_LIVENESS_OPTIONS.jsBlockBudget,
48
+ ...definedOnly(overrides.jsBlockBudget),
49
+ },
50
+ jsProbeIntervalMs: overrides.jsProbeIntervalMs,
51
+ });
52
+
53
+ export type AppLiveness = {
54
+ handleEvent: (event: BridgeEvents) => void;
55
+ handleProbeAck: (id: number) => void;
56
+ dispose: () => void;
57
+ };
58
+
59
+ /**
60
+ * Watches one app connection with two independent checks: the connection
61
+ * check (is the process reachable?) and the JS watchdog (is the JS thread
62
+ * getting a turn?). See docs/adr/0001-app-liveness-detection.md.
63
+ */
64
+ export const createAppLiveness = (options: {
65
+ socket: PingableSocket;
66
+ sendProbe: (id: number) => void;
67
+ onLost: (error: AppBridgeDisconnectedError) => void;
68
+ config: AppLivenessOptions;
69
+ }): AppLiveness => {
70
+ const connectionCheck = createConnectionCheck(options.socket, {
71
+ ...options.config.connectionCheck,
72
+ onLost: () => {
73
+ options.onLost(new AppBridgeDisconnectedError('connection-lost'));
74
+ },
75
+ });
76
+
77
+ const policy = createJsBlockBudgetPolicy(options.config.jsBlockBudget);
78
+ const watchdog = createJsWatchdog({
79
+ sendProbe: options.sendProbe,
80
+ initialBudget: getIdleBudget(options.config.jsBlockBudget),
81
+ probeIntervalMs: options.config.jsProbeIntervalMs,
82
+ onUnresponsive: (budget) => {
83
+ options.onLost(
84
+ new AppBridgeDisconnectedError('js-thread-unresponsive', budget.label),
85
+ );
86
+ },
87
+ });
88
+
89
+ return {
90
+ handleEvent: (event) => {
91
+ const budget = policy.getBudgetForEvent(event);
92
+
93
+ if (budget) {
94
+ watchdog.setBlockBudget(budget);
95
+ }
96
+ },
97
+ handleProbeAck: watchdog.notifyProbeAck,
98
+ dispose: () => {
99
+ connectionCheck.dispose();
100
+ watchdog.dispose();
101
+ },
102
+ };
103
+ };
package/src/binary.ts ADDED
@@ -0,0 +1,74 @@
1
+ import type { LinkChannel, LinkConnection } from './link/index.js';
2
+
3
+ export type OutgoingBinary = {
4
+ channelId: string;
5
+ /** Settles once every byte is handed to the link, or rejects if the channel fails. */
6
+ written: Promise<void>;
7
+ abort: (reason?: string) => void;
8
+ };
9
+
10
+ /**
11
+ * Opens a link channel and starts writing `data` into it. The channel id travels in an
12
+ * RPC message; the peer reads the bytes with `receiveBinary`. The link sends the channel's
13
+ * OPEN before any later message, so the peer always knows the id by the time it asks.
14
+ */
15
+ export const sendBinary = async (
16
+ link: LinkConnection,
17
+ data: Uint8Array,
18
+ ): Promise<OutgoingBinary> => {
19
+ const channel = await link.openChannel({
20
+ signal: new AbortController().signal,
21
+ });
22
+
23
+ const written = (async () => {
24
+ const writer = channel.writable.getWriter();
25
+ await writer.write(data);
26
+ await writer.close();
27
+ })();
28
+
29
+ return {
30
+ channelId: channel.id,
31
+ written,
32
+ abort: (reason) => channel.abort(reason),
33
+ };
34
+ };
35
+
36
+ const readAll = async (channel: LinkChannel): Promise<Uint8Array> => {
37
+ const reader = channel.readable.getReader();
38
+ const chunks: Uint8Array[] = [];
39
+ let size = 0;
40
+
41
+ for (;;) {
42
+ const { done, value } = await reader.read();
43
+
44
+ if (done) {
45
+ break;
46
+ }
47
+
48
+ chunks.push(value);
49
+ size += value.length;
50
+ }
51
+
52
+ const bytes = new Uint8Array(size);
53
+ let offset = 0;
54
+
55
+ for (const chunk of chunks) {
56
+ bytes.set(chunk, offset);
57
+ offset += chunk.length;
58
+ }
59
+
60
+ return bytes;
61
+ };
62
+
63
+ /** Accepts the channel the peer opened with `sendBinary` and reads it to the end. */
64
+ export const receiveBinary = async (
65
+ link: LinkConnection,
66
+ channelId: string,
67
+ ): Promise<Uint8Array> => {
68
+ const channel = link.acceptChannel(channelId);
69
+
70
+ // Nothing flows back to the sender; ending this direction lets the channel free its slot.
71
+ void channel.writable.close().catch(() => undefined);
72
+
73
+ return readAll(channel);
74
+ };
package/src/client.ts CHANGED
@@ -1,11 +1,15 @@
1
- import { createBinaryFrame, generateTransferId } from './binary-transfer.js';
1
+ import { sendBinary } from './binary.js';
2
+ import {
3
+ connectStandardWebSocket,
4
+ createLink,
5
+ globalClock,
6
+ type LinkConnection,
7
+ type Logger,
8
+ type StandardWebSocket,
9
+ type UplinkTransportConnection,
10
+ } from './link/index.js';
2
11
  import { serializeBridgeMessage } from './protocol.js';
3
12
  import { createRpcPeer } from './rpc-peer.js';
4
- import {
5
- createRpcTransport,
6
- type BridgeTransport,
7
- } from './transport.js';
8
- import { createWebSocketClientTransport } from './websocket-client-transport.js';
9
13
  import type {
10
14
  BridgeClientFunctions,
11
15
  BridgeServerFunctions,
@@ -39,184 +43,182 @@ export type HarnessHandle = {
39
43
  };
40
44
 
41
45
  export type ConnectToHarnessOptions = {
42
- transport?: BridgeTransport;
46
+ /** An already open link transport, used instead of dialing `url`. */
47
+ transport?: UplinkTransportConnection;
48
+ /** Creates the WebSocket to dial `url` with. Defaults to the global `WebSocket`. */
49
+ createSocket?: (url: string) => StandardWebSocket;
43
50
  };
44
51
 
45
- export { createWebSocketClientTransport };
52
+ export type { UplinkTransportConnection };
46
53
 
47
54
  const noop = (): void => undefined;
48
55
 
49
- export const connectToHarness = (
56
+ const createConsoleLogger = (): Logger => {
57
+ const logger: Logger = {
58
+ debug: noop,
59
+ info: noop,
60
+ warn: (message) => console.warn(`[harness] ${message}`),
61
+ error: (message) => console.error(`[harness] ${message}`),
62
+ child: () => logger,
63
+ };
64
+
65
+ return logger;
66
+ };
67
+
68
+ // React Native has no `crypto.randomUUID`, so channel ids come from a counter. Ids only
69
+ // need to be unique among this connection's channels.
70
+ const createChannelIdGenerator = (): (() => string) => {
71
+ let nextId = 1;
72
+ return () => `app-${nextId++}`;
73
+ };
74
+
75
+ const dial = async (
76
+ url: string,
77
+ options: ConnectToHarnessOptions,
78
+ ): Promise<UplinkTransportConnection> => {
79
+ if (options.transport) {
80
+ return options.transport;
81
+ }
82
+
83
+ try {
84
+ return await connectStandardWebSocket({
85
+ url,
86
+ createSocket: options.createSocket,
87
+ });
88
+ } catch (error) {
89
+ const detail =
90
+ error instanceof Error && error.message ? `: ${error.message}` : '';
91
+ throw new Error(`Failed to connect to Harness at ${url}${detail}`);
92
+ }
93
+ };
94
+
95
+ export const connectToHarness = async (
50
96
  url: string,
51
97
  callbacks: HarnessCallbacks,
52
98
  options: ConnectToHarnessOptions = {},
53
- ): Promise<HarnessHandle> =>
54
- new Promise((resolve, reject) => {
55
- const transport = options.transport ?? createWebSocketClientTransport(url);
56
- let settled = false;
57
- let peerClosed = false;
58
- let offOpen: () => void = noop;
59
- let offError: () => void = noop;
60
- let offClose: () => void = noop;
61
-
62
- const cleanup = () => {
63
- offOpen();
64
- offError();
65
- offClose();
66
- };
67
-
68
- const fail = (message: string) => {
69
- if (settled) {
70
- return;
71
- }
99
+ ): Promise<HarnessHandle> => {
100
+ const transport = await dial(url, options);
101
+ const link: LinkConnection = createLink(transport, {
102
+ end: 'worker',
103
+ clock: globalClock,
104
+ logger: createConsoleLogger(),
105
+ generateId: createChannelIdGenerator(),
106
+ });
107
+
108
+ const rpc = createRpcPeer<
109
+ BridgeClientFunctions,
110
+ BridgeServerFunctions,
111
+ BridgeEvents
112
+ >({
113
+ localMethods: callbacks,
114
+ transport: {
115
+ send: (message) => {
116
+ void link.write(message);
117
+ },
118
+ },
119
+ });
120
+
121
+ let peerClosed = false;
122
+ let offMessage: () => void = noop;
123
+ let offClose: () => void = noop;
124
+ let offError: () => void = noop;
72
125
 
73
- settled = true;
74
- cleanup();
75
- reject(new Error(message));
76
- };
126
+ const getLinkClosedError = () => {
127
+ return new Error('Harness bridge link is closed');
128
+ };
77
129
 
78
- const handleOpen = () => {
79
- if (settled) {
130
+ const closePeer = (reason: Error) => {
131
+ if (peerClosed) {
132
+ return;
133
+ }
134
+
135
+ peerClosed = true;
136
+ offMessage();
137
+ offClose();
138
+ offError();
139
+ rpc.close(reason);
140
+ };
141
+
142
+ const handleMessage = async (data: string) => {
143
+ const controlMessage = await rpc.handleMessage(data);
144
+
145
+ if (!controlMessage) {
146
+ return;
147
+ }
148
+
149
+ if (controlMessage.type === 'js-probe') {
150
+ void link.write(
151
+ serializeBridgeMessage({ type: 'js-probe-ack', id: controlMessage.id }),
152
+ );
153
+ }
154
+ };
155
+
156
+ offMessage = link.onData((message) => {
157
+ void handleMessage(message).catch((error) => {
158
+ closePeer(
159
+ error instanceof Error
160
+ ? error
161
+ : new Error('Received invalid Harness bridge message'),
162
+ );
163
+ void link.close();
164
+ });
165
+ });
166
+
167
+ offError = link.onError((error) => {
168
+ closePeer(error);
169
+ });
170
+
171
+ offClose = link.onClose(() => {
172
+ closePeer(new Error('Harness connection closed'));
173
+ });
174
+
175
+ return {
176
+ reportReady: (device) => {
177
+ if (link.closed) {
178
+ closePeer(getLinkClosedError());
80
179
  return;
81
180
  }
82
181
 
83
- settled = true;
84
- cleanup();
85
-
86
- const getTransportNotOpenError = () => {
87
- return new Error('Harness bridge transport is not open');
88
- };
89
-
90
- const rpc = createRpcPeer<
91
- BridgeClientFunctions,
92
- BridgeServerFunctions,
93
- BridgeEvents
94
- >({
95
- localMethods: callbacks,
96
- transport: createRpcTransport(transport),
97
- });
98
-
99
- let offMessage: () => void = noop;
100
- let offRuntimeClose: () => void = noop;
101
- let offRuntimeError: () => void = noop;
102
-
103
- const closePeer = (reason: Error) => {
104
- if (peerClosed) {
105
- return;
106
- }
107
-
108
- peerClosed = true;
109
- offMessage();
110
- offRuntimeClose();
111
- offRuntimeError();
112
- rpc.close(reason);
113
- };
114
-
115
- const handleMessage = async (data: string) => {
116
- const controlMessage = await rpc.handleMessage(data);
117
-
118
- if (!controlMessage) {
119
- return;
120
- }
121
-
122
- if (controlMessage.type === 'ping') {
123
- transport.send(
124
- serializeBridgeMessage({ type: 'pong', id: controlMessage.id }),
125
- );
126
- }
127
- };
128
-
129
- offMessage = transport.onMessage((message) => {
130
- if (typeof message !== 'string') {
131
- return;
132
- }
133
-
134
- void handleMessage(message).catch((error) => {
135
- closePeer(
136
- error instanceof Error
137
- ? error
138
- : new Error('Received invalid Harness bridge message'),
139
- );
140
-
141
- if (transport.state === 'open') {
142
- transport.close(1002, 'Invalid message');
143
- }
144
- });
145
- });
146
-
147
- offRuntimeClose = transport.onClose((event) => {
148
- closePeer(
149
- new Error(
150
- `Harness connection closed (code ${event.code}${
151
- event.reason ? `, reason: ${event.reason}` : ''
152
- })`,
153
- ),
154
- );
155
- });
156
-
157
- offRuntimeError = transport.onError((error) => {
158
- closePeer(error);
159
- });
160
-
161
- resolve({
162
- reportReady: (device) => {
163
- try {
164
- if (transport.state !== 'open') {
165
- throw getTransportNotOpenError();
166
- }
167
-
168
- transport.send(serializeBridgeMessage({ type: 'ready', device }));
169
- } catch (error) {
170
- closePeer(
171
- error instanceof Error ? error : getTransportNotOpenError(),
172
- );
173
- }
174
- },
175
- emitEvent: (event) => {
176
- rpc.sendEvent(event);
177
- },
178
- transferScreenshot: async (data, metadata) => {
179
- const transferId = generateTransferId();
180
- transport.send(createBinaryFrame(transferId, data));
181
- return rpc.invoke(
182
+ void link.write(serializeBridgeMessage({ type: 'ready', device }));
183
+ },
184
+ emitEvent: (event) => {
185
+ rpc.sendEvent(event);
186
+ },
187
+ transferScreenshot: async (data, metadata) => {
188
+ const binary = await sendBinary(link, data);
189
+
190
+ try {
191
+ const [file] = await Promise.all([
192
+ rpc.invoke(
182
193
  'device.screenshot.receive',
183
- { type: 'binary', transferId, size: data.length, mimeType: 'image/png' },
194
+ {
195
+ type: 'binary',
196
+ channelId: binary.channelId,
197
+ size: data.length,
198
+ mimeType: 'image/png',
199
+ },
184
200
  metadata,
185
- );
186
- },
187
- matchImageSnapshot: (screenshot, testPath, options, runner) =>
188
- rpc.invoke(
189
- 'test.matchImageSnapshot',
190
- screenshot,
191
- testPath,
192
- options,
193
- runner,
194
201
  ),
195
- disconnect: () => {
196
- closePeer(new Error('Harness connection closed by client'));
197
- transport.close();
198
- },
199
- });
200
- };
201
-
202
- const handleError = (error: Error) => {
203
- const detail = error.message ? `: ${error.message}` : '';
204
- fail(`Failed to connect to Harness at ${url}${detail}`);
205
- };
206
-
207
- const handleClose = (event: { code: number; reason: string }) => {
208
- fail(
209
- `Harness connection at ${url} closed before becoming ready (code ${event.code}${
210
- event.reason ? `, reason: ${event.reason}` : ''
211
- })`,
212
- );
213
- };
202
+ binary.written,
203
+ ]);
214
204
 
215
- offOpen = transport.onOpen(handleOpen);
216
- offError = transport.onError(handleError);
217
- offClose = transport.onClose(handleClose);
218
-
219
- if (transport.state === 'open') {
220
- handleOpen();
221
- }
222
- });
205
+ return file;
206
+ } catch (error) {
207
+ binary.abort();
208
+ throw error;
209
+ }
210
+ },
211
+ matchImageSnapshot: (screenshot, testPath, options, runner) =>
212
+ rpc.invoke(
213
+ 'test.matchImageSnapshot',
214
+ screenshot,
215
+ testPath,
216
+ options,
217
+ runner,
218
+ ),
219
+ disconnect: () => {
220
+ closePeer(new Error('Harness connection closed by client'));
221
+ void link.close();
222
+ },
223
+ };
224
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Checks that the app process is still reachable, using WebSocket protocol
3
+ * ping frames (RFC 6455). The app's native WebSocket answers them without
4
+ * involving JS (OkHttp on Android, SocketRocket on iOS, the network stack in
5
+ * browsers), so a blocked JS thread does not fail this check.
6
+ */
7
+ export type PingableSocket = {
8
+ ping: () => void;
9
+ on: (event: 'pong', listener: () => void) => unknown;
10
+ off: (event: 'pong', listener: () => void) => unknown;
11
+ };
12
+
13
+ export type ConnectionCheckOptions = {
14
+ intervalMs: number;
15
+ timeoutMs: number;
16
+ };
17
+
18
+ export type ConnectionCheck = {
19
+ dispose: () => void;
20
+ };
21
+
22
+ export const createConnectionCheck = (
23
+ socket: PingableSocket,
24
+ options: ConnectionCheckOptions & { onLost: () => void },
25
+ ): ConnectionCheck => {
26
+ let lastPongAt = Date.now();
27
+ let disposed = false;
28
+
29
+ const handlePong = () => {
30
+ lastPongAt = Date.now();
31
+ };
32
+
33
+ socket.on('pong', handlePong);
34
+
35
+ const dispose = () => {
36
+ if (disposed) {
37
+ return;
38
+ }
39
+
40
+ disposed = true;
41
+ clearInterval(interval);
42
+ socket.off('pong', handlePong);
43
+ };
44
+
45
+ const interval = setInterval(() => {
46
+ if (Date.now() - lastPongAt > options.timeoutMs) {
47
+ dispose();
48
+ options.onLost();
49
+ return;
50
+ }
51
+
52
+ try {
53
+ socket.ping();
54
+ } catch {
55
+ // The socket is closing; its close event ends the session.
56
+ }
57
+ }, options.intervalMs);
58
+
59
+ return { dispose };
60
+ };
package/src/errors.ts CHANGED
@@ -13,7 +13,8 @@ export class DeviceNotRespondingError extends HarnessError {
13
13
  export type AppBridgeDisconnectedReason =
14
14
  | 'app-disconnected'
15
15
  | 'app-replaced'
16
- | 'heartbeat-timeout'
16
+ | 'connection-lost'
17
+ | 'js-thread-unresponsive'
17
18
  | 'socket-error'
18
19
  | 'bridge-disposed';
19
20
 
@@ -23,8 +24,10 @@ const appBridgeDisconnectedMessage = (
23
24
  switch (reason) {
24
25
  case 'app-replaced':
25
26
  return 'The app bridge was replaced by a newer app connection. This can happen when the app reloads, restarts, or reconnects while a test file is still running.';
26
- case 'heartbeat-timeout':
27
- return 'The app bridge stopped responding during test execution. This can happen if the app was killed, crashed, became unresponsive, or lost its WebSocket connection.';
27
+ case 'connection-lost':
28
+ return 'The app stopped answering WebSocket pings during test execution. The app process was likely killed, frozen, or lost its network connection. Check .harness/crash-reports for a crash report.';
29
+ case 'js-thread-unresponsive':
30
+ return "The app's JS thread was blocked for longer than allowed during test execution.";
28
31
  case 'socket-error':
29
32
  return 'The app bridge connection failed during test execution. This can happen if the app was killed, crashed, or the underlying WebSocket connection closed unexpectedly.';
30
33
  case 'bridge-disposed':
@@ -35,8 +38,16 @@ const appBridgeDisconnectedMessage = (
35
38
  };
36
39
 
37
40
  export class AppBridgeDisconnectedError extends HarnessError {
38
- constructor(public readonly reason: AppBridgeDisconnectedReason) {
39
- super(appBridgeDisconnectedMessage(reason));
41
+ constructor(
42
+ public readonly reason: AppBridgeDisconnectedReason,
43
+ /** What the app was busy with, when known. Appended to the message. */
44
+ public readonly detail?: string
45
+ ) {
46
+ super(
47
+ detail
48
+ ? `${appBridgeDisconnectedMessage(reason)} It was busy with: ${detail}.`
49
+ : appBridgeDisconnectedMessage(reason)
50
+ );
40
51
  this.name = 'AppBridgeDisconnectedError';
41
52
  this.stack = `${this.name}: ${this.message}`;
42
53
  }