Syntax previewClick to edit
12345
export default async function streamToText(stream, options = {}) {
// TODO: read stream with a reader + TextDecoder
throw new Error('Not implemented');
}
Syntax previewClick to edit
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
import streamToText from './streamToText';
function makeStream(chunks) {
let i = 0;
let cancelled = false;
return {
getReader() {
return {
read() {
if (cancelled) return Promise.resolve({ done: true });
if (i >= chunks.length) return Promise.resolve({ done: true, value: undefined });
return Promise.resolve({ done: false, value: chunks[i++] });
},
cancel() {
cancelled = true;
}
};
}
};
}
describe('streamToText', () => {
test('concatenates chunks', async () => {
const enc = new TextEncoder();
const stream = makeStream([enc.encode('Hello '), enc.encode('World')]);
await expect(streamToText(stream)).resolves.toBe('Hello World');
});
test('handles multibyte characters across chunks', async () => {
const enc = new TextEncoder();
const bytes = enc.encode('Hi 🌍');
const part1 = bytes.slice(0, 4);
const part2 = bytes.slice(4);
const stream = makeStream([part1, part2]);
await expect(streamToText(stream)).resolves.toBe('Hi 🌍');
});
test('rejects on abort', async () => {
const enc = new TextEncoder();
const stream = makeStream([enc.encode('data')]);
const ac = new AbortController();
ac.abort(new Error('Stop'));
await expect(streamToText(stream, { signal: ac.signal })).rejects.toMatchObject({
name: 'AbortError'
});
});
test('releases the reader lock after success and read failure', async () => {
let successReleases = 0;
await expect(streamToText({
getReader: () => ({
read: async () => ({ done: true }),
releaseLock: () => { successReleases += 1; }
})
})).resolves.toBe('');
expect(successReleases).toBe(1);
let failureReleases = 0;
const failure = new Error('read failed');
await expect(streamToText({
getReader: () => ({
read: async () => { throw failure; },
releaseLock: () => { failureReleases += 1; }
})
})).rejects.toBe(failure);
expect(failureReleases).toBe(1);
});
test('releases the reader lock when decoder construction fails', async () => {
let releases = 0;
await expect(streamToText({
getReader: () => ({
read: async () => ({ done: true }),
releaseLock: () => { releases += 1; }
})
}, { encoding: 'definitely-not-an-encoding' })).rejects.toThrow();
expect(releases).toBe(1);
});
test('cancels and releases for an already-aborted signal', async () => {
let cancellations = 0;
let releases = 0;
const ac = new AbortController();
ac.abort(new Error('Stop'));
await expect(streamToText({
getReader: () => ({
read: async () => ({ done: true }),
cancel: () => { cancellations += 1; },
releaseLock: () => { releases += 1; }
})
}, { signal: ac.signal })).rejects.toMatchObject({ name: 'AbortError' });
expect(cancellations).toBe(1);
expect(releases).toBe(1);
});
test('rejects for invalid stream input', async () => {
// @ts-expect-error runtime guard
await expect(streamToText(null)).rejects.toThrow(TypeError);
});
});
Run tests to see results.