Кажется, что дело сделано, однако осталось еще две проблемы. Первая, самая важная, что делать, если имеется асинхронное апи без реализованного iterable протокола, например, тот же
ReadableStream
без
Symbol.asyncIterator
. Можно собрать асинхронный итератор вручную:
const data = await parseChunked({
[Symbol.asyncIterator]() {
const reader = stream.getReader();
return {
next: () => reader.read(),
return() { // вызовется в случае ошибки или преждевременного прерывания итерирования
reader.releaseLock();
return { done: true }; // важно
}
}
}
});
Но выглядит это как тайные заклинания. Лучше прибегнуть к асинхроному, в данном случае, генератору – да, да, вот где они нужны! Получается на пару строк больше, но код выглядит более целостным и линейным:
const data = await parseChunked({
[Symbol.asyncIterator]: async function*() {
const reader = stream.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
yield value;
}
} finally {
reader.releaseLock();
}
}
});
Стало лучше, однако если присмотрется, то обертка над генератором выглядит лишней и можно просто вызвать генератор, который вернет async iterable iterator. Внимательный читатель заметит, что здесь появились рядом два слова "iterable" и "iterator", то есть 2 в одном, что-то новенькое. Хоть это и итератор, но главное он iterable и это важно. Сделано это для того, чтобы итератор работал с
for ... of
и другими конструкциями. А получается это путем добавления итератору метода
Symbol.asyncIterator
(или
Symbol.iterator
для синхронного итератора), который возвращает
this
, то есть сам итератор. Таким элегантным трюком достигается выполнения протоколов и мы можем делать следующее:
const data = await parseChunked(async function*() {
// ...
}());
В такой записи все хорошо, кроме финальных скобок, которые могут визуально теряться и вызывать конфуз. Для этого я добавил возможность передавать в
parseChunked()
сам генератор, и он будет вызван перед началом парсинга. Кажется, что нужна какая то особая логика для определения, что переданное значение является генератором. Но по факту генератор выглядит как функция и "крякает" как функция, поэтому нет необходимости в особой детекции. Функции не iterable, поэтому не возникает конфликтов в логике. Бонусом становится возможным передавать функцию-фабрику, которая создает или возвращает iterable.
async function parseChunked(chunkEmitter) {
if (typeof chunkEmitter === 'function') {
chunkEmitter = chunkEmitter();
}
// ...
}
Таким образом, генераторы могут служить "мостом" между асинхронным апи и асинхронным итерированием. Протокол iterable делает стримы рядовыми участниками в этой схеме.
Ввиду неполной поддержки
ReadableStream[Symbol.asyncIterator]
, в json-ext введена функция
parseFromWebStream()
, которую стоит использовать вместо
parseChunked()
для большей совместимости. Эта оборачивает стрим в асинхронный генератор, если необходимо, и вызывает
parseChunked()
. В будущем необходимость в ней отпадет, но пока так.