How can I interrupt a Reader when it "hangs" (need a timeout on Reader.read() )
Asked Answered
P

4

5

This question is related to a situation that occurred when using Chrome Serial API but probably could be relevant to any ReadableStream. I studied the documentation and probably missed some feature or pattern.

A simple program is running in Chrome browser, accessing CW keyer (based on Arduino, but this is not important).

The application sends a command to the keyer and expects two binary bytes or a string as a response (particular format depends on the command sent and is not important).

In case that the serial device (not the USB/serial adapter, but the Arduino) misses the command for whatever reason, response is never sent and the function expectResponse() below will never return any data, nor will it throw any exception. As a result, the Reader remains locked, the ReadableStream therefore cannot be closed and as a consequence, the serial port cannot be closed either.

Also, depending on the application structure, in case of other command sent successfully to the keyer, it may be not possible to read the second response because the first reader blocks the stream and until it is released, new Reader cannot be created.


async function expectResponse( serialPort ) {
   const reader = serialPort.readable.getReader() ;
   let { value, done } = await reader.read() ; // this never returns because no data arrive, not possible to "break"
}

async function disconnect( serialPort ) {
   // ... some cleanup ...
   // naive attempt to unlock ReadableStream before closing 
   await serialPort.readable.getReader().releaseLock() // this will throw exception - cannot create  new reader while another one is still active and locks the stream
   // ...
   await serialPort.close(); // this will throw exception - cannot close port because readable stream is locked
}

serialPort is the object returned by navigator.serial.requestPort()

I am convinced I must have missed something important in API docs (ReadableStream or Reader API, not Serial API), but I did not find solution.

P.S. in the real app serialPort is a global variable, but it does not matter, does it?

Picard answered 16/1, 2021 at 9:56 Comment(0)
H
6

I don't think ReadableStream has timeouts built in.

I'd use Promise.race with the other promise being your timeout:

let { value, done } = await Promise.race([
    reader.read(),
    new Promise((_, reject) => setTimeout(reject, TIMEOUT, new Error("timeout")))
]);

(You'd probably put that new Promise code in a utility function.)

Promise.race watches the promises race, settling its promise based on the first settlement it sees of the promises in the array you give it. So if read's promise is fulfilled (or rejected) before the timeout promise rejects, read's settlement dictates the settlement of the race promise. Otherwise, the race promise settles based on the settlement (in this case, rejection) of the timeout promise.

Hazelwood answered 16/1, 2021 at 9:58 Comment(0)
M
1

It is possible to "break" the await operation:

According to the documentation the await reader.read() will throw TypeError if you call reader.releaseLock(). (for example with a setTimeout)

Doc: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read

try {
    await reader.read();
} catch(e) {
    console.log("you got out from await");
}

setTimeout(function() {
    reader.releaseLock();
}, TIMEOUT);
Mckinleymckinney answered 21/8, 2022 at 16:24 Comment(0)
O
1

Based on TJ's answer, I came up with this:

function promiseTimeout<T>(promise: Promise<T>, ms = 500): Promise<T> {
    // https://mcmap.net/q/1958008/-how-can-i-interrupt-a-reader-when-it-quot-hangs-quot-need-a-timeout-on-reader-read
    let timer: any
    const timeoutPromise = new Promise<T>((_, reject) => {
        timer = setTimeout(() => reject(new Error(`Promise timed out after ${ms}ms`)), ms)
    })
    promise.finally(() => {
        clearTimeout(timer)
    })
    return Promise.race([promise, timeoutPromise])
}

Which you can use like this:

let {value: chunk, done: readerDone} = await promiseTimeout(reader.read())
Operate answered 19/11, 2023 at 23:14 Comment(0)
O
1

Based on guttmann's answer, I came up with:

async function readOrTimeout(reader: ReadableStreamDefaultReader<Uint8Array>, ms = 500) {
    // https://mcmap.net/q/1958008/-how-can-i-interrupt-a-reader-when-it-quot-hangs-quot-need-a-timeout-on-reader-read
    const timer = setTimeout(() => {
        reader.releaseLock()
    }, ms)
    try {
        return await reader.read()
    } finally {
        clearTimeout(timer)
    }
}

Use it like this:

let {value: chunk, done: readerDone} = await readOrTimeout(reader)
Operate answered 19/11, 2023 at 23:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.