Home > Software design >  How can I close a Web Serial port that I've piped through a TransformStream?
How can I close a Web Serial port that I've piped through a TransformStream?

Time:02-26

I have a Web Serial port that I want to read some data from. I'd like to use TransformStreams to do some processing (e.g. decode bytes to strings, separate out logical messages, etc) by using pipeThrough to transform the data. However, once I do this, I can no longer release the lock on the port by calling reader.releaseLock(). What am I doing wrong here?

This code works how I expect (running without dependencies in the browser in a secure context):

async serialTestWorking() {
  const port = await navigator.serial.requestPort();

  await port.open({baudRate: 9600});
  console.log("Is locked after open?", port.readable.locked);
  // Prints "Is locked after open? false"

  let reader = port.readable.getReader();
  console.log("Is locked after getReader?", port.readable.locked);
  // Prints "Is locked after getReader? true"

  reader.releaseLock();
  console.log("Is locked after releaseLock?", port.readable.locked);
  // Prints "Is locked after releaseLock? false"

  await port.close();
  console.log("Port closed");
  // Prints "Port closed"
}

However, if I use pipeThrough to send the output through a do-nothing TransformStream, it all falls apart. The lock isn't released at releaseLock and the final close fails to work.

async serialTestBroken() {
  const port = await navigator.serial.requestPort();

  await port.open({baudRate: 9600});
  console.log("Is locked after open?", port.readable.locked);
  // Prints "Is locked after open? false"

  let reader = port.readable.pipeThrough(new TransformStream()).getReader();
  console.log("Is locked after getReader?", port.readable.locked);
  // Prints "Is locked after getReader? true"

  reader.releaseLock();
  console.log("Is locked after releaseLock?", port.readable.locked);
  // Prints "Is locked after releaseLock? true"

  await port.close();
  console.log("Port closed");
  // Doesn't make it to the log line
  //throws "TypeError: Failed to execute 'close' on 'SerialPort': Cannot cancel a locked stream"
}

What am I doing wrong here? Does releasing the lock on the TransformStream really not propagate upstream? Do I have to keep track of an instance of every transformer in my pipeline so I can be sure to unlock them all?

The streams spec says that piping locks the readable and writable streams for the duration of the pipe operation.

Piping locks the readable and writable streams, preventing them from being manipulated for the duration of the pipe operation. This allows the implementation to perform important optimizations, such as directly shuttling data from the underlying source to the underlying sink while bypassing many of the intermediate queues.

Is there some other way I have to indicate that the "piping operation" is completed?

CodePudding user response:

As explained in https://web.dev/serial/#close-port, port.close() closes the serial port if its readable and writable members are unlocked, meaning releaseLock() has been called for their respective reader and writer.

Closing a serial port is a bit more complicated when using transform streams though. Call reader.cancel(), then call writer.close() and port.close(). This propagates errors through the transform streams to the underlying serial port. Because error propagation doesn't happen immediately, you need to use the readableStreamClosed and writableStreamClosed promises created earlier to detect when port.readable and port.writable have been unlocked. Cancelling the reader causes the stream to be aborted; this is why you must catch and ignore the resulting error.

const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();

// Listen to data coming from the serial device.
while (true) {
  const { value, done } = await reader.read();
  if (done) {
    reader.releaseLock();
    break;
  }
  // value is a string.
  console.log(value);
}
// Later...

const textEncoder = new TextEncoderStream();
const writer = textEncoder.writable.getWriter();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);

reader.cancel();
await readableStreamClosed.catch(() => { /* Ignore the error */ });

writer.close();
await writableStreamClosed;

await port.close();
  • Related