@react-native-harness/bridge 1.3.0 → 1.4.0-rc.1

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 (69) hide show
  1. package/dist/__tests__/client.test.d.ts +2 -0
  2. package/dist/__tests__/client.test.d.ts.map +1 -0
  3. package/dist/__tests__/client.test.js +81 -0
  4. package/dist/__tests__/heartbeat.test.d.ts +2 -0
  5. package/dist/__tests__/heartbeat.test.d.ts.map +1 -0
  6. package/dist/__tests__/heartbeat.test.js +37 -0
  7. package/dist/__tests__/protocol.test.d.ts +2 -0
  8. package/dist/__tests__/protocol.test.d.ts.map +1 -0
  9. package/dist/__tests__/protocol.test.js +37 -0
  10. package/dist/__tests__/rpc-peer.test.d.ts +2 -0
  11. package/dist/__tests__/rpc-peer.test.d.ts.map +1 -0
  12. package/dist/__tests__/rpc-peer.test.js +127 -0
  13. package/dist/client.d.ts +7 -14
  14. package/dist/client.d.ts.map +1 -1
  15. package/dist/client.js +96 -42
  16. package/dist/errors.d.ts +5 -0
  17. package/dist/errors.d.ts.map +1 -1
  18. package/dist/errors.js +23 -0
  19. package/dist/heartbeat.d.ts +13 -0
  20. package/dist/heartbeat.d.ts.map +1 -0
  21. package/dist/heartbeat.js +49 -0
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/protocol.d.ts +51 -0
  26. package/dist/protocol.d.ts.map +1 -0
  27. package/dist/protocol.js +132 -0
  28. package/dist/rpc-peer.d.ts +28 -0
  29. package/dist/rpc-peer.d.ts.map +1 -0
  30. package/dist/rpc-peer.js +146 -0
  31. package/dist/server.d.ts +1 -20
  32. package/dist/server.d.ts.map +1 -1
  33. package/dist/server.js +122 -71
  34. package/dist/shared/test-collector.d.ts +2 -0
  35. package/dist/shared/test-collector.d.ts.map +1 -1
  36. package/dist/shared/test-runner.d.ts +13 -0
  37. package/dist/shared/test-runner.d.ts.map +1 -1
  38. package/dist/shared.d.ts +0 -2
  39. package/dist/shared.d.ts.map +1 -1
  40. package/dist/transport.d.ts +22 -0
  41. package/dist/transport.d.ts.map +1 -0
  42. package/dist/transport.js +22 -0
  43. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  44. package/dist/websocket-client-transport.d.ts +3 -0
  45. package/dist/websocket-client-transport.d.ts.map +1 -0
  46. package/dist/websocket-client-transport.js +45 -0
  47. package/dist/websocket-server-transport.d.ts +4 -0
  48. package/dist/websocket-server-transport.d.ts.map +1 -0
  49. package/dist/websocket-server-transport.js +43 -0
  50. package/eslint.config.mjs +5 -1
  51. package/package.json +6 -5
  52. package/src/__tests__/client.test.ts +99 -0
  53. package/src/__tests__/heartbeat.test.ts +47 -0
  54. package/src/__tests__/protocol.test.ts +51 -0
  55. package/src/__tests__/rpc-peer.test.ts +181 -0
  56. package/src/client.ts +146 -57
  57. package/src/errors.ts +32 -0
  58. package/src/heartbeat.ts +67 -0
  59. package/src/index.ts +1 -0
  60. package/src/protocol.ts +233 -0
  61. package/src/rpc-peer.ts +222 -0
  62. package/src/server.ts +179 -114
  63. package/src/shared/test-collector.ts +3 -0
  64. package/src/shared/test-runner.ts +14 -0
  65. package/src/shared.ts +0 -2
  66. package/src/transport.ts +47 -0
  67. package/src/websocket-client-transport.ts +85 -0
  68. package/src/websocket-server-transport.ts +54 -0
  69. package/vite.config.ts +18 -0
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ deserializeBridgeError,
4
+ parseBridgeMessage,
5
+ serializeBridgeError,
6
+ serializeBridgeMessage,
7
+ } from '../protocol.js';
8
+
9
+ describe('bridge protocol', () => {
10
+ it('round-trips invoke messages', () => {
11
+ const raw = serializeBridgeMessage({
12
+ type: 'invoke',
13
+ id: 1,
14
+ method: 'runTests',
15
+ args: ['example.ts', { runner: '/runner.js' }],
16
+ });
17
+
18
+ expect(parseBridgeMessage(raw)).toEqual({
19
+ type: 'invoke',
20
+ id: 1,
21
+ method: 'runTests',
22
+ args: ['example.ts', { runner: '/runner.js' }],
23
+ });
24
+ });
25
+
26
+ it('serializes and restores errors with metadata', () => {
27
+ const cause = new Error('inner');
28
+ const error = new TypeError('boom', { cause });
29
+ error.stack = 'stack trace';
30
+
31
+ const restored = deserializeBridgeError(serializeBridgeError(error));
32
+
33
+ expect(restored.name).toBe('TypeError');
34
+ expect(restored.message).toBe('boom');
35
+ expect(restored.stack).toBe('stack trace');
36
+ expect(restored.cause).toBe(cause);
37
+ });
38
+
39
+ it('serializes non-Error thrown values explicitly', () => {
40
+ expect(serializeBridgeError('boom')).toEqual({
41
+ name: 'NonErrorThrown',
42
+ message: 'boom',
43
+ });
44
+ });
45
+
46
+ it('rejects malformed messages', () => {
47
+ expect(() => parseBridgeMessage('{"type":"invoke","id":"1"}')).toThrow(
48
+ 'Invalid bridge message: id must be a number',
49
+ );
50
+ });
51
+ });
@@ -0,0 +1,181 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createRpcPeer } from '../rpc-peer.js';
3
+ import type { BridgeEvents } from '../shared.js';
4
+ import type { RpcTransport } from '../transport.js';
5
+
6
+ type LocalMethods = {
7
+ add: (left: number, right: number) => Promise<number>;
8
+ fail: () => Promise<never>;
9
+ };
10
+
11
+ const createMockTransport = (): RpcTransport & { messages: string[] } => {
12
+ const messages: string[] = [];
13
+
14
+ return {
15
+ state: 'open',
16
+ messages,
17
+ send: (message) => {
18
+ messages.push(message);
19
+ },
20
+ };
21
+ };
22
+
23
+ describe('rpc-peer', () => {
24
+ it('resolves successful RPC calls', async () => {
25
+ let serverHandleMessage: ((message: string) => Promise<unknown>) | null = null;
26
+
27
+ const client = createRpcPeer<Record<string, never>, { add: LocalMethods['add'] }, BridgeEvents>({
28
+ localMethods: {},
29
+ transport: {
30
+ state: 'open',
31
+ send: (message) => {
32
+ if (!serverHandleMessage) {
33
+ throw new Error('Server peer not initialized');
34
+ }
35
+
36
+ void serverHandleMessage(message);
37
+ },
38
+ },
39
+ });
40
+
41
+ const server = createRpcPeer<LocalMethods, Record<string, never>, BridgeEvents>({
42
+ localMethods: {
43
+ add: async (left, right) => left + right,
44
+ fail: async () => {
45
+ throw new Error('unused');
46
+ },
47
+ },
48
+ transport: {
49
+ state: 'open',
50
+ send: (message) => {
51
+ void client.handleMessage(message);
52
+ },
53
+ },
54
+ });
55
+
56
+ serverHandleMessage = server.handleMessage;
57
+
58
+ const result = await client.invoke('add', 2, 3);
59
+
60
+ expect(result).toBe(5);
61
+ });
62
+
63
+ it('rejects failed RPC calls with restored errors', async () => {
64
+ const client = createRpcPeer<Record<string, never>, { fail: () => Promise<void> }, BridgeEvents>({
65
+ localMethods: {},
66
+ transport: createMockTransport(),
67
+ });
68
+
69
+ const server = createRpcPeer<
70
+ { fail: () => Promise<void> },
71
+ Record<string, never>,
72
+ BridgeEvents
73
+ >({
74
+ localMethods: {
75
+ fail: async () => {
76
+ throw new TypeError('boom');
77
+ },
78
+ },
79
+ transport: {
80
+ state: 'open',
81
+ send: (message) => {
82
+ void client.handleMessage(message);
83
+ },
84
+ },
85
+ });
86
+
87
+ await server.handleMessage(
88
+ JSON.stringify({ type: 'invoke', id: 1, method: 'fail', args: [] }),
89
+ );
90
+
91
+ await expect(client.handleMessage('{"type":"return","id":1,"ok":false,"error":{"name":"TypeError","message":"boom"}}')).resolves.toBeNull();
92
+ });
93
+
94
+ it('rejects unknown methods', async () => {
95
+ let response = '';
96
+ const peer = createRpcPeer<Record<string, never>, Record<string, never>, BridgeEvents>({
97
+ localMethods: {},
98
+ transport: {
99
+ state: 'open',
100
+ send: (message) => {
101
+ response = message;
102
+ },
103
+ },
104
+ });
105
+
106
+ await peer.handleMessage(
107
+ JSON.stringify({ type: 'invoke', id: 1, method: 'missing', args: [] }),
108
+ );
109
+
110
+ expect(JSON.parse(response)).toMatchObject({
111
+ type: 'return',
112
+ id: 1,
113
+ ok: false,
114
+ });
115
+ });
116
+
117
+ it('dispatches incoming events', async () => {
118
+ const onEvent = vi.fn();
119
+ const peer = createRpcPeer<Record<string, never>, Record<string, never>, BridgeEvents>({
120
+ localMethods: {},
121
+ transport: createMockTransport(),
122
+ onEvent,
123
+ });
124
+
125
+ await peer.handleMessage(
126
+ JSON.stringify({
127
+ type: 'event',
128
+ event: { type: 'collection-started', file: 'example.ts' },
129
+ }),
130
+ );
131
+
132
+ expect(onEvent).toHaveBeenCalledWith({
133
+ type: 'collection-started',
134
+ file: 'example.ts',
135
+ });
136
+ });
137
+
138
+ it('rejects pending calls when closed', async () => {
139
+ const peer = createRpcPeer<
140
+ Record<string, never>,
141
+ { runTests: (path: string, options: { runner: string }) => Promise<void> },
142
+ BridgeEvents
143
+ >({
144
+ localMethods: {},
145
+ transport: createMockTransport(),
146
+ });
147
+
148
+ const pending = peer.invoke('runTests', 'example.ts', { runner: '/runner.js' });
149
+ peer.close(new Error('closed'));
150
+
151
+ await expect(pending).rejects.toThrow('closed');
152
+ });
153
+
154
+ it('rejects pending calls on timeout', async () => {
155
+ const peer = createRpcPeer<
156
+ Record<string, never>,
157
+ { runTests: (path: string, options: { runner: string }) => Promise<void> },
158
+ BridgeEvents
159
+ >({
160
+ localMethods: {},
161
+ transport: createMockTransport(),
162
+ callTimeoutMs: 10,
163
+ createTimeoutError: () => new Error('timed out'),
164
+ });
165
+
166
+ const pending = peer.invoke('runTests', 'example.ts', { runner: '/runner.js' });
167
+
168
+ await expect(pending).rejects.toThrow('timed out');
169
+ });
170
+
171
+ it('throws on malformed messages', async () => {
172
+ const peer = createRpcPeer<Record<string, never>, Record<string, never>, BridgeEvents>({
173
+ localMethods: {},
174
+ transport: createMockTransport(),
175
+ });
176
+
177
+ await expect(peer.handleMessage('{"type":"invoke"}')).rejects.toThrow(
178
+ 'Invalid bridge message: id must be a number',
179
+ );
180
+ });
181
+ });
package/src/client.ts CHANGED
@@ -1,6 +1,11 @@
1
- import { createBirpc } from 'birpc';
2
- import { deserialize, serialize } from './serializer.js';
3
1
  import { createBinaryFrame, generateTransferId } from './binary-transfer.js';
2
+ import { serializeBridgeMessage } from './protocol.js';
3
+ 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';
4
9
  import type {
5
10
  BridgeClientFunctions,
6
11
  BridgeServerFunctions,
@@ -12,27 +17,17 @@ import type {
12
17
  TestSuiteResult,
13
18
  } from './shared.js';
14
19
 
15
- // ---------------------------------------------------------------------------
16
- // Public types
17
- // ---------------------------------------------------------------------------
18
-
19
- /** Handlers the app must implement for the CLI to call into. */
20
20
  export type HarnessCallbacks = {
21
21
  runTests: (path: string, options: TestExecutionOptions) => Promise<TestSuiteResult>;
22
22
  };
23
23
 
24
- /** The app-side handle returned by connectToHarness. */
25
24
  export type HarnessHandle = {
26
- /** Call once when the app is initialised and ready to run tests. */
27
25
  reportReady: (device: DeviceDescriptor) => void;
28
- /** Forward a test or bundler event to the CLI. */
29
26
  emitEvent: (event: BridgeEvents) => void;
30
- /** Send a screenshot to the CLI and receive a file reference for snapshot comparison. */
31
27
  transferScreenshot: (
32
28
  data: Uint8Array,
33
29
  metadata: { width: number; height: number },
34
30
  ) => Promise<FileReference>;
35
- /** Request an image snapshot comparison on the CLI. */
36
31
  matchImageSnapshot: (
37
32
  screenshot: FileReference,
38
33
  testPath: string,
@@ -42,83 +37,173 @@ export type HarnessHandle = {
42
37
  disconnect: () => void;
43
38
  };
44
39
 
45
- // ---------------------------------------------------------------------------
46
- // Factory
47
- // ---------------------------------------------------------------------------
48
-
49
- /**
50
- * Connect the app to the CLI harness bridge.
51
- *
52
- * Pass the handlers the CLI can call (runTests). Returns a HarnessHandle
53
- * exposing the operations the app needs to drive a test run. The binary
54
- * transfer protocol and RPC wiring are fully encapsulated.
55
- */
40
+ export type ConnectToHarnessOptions = {
41
+ transport?: BridgeTransport;
42
+ };
43
+
44
+ export { createWebSocketClientTransport };
45
+
46
+ const noop = (): void => undefined;
47
+
56
48
  export const connectToHarness = (
57
49
  url: string,
58
50
  callbacks: HarnessCallbacks,
51
+ options: ConnectToHarnessOptions = {},
59
52
  ): Promise<HarnessHandle> =>
60
53
  new Promise((resolve, reject) => {
61
- const ws = new WebSocket(url);
62
- ws.binaryType = 'arraybuffer';
54
+ const transport = options.transport ?? createWebSocketClientTransport(url);
63
55
  let settled = false;
56
+ let peerClosed = false;
57
+ let offOpen: () => void = noop;
58
+ let offError: () => void = noop;
59
+ let offClose: () => void = noop;
64
60
 
65
61
  const cleanup = () => {
66
- ws.removeEventListener('open', handleOpen);
67
- ws.removeEventListener('error', handleError);
68
- ws.removeEventListener('close', handleClose);
62
+ offOpen();
63
+ offError();
64
+ offClose();
69
65
  };
70
66
 
71
67
  const fail = (message: string) => {
72
- if (settled) return;
68
+ if (settled) {
69
+ return;
70
+ }
71
+
73
72
  settled = true;
74
73
  cleanup();
75
74
  reject(new Error(message));
76
75
  };
77
76
 
78
77
  const handleOpen = () => {
78
+ if (settled) {
79
+ return;
80
+ }
81
+
79
82
  settled = true;
80
83
  cleanup();
81
84
 
82
- const rpc = createBirpc<BridgeServerFunctions, BridgeClientFunctions>(
83
- callbacks,
84
- {
85
- post: (data) => ws.send(data),
86
- on: (handler) => {
87
- ws.addEventListener('message', (event: MessageEvent<string | ArrayBuffer>) => {
88
- if (typeof event.data === 'string') handler(event.data);
89
- });
90
- },
91
- serialize,
92
- deserialize,
93
- },
94
- );
85
+ const getTransportNotOpenError = () => {
86
+ return new Error('Harness bridge transport is not open');
87
+ };
88
+
89
+ const rpc = createRpcPeer<
90
+ BridgeClientFunctions,
91
+ BridgeServerFunctions,
92
+ BridgeEvents
93
+ >({
94
+ localMethods: callbacks,
95
+ transport: createRpcTransport(transport),
96
+ });
97
+
98
+ let offMessage: () => void = noop;
99
+ let offRuntimeClose: () => void = noop;
100
+ let offRuntimeError: () => void = noop;
101
+
102
+ const closePeer = (reason: Error) => {
103
+ if (peerClosed) {
104
+ return;
105
+ }
106
+
107
+ peerClosed = true;
108
+ offMessage();
109
+ offRuntimeClose();
110
+ offRuntimeError();
111
+ rpc.close(reason);
112
+ };
113
+
114
+ const handleMessage = async (data: string) => {
115
+ const controlMessage = await rpc.handleMessage(data);
116
+
117
+ if (!controlMessage) {
118
+ return;
119
+ }
120
+
121
+ if (controlMessage.type === 'ping') {
122
+ transport.send(
123
+ serializeBridgeMessage({ type: 'pong', id: controlMessage.id }),
124
+ );
125
+ }
126
+ };
127
+
128
+ offMessage = transport.onMessage((message) => {
129
+ if (typeof message !== 'string') {
130
+ return;
131
+ }
132
+
133
+ void handleMessage(message).catch((error) => {
134
+ closePeer(
135
+ error instanceof Error
136
+ ? error
137
+ : new Error('Received invalid Harness bridge message'),
138
+ );
139
+
140
+ if (transport.state === 'open') {
141
+ transport.close(1002, 'Invalid message');
142
+ }
143
+ });
144
+ });
145
+
146
+ offRuntimeClose = transport.onClose((event) => {
147
+ closePeer(
148
+ new Error(
149
+ `Harness connection closed (code ${event.code}${
150
+ event.reason ? `, reason: ${event.reason}` : ''
151
+ })`,
152
+ ),
153
+ );
154
+ });
155
+
156
+ offRuntimeError = transport.onError((error) => {
157
+ closePeer(error);
158
+ });
95
159
 
96
160
  resolve({
97
- reportReady: (device) => void rpc.reportReady(device),
98
- emitEvent: (event) => void rpc.emitEvent(event.type, event),
161
+ reportReady: (device) => {
162
+ try {
163
+ if (transport.state !== 'open') {
164
+ throw getTransportNotOpenError();
165
+ }
166
+
167
+ transport.send(serializeBridgeMessage({ type: 'ready', device }));
168
+ } catch (error) {
169
+ closePeer(
170
+ error instanceof Error ? error : getTransportNotOpenError(),
171
+ );
172
+ }
173
+ },
174
+ emitEvent: (event) => {
175
+ rpc.sendEvent(event);
176
+ },
99
177
  transferScreenshot: async (data, metadata) => {
100
178
  const transferId = generateTransferId();
101
- ws.send(createBinaryFrame(transferId, data));
102
- return rpc['device.screenshot.receive'](
179
+ transport.send(createBinaryFrame(transferId, data));
180
+ return rpc.invoke(
181
+ 'device.screenshot.receive',
103
182
  { type: 'binary', transferId, size: data.length, mimeType: 'image/png' },
104
183
  metadata,
105
184
  );
106
185
  },
107
186
  matchImageSnapshot: (screenshot, testPath, options, runner) =>
108
- rpc['test.matchImageSnapshot'](screenshot, testPath, options, runner),
109
- disconnect: () => ws.close(),
187
+ rpc.invoke(
188
+ 'test.matchImageSnapshot',
189
+ screenshot,
190
+ testPath,
191
+ options,
192
+ runner,
193
+ ),
194
+ disconnect: () => {
195
+ closePeer(new Error('Harness connection closed by client'));
196
+ transport.close();
197
+ },
110
198
  });
111
199
  };
112
200
 
113
- const handleError = (event: Event & { message?: string }) => {
114
- const detail =
115
- typeof event.message === 'string' && event.message
116
- ? `: ${event.message}`
117
- : '';
201
+ const handleError = (error: Error) => {
202
+ const detail = error.message ? `: ${error.message}` : '';
118
203
  fail(`Failed to connect to Harness at ${url}${detail}`);
119
204
  };
120
205
 
121
- const handleClose = (event: CloseEvent) => {
206
+ const handleClose = (event: { code: number; reason: string }) => {
122
207
  fail(
123
208
  `Harness connection at ${url} closed before becoming ready (code ${event.code}${
124
209
  event.reason ? `, reason: ${event.reason}` : ''
@@ -126,7 +211,11 @@ export const connectToHarness = (
126
211
  );
127
212
  };
128
213
 
129
- ws.addEventListener('open', handleOpen);
130
- ws.addEventListener('error', handleError);
131
- ws.addEventListener('close', handleClose);
214
+ offOpen = transport.onOpen(handleOpen);
215
+ offError = transport.onError(handleError);
216
+ offClose = transport.onClose(handleClose);
217
+
218
+ if (transport.state === 'open') {
219
+ handleOpen();
220
+ }
132
221
  });
package/src/errors.ts CHANGED
@@ -9,3 +9,35 @@ export class DeviceNotRespondingError extends HarnessError {
9
9
  this.name = 'DeviceNotRespondingError';
10
10
  }
11
11
  }
12
+
13
+ export type AppBridgeDisconnectedReason =
14
+ | 'app-disconnected'
15
+ | 'app-replaced'
16
+ | 'heartbeat-timeout'
17
+ | 'socket-error'
18
+ | 'bridge-disposed';
19
+
20
+ const appBridgeDisconnectedMessage = (
21
+ reason: AppBridgeDisconnectedReason,
22
+ ): string => {
23
+ switch (reason) {
24
+ case 'app-replaced':
25
+ 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.';
28
+ case 'socket-error':
29
+ 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
+ case 'bridge-disposed':
31
+ return 'The app bridge was disposed before the test file finished running.';
32
+ case 'app-disconnected':
33
+ return 'The app bridge disconnected during test execution. This can happen if the app was killed, crashed, reloaded, or restarted while the test file was running.';
34
+ }
35
+ };
36
+
37
+ export class AppBridgeDisconnectedError extends HarnessError {
38
+ constructor(public readonly reason: AppBridgeDisconnectedReason) {
39
+ super(appBridgeDisconnectedMessage(reason));
40
+ this.name = 'AppBridgeDisconnectedError';
41
+ this.stack = `${this.name}: ${this.message}`;
42
+ }
43
+ }
@@ -0,0 +1,67 @@
1
+ export const DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;
2
+ export const DEFAULT_HEARTBEAT_TIMEOUT_MS = 20_000;
3
+
4
+ export type BridgeHeartbeat = {
5
+ notifyPong: (id: number) => void;
6
+ dispose: () => void;
7
+ };
8
+
9
+ export const createHeartbeat = (options: {
10
+ sendPing: (id: number) => void;
11
+ onTimeout: () => void;
12
+ intervalMs?: number;
13
+ timeoutMs?: number;
14
+ }): BridgeHeartbeat => {
15
+ const intervalMs = options.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
16
+ const timeoutMs = options.timeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
17
+ let nextPingId = 1;
18
+ let pendingPingId: number | null = null;
19
+ let disposed = false;
20
+ let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
21
+
22
+ const clearPendingTimeout = () => {
23
+ if (timeoutHandle) {
24
+ clearTimeout(timeoutHandle);
25
+ timeoutHandle = null;
26
+ }
27
+ };
28
+
29
+ const intervalHandle = setInterval(() => {
30
+ if (disposed || pendingPingId !== null) {
31
+ return;
32
+ }
33
+
34
+ const pingId = nextPingId++;
35
+ pendingPingId = pingId;
36
+ options.sendPing(pingId);
37
+ timeoutHandle = setTimeout(() => {
38
+ if (disposed || pendingPingId !== pingId) {
39
+ return;
40
+ }
41
+
42
+ pendingPingId = null;
43
+ timeoutHandle = null;
44
+ options.onTimeout();
45
+ }, timeoutMs);
46
+ }, intervalMs);
47
+
48
+ return {
49
+ notifyPong: (id) => {
50
+ if (id !== pendingPingId) {
51
+ return;
52
+ }
53
+
54
+ pendingPingId = null;
55
+ clearPendingTimeout();
56
+ },
57
+ dispose: () => {
58
+ if (disposed) {
59
+ return;
60
+ }
61
+
62
+ disposed = true;
63
+ clearInterval(intervalHandle);
64
+ clearPendingTimeout();
65
+ },
66
+ };
67
+ };
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './shared.js';
2
2
  export * from './binary-transfer.js';
3
+ export * from './transport.js';