215 lines
6.6 KiB
TypeScript
215 lines
6.6 KiB
TypeScript
import { expect, test, vi } from 'vitest';
|
|
|
|
import { AsyncPushIterator } from '../../source/sse-session/async-push-iterator.ts';
|
|
|
|
/**
|
|
* Collects every value from the iterator into an array.
|
|
*
|
|
* @param iterator - Iterator under test.
|
|
*/
|
|
const collectAll = async <T>(iterator: AsyncPushIterator<T>): Promise<T[]> => {
|
|
const results: T[] = [];
|
|
|
|
for await (const value of iterator) {
|
|
results.push(value);
|
|
}
|
|
|
|
return results;
|
|
};
|
|
|
|
/**
|
|
* Tests that values pushed while a consumer is already waiting are delivered in order.
|
|
*/
|
|
const testPushComposedPushAndConsume = async (): Promise<void> => {
|
|
const iterator = new AsyncPushIterator<number>();
|
|
|
|
const result = (): Promise<number[]> => collectAll(iterator);
|
|
|
|
iterator.push(1);
|
|
iterator.push(2);
|
|
iterator.push(3);
|
|
iterator.close();
|
|
|
|
await expect(result()).resolves.toEqual([ 1, 2, 3 ]);
|
|
};
|
|
|
|
/**
|
|
* Tests that values pushed before `for await...of` starts are buffered and yielded
|
|
* once the consumer begins reading.
|
|
*/
|
|
const testPushComposedBuffersValuesPushedBeforeLoopStarts = async (): Promise<void> => {
|
|
const iterator = new AsyncPushIterator<number>();
|
|
|
|
iterator.push(1);
|
|
iterator.push(2);
|
|
iterator.push(3);
|
|
|
|
const result = collectAll(iterator);
|
|
|
|
iterator.close();
|
|
|
|
await expect(result).resolves.toEqual([ 1, 2, 3 ]);
|
|
};
|
|
|
|
/**
|
|
* Tests that the iterator completes with no values when nothing was pushed.
|
|
*/
|
|
const testPushComposedResolvesWithNoValues = async (): Promise<void> => {
|
|
const iterator = new AsyncPushIterator<number>();
|
|
|
|
const result = async (): Promise<number[]> => collectAll(iterator);
|
|
|
|
iterator.close();
|
|
|
|
await expect(result()).resolves.toEqual([]);
|
|
};
|
|
|
|
/**
|
|
* Tests that values pushed after {@link AsyncPushIterator.close} are ignored.
|
|
*/
|
|
const testPushComposedIgnoresValuesAfterClose = async (): Promise<void> => {
|
|
const iterator = new AsyncPushIterator<number>();
|
|
|
|
const result = async (): Promise<number[]> => collectAll(iterator);
|
|
|
|
iterator.push(1);
|
|
iterator.push(2);
|
|
iterator.push(3);
|
|
iterator.close();
|
|
iterator.push(4);
|
|
|
|
await expect(result()).resolves.toEqual([ 1, 2, 3 ]);
|
|
};
|
|
|
|
/**
|
|
* Tests that only one async consumer can read from the composed ReadableStream at a time.
|
|
*
|
|
* Unlike the hand-rolled async-push-iterator, the second consumer fails with a
|
|
* stream lock error rather than TooManyAsyncIteratorsError.
|
|
*/
|
|
const testPushComposedRejectsMultipleConsumers = async (): Promise<void> => {
|
|
const iterator = new AsyncPushIterator<number>();
|
|
|
|
const failureFlag = vi.fn();
|
|
|
|
const successfulIterator = (): Promise<number[]> => collectAll(iterator);
|
|
|
|
const failedIterator = async (): Promise<void> => {
|
|
try {
|
|
/* eslint-disable-next-line */
|
|
for await (const _value of iterator) {
|
|
}
|
|
} catch (error) {
|
|
failureFlag();
|
|
}
|
|
};
|
|
|
|
const promises = [ successfulIterator(), failedIterator() ];
|
|
|
|
iterator.close();
|
|
|
|
await Promise.all(promises);
|
|
|
|
expect(failureFlag).toHaveBeenCalledOnce();
|
|
};
|
|
|
|
/**
|
|
* Tests that closing before iteration starts lets the loop finish immediately.
|
|
*/
|
|
const testPushComposedResolvesWhenClosedBeforeLoop = async (): Promise<void> => {
|
|
const iterator = new AsyncPushIterator<number>();
|
|
|
|
iterator.close();
|
|
|
|
await expect(collectAll(iterator)).resolves.toEqual([]);
|
|
};
|
|
|
|
/**
|
|
* Tests that breaking out of `for await...of` early does not cancel the stream.
|
|
*
|
|
* {@link AsyncPushIterator} uses `preventCancel: true` so producers can keep pushing
|
|
* and a later consumer can read the remaining values.
|
|
*/
|
|
const testPushComposedAllowsPushingAfterEarlyBreak = async (): Promise<void> => {
|
|
const iterator = new AsyncPushIterator<number>();
|
|
|
|
iterator.push(1);
|
|
|
|
const firstPass: number[] = [];
|
|
|
|
for await (const value of iterator) {
|
|
firstPass.push(value);
|
|
break;
|
|
}
|
|
|
|
iterator.push(2);
|
|
iterator.push(3);
|
|
iterator.close();
|
|
|
|
const secondPass = await collectAll(iterator);
|
|
|
|
expect(firstPass).toEqual([ 1 ]);
|
|
expect(secondPass).toEqual([ 2, 3 ]);
|
|
};
|
|
|
|
/**
|
|
* Tests that the iterator rejects after {@link AsyncPushIterator.error} is called.
|
|
*/
|
|
const testPushIteratorRejectsAfterError = async (): Promise<void> => {
|
|
const iterator = new AsyncPushIterator<number>();
|
|
|
|
iterator.error(new Error('Stream has been closed for a test'));
|
|
|
|
await expect(collectAll(iterator)).rejects.toThrow('Stream has been closed for a test');
|
|
};
|
|
|
|
/**
|
|
* Tests that the iterator closes the stream when error() is called.
|
|
*/
|
|
const testPushIteratorClosesWhenErrorIsCalled = async (): Promise<void> => {
|
|
const iterator = new AsyncPushIterator<number>();
|
|
|
|
iterator.error(new Error('Stream has been closed for a test'));
|
|
expect(iterator.closed).toBe(true);
|
|
};
|
|
|
|
/**
|
|
* Tests that subsequent calls to error() are ignored.
|
|
*/
|
|
const testPushIteratorIgnoresSubsequentErrorCalls = async (): Promise<void> => {
|
|
const iterator = new AsyncPushIterator<number>();
|
|
|
|
iterator.error(new Error('Stream has been closed for a test'));
|
|
expect(iterator.closed).toBe(true);
|
|
|
|
expect(iterator.error(new Error('Second error'))).toBe(undefined);
|
|
};
|
|
|
|
/**
|
|
* Tests that a consumer can check if the iterator is closed.
|
|
*/
|
|
const testPushIteratorCanCheckIfClosed = async (): Promise<void> => {
|
|
const iterator = new AsyncPushIterator<number>();
|
|
|
|
expect(iterator.closed).toBe(false);
|
|
|
|
iterator.close();
|
|
expect(iterator.closed).toBe(true);
|
|
};
|
|
|
|
const runTests = async (): Promise<void> => {
|
|
test('AsyncPushIterator: pushes and consumes values', testPushComposedPushAndConsume);
|
|
test('AsyncPushIterator: buffers values pushed before the for-await loop starts', testPushComposedBuffersValuesPushedBeforeLoopStarts);
|
|
test('AsyncPushIterator: resolves with no values when nothing was pushed', testPushComposedResolvesWithNoValues);
|
|
test('AsyncPushIterator: ignores values pushed after close', testPushComposedIgnoresValuesAfterClose);
|
|
test('AsyncPushIterator: rejects multiple consumers', testPushComposedRejectsMultipleConsumers);
|
|
test('AsyncPushIterator: resolves immediately when closed before the loop starts', testPushComposedResolvesWhenClosedBeforeLoop);
|
|
test('AsyncPushIterator: keeps the stream open after an early break', testPushComposedAllowsPushingAfterEarlyBreak);
|
|
test('AsyncPushIterator: rejects after error', testPushIteratorRejectsAfterError);
|
|
test('AsyncPushIterator: closes the stream when error() is called', testPushIteratorClosesWhenErrorIsCalled);
|
|
test('AsyncPushIterator: ignores subsequent error() calls', testPushIteratorIgnoresSubsequentErrorCalls);
|
|
test('AsyncPushIterator: can check if closed', testPushIteratorCanCheckIfClosed);
|
|
};
|
|
|
|
await runTests();
|