Skip to content

Add CachedIterable.from #3

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

Merged
merged 2 commits into from
Jun 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions src/cached_async_iterable.mjs
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import CachedIterable from "./cached_iterable.mjs";

/*
* 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 CachedAsyncIterable {
export default class CachedAsyncIterable extends CachedIterable {
/**
* Create an `CachedAsyncIterable` instance.
*
* @param {Iterable} iterable
* @returns {CachedAsyncIterable}
*/
constructor(iterable) {
super();

if (Symbol.asyncIterator in Object(iterable)) {
this.iterator = iterable[Symbol.asyncIterator]();
} else if (Symbol.iterator in Object(iterable)) {
this.iterator = iterable[Symbol.iterator]();
} else {
throw new TypeError("Argument must implement the iteration protocol.");
}

this.seen = [];
}

/**
Expand All @@ -30,15 +32,15 @@ export default class CachedAsyncIterable {
* cached elements of the original (async or sync) iterable.
*/
[Symbol.iterator]() {
const {seen} = this;
const cached = this;
let cur = 0;

return {
next() {
if (seen.length === cur) {
if (cached.length === cur) {
return {value: undefined, done: true};
}
return seen[cur++];
return cached[cur++];
}
};
}
Expand All @@ -52,15 +54,15 @@ export default class CachedAsyncIterable {
* iterable.
*/
[Symbol.asyncIterator]() {
const { seen, iterator } = this;
const cached = this;
let cur = 0;

return {
async next() {
if (seen.length <= cur) {
seen.push(await iterator.next());
if (cached.length <= cur) {
cached.push(await cached.iterator.next());
}
return seen[cur++];
return cached[cur++];
}
};
}
Expand All @@ -72,15 +74,14 @@ export default class CachedAsyncIterable {
* @param {number} count - number of elements to consume
*/
async 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(await iterator.next());
if (this.length === 0 || this[this.length - 1].done === false) {
this.push(await this.iterator.next());
}
}
// Return the last cached {value, done} object to allow the calling
// code to decide if it needs to call touchNext again.
return seen[seen.length - 1];
return this[this.length - 1];
}
}
20 changes: 20 additions & 0 deletions src/cached_iterable.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Base CachedIterable class.
*/
export default class CachedIterable extends Array {
/**
* Create a `CachedIterable` instance from an iterable or, if another
* instance of `CachedIterable` is passed, return it without any
* modifications.
*
* @param {Iterable} iterable
* @returns {CachedIterable}
*/
static from(iterable) {
if (iterable instanceof this) {
return iterable;
}

return new this(iterable);
}
}
23 changes: 12 additions & 11 deletions src/cached_sync_iterable.mjs
Original file line number Diff line number Diff line change
@@ -1,36 +1,38 @@
import CachedIterable from "./cached_iterable.mjs";

/*
* 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 {
export default class CachedSyncIterable extends CachedIterable {
/**
* Create an `CachedSyncIterable` instance.
*
* @param {Iterable} iterable
* @returns {CachedSyncIterable}
*/
constructor(iterable) {
super();

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;
const cached = this;
let cur = 0;

return {
next() {
if (seen.length <= cur) {
seen.push(iterator.next());
if (cached.length <= cur) {
cached.push(cached.iterator.next());
}
return seen[cur++];
return cached[cur++];
}
};
}
Expand All @@ -42,15 +44,14 @@ export default class CachedSyncIterable {
* @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());
if (this.length === 0 || this[this.length - 1].done === false) {
this.push(this.iterator.next());
}
}
// Return the last cached {value, done} object to allow the calling
// code to decide if it needs to call touchNext again.
return seen[seen.length - 1];
return this[this.length - 1];
}
}
30 changes: 23 additions & 7 deletions test/cached_async_iterable_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ suite("CachedAsyncIterable", function() {
});
});

suite("from()", function() {
test("pass any iterable", async function() {
const iterable = CachedAsyncIterable.from([1, 2]);
// No cached elements yet.
assert.deepEqual([...iterable], []);
// Deplete the original iterable.
assert.deepEqual(await toArray(iterable), [1, 2]);
});

test("pass another CachedAsyncIterable", function() {
const iterable1 = new CachedAsyncIterable([1, 2]);
const iterable2 = CachedAsyncIterable.from(iterable1);
assert.equal(iterable1, iterable2);
});
});

suite("sync iteration over cached elements", function(){
let o1, o2;

Expand Down Expand Up @@ -170,33 +186,33 @@ suite("CachedAsyncIterable", function() {

test("consumes an element into the cache", async function() {
const iterable = new CachedAsyncIterable(generateMessages());
assert.equal(iterable.seen.length, 0);
assert.equal(iterable.length, 0);
await iterable.touchNext();
assert.equal(iterable.seen.length, 1);
assert.equal(iterable.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);
assert.equal(iterable.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);
assert.equal(iterable.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);
assert.equal(iterable.length, 3);

await iterable.touchNext();
assert.equal(iterable.seen.length, 3);
assert.equal(iterable.length, 3);
});

test("works on an empty iterable", async function() {
Expand All @@ -207,7 +223,7 @@ suite("CachedAsyncIterable", function() {
await iterable.touchNext();
await iterable.touchNext();
await iterable.touchNext();
assert.equal(iterable.seen.length, 1);
assert.equal(iterable.length, 1);
});

test("iteration for such cache works", async function() {
Expand Down
27 changes: 20 additions & 7 deletions test/cached_sync_iterable_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ suite("CachedSyncIterable", function() {
});
});

suite("from()", function() {
test("pass any iterable", function() {
const iterable = CachedSyncIterable.from([1, 2]);
assert.deepEqual([...iterable], [1, 2]);
});

test("pass another CachedSyncIterable", function() {
const iterable1 = new CachedSyncIterable([1, 2]);
const iterable2 = CachedSyncIterable.from(iterable1);
assert.equal(iterable1, iterable2);
});
});

suite("sync iteration", function(){
let o1, o2;

Expand Down Expand Up @@ -93,41 +106,41 @@ suite("CachedSyncIterable", function() {

test("consumes an element into the cache", function() {
const iterable = new CachedSyncIterable([o1, o2]);
assert.equal(iterable.seen.length, 0);
assert.equal(iterable.length, 0);
iterable.touchNext();
assert.equal(iterable.seen.length, 1);
assert.equal(iterable.length, 1);
});

test("allows to consume multiple elements into the cache", function() {
const iterable = new CachedSyncIterable([o1, o2]);
iterable.touchNext();
iterable.touchNext();
assert.equal(iterable.seen.length, 2);
assert.equal(iterable.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);
assert.equal(iterable.length, 2);
});

test("stops at the last element", function() {
const iterable = new CachedSyncIterable([o1, o2]);
iterable.touchNext();
iterable.touchNext();
iterable.touchNext();
assert.equal(iterable.seen.length, 3);
assert.equal(iterable.length, 3);

iterable.touchNext();
assert.equal(iterable.seen.length, 3);
assert.equal(iterable.length, 3);
});

test("works on an empty iterable", function() {
const iterable = new CachedSyncIterable([]);
iterable.touchNext();
iterable.touchNext();
iterable.touchNext();
assert.equal(iterable.seen.length, 1);
assert.equal(iterable.length, 1);
});

test("iteration for such cache works", function() {
Expand Down