From 79f1a26171da0fe0b4e915a4ed7dcd59b1cc5a0c Mon Sep 17 00:00:00 2001 From: Jesse Wright Date: Sun, 10 Apr 2022 21:42:00 +1000 Subject: [PATCH 1/5] chore: WIP --- asynciterator.ts | 156 +++++++++++++++++-------------------- linkedlist.ts | 34 ++++++++ test/LinkedList-test.js | 37 +++++++++ test/UnionIterator-test.js | 53 +------------ 4 files changed, 147 insertions(+), 133 deletions(-) diff --git a/asynciterator.ts b/asynciterator.ts index f1e7bc19..59e3331b 100644 --- a/asynciterator.ts +++ b/asynciterator.ts @@ -1243,6 +1243,9 @@ export class TransformIterator extends BufferedIterator { function destinationEmitError(this: InternalSource, error: Error) { this._destination.emit('error', error); } +function destinationSetReadable(this: InternalSource) { + this._destination.readable = true; +} function destinationCloseWhenDone(this: InternalSource) { (this._destination as any)._closeWhenDone(); } @@ -1510,12 +1513,12 @@ export class MultiTransformIterator extends TransformIterator { /** An iterator that generates items by reading from multiple other iterators. - @extends module:asynciterator.BufferedIterator + @extends module:asynciterator.AsyncIterator */ -export class UnionIterator extends BufferedIterator { - private _sources : InternalSource[] = []; - private _pending? : { sources?: AsyncIterator>> }; - private _currentSource = -1; +export class UnionIterator extends AsyncIterator { + private _sources : AsyncIterator>; + private buffer = new LinkedList>(); + private _sourceStarted: boolean; /** Creates a new `UnionIterator`. @@ -1526,101 +1529,88 @@ export class UnionIterator extends BufferedIterator { AsyncIteratorOrArray>> | AsyncIteratorOrArray>>, options: BufferedIteratorOptions = {}) { - super(options); - const autoStart = options.autoStart !== false; + super(); - // Sources have been passed as an iterator - if (isEventEmitter(sources)) { - sources.on('error', error => this.emit('error', error)); - this._pending = { sources: sources as AsyncIterator>> }; - if (autoStart) - this._loadSources(); - } - // Sources have been passed as a non-empty array - else if (Array.isArray(sources) && sources.length > 0) { - for (const source of sources) - this._addSource(source as MaybePromise>); - } - // Sources are an empty list - else if (autoStart) { - this.close(); + // Remove this in the next major version + if (Array.isArray(sources)) { + for (const source of sources) { + if (!isPromise(source)) { + // @ts-ignore + source._destination = this; + source.on('error', destinationEmitError); + } + } } - } - // Loads sources passed as an iterator - protected _loadSources() { - // Obtain sources iterator - const sources = this._pending!.sources!; - delete this._pending!.sources; + // @ts-ignore + this._sources = (Array.isArray(sources) ? fromArray(sources) : wrap(sources)).map>((it: any) => isPromise(it) ? wrap(it as any) : (it as AsyncIterator)); + + // @ts-ignore + this._addSource(this._sources); - // Close immediately if done - if (sources.done) { - delete this._pending; + this._sourceStarted = options.autoStart !== false; + if (this._sources.done && this._sourceStarted) this.close(); - } - // Otherwise, set up source reading - else { - sources.on('data', source => { - this._addSource(source as MaybePromise>); - this._fillBufferAsync(); - }); - sources.on('end', () => { - delete this._pending; - this._fillBuffer(); - }); - } + else + this.readable = true; } // Adds the given source to the internal sources array - protected _addSource(source: MaybePromise>) { - if (isPromise(source)) - source = wrap(source) as any as InternalSource; - if (!source.done) { - this._sources.push(source); - source._destination = this; - source.on('error', destinationEmitError); - source.on('readable', destinationFillBuffer); - source.on('end', destinationRemoveEmptySources); - } + protected _addSource(source: InternalSource) { + source._destination = this; + source.on('error', destinationEmitError); + source.on('readable', destinationSetReadable); + source.on('end', destinationRemoveEmptySources); + } + + protected _removeSource(source: InternalSource) { + source.removeListener('error', destinationEmitError); + source.removeListener('readable', destinationSetReadable); + source.removeListener('end', destinationRemoveEmptySources); + // @ts-ignore + delete source._destination; } - // Removes sources that will no longer emit items protected _removeEmptySources() { - this._sources = this._sources.filter((source, index) => { - // Adjust the index of the current source if needed - if (source.done && index <= this._currentSource) - this._currentSource--; - return !source.done; + this.buffer.filter((source: any) => { + if (source.done) { + this._removeSource(source); + return false; + } + return true; }); - this._fillBuffer(); + if (this.buffer.empty && this._sources.done && this._sourceStarted) + this.close(); + } + + public close() { + this._removeSource(this._sources as any); + super.close(); } + // Reads items from the next sources - protected _read(count: number, done: () => void): void { - // Start source loading if needed - if (this._pending?.sources) - this._loadSources(); - - // Try to read `count` items - let lastCount = 0, item : T | null; - while (lastCount !== (lastCount = count)) { - // Try every source at least once - for (let i = 0; i < this._sources.length && count > 0; i++) { - // Pick the next source - this._currentSource = (this._currentSource + 1) % this._sources.length; - const source = this._sources[this._currentSource]; - // Attempt to read an item from that source - if ((item = source.read()) !== null) { - count--; - this._push(item); - } - } + public read(): T | null { + if (!this._sourceStarted) + this._sourceStarted = true; + + const { buffer, _sources } = this; + let item: T | null; + let iterator: AsyncIterator | null; + for (iterator of buffer) { + if (iterator.readable && (item = iterator.read()) !== null) + return item; } + while ((iterator = _sources.read()) !== null) { + this._addSource(iterator as any); + this.buffer.push(iterator); - // Close this iterator if all of its sources have been read - if (!this._pending && this._sources.length === 0) - this.close(); - done(); + if ((item = iterator.read()) !== null) + return item; + } + this.readable = false; + this._removeEmptySources(); + return null; } } diff --git a/linkedlist.ts b/linkedlist.ts index 63e32eb5..611e5636 100644 --- a/linkedlist.ts +++ b/linkedlist.ts @@ -34,8 +34,42 @@ export default class LinkedList { return value; } + filter(filter: (item: V) => boolean) { + let last: LinkedNode | null; + let next: LinkedNode | null; + while (this._head !== null && !filter(this._head.value)) { + this._head = this._head.next + this._length--; + } + if (this._head === null) { + this._tail = null; + return + } + last = this._head; + next = this._head.next; + while (next !== null) { + if (filter(next.value)) { + last = next; + next = next.next + } else { + next = next.next + last.next = next + this._length--; + } + } + this._tail = last; + } + clear() { this._length = 0; this._head = this._tail = null; } + + *[Symbol.iterator]() { + let node = this._head; + while (node !== null) { + yield node.value; + node = node.next; + } + } } diff --git a/test/LinkedList-test.js b/test/LinkedList-test.js index 36651cee..bd138089 100644 --- a/test/LinkedList-test.js +++ b/test/LinkedList-test.js @@ -1,3 +1,4 @@ +import { expect } from 'chai'; import LinkedList from '../dist/linkedlist.js'; describe('LinkedList', () => { @@ -93,4 +94,40 @@ describe('LinkedList', () => { expect(list.length).to.equal(2); }); }); + + describe('Testing filter', () => { + beforeEach(() => { + list = new LinkedList(); + list.push(1); + list.push(2); + list.push(3); + list.push(4); + }); + + it('Should remove odd elements', () => { + list.filter(x => x % 2 === 0); + expect(list.length).to.equal(2); + expect([...list]).to.deep.equal([2, 4]); + }); + + it('Should remove even elements', () => { + list.filter(x => x % 2 === 1); + expect(list.length).to.equal(2); + expect([...list]).to.deep.equal([1, 3]); + }); + + it('Should remove all elements', () => { + list.filter(x => false); + expect(list.length).to.equal(0); + expect([...list]).to.deep.equal([]); + expect(list.empty).to.equal(true); + }); + + it('Should remove no elements', () => { + list.filter(x => true); + expect(list.length).to.equal(4); + expect([...list]).to.deep.equal([1, 2, 3, 4]); + expect(list.empty).to.equal(false); + }); + }); }); diff --git a/test/UnionIterator-test.js b/test/UnionIterator-test.js index c7714eca..e9d9819e 100644 --- a/test/UnionIterator-test.js +++ b/test/UnionIterator-test.js @@ -233,15 +233,6 @@ describe('UnionIterator', () => { it('should not have ended', () => { iterator.ended.should.be.false; }); - - it('should pass errors', () => { - const callback = sinon.spy(); - const error = new Error('error'); - iterator.once('error', callback); - sourceIterator.emit('error', error); - callback.should.have.been.calledOnce; - callback.should.have.been.calledWith(error); - }); }); describe('after reading', () => { @@ -277,15 +268,6 @@ describe('UnionIterator', () => { it('should not have ended', () => { iterator.ended.should.be.false; }); - - it('should pass errors', () => { - const callback = sinon.spy(); - const error = new Error('error'); - iterator.once('error', callback); - sourceIterator.emit('error', error); - callback.should.have.been.calledOnce; - callback.should.have.been.calledWith(error); - }); }); describe('after reading', () => { @@ -314,22 +296,9 @@ describe('UnionIterator', () => { }); describe('before reading', () => { - it('should not have read the sources', () => { - sourceIterator.read.should.not.have.been.called; - }); - it('should not have ended', () => { iterator.ended.should.be.false; }); - - it('should pass errors', () => { - const callback = sinon.spy(); - const error = new Error('error'); - iterator.once('error', callback); - sourceIterator.emit('error', error); - callback.should.have.been.calledOnce; - callback.should.have.been.calledWith(error); - }); }); describe('after reading', () => { @@ -369,15 +338,6 @@ describe('UnionIterator', () => { it('should not have ended', () => { iterator.ended.should.be.false; }); - - it('should pass errors', () => { - const callback = sinon.spy(); - const error = new Error('error'); - iterator.once('error', callback); - sourceIterator.emit('error', error); - callback.should.have.been.calledOnce; - callback.should.have.been.calledWith(error); - }); }); describe('after reading', () => { @@ -433,11 +393,6 @@ describe('UnionIterator', () => { (await toArray(iterator)).should.be.instanceof(Array); }); - it('should allow the _read method to be called multiple times', () => { - iterator._read(1, noop); - iterator._read(1, noop); - }); - it('should make a round-robin union of the data elements', async () => { (await toArray(iterator)).sort().should.eql([0, 1, 2, 3, 4, 5, 6]); }); @@ -492,15 +447,15 @@ describe('UnionIterator', () => { it('should read 2 streams in round-robin order', async () => { // Read 4 buffered items expect(iterator.read()).to.equal(3); - expect(iterator.read()).to.equal(6); expect(iterator.read()).to.equal(4); - expect(iterator.read()).to.equal(7); + expect(iterator.read()).to.equal(5); + expect(iterator.read()).to.equal(6); // Buffer await new Promise(resolve => scheduleTask(resolve)); // Read remaining items - expect(iterator.read()).to.equal(5); + expect(iterator.read()).to.equal(7); expect(iterator.read()).to.be.null; }); @@ -541,5 +496,3 @@ function toArray(stream) { stream.on('end', () => resolve(array)); }); } - -function noop() { /* */ } From 990dab435c4061c270b580de26cdb1097a5fcfaa Mon Sep 17 00:00:00 2001 From: Jesse Wright Date: Fri, 15 Apr 2022 22:38:59 +1000 Subject: [PATCH 2/5] resolve https://github.com/RubenVerborgh/AsyncIterator/pull/65#discussion_r850790358, https://github.com/RubenVerborgh/AsyncIterator/pull/65#discussion_r850784304 and https://github.com/RubenVerborgh/AsyncIterator/pull/65#discussion_r850777147 --- asynciterator.ts | 15 ++++++++++----- linkedlist.ts | 4 ++-- package-lock.json | 13 +++++++++++++ package.json | 1 + perf/UnionIterator-perf.js | 31 +++++++++++++++++++++++++++++++ test/LinkedList-test.js | 10 +++++----- 6 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 perf/UnionIterator-perf.js diff --git a/asynciterator.ts b/asynciterator.ts index 59e3331b..d261b39e 100644 --- a/asynciterator.ts +++ b/asynciterator.ts @@ -1519,6 +1519,7 @@ export class UnionIterator extends AsyncIterator { private _sources : AsyncIterator>; private buffer = new LinkedList>(); private _sourceStarted: boolean; + private _maxBufferSize: number; /** Creates a new `UnionIterator`. @@ -1549,6 +1550,7 @@ export class UnionIterator extends AsyncIterator { this._addSource(this._sources); this._sourceStarted = options.autoStart !== false; + this._maxBufferSize = options.maxBufferSize || Infinity; if (this._sources.done && this._sourceStarted) this.close(); else @@ -1572,7 +1574,7 @@ export class UnionIterator extends AsyncIterator { } protected _removeEmptySources() { - this.buffer.filter((source: any) => { + this.buffer.mutateFilter((source: any) => { if (source.done) { this._removeSource(source); return false; @@ -1597,19 +1599,22 @@ export class UnionIterator extends AsyncIterator { const { buffer, _sources } = this; let item: T | null; let iterator: AsyncIterator | null; - for (iterator of buffer) { - if (iterator.readable && (item = iterator.read()) !== null) + let node = buffer._head; + while (node !== null) { + if (node.value.readable && (item = node.value.read()) !== null) return item; + node = node.next; } - while ((iterator = _sources.read()) !== null) { + + while (this.buffer.length < this._maxBufferSize && (iterator = _sources.read()) !== null) { this._addSource(iterator as any); this.buffer.push(iterator); if ((item = iterator.read()) !== null) return item; } - this.readable = false; this._removeEmptySources(); + this.readable = false; return null; } } diff --git a/linkedlist.ts b/linkedlist.ts index 611e5636..60f9df0a 100644 --- a/linkedlist.ts +++ b/linkedlist.ts @@ -5,7 +5,7 @@ interface LinkedNode { export default class LinkedList { private _length: number = 0; - private _head: LinkedNode | null = null; + _head: LinkedNode | null = null; private _tail: LinkedNode | null = null; get length() { return this._length; } @@ -34,7 +34,7 @@ export default class LinkedList { return value; } - filter(filter: (item: V) => boolean) { + mutateFilter(filter: (item: V) => boolean) { let last: LinkedNode | null; let next: LinkedNode | null; while (this._head !== null && !filter(this._head.value)) { diff --git a/package-lock.json b/package-lock.json index 7fd124d9..6e0ff3b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "c8": "^7.2.0", "chai": "^4.2.0", "eslint": "^8.0.0", + "event-emitter-promisify": "^1.1.0", "husky": "^4.2.5", "jaguarjs-jsdoc": "^1.1.0", "jsdoc": "^3.5.5", @@ -2219,6 +2220,12 @@ "node": ">=0.10.0" } }, + "node_modules/event-emitter-promisify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/event-emitter-promisify/-/event-emitter-promisify-1.1.0.tgz", + "integrity": "sha512-uyHG8gjwYGDlKoo0Txtx/u1HI1ubj0FK0rVqI4O0s1EymQm4iAEMbrS5B+XFlSaS8SZ3xzoKX+YHRZk8Nk/bXg==", + "dev": true + }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -7786,6 +7793,12 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "event-emitter-promisify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/event-emitter-promisify/-/event-emitter-promisify-1.1.0.tgz", + "integrity": "sha512-uyHG8gjwYGDlKoo0Txtx/u1HI1ubj0FK0rVqI4O0s1EymQm4iAEMbrS5B+XFlSaS8SZ3xzoKX+YHRZk8Nk/bXg==", + "dev": true + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", diff --git a/package.json b/package.json index e68befd5..c0842d0b 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "c8": "^7.2.0", "chai": "^4.2.0", "eslint": "^8.0.0", + "event-emitter-promisify": "^1.1.0", "husky": "^4.2.5", "jaguarjs-jsdoc": "^1.1.0", "jsdoc": "^3.5.5", diff --git a/perf/UnionIterator-perf.js b/perf/UnionIterator-perf.js new file mode 100644 index 00000000..3386ba1a --- /dev/null +++ b/perf/UnionIterator-perf.js @@ -0,0 +1,31 @@ +import { UnionIterator, range } from '../dist/asynciterator.js'; +import { promisifyEventEmitter } from 'event-emitter-promisify' + +let it; + +// Warmup + +console.time('For loop with 5x10^9 elems'); +for (let i = 0; i < 5_000_000_000; i++); +console.timeEnd('For loop with 5x10^9 elems'); + +console.time('UnionIterator 2 x 10^7 iterators'); +for (let i = 0; i < 5; i++) { + it = new UnionIterator([range(0, 10_000_000), range(0, 10_000_000)]); + await promisifyEventEmitter(it.on('data', () => {})) +} +console.timeEnd('UnionIterator 2 x 10^7 iterators'); + +console.time('UnionIterator 1000 x 20_000 iterators'); +for (let i = 0; i < 5; i++) { + it = new UnionIterator(range(0, 1000).map(() => range(0, 20_000))); + await promisifyEventEmitter(it.on('data', () => {})) +} +console.timeEnd('UnionIterator 1000 x 20_000 iterators'); + +console.time('UnionIterator 1000 x 20_000 iterators - maxBufferSize of 1'); +for (let i = 0; i < 5; i++) { + it = new UnionIterator(range(0, 1000).map(() => range(0, 20_000))); + await promisifyEventEmitter(it.on('data', () => {})) +} +console.timeEnd('UnionIterator 1000 x 20_000 iterators - maxBufferSize of 1'); diff --git a/test/LinkedList-test.js b/test/LinkedList-test.js index bd138089..ebe8b424 100644 --- a/test/LinkedList-test.js +++ b/test/LinkedList-test.js @@ -95,7 +95,7 @@ describe('LinkedList', () => { }); }); - describe('Testing filter', () => { + describe('Testing mutateFilter', () => { beforeEach(() => { list = new LinkedList(); list.push(1); @@ -105,26 +105,26 @@ describe('LinkedList', () => { }); it('Should remove odd elements', () => { - list.filter(x => x % 2 === 0); + list.mutateFilter(x => x % 2 === 0); expect(list.length).to.equal(2); expect([...list]).to.deep.equal([2, 4]); }); it('Should remove even elements', () => { - list.filter(x => x % 2 === 1); + list.mutateFilter(x => x % 2 === 1); expect(list.length).to.equal(2); expect([...list]).to.deep.equal([1, 3]); }); it('Should remove all elements', () => { - list.filter(x => false); + list.mutateFilter(x => false); expect(list.length).to.equal(0); expect([...list]).to.deep.equal([]); expect(list.empty).to.equal(true); }); it('Should remove no elements', () => { - list.filter(x => true); + list.mutateFilter(x => true); expect(list.length).to.equal(4); expect([...list]).to.deep.equal([1, 2, 3, 4]); expect(list.empty).to.equal(false); From f2fb790742c628cf92d56c5bf1020284e9b7db32 Mon Sep 17 00:00:00 2001 From: Jesse Wright Date: Fri, 15 Apr 2022 22:49:03 +1000 Subject: [PATCH 3/5] chore: add test for maxBufferSize on UnionIterator and fix append/prepend method --- asynciterator.ts | 12 ++++++++++-- test/SimpleTransformIterator-test.js | 8 -------- test/UnionIterator-test.js | 11 +++++++++++ 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/asynciterator.ts b/asynciterator.ts index d261b39e..f29852ca 100644 --- a/asynciterator.ts +++ b/asynciterator.ts @@ -479,7 +479,10 @@ export class AsyncIterator extends EventEmitter { @returns {module:asynciterator.AsyncIterator} A new iterator that prepends items to this iterator */ prepend(items: T[] | AsyncIterator): AsyncIterator { - return this.transform({ prepend: items }); + return new UnionIterator( + [Array.isArray(items) ? new ArrayIterator(items, { autoStart: false }) : items, this], + { maxBufferSize: 1 } + ); } /** @@ -489,7 +492,10 @@ export class AsyncIterator extends EventEmitter { @returns {module:asynciterator.AsyncIterator} A new iterator that appends items to this iterator */ append(items: T[] | AsyncIterator): AsyncIterator { - return this.transform({ append: items }); + return new UnionIterator( + [this, Array.isArray(items) ? new ArrayIterator(items, { autoStart: false }) : items], + { maxBufferSize: 1 } + ); } /** @@ -1583,6 +1589,8 @@ export class UnionIterator extends AsyncIterator { }); if (this.buffer.empty && this._sources.done && this._sourceStarted) this.close(); + else + this.readable = true; } public close() { diff --git a/test/SimpleTransformIterator-test.js b/test/SimpleTransformIterator-test.js index e033b8e6..59a56615 100644 --- a/test/SimpleTransformIterator-test.js +++ b/test/SimpleTransformIterator-test.js @@ -1256,10 +1256,6 @@ describe('SimpleTransformIterator', () => { result.on('end', done); }); - it('should be a SimpleTransformIterator', () => { - result.should.be.an.instanceof(SimpleTransformIterator); - }); - it('should prepend the items', () => { items.should.deep.equal(['i', 'ii', 'iii', 'a', 'b', 'c']); }); @@ -1286,10 +1282,6 @@ describe('SimpleTransformIterator', () => { result.on('end', done); }); - it('should be a SimpleTransformIterator', () => { - result.should.be.an.instanceof(SimpleTransformIterator); - }); - it('should append the items', () => { items.should.deep.equal(['a', 'b', 'c', 'I', 'II', 'III']); }); diff --git a/test/UnionIterator-test.js b/test/UnionIterator-test.js index e9d9819e..e91d591f 100644 --- a/test/UnionIterator-test.js +++ b/test/UnionIterator-test.js @@ -68,6 +68,17 @@ describe('UnionIterator', () => { (await toArray(iterator)).sort().should.eql([0, 1, 2]); }); + it('should include all data from 1 non-empty and 4 empty sources - with maxBufferSize: 1', async () => { + const iterator = new UnionIterator([ + new EmptyIterator(), + new EmptyIterator(), + range(0, 2), + new EmptyIterator(), + new EmptyIterator(), + ], { maxBufferSize: 1 }); + (await toArray(iterator)).sort().should.eql([0, 1, 2]); + }); + describe('when constructed with an array of 0 sources', () => { let iterator; before(() => { From 1fd0cdc6339357295046a42058633fccf041d486 Mon Sep 17 00:00:00 2001 From: Jesse Wright Date: Fri, 15 Apr 2022 22:55:19 +1000 Subject: [PATCH 4/5] chore: add tests for append/prepend --- test/AsyncIterator-test.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/AsyncIterator-test.js b/test/AsyncIterator-test.js index 37a74d60..a1b8cc72 100644 --- a/test/AsyncIterator-test.js +++ b/test/AsyncIterator-test.js @@ -5,9 +5,11 @@ import { ENDED, DESTROYED, scheduleTask, + range, } from '../dist/asynciterator.js'; import { EventEmitter } from 'events'; +import { expect } from 'chai'; describe('AsyncIterator', () => { describe('The AsyncIterator module', () => { @@ -1307,4 +1309,22 @@ describe('AsyncIterator', () => { }); }); }); + + describe('Testing #append', () => { + it('Should append an array', async () => { + expect(await range(0, 1).append([2, 3, 4]).toArray()).to.deep.equal([0, 1, 2, 3, 4]); + }); + it('Should append an iterator', async () => { + expect(await range(0, 1).append(range(2, 4)).toArray()).to.deep.equal([0, 1, 2, 3, 4]); + }); + }); + + describe('Testing #prepend', () => { + it('Should prepend an array', async () => { + expect(await range(0, 1).prepend([2, 3, 4]).toArray()).to.deep.equal([2, 3, 4, 0, 1]); + }); + it('Should prepend an iterator', async () => { + expect(await range(0, 1).prepend(range(2, 4)).toArray()).to.deep.equal([2, 3, 4, 0, 1]); + }); + }); }); From 5e66ac2c6b1710ca67878a5d351c0759ab36b1bc Mon Sep 17 00:00:00 2001 From: Jesse Wright Date: Fri, 15 Apr 2022 22:58:23 +1000 Subject: [PATCH 5/5] chore: add note on [Symbol.iterator] --- linkedlist.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/linkedlist.ts b/linkedlist.ts index 60f9df0a..45fcf540 100644 --- a/linkedlist.ts +++ b/linkedlist.ts @@ -65,6 +65,8 @@ export default class LinkedList { this._head = this._tail = null; } + // This iterator does not keep yielding items as they are pushed into the list. + // It synchronously runs until the current end of the list and that's it. *[Symbol.iterator]() { let node = this._head; while (node !== null) {