Co-Study4Grid / frontend /src /utils /ndjsonStream.test.ts
github-actions[bot]
Deploy 7688ef1
13d4e44
Raw
History Blame Contribute Delete
4.71 kB
// Copyright (c) 2025-2026, RTE (https://www.rte-france.com)
// This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0.
// If a copy of the Mozilla Public License, version 2.0 was not distributed with this file,
// you can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0
import { describe, it, expect } from 'vitest';
import { parseNdjsonStream, type NdjsonSource } from './ndjsonStream';
/** Build a source whose body emits the given string chunks in order. */
function sourceFromChunks(chunks: string[]): NdjsonSource {
const enc = new TextEncoder();
let i = 0;
return {
body: new ReadableStream<Uint8Array>({
pull(controller) {
if (i < chunks.length) controller.enqueue(enc.encode(chunks[i++]));
else controller.close();
},
}),
};
}
async function collect(src: NdjsonSource, opts?: { signal?: AbortSignal }): Promise<unknown[]> {
const out: unknown[] = [];
for await (const ev of parseNdjsonStream(src, opts)) out.push(ev);
return out;
}
describe('parseNdjsonStream', () => {
it('yields one parsed event per complete line', async () => {
const out = await collect(sourceFromChunks([
JSON.stringify({ type: 'a' }) + '\n' + JSON.stringify({ type: 'b' }) + '\n',
]));
expect(out).toEqual([{ type: 'a' }, { type: 'b' }]);
});
it('carries a partial line across chunk boundaries', async () => {
const line = JSON.stringify({ type: 'split', n: 42 });
const mid = Math.floor(line.length / 2);
const out = await collect(sourceFromChunks([line.slice(0, mid), line.slice(mid) + '\n']));
expect(out).toEqual([{ type: 'split', n: 42 }]);
});
it('splits a multi-line chunk emitted at once', async () => {
const out = await collect(sourceFromChunks([
[JSON.stringify({ i: 1 }), JSON.stringify({ i: 2 }), JSON.stringify({ i: 3 }), ''].join('\n'),
]));
expect(out).toEqual([{ i: 1 }, { i: 2 }, { i: 3 }]);
});
it('flushes a trailing line that lacks a final newline', async () => {
const out = await collect(sourceFromChunks([
JSON.stringify({ type: 'a' }) + '\n' + JSON.stringify({ type: 'last' }),
]));
expect(out).toEqual([{ type: 'a' }, { type: 'last' }]);
});
it('skips blank lines', async () => {
const out = await collect(sourceFromChunks(['\n\n' + JSON.stringify({ type: 'a' }) + '\n\n']));
expect(out).toEqual([{ type: 'a' }]);
});
it('skips a complete line that fails JSON.parse', async () => {
const out = await collect(sourceFromChunks([
'not json\n' + JSON.stringify({ type: 'ok' }) + '\n',
]));
expect(out).toEqual([{ type: 'ok' }]);
});
it('does not yield a malformed trailing line', async () => {
const out = await collect(sourceFromChunks([JSON.stringify({ type: 'a' }) + '\n{ partial']));
expect(out).toEqual([{ type: 'a' }]);
});
it('returns nothing for a null body', async () => {
const out = await collect({ body: null });
expect(out).toEqual([]);
});
it('returns nothing when the signal is already aborted', async () => {
const ctrl = new AbortController();
ctrl.abort();
const out = await collect(
sourceFromChunks([JSON.stringify({ type: 'a' }) + '\n']),
{ signal: ctrl.signal },
);
expect(out).toEqual([]);
});
it('stops without flushing the partial buffer when aborted mid-stream', async () => {
const enc = new TextEncoder();
const ctrl = new AbortController();
let pulled = 0;
const src: NdjsonSource = {
body: new ReadableStream<Uint8Array>({
pull(controller) {
pulled++;
if (pulled === 1) {
// One complete line + a VALID but unterminated trailing line.
controller.enqueue(enc.encode(
JSON.stringify({ type: 'first' }) + '\n' + JSON.stringify({ type: 'trailing' }),
));
} else {
// Abort before any more data / close arrives.
ctrl.abort();
}
},
}),
};
const out = await collect(src, { signal: ctrl.signal });
// The completed line is delivered; the unterminated trailing line is
// NOT flushed because the stream was aborted, not finished.
expect(out).toEqual([{ type: 'first' }]);
});
});