Scenario
I'm given a function with an asynchronous callback like
let readFile: (path: string, callback: (line: string, eof: boolean) => void) => void
Though I would prefer a function using AsyncIterable/AsyncGenerator signature instead:
let readFileV2: (path: string) => AsyncIterable<string>
Problem
Without readFileV2, I have to read a file like
let file = await new Promise((res, err) => {
let file = ''
readFile('./myfile.txt', (line, eof) => {
if (eof) { return res(file) }
file += line + '\n'
})
})
.. while readFileV2 allows me to do it cleaner like
let file = '';
for await (let line of readFileV2('./myfile.txt')) {
file += line + '\n'
}
Question
Is there a way for me to transform readFile into readFileV2?
Updated for clarification:
Is there a general approach to transform a function with an async callback argument to an AsyncGenerator/AsyncIterable variant?
And can this approach be demonstrated on the readFile function above?
References
I see two related questions here:
- How to convert Node.js async streaming callback into an async generator?
- How to convert callback-based async function to async generator
However, they don't seem to provide a clear answer.
Disclaimer at the outset: I am answering the following question:
It is quite possible that this isn't the right thing to be doing in general, since consumers of
AsyncIterable<T>may process data slowly or abort early, and a function of type(...args: [...A, (data: T, done: boolean) => void]) => voidcan't possibly react to that; it will callcallbackonce per piece of data, whenever it wants, and it will not stop until it feels like it.Still, here is one possible implementation:
Essentially we provide a queue of data values,
values, which gets written to inside the callback passed tofn, and which gets read from inside a generator function. This is accomplished by a chain of promises; the first promise is created manually, and each time data is available, it resolves the current promise and pushes new values with a new promise onto the queue. The generator function awaits these promises, pulls data off the queue, and removes the consumed data.To test it, someone needs to provide
fn. Here's one possibility:The
provideDatafunction accepts a callback and calls it once per second with successive lines of an array. And now we transform it:And let's test the transformer:
Looks good.
Is it perfect? Does it have weird side effects in response to weird situations (e.g., are you going to iterate it multiple times)? Should it handle errors in a particular way? Are there recommended solutions elsewhere? Not sure. This is just a possible implementation of
transformthat adheres to the contract laid out in the question as asked.Playground link to code