Skip to content

Commit

Permalink
Separate out CachedSyncIterable and CachedAsyncIterable.
Browse files Browse the repository at this point in the history
  • Loading branch information
stasm committed May 31, 2018
1 parent 05684dc commit 1ba84c6
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 90 deletions.
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -21,15 +24,15 @@ 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--) {
yield 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]);
Expand All @@ -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/
Expand Down
33 changes: 12 additions & 21 deletions src/cached_iterable.mjs → src/cached_async_iterable.mjs
Original file line number Diff line number Diff line change
@@ -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)) {
Expand All @@ -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;
Expand All @@ -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());
}
}
}
}
53 changes: 53 additions & 0 deletions src/cached_sync_iterable.mjs
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
}
3 changes: 2 additions & 1 deletion src/index.mjs
Original file line number Diff line number Diff line change
@@ -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";
176 changes: 176 additions & 0 deletions test/cached_async_iterable_test.js
Original file line number Diff line number Diff line change
@@ -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]);
});
});
});
Loading

0 comments on commit 1ba84c6

Please sign in to comment.