From 5b2c25d8ee435eaf0cba02c166f9b2219468b2e5 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 20 Aug 2025 10:01:17 -0500 Subject: [PATCH] Try-fix webserial disconnect not actually disconnecting (#796) * Try-fix webserial disconnects * Instantiate abort controller on new connection --- .../transport-web-serial/src/transport.ts | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/transport-web-serial/src/transport.ts b/packages/transport-web-serial/src/transport.ts index 47d98586..1c1706dc 100644 --- a/packages/transport-web-serial/src/transport.ts +++ b/packages/transport-web-serial/src/transport.ts @@ -5,6 +5,8 @@ export class TransportWebSerial implements Types.Transport { private _toDevice: WritableStream; private _fromDevice: ReadableStream; private connection: SerialPort; + private pipePromise: Promise | null = null; + private abortController: AbortController; public static async create(baudRate?: number): Promise { const port = await navigator.serial.requestPort(); @@ -26,8 +28,13 @@ export class TransportWebSerial implements Types.Transport { } this.connection = connection; + this.abortController = new AbortController(); - Utils.toDeviceStream.readable.pipeTo(connection.writable); + // Set up the pipe with abort signal for clean cancellation + this.pipePromise = Utils.toDeviceStream.readable.pipeTo( + connection.writable, + { signal: this.abortController.signal } + ); this._toDevice = Utils.toDeviceStream.writable; this._fromDevice = connection.readable.pipeThrough( @@ -43,7 +50,54 @@ export class TransportWebSerial implements Types.Transport { return this._fromDevice; } - disconnect() { - return this.connection.close(); + /** + * Safely disconnects the serial port, following best practices from + * https://github.com/WICG/serial/. Cancels any active pipe + * operations and only closes the port after streams are unlocked. + */ + async disconnect() { + try { + this.abortController.abort(); + + if (this.pipePromise) { + try { + await this.pipePromise; + } catch (error) { + if (error instanceof Error && error.name !== 'AbortError') { + throw error; + } + } + } + + // Cancel any remaining streams + if (this._fromDevice && this._fromDevice.locked) { + try { + await this._fromDevice.cancel(); + } catch (error) { + // Stream cancellation might fail if already cancelled + } + } + + await this.connection.close(); + + } catch (error) { + // If we can't close cleanly, let the browser handle cleanup + console.warn('Could not cleanly disconnect serial port:', error); + } + } + + /** + * Reconnects the transport by creating a new AbortController and re-establishing + * the pipe connection. Only call this after disconnect() or if the connection failed. + */ + async reconnect() { + // Create a new AbortController for the new connection + this.abortController = new AbortController(); + + // Re-establish the pipe connection + this.pipePromise = Utils.toDeviceStream.readable.pipeTo( + this.connection.writable, + { signal: this.abortController.signal } + ); } }