react-native-web-serial-api 0.1.0 → 0.2.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 (112) hide show
  1. package/README.md +188 -117
  2. package/TESTING.md +417 -176
  3. package/android/build.gradle +14 -0
  4. package/android/src/main/java/dev/webserialapi/NativeUsbSerialModule.java +74 -11
  5. package/android/src/main/java/dev/webserialapi/PortPickerActivity.java +61 -59
  6. package/bin/expose-serial.js +205 -0
  7. package/lib/commonjs/UsbSerial.js +1 -1
  8. package/lib/commonjs/WebSerial.js +110 -26
  9. package/lib/commonjs/WebSerial.js.map +1 -1
  10. package/lib/commonjs/index.js +2 -2
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/lib/event-target.js +3 -1
  13. package/lib/commonjs/lib/event-target.js.map +1 -1
  14. package/lib/commonjs/lib/web-streams.js +42 -0
  15. package/lib/commonjs/lib/web-streams.js.map +1 -0
  16. package/lib/commonjs/testing/device-fixture.js +70 -0
  17. package/lib/commonjs/testing/device-fixture.js.map +1 -0
  18. package/lib/commonjs/testing/expose.js +91 -0
  19. package/lib/commonjs/testing/expose.js.map +1 -0
  20. package/lib/commonjs/testing/harness.js +98 -0
  21. package/lib/commonjs/testing/harness.js.map +1 -0
  22. package/lib/commonjs/testing/{virtual-serial.js → in-memory-serial-transport.js} +66 -28
  23. package/lib/commonjs/testing/in-memory-serial-transport.js.map +1 -0
  24. package/lib/commonjs/testing/index.js +100 -17
  25. package/lib/commonjs/testing/index.js.map +1 -1
  26. package/lib/commonjs/testing/install-in-memory-serial-transport.js +54 -0
  27. package/lib/commonjs/testing/install-in-memory-serial-transport.js.map +1 -0
  28. package/lib/commonjs/testing/serial-client.js +277 -0
  29. package/lib/commonjs/testing/serial-client.js.map +1 -0
  30. package/lib/commonjs/testing/{serial-device.js → simulated-device.js} +17 -17
  31. package/lib/commonjs/testing/simulated-device.js.map +1 -0
  32. package/lib/commonjs/testing/test-suite.js +142 -0
  33. package/lib/commonjs/testing/test-suite.js.map +1 -0
  34. package/lib/commonjs/transport.js +3 -3
  35. package/lib/commonjs/websocket/WebSocketSerialTransport.js +659 -0
  36. package/lib/commonjs/websocket/WebSocketSerialTransport.js.map +1 -0
  37. package/lib/commonjs/websocket/bridge.js +234 -0
  38. package/lib/commonjs/websocket/bridge.js.map +1 -0
  39. package/lib/commonjs/websocket/index.js +33 -0
  40. package/lib/commonjs/websocket/index.js.map +1 -0
  41. package/lib/commonjs/websocket/protocol.js +55 -0
  42. package/lib/commonjs/websocket/protocol.js.map +1 -0
  43. package/lib/commonjs/websocket/serial-device-bridge.js +130 -0
  44. package/lib/commonjs/websocket/serial-device-bridge.js.map +1 -0
  45. package/lib/typescript/src/UsbSerial.d.ts +1 -1
  46. package/lib/typescript/src/WebSerial.d.ts +7 -7
  47. package/lib/typescript/src/WebSerial.d.ts.map +1 -1
  48. package/lib/typescript/src/index.d.ts +1 -1
  49. package/lib/typescript/src/index.d.ts.map +1 -1
  50. package/lib/typescript/src/lib/event-target.d.ts +2 -0
  51. package/lib/typescript/src/lib/event-target.d.ts.map +1 -1
  52. package/lib/typescript/src/lib/web-streams.d.ts +9 -0
  53. package/lib/typescript/src/lib/web-streams.d.ts.map +1 -0
  54. package/lib/typescript/src/testing/device-fixture.d.ts +70 -0
  55. package/lib/typescript/src/testing/device-fixture.d.ts.map +1 -0
  56. package/lib/typescript/src/testing/expose.d.ts +71 -0
  57. package/lib/typescript/src/testing/expose.d.ts.map +1 -0
  58. package/lib/typescript/src/testing/harness.d.ts +34 -0
  59. package/lib/typescript/src/testing/harness.d.ts.map +1 -0
  60. package/lib/typescript/src/testing/{virtual-serial.d.ts → in-memory-serial-transport.d.ts} +37 -26
  61. package/lib/typescript/src/testing/in-memory-serial-transport.d.ts.map +1 -0
  62. package/lib/typescript/src/testing/index.d.ts +18 -8
  63. package/lib/typescript/src/testing/index.d.ts.map +1 -1
  64. package/lib/typescript/src/testing/install-in-memory-serial-transport.d.ts +25 -0
  65. package/lib/typescript/src/testing/install-in-memory-serial-transport.d.ts.map +1 -0
  66. package/lib/typescript/src/testing/serial-client.d.ts +62 -0
  67. package/lib/typescript/src/testing/serial-client.d.ts.map +1 -0
  68. package/lib/typescript/src/testing/{serial-device.d.ts → simulated-device.d.ts} +23 -23
  69. package/lib/typescript/src/testing/simulated-device.d.ts.map +1 -0
  70. package/lib/typescript/src/testing/test-suite.d.ts +75 -0
  71. package/lib/typescript/src/testing/test-suite.d.ts.map +1 -0
  72. package/lib/typescript/src/transport.d.ts +3 -3
  73. package/lib/typescript/src/websocket/WebSocketSerialTransport.d.ts +111 -0
  74. package/lib/typescript/src/websocket/WebSocketSerialTransport.d.ts.map +1 -0
  75. package/lib/typescript/src/websocket/bridge.d.ts +66 -0
  76. package/lib/typescript/src/websocket/bridge.d.ts.map +1 -0
  77. package/lib/typescript/src/websocket/index.d.ts +19 -0
  78. package/lib/typescript/src/websocket/index.d.ts.map +1 -0
  79. package/lib/typescript/src/websocket/protocol.d.ts +64 -0
  80. package/lib/typescript/src/websocket/protocol.d.ts.map +1 -0
  81. package/lib/typescript/src/websocket/serial-device-bridge.d.ts +32 -0
  82. package/lib/typescript/src/websocket/serial-device-bridge.d.ts.map +1 -0
  83. package/package.json +21 -3
  84. package/src/UsbSerial.ts +1 -1
  85. package/src/WebSerial.ts +134 -35
  86. package/src/index.ts +4 -1
  87. package/src/lib/event-target.ts +12 -0
  88. package/src/lib/web-streams.ts +43 -0
  89. package/src/testing/device-fixture.ts +150 -0
  90. package/src/testing/expose.ts +147 -0
  91. package/src/testing/harness.ts +124 -0
  92. package/src/testing/{virtual-serial.ts → in-memory-serial-transport.ts} +95 -56
  93. package/src/testing/index.ts +69 -21
  94. package/src/testing/install-in-memory-serial-transport.ts +65 -0
  95. package/src/testing/serial-client.ts +313 -0
  96. package/src/testing/{serial-device.ts → simulated-device.ts} +23 -23
  97. package/src/testing/test-suite.ts +186 -0
  98. package/src/transport.ts +3 -3
  99. package/src/websocket/WebSocketSerialTransport.ts +796 -0
  100. package/src/websocket/bridge.ts +299 -0
  101. package/src/websocket/index.ts +38 -0
  102. package/src/websocket/protocol.ts +101 -0
  103. package/src/websocket/serial-device-bridge.ts +160 -0
  104. package/lib/commonjs/testing/install.js +0 -54
  105. package/lib/commonjs/testing/install.js.map +0 -1
  106. package/lib/commonjs/testing/serial-device.js.map +0 -1
  107. package/lib/commonjs/testing/virtual-serial.js.map +0 -1
  108. package/lib/typescript/src/testing/install.d.ts +0 -25
  109. package/lib/typescript/src/testing/install.d.ts.map +0 -1
  110. package/lib/typescript/src/testing/serial-device.d.ts.map +0 -1
  111. package/lib/typescript/src/testing/virtual-serial.d.ts.map +0 -1
  112. package/src/testing/install.ts +0 -65
package/src/UsbSerial.ts CHANGED
@@ -266,7 +266,7 @@ export function getUsbSerial(): SerialTransport {
266
266
 
267
267
  /**
268
268
  * Override the transport returned by {@link getUsbSerial}. Pass a
269
- * {@link SerialTransport} (e.g. a `VirtualSerialTransport`) to make the
269
+ * {@link SerialTransport} (e.g. an `InMemorySerialTransport`) to make the
270
270
  * singleton `serial` instance — and any `new Serial()` created without an
271
271
  * explicit transport — talk to it instead of real hardware. Pass `null` to
272
272
  * clear the override.
package/src/WebSerial.ts CHANGED
@@ -1,11 +1,11 @@
1
- import {
2
- ByteLengthQueuingStrategy,
3
- ReadableStream,
4
- WritableStream,
5
- } from 'web-streams-polyfill';
6
1
  import {DOMException} from './lib/dom-exception';
7
2
  import {Event, EventTarget, setEventParent} from './lib/event-target';
8
3
  import {createDeferredPromise} from './lib/promise';
4
+ import {
5
+ ByteLengthQueuingStrategyImpl as ByteLengthQueuingStrategy,
6
+ ReadableStreamImpl as ReadableStream,
7
+ WritableStreamImpl as WritableStream,
8
+ } from './lib/web-streams';
9
9
  import type {
10
10
  ConnectEvent,
11
11
  DataEvent,
@@ -23,14 +23,14 @@ import {getUsbSerial} from './UsbSerial';
23
23
  * even - Data word plus parity bit has even parity.
24
24
  * odd - Data word plus parity bit has odd parity.
25
25
  */
26
- type ParityType = 'none' | 'even' | 'odd';
26
+ export type Parity = 'none' | 'even' | 'odd';
27
27
 
28
28
  /**
29
29
  * @see https://wicg.github.io/serial/#dom-flowcontroltype
30
30
  * none - No flow control is enabled.
31
31
  * hardware - Hardware flow control using the RTS and CTS signals is enabled.
32
32
  */
33
- type FlowControlType = 'none' | 'hardware';
33
+ type FlowControl = 'none' | 'hardware';
34
34
 
35
35
  /**
36
36
  * @see https://wicg.github.io/serial/#dom-serialoptions
@@ -60,7 +60,7 @@ export type SerialOptions = {
60
60
  /**
61
61
  * The parity mode.
62
62
  */
63
- parity?: ParityType;
63
+ parity?: Parity;
64
64
  /**
65
65
  * A positive, non-zero value indicating the size of the read and write
66
66
  * buffers that should be created.
@@ -69,7 +69,7 @@ export type SerialOptions = {
69
69
  /**
70
70
  * The flow control mode.
71
71
  */
72
- flowControl?: FlowControlType;
72
+ flowControl?: FlowControl;
73
73
  };
74
74
 
75
75
  /**
@@ -175,21 +175,23 @@ type PortInternals = {
175
175
  setDeviceId(id: number): void;
176
176
  /** Reset to a closed, re-openable state after the device is physically lost. */
177
177
  handleDeviceLost(): void;
178
+ /** Reset to a closed, re-openable state after a clean physical detach. */
179
+ handleDeviceDetached(): void;
178
180
  };
179
181
  const portInternals = new WeakMap<SerialPort, PortInternals>();
180
182
 
181
183
  const kDefaultDataBits = 8;
182
184
  const kDefaultStopBits = 1;
183
- const kDefaultParity: ParityType = 'none';
185
+ const kDefaultParity: Parity = 'none';
184
186
  const kDefaultBufferSize = 255;
185
- const kDefaultFlowControl: FlowControlType = 'none';
187
+ const kDefaultFlowControl: FlowControl = 'none';
186
188
 
187
189
  const kAcceptableDataBits = [7, 8] as const;
188
190
  const kAcceptableStopBits = [1, 2] as const;
189
- const kAcceptableParity: ParityType[] = ['none', 'even', 'odd'];
190
- const kAcceptableFlowControl: FlowControlType[] = ['none', 'hardware'];
191
+ const kAcceptableParity: Parity[] = ['none', 'even', 'odd'];
192
+ const kAcceptableFlowControl: FlowControl[] = ['none', 'hardware'];
191
193
 
192
- function parityToNative(parity: ParityType): number {
194
+ function parityToNative(parity: Parity): number {
193
195
  switch (parity) {
194
196
  case 'odd':
195
197
  return 1;
@@ -200,17 +202,34 @@ function parityToNative(parity: ParityType): number {
200
202
  }
201
203
  }
202
204
 
203
- const FLOW_CONTROL_NATIVE: Readonly<
204
- Record<FlowControlType, 'RTS_CTS' | 'NONE'>
205
- > = {
205
+ const FLOW_CONTROL_NATIVE: Readonly<Record<FlowControl, 'RTS_CTS' | 'NONE'>> = {
206
206
  none: 'NONE',
207
207
  hardware: 'RTS_CTS',
208
208
  };
209
209
 
210
- function flowControlToNative(flowControl: FlowControlType): 'RTS_CTS' | 'NONE' {
210
+ function flowControlToNative(flowControl: FlowControl): 'RTS_CTS' | 'NONE' {
211
211
  return FLOW_CONTROL_NATIVE[flowControl];
212
212
  }
213
213
 
214
+ /**
215
+ * Normalise a writable chunk to a plain byte array. The Web Serial writable
216
+ * accepts any BufferSource — a bare `ArrayBuffer` or any `ArrayBufferView`
217
+ * (`DataView`, typed arrays, including views with a non-zero `byteOffset`). A
218
+ * naive `Array.from(chunk)` only works for the iterable typed arrays and
219
+ * silently yields `[]` for an `ArrayBuffer`/`DataView`, so handle each shape.
220
+ */
221
+ function bufferSourceToBytes(chunk: ArrayBufferView | ArrayBuffer): number[] {
222
+ if (chunk instanceof Uint8Array) {
223
+ return Array.from(chunk);
224
+ }
225
+ if (ArrayBuffer.isView(chunk)) {
226
+ return Array.from(
227
+ new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength),
228
+ );
229
+ }
230
+ return Array.from(new Uint8Array(chunk as ArrayBuffer));
231
+ }
232
+
214
233
  /**
215
234
  * Methods on this interface typically complete asynchronously, queuing work on
216
235
  * the serial port task source.
@@ -252,13 +271,8 @@ export class SerialPort extends EventTarget {
252
271
  #portNumber: number;
253
272
  #usbVendorId?: number;
254
273
  #usbProductId?: number;
255
-
256
- // Tracks the current state of output signals for partial updates
257
- #outputSignals: Required<SerialOutputSignals> = {
258
- dataTerminalReady: false,
259
- requestToSend: false,
260
- break: false,
261
- };
274
+ // Baud rate of the current open(), used to size the native write timeout.
275
+ #baudRate: number = 0;
262
276
 
263
277
  #dataSubscription: {remove: () => void} | null = null;
264
278
  #errorSubscription: {remove: () => void} | null = null;
@@ -284,11 +298,26 @@ export class SerialPort extends EventTarget {
284
298
  this.#writeFatal = false;
285
299
  this.#pendingClosePromise = null;
286
300
  this.#bufferSize = undefined;
301
+ this.#baudRate = 0;
287
302
  this.#state = state;
288
303
  this.#connected = false;
289
304
  this.#forgetRequested = state === 'forgotten';
290
305
  }
291
306
 
307
+ /**
308
+ * Compute a native write timeout (ms) large enough to actually drain `length`
309
+ * bytes at the negotiated baud rate. A fixed 2 s timeout spuriously fails
310
+ * large writes on slow links; this scales with the payload while keeping a 2 s
311
+ * floor. ~10 bits per byte (start + 8 data + stop) with a 2× safety margin.
312
+ */
313
+ #writeTimeoutFor(length: number): number {
314
+ const kMinTimeoutMs = 2000;
315
+ /* istanbul ignore next — baudRate is always > 0 when the port is open */
316
+ const baud = this.#baudRate > 0 ? this.#baudRate : 9600;
317
+ const estimatedMs = Math.ceil((length * 10 * 1000) / baud) * 2;
318
+ return Math.max(kMinTimeoutMs, estimatedMs);
319
+ }
320
+
292
321
  constructor(
293
322
  usb: SerialTransport,
294
323
  deviceId: number,
@@ -315,6 +344,7 @@ export class SerialPort extends EventTarget {
315
344
  serialPortDeviceIds.set(this, id);
316
345
  },
317
346
  handleDeviceLost: () => this.#handleDeviceLost(),
347
+ handleDeviceDetached: () => this.#handleDeviceDetached(),
318
348
  });
319
349
  }
320
350
 
@@ -349,6 +379,53 @@ export class SerialPort extends EventTarget {
349
379
  this.dispatchEvent(new Event('disconnect'));
350
380
  }
351
381
 
382
+ /**
383
+ * Reset this port to a closed, re-openable state after a clean physical
384
+ * detach. Unlike device loss, buffered readable bytes should still be
385
+ * observable before the stream ends.
386
+ */
387
+ #handleDeviceDetached(): void {
388
+ const wasForgotten = this.#state === 'forgotten';
389
+ const readable = this.#readable;
390
+ const writable = this.#writable;
391
+
392
+ // Keep the public event and state transition synchronous so existing
393
+ // callers observe the detach immediately, but defer the stream teardown by
394
+ // one microtask to let already-queued data drain first.
395
+ this.#state = wasForgotten ? 'forgotten' : 'closed';
396
+ this.#connected = false;
397
+ this.#readable = null;
398
+ this.#writable = null;
399
+ this.dispatchEvent(new Event('disconnect'));
400
+
401
+ queueMicrotask(() => {
402
+ // Close the readable stream so any bytes already queued can still be
403
+ // drained by the consumer before EOF.
404
+ try {
405
+ if (readable) this.#readableController?.close();
406
+ } catch {}
407
+
408
+ // A detached device can no longer accept writes, so reject any writable
409
+ // traffic and clear local state.
410
+ const lost = new DOMException(
411
+ 'The device has been lost.',
412
+ 'NetworkError',
413
+ );
414
+ if (writable) {
415
+ this.#writeFatal = true;
416
+ try {
417
+ this.#writableController?.error(lost);
418
+ } catch {}
419
+ }
420
+
421
+ // Mirror the stream-closing bookkeeping without treating this as a hard
422
+ // read fatal error.
423
+ this.#handleClosingReadableStream();
424
+ this.#handleClosingWritableStream();
425
+ this.#resetToClosedState(wasForgotten ? 'forgotten' : 'closed');
426
+ });
427
+ }
428
+
352
429
  /**
353
430
  * @see https://wicg.github.io/serial/#dom-serialport-onconnect
354
431
  */
@@ -427,7 +504,15 @@ export class SerialPort extends EventTarget {
427
504
  event.deviceId === deviceId &&
428
505
  event.portNumber === portNumber
429
506
  ) {
430
- controller.enqueue(new Uint8Array(event.data));
507
+ try {
508
+ controller.enqueue(new Uint8Array(event.data));
509
+ } catch {
510
+ // The stream may already be closing/closed — e.g. a data event
511
+ // racing cancel()/close(), where the stream is closed but this
512
+ // subscription is not yet torn down. Dropping the chunk is
513
+ // correct (the consumer has stopped reading) and avoids throwing
514
+ // out of the transport's event-dispatch loop.
515
+ }
431
516
  }
432
517
  });
433
518
 
@@ -496,8 +581,14 @@ export class SerialPort extends EventTarget {
496
581
  self.#writableController = controller;
497
582
  },
498
583
  async write(chunk) {
584
+ const bytes = bufferSourceToBytes(chunk);
499
585
  try {
500
- await self.#usb.write(deviceId, portNumber, Array.from(chunk));
586
+ await self.#usb.write(
587
+ deviceId,
588
+ portNumber,
589
+ bytes,
590
+ self.#writeTimeoutFor(bytes.length),
591
+ );
501
592
  } catch (e) {
502
593
  // If the port was disconnected, set this.[[writeFatal]] to true.
503
594
  self.#writeFatal = true;
@@ -696,6 +787,7 @@ export class SerialPort extends EventTarget {
696
787
 
697
788
  this.#state = 'opened'; // 9.3. Set this.[[state]] to "opened".
698
789
  this.#bufferSize = bufferSize; // 9.4. Set this.[[bufferSize]] to options["bufferSize"].
790
+ this.#baudRate = options.baudRate;
699
791
 
700
792
  // When the port becomes logically connected:
701
793
  this.#connected = true; // 2. Set port.[[connected]] to true.
@@ -703,6 +795,13 @@ export class SerialPort extends EventTarget {
703
795
  // device presence (attach/detach), dispatched by Serial from the native
704
796
  // USB state events — not logical open()/close(). So we do not dispatch them
705
797
  // here; that also avoids re-entrancy with auto-reconnect listeners.
798
+
799
+ // Materialise the readable eagerly so its data subscription is live the
800
+ // instant the native read pump (startReading, above) begins emitting. The
801
+ // device may transmit immediately on open; without an active subscription
802
+ // those first bytes would be delivered to no listener and silently lost in
803
+ // the window between open() resolving and the first port.readable access.
804
+ void this.readable;
706
805
  }
707
806
 
708
807
  /**
@@ -814,9 +913,6 @@ export class SerialPort extends EventTarget {
814
913
  throw new TypeError('At least one signal must be specified.');
815
914
  }
816
915
 
817
- // Merge with current output signal state for partial updates
818
- this.#outputSignals = {...this.#outputSignals, ...signals};
819
-
820
916
  const promises: Promise<void>[] = [];
821
917
 
822
918
  // 4.1. If signals["dataTerminalReady"] is present, invoke the operating
@@ -965,8 +1061,8 @@ export class Serial extends EventTarget {
965
1061
 
966
1062
  /**
967
1063
  * @param transport Optional transport override. When provided, this `Serial`
968
- * talks to it instead of the global native module — the seam used by tests
969
- * and the virtual-device harness (`new Serial(new VirtualSerialTransport())`).
1064
+ * talks to it instead of the global native module — the transport override used by tests
1065
+ * and the virtual serial device harness (`new Serial(new InMemorySerialTransport())`).
970
1066
  * Omit it for the normal native-backed instance; the singleton `serial`
971
1067
  * export is created this way and can still be redirected globally via
972
1068
  * `setUsbSerial()`.
@@ -1042,13 +1138,16 @@ export class Serial extends EventTarget {
1042
1138
  // SerialPort instance so a re-attach can reuse it. handleDeviceLost()
1043
1139
  // dispatches "disconnect" on the port.
1044
1140
  this.#usb.onDisconnect((event: ConnectEvent) => {
1141
+ const wasLost = (event as ConnectEvent & {lost?: boolean}).lost ?? true;
1045
1142
  const prefix = `${event.deviceId}:`;
1046
1143
  let matched = false;
1047
1144
  for (const [key, port] of [...this.#knownPorts.entries()]) {
1048
1145
  if (key.startsWith(prefix)) {
1049
- // handleDeviceLost() dispatches "disconnect" on the port, which
1050
- // bubbles here with event.target === the port (per spec).
1051
- portInternals.get(port)?.handleDeviceLost();
1146
+ // Physical loss tears down the stream immediately; a clean detach
1147
+ // lets any already-buffered bytes drain before EOF.
1148
+ const internals = portInternals.get(port);
1149
+ if (wasLost) internals?.handleDeviceLost();
1150
+ else internals?.handleDeviceDetached();
1052
1151
  matched = true;
1053
1152
  }
1054
1153
  }
package/src/index.ts CHANGED
@@ -14,5 +14,8 @@ export {Serial, SerialPort} from './WebSerial';
14
14
 
15
15
  import * as UsbSerial from './UsbSerial';
16
16
 
17
- export {Event, EventTarget} from './lib/event-target';
17
+ export {
18
+ EventImpl as Event,
19
+ EventTargetImpl as EventTarget,
20
+ } from './lib/event-target';
18
21
  export {UsbSerial};
@@ -193,3 +193,15 @@ export class EventTarget {
193
193
  }
194
194
  }
195
195
  }
196
+
197
+ type MaybeGlobal = Record<string, unknown>;
198
+
199
+ export const EventImpl =
200
+ typeof (globalThis as MaybeGlobal).Event === 'function'
201
+ ? ((globalThis as MaybeGlobal).Event as typeof Event)
202
+ : Event;
203
+
204
+ export const EventTargetImpl =
205
+ typeof (globalThis as MaybeGlobal).EventTarget === 'function'
206
+ ? ((globalThis as MaybeGlobal).EventTarget as typeof EventTarget)
207
+ : EventTarget;
@@ -0,0 +1,43 @@
1
+ import {
2
+ ByteLengthQueuingStrategy as ByteLengthQueuingStrategyPolyfill,
3
+ ReadableStream as ReadableStreamPolyfill,
4
+ WritableStream as WritableStreamPolyfill,
5
+ } from 'web-streams-polyfill';
6
+
7
+ type MaybeGlobal = Record<string, unknown>;
8
+
9
+ function getCtor<T>(name: string, fallback: T): T {
10
+ const value = (globalThis as MaybeGlobal)[name];
11
+ return typeof value === 'function' ? (value as T) : fallback;
12
+ }
13
+
14
+ export {
15
+ ByteLengthQueuingStrategyPolyfill,
16
+ ReadableStreamPolyfill,
17
+ WritableStreamPolyfill,
18
+ };
19
+
20
+ export const ReadableStreamImpl = getCtor(
21
+ 'ReadableStream',
22
+ ReadableStreamPolyfill,
23
+ );
24
+
25
+ export const WritableStreamImpl = getCtor(
26
+ 'WritableStream',
27
+ WritableStreamPolyfill,
28
+ );
29
+
30
+ export const ByteLengthQueuingStrategyImpl = getCtor(
31
+ 'ByteLengthQueuingStrategy',
32
+ ByteLengthQueuingStrategyPolyfill,
33
+ );
34
+
35
+ // Companion TYPE aliases so that `Impl as X` imports carry a type too. Without
36
+ // these, an annotation like `ReadableStream<Uint8Array>` (where `ReadableStream`
37
+ // is the imported value) falls back to the ambient *global* ReadableStream and
38
+ // no longer matches the polyfill instances actually constructed — the runtime
39
+ // pick stays native-or-polyfill; only the static type is anchored to the
40
+ // (structurally faithful) polyfill instance type.
41
+ export type ReadableStreamImpl<R = unknown> = ReadableStreamPolyfill<R>;
42
+ export type WritableStreamImpl<W = unknown> = WritableStreamPolyfill<W>;
43
+ export type ByteLengthQueuingStrategyImpl = ByteLengthQueuingStrategyPolyfill;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * `createDeviceFixture` — the one-call fixture for testing serial code. It wires a
3
+ * {@link SimulatedDevice} simulator to an {@link InMemorySerialTransport} + `Serial`,
4
+ * and hands back everything a test needs to drive *both* sides:
5
+ *
6
+ * - `serial`/`port` — what the app-under-test consumes,
7
+ * - `simulatedDevice` (typed) + `device` handle — drive the device (inject data,
8
+ * move the GPS, inject faults),
9
+ * - `client` — a host-side {@link SerialClient} (for protocol tests), and
10
+ * - `whenOpened()/whenClosed()` — `await` the app connecting.
11
+ *
12
+ * @example Test how your app reacts to a device event
13
+ * const {simulatedDevice, device, whenOpened} = await createDeviceFixture(new MyGps());
14
+ * renderMyApp(); // opens the port itself
15
+ * await whenOpened();
16
+ * simulatedDevice.update({latitude: 51.48, longitude: 0});
17
+ * // …assert your app's UI updated
18
+ */
19
+
20
+ import {setUsbSerial} from '../UsbSerial';
21
+ import type {SerialPort} from '../WebSerial';
22
+ import {Serial} from '../WebSerial';
23
+ import type {
24
+ DeviceOptions,
25
+ InMemorySerialTransportOptions,
26
+ } from './in-memory-serial-transport';
27
+ import {
28
+ type DeviceHandle,
29
+ InMemorySerialTransport,
30
+ } from './in-memory-serial-transport';
31
+ import {SerialClient} from './serial-client';
32
+ import type {
33
+ SimulatedDevice,
34
+ SimulatedDeviceOpenOptions,
35
+ } from './simulated-device';
36
+
37
+ export type DeviceFixtureOptions = {
38
+ /** Per-device transport knobs (single-device form). */
39
+ device?: DeviceOptions;
40
+ /** Per-device transport knobs, by index (multi-device form). */
41
+ devices?: DeviceOptions[];
42
+ /** Transport-level options (latencyMs, chunkSize, autoGrantPermission). */
43
+ transport?: InMemorySerialTransportOptions;
44
+ /** Whether each device is already permitted. Defaults to true (so it lists). */
45
+ hasPermission?: boolean;
46
+ /** Also `setUsbSerial(transport)` so a running app's `serial` sees it. Default false. */
47
+ installGlobally?: boolean;
48
+ /** Options for the host-side {@link SerialClient}. */
49
+ client?: {defaultTimeoutMs?: number};
50
+ };
51
+
52
+ export type MountedDeviceFixture<D extends SimulatedDevice = SimulatedDevice> =
53
+ {
54
+ transport: InMemorySerialTransport;
55
+ serial: Serial;
56
+ port: SerialPort;
57
+ /** The transport-side handle: push/emitError/failNext/written/attach/detach/… */
58
+ device: DeviceHandle;
59
+ /** The concrete device simulator, typed (e.g. call `gps.update(...)`). */
60
+ simulatedDevice: D;
61
+ /** A host-side client bound to `port` — NOT opened (call `client.open()`). */
62
+ client: SerialClient;
63
+ /** Resolve when the app opens the port (now if already open). */
64
+ whenOpened(): Promise<SimulatedDeviceOpenOptions>;
65
+ /** Resolve when the app closes the port (now if not open). */
66
+ whenClosed(): Promise<void>;
67
+ };
68
+
69
+ export type MountedDeviceFixtures = {
70
+ transport: InMemorySerialTransport;
71
+ serial: Serial;
72
+ ports: SerialPort[];
73
+ devices: DeviceHandle[];
74
+ simulatedDevices: SimulatedDevice[];
75
+ clients: SerialClient[];
76
+ /** Resolve when device `index` opens, or any device when `index` is omitted. */
77
+ whenOpened(index?: number): Promise<SimulatedDeviceOpenOptions>;
78
+ /** Resolve when device `index` closes, or any device when `index` is omitted. */
79
+ whenClosed(index?: number): Promise<void>;
80
+ };
81
+
82
+ export function createDeviceFixture<D extends SimulatedDevice>(
83
+ device: D,
84
+ options?: DeviceFixtureOptions,
85
+ ): Promise<MountedDeviceFixture<D>>;
86
+ export function createDeviceFixture(
87
+ devices: SimulatedDevice[],
88
+ options?: DeviceFixtureOptions,
89
+ ): Promise<MountedDeviceFixtures>;
90
+ export async function createDeviceFixture(
91
+ deviceOrDevices: SimulatedDevice | SimulatedDevice[],
92
+ options: DeviceFixtureOptions = {},
93
+ ): Promise<MountedDeviceFixture | MountedDeviceFixtures> {
94
+ const list = Array.isArray(deviceOrDevices)
95
+ ? deviceOrDevices
96
+ : [deviceOrDevices];
97
+ const hasPermission = options.hasPermission ?? true;
98
+ const transport = new InMemorySerialTransport(options.transport);
99
+ const handles = list.map((dev, i) =>
100
+ transport.addDevice(dev, {
101
+ hasPermission,
102
+ ...(Array.isArray(deviceOrDevices)
103
+ ? options.devices?.[i]
104
+ : options.device),
105
+ }),
106
+ );
107
+ const serial = new Serial(transport);
108
+ if (options.installGlobally) setUsbSerial(transport);
109
+
110
+ const ports = await serial.getPorts();
111
+ const clients = ports.map(
112
+ p =>
113
+ new SerialClient(p, {
114
+ defaultTimeoutMs: options.client?.defaultTimeoutMs,
115
+ }),
116
+ );
117
+
118
+ if (!Array.isArray(deviceOrDevices)) {
119
+ const handle = handles[0];
120
+ const port = ports[0];
121
+ if (!port) throw new Error('createDeviceFixture: device did not enumerate');
122
+ return {
123
+ transport,
124
+ serial,
125
+ port,
126
+ device: handle,
127
+ simulatedDevice: deviceOrDevices,
128
+ client: clients[0],
129
+ whenOpened: () => handle.whenOpened(),
130
+ whenClosed: () => handle.whenClosed(),
131
+ };
132
+ }
133
+
134
+ return {
135
+ transport,
136
+ serial,
137
+ ports,
138
+ devices: handles,
139
+ simulatedDevices: list,
140
+ clients,
141
+ whenOpened: (index?: number) =>
142
+ index === undefined
143
+ ? Promise.race(handles.map(h => h.whenOpened()))
144
+ : handles[index].whenOpened(),
145
+ whenClosed: (index?: number) =>
146
+ index === undefined
147
+ ? Promise.race(handles.map(h => h.whenClosed()))
148
+ : handles[index].whenClosed(),
149
+ };
150
+ }