Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: maybeIterator #68

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
47 changes: 46 additions & 1 deletion asynciterator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,6 @@ export class SingletonIterator<T> extends AsyncIterator<T> {
}
}


/**
An iterator that emits the items of a given array.
@extends module:asynciterator.AsyncIterator
Expand Down Expand Up @@ -1966,3 +1965,49 @@ type SourceExpression<T> =

type InternalSource<T> =
AsyncIterator<T> & { _destination: AsyncIterator<any> };

/**
* @param source An AsyncIterator
* @returns The AsyncIterator if it is not empty, otherwise undefined
*/
export async function ensureNonEmpty<T>(source: AsyncIterator<T>): Promise<null | AsyncIterator<T>> {
return new Promise((res, rej) => {
let item;

if ((item = source.read()) !== null) {
res(source.prepend([item]));
return;
}
if (source.done) {
res(null);
return;
}

function onReadable() {
if ((item = source.read()) !== null) {
cleanup();
res(source.prepend([item]));
return;
}
if (source.done) {
cleanup();
res(null);
}
}

function err(e: Error) {
cleanup();
rej(e);
}

function cleanup() {
source.removeListener('readable', onReadable);
source.removeListener('end', onReadable);
source.removeListener('error', err);
}

source.on('readable', onReadable);
source.on('end', onReadable);
source.on('error', err);
});
}
125 changes: 125 additions & 0 deletions test/ensureNonEmpty-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { expect } from 'chai';
import {
AsyncIterator,
fromArray,
ensureNonEmpty,
range,
empty,
scheduleTask,
} from '../dist/asynciterator.js';

class MyIterator extends AsyncIterator {
read() {
this.close();
return null;
}
}


class MyBufferingIterator extends AsyncIterator {
constructor() {
super();
this.i = 10;
}

read() {
if (this.i-- < 0) {
this.close();
}
else {
scheduleTask(() => {
if (this.readable)
this.emit('readable');
else
this.readable = true;
});
}
return null;
}
}


class MyItemBufferingIterator extends AsyncIterator {
constructor() {
super();
this.i = 10;
}

read() {
this.i--;
if (this.i < 0) {
this.close();
}
else {
scheduleTask(() => {
if (this.readable)
this.emit('readable');
else
this.readable = true;
});
}
return this.i % 2 === 0 ? this.i : null;
}
}

describe('ensureNonEmpty', () => {
// TODO:
describe('Should return null on empty iterators', () => {
it('fromArray', async () => {
expect(await ensureNonEmpty(fromArray([]))).to.be.null;
});
it('range', async () => {
expect(await ensureNonEmpty(range(0, -1))).to.be.null;
});
it('MyIterator', async () => {
expect(await ensureNonEmpty(new MyIterator())).to.be.null;
});
it('empty', async () => {
expect(await ensureNonEmpty(empty())).to.be.null;
});
it('awaited empty', async () => {
const e = empty();
// Add an await so that scheduleMacroTask will have run
await Promise.resolve();

expect(await ensureNonEmpty(e)).to.be.null;
});
it('MyBufferingIterator', async () => {
expect(await ensureNonEmpty(new MyBufferingIterator())).to.be.null;
});
});

describe('Should return an iterator with all elements if the iterator is not empty', () => {
it('fromArray', async () => {
expect(await (await ensureNonEmpty(fromArray([1, 2, 3]))).toArray()).to.deep.equal([1, 2, 3]);
});
it('range 1-3', async () => {
expect(await (await ensureNonEmpty(range(1, 3))).toArray()).to.deep.equal([1, 2, 3]);
});
it('range 1-1', async () => {
expect(await (await ensureNonEmpty(range(1, 1))).toArray()).to.deep.equal([1]);
});
it('MyItemBufferingIterator', async () => {
expect(await (await ensureNonEmpty(new MyItemBufferingIterator())).toArray()).to.deep.equal([8, 6, 4, 2, 0]);
});
});

// TODO: Add better error coverage - it is *possible* that there may be a bug
// that occurs when errors are thrown when we are *not* in the awaitReadable
// code section
it('Should reject on error before first element', async () => {
const iterator = new AsyncIterator();
scheduleTask(() => { iterator.emit('error', new Error('myError')); });

let error = false;

try {
await ensureNonEmpty(iterator);
}
catch (e) {
error = true;
}

expect(error).to.be.true;
});
});