251 lines
7.5 KiB
TypeScript
251 lines
7.5 KiB
TypeScript
import { asyncifyIterable, chunkedAsyncIterable, peekableAsyncIterable } from '@crawlee/utils';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
describe('asyncifyIterable', () => {
|
|
it('should convert a regular array to async iterable', async () => {
|
|
const asyncIterable = asyncifyIterable([1, 2, 3, 4, 5]);
|
|
|
|
const result = [];
|
|
for await (const item of asyncIterable) {
|
|
result.push(item);
|
|
}
|
|
|
|
expect(result).toEqual([1, 2, 3, 4, 5]);
|
|
});
|
|
|
|
it('should handle empty arrays', async () => {
|
|
const asyncIterable = asyncifyIterable([]);
|
|
|
|
const result: unknown[] = [];
|
|
for await (const item of asyncIterable) {
|
|
result.push(item);
|
|
}
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should work with Set', async () => {
|
|
const asyncIterable = asyncifyIterable(new Set([1, 2, 3, 2, 1]));
|
|
|
|
const result = [];
|
|
for await (const item of asyncIterable) {
|
|
result.push(item);
|
|
}
|
|
|
|
expect(result).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
it('should work with generator function', async () => {
|
|
function* generator() {
|
|
yield 1;
|
|
yield 2;
|
|
yield 3;
|
|
}
|
|
|
|
const asyncIterable = asyncifyIterable(generator());
|
|
|
|
const result = [];
|
|
for await (const item of asyncIterable) {
|
|
result.push(item);
|
|
}
|
|
|
|
expect(result).toEqual([1, 2, 3]);
|
|
});
|
|
});
|
|
|
|
describe('chunkedAsyncIterable', () => {
|
|
it('should chunk an async iterable into specified sizes', async () => {
|
|
const result = [];
|
|
for await (const chunk of chunkedAsyncIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3)) {
|
|
result.push(chunk);
|
|
}
|
|
|
|
expect(result).toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]);
|
|
});
|
|
|
|
it('should handle chunk size of 1', async () => {
|
|
const result = [];
|
|
for await (const chunk of chunkedAsyncIterable([1, 2, 3], 1)) {
|
|
result.push(chunk);
|
|
}
|
|
|
|
expect(result).toEqual([[1], [2], [3]]);
|
|
});
|
|
|
|
it('should handle chunk size larger than input', async () => {
|
|
const chunked = chunkedAsyncIterable([1, 2, 3], 10);
|
|
|
|
const result = [];
|
|
for await (const chunk of chunked) {
|
|
result.push(chunk);
|
|
}
|
|
|
|
expect(result).toEqual([[1, 2, 3]]);
|
|
});
|
|
|
|
it('should handle empty async iterable', async () => {
|
|
const result = [];
|
|
for await (const chunk of chunkedAsyncIterable([], 3)) {
|
|
result.push(chunk);
|
|
}
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should accept a callback for dynamic chunk size', async () => {
|
|
let size = 3;
|
|
const result = [];
|
|
for await (const chunk of chunkedAsyncIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], () => size)) {
|
|
result.push(chunk);
|
|
size = 2; // shrink after first chunk
|
|
}
|
|
|
|
expect(result).toEqual([[1, 2, 3], [4, 5], [6, 7], [8, 9], [10]]);
|
|
});
|
|
|
|
it('should stop iterating when dynamic chunk size drops to zero', async () => {
|
|
let size = 2;
|
|
const result = [];
|
|
for await (const chunk of chunkedAsyncIterable([1, 2, 3, 4, 5, 6], () => size)) {
|
|
result.push(chunk);
|
|
size = 0; // signal stop after first chunk
|
|
}
|
|
|
|
expect(result).toEqual([[1, 2]]);
|
|
});
|
|
|
|
it('should leave the underlying iterator drainable after partial consumption', async () => {
|
|
async function* source() {
|
|
yield 1;
|
|
yield 2;
|
|
yield 3;
|
|
yield 4;
|
|
yield 5;
|
|
}
|
|
|
|
const iterator = source();
|
|
|
|
// Consume only the first chunk via chunkedAsyncIterable
|
|
let size = 2;
|
|
for await (const _ of chunkedAsyncIterable(iterator, () => size)) {
|
|
size = 0; // stop after first chunk
|
|
}
|
|
|
|
// The underlying iterator should still be drainable
|
|
const remaining: number[] = [];
|
|
for await (const value of iterator) {
|
|
remaining.push(value);
|
|
}
|
|
|
|
expect(remaining).toEqual([3, 4, 5]);
|
|
});
|
|
|
|
it('should throw error for invalid chunk size', async () => {
|
|
await expect(
|
|
(async () => {
|
|
for await (const _ of chunkedAsyncIterable([1, 2, 3], 0)) {
|
|
// Empty block
|
|
}
|
|
})(),
|
|
).rejects.toThrow();
|
|
await expect(
|
|
(async () => {
|
|
for await (const _ of chunkedAsyncIterable([1, 2, 3], -1)) {
|
|
// Empty block
|
|
}
|
|
})(),
|
|
).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('peekableAsyncIterable', () => {
|
|
it('should allow peeking at the next value without advancing', async () => {
|
|
const iterable = peekableAsyncIterable([1, 2, 3]);
|
|
const iterator = iterable[Symbol.asyncIterator]();
|
|
|
|
const peeked = await iterator.peek();
|
|
expect(peeked).toBe(1);
|
|
|
|
// Peeking again should return the same value
|
|
const peekedAgain = await iterator.peek();
|
|
expect(peekedAgain).toBe(1);
|
|
|
|
// Now iterate and verify we get the peeked value first
|
|
const results = [];
|
|
for await (const value of iterable) {
|
|
results.push(value);
|
|
}
|
|
|
|
expect(results).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
it('should return undefined when peeking at empty iterable', async () => {
|
|
const iterable = peekableAsyncIterable([]);
|
|
const iterator = iterable[Symbol.asyncIterator]();
|
|
|
|
const peeked = await iterator.peek();
|
|
expect(peeked).toBeUndefined();
|
|
});
|
|
|
|
it('should handle peek after exhausting the iterable', async () => {
|
|
const iterable = peekableAsyncIterable([1]);
|
|
|
|
// Consume the iterable
|
|
const results = [];
|
|
for await (const value of iterable) {
|
|
results.push(value);
|
|
}
|
|
expect(results).toEqual([1]);
|
|
|
|
// Get a fresh iterator and peek should return undefined
|
|
const iterator = iterable[Symbol.asyncIterator]();
|
|
const peeked = await iterator.peek();
|
|
expect(peeked).toBeUndefined();
|
|
});
|
|
|
|
it('should work with manual iteration', async () => {
|
|
const iterable = peekableAsyncIterable([10, 20, 30]);
|
|
const iterator = iterable[Symbol.asyncIterator]();
|
|
|
|
// Peek first
|
|
expect(await iterator.peek()).toBe(10);
|
|
|
|
const first = await iterator.next();
|
|
expect(first.value).toBe(10);
|
|
expect(first.done).toBe(false);
|
|
expect(await iterator.peek()).toBe(20);
|
|
|
|
const second = await iterator.next();
|
|
expect(second.value).toBe(20);
|
|
expect(second.done).toBe(false);
|
|
expect(await iterator.peek()).toBe(30);
|
|
|
|
const third = await iterator.next();
|
|
expect(third.value).toBe(30);
|
|
expect(third.done).toBe(false);
|
|
expect(await iterator.peek()).toBe(undefined);
|
|
|
|
const done = await iterator.next();
|
|
expect(done.done).toBe(true);
|
|
expect(await iterator.peek()).toBe(undefined);
|
|
});
|
|
|
|
it('should handle peek on single element iterable', async () => {
|
|
const iterable = peekableAsyncIterable([42]);
|
|
const iterator = iterable[Symbol.asyncIterator]();
|
|
|
|
expect(await iterator.peek()).toBe(42);
|
|
|
|
const results = [];
|
|
for await (const value of iterable) {
|
|
results.push(value);
|
|
}
|
|
|
|
expect(results).toEqual([42]);
|
|
|
|
// Get a fresh iterator and peek after exhaustion
|
|
const newIterator = iterable[Symbol.asyncIterator]();
|
|
expect(await newIterator.peek()).toBeUndefined();
|
|
});
|
|
});
|