diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99e011e..fea7980 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,8 @@ jobs: strategy: matrix: node_version: + - 14.17 + - 18.0 - 20 steps: - uses: actions/checkout@v2 @@ -78,6 +80,7 @@ jobs: node_version: - 14.17.0 - 16 + - 18.0 - 18 - 20 - 22 diff --git a/src/parse-chunked.js b/src/parse-chunked.js index 455e9ea..3e3ef8f 100644 --- a/src/parse-chunked.js +++ b/src/parse-chunked.js @@ -1,11 +1,9 @@ +import { isIterable } from './utils.js'; + const STACK_OBJECT = 1; const STACK_ARRAY = 2; const decoder = new TextDecoder(); -function isObject(value) { - return value !== null && typeof value === 'object'; -} - function adjustPosition(error, parser) { if (error.name === 'SyntaxError' && parser.jsonParseOffset) { error.message = error.message.replace(/at position (\d+)/, (_, pos) => @@ -28,15 +26,15 @@ function append(array, elements) { } export async function parseChunked(chunkEmitter) { - const iterator = typeof chunkEmitter === 'function' + const iterable = typeof chunkEmitter === 'function' ? chunkEmitter() : chunkEmitter; - if (isObject(iterator) && (Symbol.iterator in iterator || Symbol.asyncIterator in iterator)) { + if (isIterable(iterable)) { let parser = new ChunkParser(); try { - for await (const chunk of iterator) { + for await (const chunk of iterable) { if (typeof chunk !== 'string' && !ArrayBuffer.isView(chunk)) { throw new TypeError('Invalid chunk: Expected string, TypedArray or Buffer'); } @@ -47,8 +45,6 @@ export async function parseChunked(chunkEmitter) { return parser.finish(); } catch (e) { throw adjustPosition(e, parser); - } finally { - parser = null; } } diff --git a/src/utils.js b/src/utils.js index 263305e..b6fa42f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,4 +1,15 @@ -function replaceValue(holder, key, value, replacer) { +export function isIterable(value) { + return ( + typeof value === 'object' && + value !== null && + ( + typeof value[Symbol.iterator] === 'function' || + typeof value[Symbol.asyncIterator] === 'function' + ) + ); +} + +export function replaceValue(holder, key, value, replacer) { if (value && typeof value.toJSON === 'function') { value = value.toJSON(); } @@ -26,7 +37,7 @@ function replaceValue(holder, key, value, replacer) { return value; } -function normalizeReplacer(replacer) { +export function normalizeReplacer(replacer) { if (typeof replacer === 'function') { return replacer; } @@ -46,7 +57,7 @@ function normalizeReplacer(replacer) { return null; } -function normalizeSpace(space) { +export function normalizeSpace(space) { if (typeof space === 'number') { if (!Number.isFinite(space) || space < 1) { return false; @@ -61,9 +72,3 @@ function normalizeSpace(space) { return false; } - -export { - replaceValue, - normalizeReplacer, - normalizeSpace -}; diff --git a/src/web-streams.js b/src/web-streams.js index 997a839..47e887f 100644 --- a/src/web-streams.js +++ b/src/web-streams.js @@ -1,22 +1,23 @@ /* eslint-env browser */ import { parseChunked } from './parse-chunked.js'; import { stringifyChunked } from './stringify-chunked.js'; +import { isIterable } from './utils.js'; export function parseFromWebStream(stream) { // 2024/6/17: currently, an @@asyncIterator on a ReadableStream is not widely supported, // therefore use a fallback using a reader // https://caniuse.com/mdn-api_readablestream_--asynciterator - return parseChunked(stream && Symbol.asyncIterator in stream ? stream : async function*() { + return parseChunked(isIterable(stream) ? stream : async function*() { const reader = stream.getReader(); while (true) { - const { done, chunk } = await reader.read(); + const { value, done } = await reader.read(); if (done) { break; } - yield chunk; + yield value; } }); } diff --git a/test/parse-chunked.js b/test/parse-chunked.js index 37ce424..225da5b 100644 --- a/test/parse-chunked.js +++ b/test/parse-chunked.js @@ -322,7 +322,9 @@ describe('parseChunked()', () => { () => ['[1, 2,', 3, ']'], () => 123, () => new Uint8Array([1, 2, 3]), - { on() {} } + { on() {} }, + { [Symbol.iterator]: null }, + { [Symbol.asyncIterator]: null } ]; for (const value of badValues) { diff --git a/test/web-streams.js b/test/web-streams.js index 7a386e8..54f0296 100644 --- a/test/web-streams.js +++ b/test/web-streams.js @@ -41,6 +41,15 @@ describeIfSupported('parseFromWebStream()', () => { assert.deepStrictEqual(actual, { foo: 123 }); }); + + it('should parse ReadableStream with no @@asyncIterator', async () => { + const nonIterableReadableStream = Object.assign(createReadableStream(['{"foo', '":123', '}']), { + [Symbol.asyncIterator]: null + }); + const actual = await parseFromWebStream(nonIterableReadableStream); + + assert.deepStrictEqual(actual, { foo: 123 }); + }); }); describeIfSupported('createStringifyWebStream()', () => {