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

Implement listener subscription as async iterator protocol #20

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5656357
feat: register event listener with asynchronous iterator protocol - c…
lorenzofox3 Jan 2, 2018
78cbcd6
keep single line block formatting
lorenzofox3 Jan 2, 2018
b50ec91
fix xo and test setting
lorenzofox3 Jan 3, 2018
c904739
enable xo
lorenzofox3 Jan 3, 2018
c951511
removed useless eslint flag comments
lorenzofox3 Jan 3, 2018
d9b617a
split async iterator behavior to different file
lorenzofox3 Jan 3, 2018
cac49ab
Revert "split async iterator behavior to different file"
novemberborn Jan 4, 2018
ade1383
Merge branch 'master' into pr/20
novemberborn Jan 4, 2018
a4d414d
Implement async iterator protocol without generators, fix tests
novemberborn Jan 4, 2018
a6a3476
Fix linting error
novemberborn Jan 6, 2018
e91559c
Simplify next() implementation
novemberborn Jan 6, 2018
c4ca867
Handle edge case where return() is called by an earlier listener for …
novemberborn Jan 6, 2018
e18c260
Implement return() according to spec
novemberborn Jan 6, 2018
edb7ac5
Remove unnecessary tsconfig files in test fixtures
novemberborn Jan 6, 2018
756b47a
Change how TypeScript is loaded in types test
novemberborn Jan 6, 2018
7ef2983
Return async iterator from .events(), not .on()
novemberborn Jan 6, 2018
463c4e6
Implement .anyEvent()
novemberborn Jan 6, 2018
3f9aa37
Tweak AVA's Babel options
novemberborn Jan 6, 2018
ef596e6
Support running tests without for-await-of transpilation
novemberborn Jan 6, 2018
361cddd
Fix for-await transpilation
novemberborn Jan 7, 2018
735ee10
Ensure async iterators return non-promise values
novemberborn Jan 7, 2018
6d73663
Merge branch 'master' into pr/20
novemberborn Jan 20, 2018
0afc521
Tweak iterator implementation now that scheduling is more consistent
novemberborn Jan 20, 2018
4cdcd6f
Remove wrongly placed documentation
novemberborn Jan 20, 2018
94f54c0
Separate iterator production
novemberborn Jan 20, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 46 additions & 2 deletions index.js
Expand Up @@ -8,6 +8,45 @@ function assertEventName(eventName) {
}
}

function iterator(emitter, eventName) {
let flush = null;
let queue = [];
const off = emitter.on(eventName, data => {
if (flush) {
flush(data);
} else {
queue.push(data);
}
});

return {
async next() {
if (!queue) {
return {done: true};
}

if (queue.length > 0) {
return {done: false, value: queue.shift()};
}

const value = await new Promise(resolve => {
flush = data => {
resolve(data);
flush = null;
};
});
return {done: false, value};
},
return() {
off();
queue = null;
Copy link
Contributor

@dinoboff dinoboff Jan 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be async too, take a value argument and return {done: true, value}.

https://tc39.github.io/proposal-async-iteration/#sec-asynciterator-interface

},
[Symbol.asyncIterator]() {
return this;
}
};
}

class Emittery {
constructor() {
this._events = new Map();
Expand All @@ -24,8 +63,13 @@ class Emittery {

on(eventName, listener) {
assertEventName(eventName);
this._getListeners(eventName).add(listener);
return this.off.bind(this, eventName, listener);

if (typeof listener === 'function') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this check the number of arguments passed, rather than the listener type? It seems a little too easy to accidentally create an asynchronous iterator otherwise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you wish. Although it is a public method with a specific contract so I think this alternative is better. Otherwise it means I'll need to check for "arguments" which is not correct in strict mode; Or use the spread operator on the signature which makes the contract less obvious in my opinion

this._getListeners(eventName).add(listener);
return this.off.bind(this, eventName, listener);
}

return iterator(this, eventName);
}

off(eventName, listener) {
Expand Down
16 changes: 16 additions & 0 deletions package.json
Expand Up @@ -54,6 +54,8 @@
"ava": "*",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-eslint": "^8.1.2",
"babel-plugin-transform-async-generator-functions": "^6.24.1",
"babel-plugin-transform-async-to-generator": "^6.24.1",
"babel-plugin-transform-es2015-spread": "^6.22.0",
"codecov": "^3.0.0",
Expand All @@ -64,6 +66,17 @@
"typescript": "^2.6.2",
"xo": "*"
},
"ava": {
"babel": {
"plugins": [
"transform-async-generator-functions"
],
"presets": [
"@ava/stage-4",
"@ava/transform-test-files"
]
}
},
"babel": {
"plugins": [
"transform-async-to-generator",
Expand All @@ -76,5 +89,8 @@
"lcov",
"text"
]
},
"xo": {
"parser": "babel-eslint"
}
}
22 changes: 22 additions & 0 deletions readme.md
Expand Up @@ -47,6 +47,28 @@ Returns an unsubscribe method.

Using the same listener multiple times for the same event will result in only one method call per emitted event.

If you use the method with only the first argument, it will return an asynchronous iterator. Your listener will therefore be the loop body and you'll be able to
unsubscribe to the event simply by breaking the loop.

```Javascript
const emitter = new Emittery();
//subscribe
(async function () {
for await (let t of emitter.on('foo')) {
if(t >10){
break;//unsubscribe
}
console.log(t);
}
})();

let count = 0;
setInterval(function () {
count++;
emitter.emit('foo', count);
}, 1000);
```

##### listener(data)

#### off(eventName, [listener])
Expand Down
39 changes: 39 additions & 0 deletions test/_run.js
@@ -1,6 +1,24 @@
import test from 'ava';
import delay from 'delay';

// babel-plugin-transform-async-generator-functions assumes
// `Symbol.asyncIterator` exists, so stub it for iterator tests.
function stubAsyncIteratorSymbol(next) {
return async (...args) => {
if (!Symbol.asyncIterator) {
Symbol.asyncIterator = Symbol.for('Emittery.asyncIterator');
}

try {
return await next(...args);
} finally {
if (Symbol.asyncIterator === Symbol.for('Emittery.asyncIterator')) {
delete Symbol.asyncIterator;
}
}
};
}

module.exports = Emittery => {
test('on()', t => {
const emitter = new Emittery();
Expand Down Expand Up @@ -37,6 +55,27 @@ module.exports = Emittery => {
t.is(emitter._events.get('🦄').size, 1);
});

test.serial('on() - async iterator', stubAsyncIteratorSymbol(async t => {
const emitter = new Emittery();
const iterator = emitter.on('🦄');

await emitter.emit('🦄', '🌈');
setTimeout(() => {
emitter.emit('🦄', '🌟');
}, 10);

t.plan(3);
const expected = ['🌈', '🌟'];
for await (const data of iterator) {
t.deepEqual(data, expected.shift());
if (expected.length === 0) {
break;
}
}

t.deepEqual(await iterator.next(), {done: true});
}));

test('off()', t => {
const emitter = new Emittery();
const listener = () => {};
Expand Down