diff --git a/README.md b/README.md index af2bf8d..0b53d8e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ # cached-iterable -`cached-iterable` exposes the `CachedItearble` class which implements the -[iterable protocol][]. +`cached-iterable` exposes two classes which implement the [iterable +protocol][]: + + - `CachedSyncIterable`, + - `CachedAsyncIterable`. You can wrap any iterable in these classes to create a new iterable which caches the yielded elements. This is useful for iterating over an iterable many @@ -21,7 +24,7 @@ can install it from the npm registry or use it as a standalone script (as the ```js import assert from "assert"; -import {CachedIterable} from "cached-iterable"; +import {CachedSyncIterable} from "cached-iterable"; function * countdown(i) { while (i--) { @@ -29,7 +32,7 @@ function * countdown(i) { } } -let numbers = new CachedIterable(countdown(3)); +let numbers = new CachedSyncIterable(countdown(3)); // `numbers` can be iterated over multiple times. assert.deepEqual([...numbers], [3, 2, 1, 0]); @@ -42,7 +45,7 @@ For legacy browsers, the `compat` build has been transpiled using Babel's [env preset][]. It requires the regenerator runtime provided by [babel-polyfill][]. ```javascript -import {CachedIterable} from 'cached-iterable/compat'; +import {CachedSyncIterable} from 'cached-iterable/compat'; ``` [env preset]: https://babeljs.io/docs/plugins/preset-env/ diff --git a/src/cached_iterable.mjs b/src/cached_async_iterable.mjs similarity index 64% rename from src/cached_iterable.mjs rename to src/cached_async_iterable.mjs index a1cb558..9c03c1e 100644 --- a/src/cached_iterable.mjs +++ b/src/cached_async_iterable.mjs @@ -1,15 +1,15 @@ /* - * CachedIterable caches the elements yielded by an iterable. + * CachedAsyncIterable caches the elements yielded by an async iterable. * * It can be used to iterate over an iterable many times without depleting the * iterable. */ -export default class CachedIterable { +export default class CachedAsyncIterable { /** - * Create an `CachedIterable` instance. + * Create an `CachedAsyncIterable` instance. * * @param {Iterable} iterable - * @returns {CachedIterable} + * @returns {CachedAsyncIterable} */ constructor(iterable) { if (Symbol.asyncIterator in Object(iterable)) { @@ -23,20 +23,6 @@ export default class CachedIterable { this.seen = []; } - [Symbol.iterator]() { - const { seen, iterator } = this; - let cur = 0; - - return { - next() { - if (seen.length <= cur) { - seen.push(iterator.next()); - } - return seen[cur++]; - } - }; - } - [Symbol.asyncIterator]() { const { seen, iterator } = this; let cur = 0; @@ -54,11 +40,16 @@ export default class CachedIterable { /** * This method allows user to consume the next element from the iterator * into the cache. + * + * @param {number} count - number of elements to consume */ - touchNext() { + async touchNext(count = 1) { const { seen, iterator } = this; - if (seen.length === 0 || seen[seen.length - 1].done === false) { - seen.push(iterator.next()); + let idx = 0; + while (idx++ < count) { + if (seen.length === 0 || seen[seen.length - 1].done === false) { + seen.push(await iterator.next()); + } } } } diff --git a/src/cached_sync_iterable.mjs b/src/cached_sync_iterable.mjs new file mode 100644 index 0000000..1609809 --- /dev/null +++ b/src/cached_sync_iterable.mjs @@ -0,0 +1,53 @@ +/* + * CachedSyncIterable caches the elements yielded by an iterable. + * + * It can be used to iterate over an iterable many times without depleting the + * iterable. + */ +export default class CachedSyncIterable { + /** + * Create an `CachedSyncIterable` instance. + * + * @param {Iterable} iterable + * @returns {CachedSyncIterable} + */ + constructor(iterable) { + if (Symbol.iterator in Object(iterable)) { + this.iterator = iterable[Symbol.iterator](); + } else { + throw new TypeError("Argument must implement the iteration protocol."); + } + + this.seen = []; + } + + [Symbol.iterator]() { + const { seen, iterator } = this; + let cur = 0; + + return { + next() { + if (seen.length <= cur) { + seen.push(iterator.next()); + } + return seen[cur++]; + } + }; + } + + /** + * This method allows user to consume the next element from the iterator + * into the cache. + * + * @param {number} count - number of elements to consume + */ + touchNext(count = 1) { + const { seen, iterator } = this; + let idx = 0; + while (idx++ < count) { + if (seen.length === 0 || seen[seen.length - 1].done === false) { + seen.push(iterator.next()); + } + } + } +} diff --git a/src/index.mjs b/src/index.mjs index 13c4a8c..0b32d1f 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -1 +1,2 @@ -export {default as CachedIterable} from "./cached_iterable.mjs"; +export {default as CachedSyncIterable} from "./cached_sync_iterable.mjs"; +export {default as CachedAsyncIterable} from "./cached_async_iterable.mjs"; diff --git a/test/cached_async_iterable_test.js b/test/cached_async_iterable_test.js new file mode 100644 index 0000000..407dd98 --- /dev/null +++ b/test/cached_async_iterable_test.js @@ -0,0 +1,176 @@ +import assert from "assert"; +import {CachedAsyncIterable} from "../src/index"; + +/** + * Return a promise for an array with all the elements of the iterable. + * + * It uses for-await to support async iterables which can't be spread with + * ...iterable. See https://github.com/tc39/proposal-async-iteration/issues/103 + * + */ +async function toArray(iterable) { + const result = []; + for await (const elem of iterable) { + result.push(elem); + } + return result; +} + +suite("CachedAsyncIterable", function() { + suite("constructor errors", function(){ + test("no argument", function() { + function run() { + new CachedAsyncIterable(); + } + + assert.throws(run, TypeError); + assert.throws(run, /iteration protocol/); + }); + + test("null argument", function() { + function run() { + new CachedAsyncIterable(null); + } + + assert.throws(run, TypeError); + assert.throws(run, /iteration protocol/); + }); + + test("bool argument", function() { + function run() { + new CachedAsyncIterable(1); + } + + assert.throws(run, TypeError); + assert.throws(run, /iteration protocol/); + }); + + test("number argument", function() { + function run() { + new CachedAsyncIterable(1); + } + + assert.throws(run, TypeError); + assert.throws(run, /iteration protocol/); + }); + }); + + suite("async iteration", function(){ + let o1, o2; + + suiteSetup(function() { + o1 = Object(); + o2 = Object(); + }); + + test("lazy iterable", async function() { + async function *generate() { + yield *[o1, o2]; + } + + const iterable = new CachedAsyncIterable(generate()); + assert.deepEqual(await toArray(iterable), [o1, o2]); + }); + + test("lazy iterable works more than once", async function() { + async function *generate() { + let i = 2; + + while (--i) { + yield Object(); + } + } + + const iterable = new CachedAsyncIterable(generate()); + const first = await toArray(iterable); + assert.deepEqual(await toArray(iterable), first); + }); + }); + + suite("async touchNext", function(){ + let o1, o2, generateMessages; + + suiteSetup(function() { + o1 = Object(); + o2 = Object(); + + generateMessages = async function *generateMessages() { + yield *[o1, o2]; + } + }); + + test("consumes an element into the cache", async function() { + const iterable = new CachedAsyncIterable(generateMessages()); + assert.equal(iterable.seen.length, 0); + await iterable.touchNext(); + assert.equal(iterable.seen.length, 1); + }); + + test("allows to consume multiple elements into the cache", async function() { + const iterable = new CachedAsyncIterable(generateMessages()); + await iterable.touchNext(); + await iterable.touchNext(); + assert.equal(iterable.seen.length, 2); + }); + + test("allows to consume multiple elements at once", async function() { + const iterable = new CachedAsyncIterable(generateMessages()); + await iterable.touchNext(2); + assert.equal(iterable.seen.length, 2); + }); + + test("stops at the last element", async function() { + const iterable = new CachedAsyncIterable(generateMessages()); + await iterable.touchNext(); + await iterable.touchNext(); + await iterable.touchNext(); + assert.equal(iterable.seen.length, 3); + + await iterable.touchNext(); + assert.equal(iterable.seen.length, 3); + }); + + test("works on an empty iterable", async function() { + async function *generateEmptyMessages() { + yield *[]; + } + const iterable = new CachedAsyncIterable(generateEmptyMessages()); + await iterable.touchNext(); + await iterable.touchNext(); + await iterable.touchNext(); + assert.equal(iterable.seen.length, 1); + }); + + test("iteration for such cache works", async function() { + const iterable = new CachedAsyncIterable(generateMessages()); + await iterable.touchNext(); + await iterable.touchNext(); + await iterable.touchNext(); + + // It's a bit quirky compared to the sync counterpart, + // but there's no good way to fold async iterator into + // an array. + let values = []; + for await (let elem of iterable) { + values.push(elem); + } + assert.deepEqual(values, [o1, o2]); + }); + + test("async version handles sync iterator", async function() { + const iterable = new CachedAsyncIterable([o1, o2]); + await iterable.touchNext(); + await iterable.touchNext(); + await iterable.touchNext(); + + // It's a bit quirky compared to the sync counterpart, + // but there's no good way to fold async iterator into + // an array. + let values = []; + for await (let elem of iterable) { + values.push(elem); + } + assert.deepEqual(values, [o1, o2]); + }); + }); +}); diff --git a/test/cached_iterable_test.js b/test/cached_sync_iterable_test.js similarity index 61% rename from test/cached_iterable_test.js rename to test/cached_sync_iterable_test.js index 1bcc328..e145a98 100644 --- a/test/cached_iterable_test.js +++ b/test/cached_sync_iterable_test.js @@ -1,27 +1,11 @@ import assert from "assert"; +import {CachedSyncIterable} from "../src/index"; -import CachedIterable from "../src/cached_iterable"; - -/** - * Return a promise for an array with all the elements of the iterable. - * - * It uses for-await to support async iterables which can't be spread with - * ...iterable. See https://github.com/tc39/proposal-async-iteration/issues/103 - * - */ -async function toArray(iterable) { - const result = []; - for await (const elem of iterable) { - result.push(elem); - } - return result; -} - -suite("CachedIterable", function() { +suite("CachedSyncIterable", function() { suite("constructor errors", function(){ test("no argument", function() { function run() { - new CachedIterable(); + new CachedSyncIterable(); } assert.throws(run, TypeError); @@ -30,7 +14,7 @@ suite("CachedIterable", function() { test("null argument", function() { function run() { - new CachedIterable(null); + new CachedSyncIterable(null); } assert.throws(run, TypeError); @@ -39,7 +23,7 @@ suite("CachedIterable", function() { test("bool argument", function() { function run() { - new CachedIterable(1); + new CachedSyncIterable(1); } assert.throws(run, TypeError); @@ -48,7 +32,7 @@ suite("CachedIterable", function() { test("number argument", function() { function run() { - new CachedIterable(1); + new CachedSyncIterable(1); } assert.throws(run, TypeError); @@ -65,12 +49,12 @@ suite("CachedIterable", function() { }); test("eager iterable", function() { - const iterable = new CachedIterable([o1, o2]); + const iterable = new CachedSyncIterable([o1, o2]); assert.deepEqual([...iterable], [o1, o2]); }); test("eager iterable works more than once", function() { - const iterable = new CachedIterable([o1, o2]); + const iterable = new CachedSyncIterable([o1, o2]); assert.deepEqual([...iterable], [o1, o2]); assert.deepEqual([...iterable], [o1, o2]); }); @@ -80,7 +64,7 @@ suite("CachedIterable", function() { yield *[o1, o2]; } - const iterable = new CachedIterable(generate()); + const iterable = new CachedSyncIterable(generate()); assert.deepEqual([...iterable], [o1, o2]); }); @@ -93,44 +77,12 @@ suite("CachedIterable", function() { } } - const iterable = new CachedIterable(generate()); + const iterable = new CachedSyncIterable(generate()); const first = [...iterable]; assert.deepEqual([...iterable], first); }); }); - suite("async iteration", function(){ - let o1, o2; - - suiteSetup(function() { - o1 = Object(); - o2 = Object(); - }); - - test("lazy iterable", async function() { - async function *generate() { - yield *[o1, o2]; - } - - const iterable = new CachedIterable(generate()); - assert.deepEqual(await toArray(iterable), [o1, o2]); - }); - - test("lazy iterable works more than once", async function() { - async function *generate() { - let i = 2; - - while (--i) { - yield Object(); - } - } - - const iterable = new CachedIterable(generate()); - const first = await toArray(iterable); - assert.deepEqual(await toArray(iterable), first); - }); - }); - suite("touchNext", function(){ let o1, o2; @@ -140,21 +92,27 @@ suite("CachedIterable", function() { }); test("consumes an element into the cache", function() { - const iterable = new CachedIterable([o1, o2]); + const iterable = new CachedSyncIterable([o1, o2]); assert.equal(iterable.seen.length, 0); iterable.touchNext(); assert.equal(iterable.seen.length, 1); }); test("allows to consume multiple elements into the cache", function() { - const iterable = new CachedIterable([o1, o2]); + const iterable = new CachedSyncIterable([o1, o2]); iterable.touchNext(); iterable.touchNext(); assert.equal(iterable.seen.length, 2); }); + test("allows to consume multiple elements at once", function() { + const iterable = new CachedSyncIterable([o1, o2]); + iterable.touchNext(2); + assert.equal(iterable.seen.length, 2); + }); + test("stops at the last element", function() { - const iterable = new CachedIterable([o1, o2]); + const iterable = new CachedSyncIterable([o1, o2]); iterable.touchNext(); iterable.touchNext(); iterable.touchNext(); @@ -165,7 +123,7 @@ suite("CachedIterable", function() { }); test("works on an empty iterable", function() { - const iterable = new CachedIterable([]); + const iterable = new CachedSyncIterable([]); iterable.touchNext(); iterable.touchNext(); iterable.touchNext(); @@ -173,7 +131,7 @@ suite("CachedIterable", function() { }); test("iteration for such cache works", function() { - const iterable = new CachedIterable([o1, o2]); + const iterable = new CachedSyncIterable([o1, o2]); iterable.touchNext(); iterable.touchNext(); iterable.touchNext();