Skip to content

Commit 393ec2f

Browse files
authored
Merge pull request #3 from r24y/feat/nextCallDuring
New: Introduce `nthCallDuring` and `nextCallDuring`
2 parents 134dbab + 716258e commit 393ec2f

File tree

8 files changed

+215
-45
lines changed

8 files changed

+215
-45
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules/
2-
/lib-test/
2+
/lib-test/
3+
/lib/

README.md

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,93 @@ increment();
113113
// Prints `counter value is 3` since it waits for the 3rd call before resolving the Promise.
114114
```
115115

116+
### nextCallDuring(fn)
117+
118+
Wait for the function to be called from a callback.
119+
120+
```js
121+
let counter = 0;
122+
123+
const increment = anticipatedCall(() => {
124+
counter = counter + 1;
125+
});
126+
127+
increment.nextCallDuring(() => {
128+
counter = 5;
129+
increment();
130+
}).then(() => console.log(`counter value is ${counter}`));
131+
// Prints `counter value is 6`
132+
```
133+
134+
### nthCallDuring(n, fn)
135+
136+
Like `nextCallDuring()`, but wait for the function to be called `n` times.
137+
138+
139+
```js
140+
let counter = 0;
141+
142+
const increment = anticipatedCall(() => {
143+
counter = counter + 1;
144+
});
145+
146+
increment.nthCallDuring(3, () => {
147+
counter = 5;
148+
increment();
149+
increment();
150+
increment();
151+
}).then(() => console.log(`counter value is ${counter}`));
152+
// Prints `counter value is 8`
153+
```
154+
155+
## Usage note
156+
157+
In some cases, it's important to keep in mind that the returned Promise will resolve _at the end of the call frame_. Resolving a Promise is an asynchronous operation. Since JavaScript does one thing at a time (for the most part), when the Promise is resolved, it waits for the currently running function to stop executing (for a regular function, this happens on return; for an `async` function, this happens on `await`). If you're tracking state -- for instance, using a variable in a closure -- you might get unexpected results.
158+
159+
Here's an example:
160+
161+
```js
162+
let counter = 0;
163+
164+
const increment = anticipatedCall(() => {
165+
counter = counter + 1;
166+
});
167+
168+
increment.nextCallDuring(() => {
169+
increment();
170+
increment();
171+
increment();
172+
}).then(() => console.log(`counter value is ${counter}`));
173+
// Prints `counter value is 3`... but why?
174+
```
175+
176+
`anticipated-call` was told to wait for the next invocation, but it didn't return until `increment()` had been called _three_ times! This is because the callback inside `nextCallDuring` needed to complete execution before the Promise could resolve.
177+
178+
If the callback were an asynchronous function that yielded execution after the call, it would behave as might be expected:
179+
180+
```js
181+
let counter = 0;
182+
183+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
184+
185+
const increment = anticipatedCall(() => {
186+
counter = counter + 1;
187+
});
188+
189+
increment.nextCallDuring(async () => {
190+
increment();
191+
await delay(0);
192+
increment();
193+
await delay(0);
194+
increment();
195+
}).then(() => console.log(`counter value is ${counter}`));
196+
// Prints `counter value is 1`
197+
```
198+
199+
The purpose of introducing `delay(0)` is to interrupt the call frame to allow the `anticipated-call` to have a chance to respond.
200+
201+
If you're interested in learning more, I suggest reading about the JavaScript event loop ([this article](https://hackernoon.com/understanding-js-the-event-loop-959beae3ac40) is a great start).
202+
116203
## Contributing
117204

118-
This project uses [ESLint-style commit messages](https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-eslint/convention.md).
205+
This project uses [ESLint-style commit messages](https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-eslint/readme.md).

lib/index.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ var Anticipated = function (_EventEmitter) {
3636
_this2.addListener('apply', didCall);
3737
});
3838
}
39+
}, {
40+
key: 'nthCallDuring',
41+
value: function nthCallDuring(n, f) {
42+
var promise = this.nthNextCall(n);
43+
f();
44+
return promise;
45+
}
46+
}, {
47+
key: 'nextCallDuring',
48+
value: function nextCallDuring(f) {
49+
return this.nthCallDuring(1, f);
50+
}
3951
}, {
4052
key: 'get',
4153
value: function get(target, name) {
@@ -68,4 +80,6 @@ function anticipateCall() {
6880
return new Proxy(fn, new Anticipated());
6981
}
7082

71-
module.exports = anticipateCall;
83+
module.exports = anticipateCall;
84+
module.exports.Anticipated = Anticipated;
85+
module.exports.noop = noop;

src/__tests__/examples.test.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,59 @@ describe('Example test suite', function () {
2424
assert(ex.value === 37, 'ex.value should equal 37');
2525
});
2626
});
27+
28+
describe('Method demos', function () {
29+
describe('nextCallDuring', function () {
30+
it('should respond as expected', async () => {
31+
let counter = 0;
32+
33+
const increment = anticipatedCall(() => {
34+
counter = counter + 1;
35+
});
36+
37+
return increment.nextCallDuring(() => {
38+
counter = 5;
39+
increment();
40+
}).then(() => {
41+
expect(`counter value is ${counter}`).toEqual('counter value is 6');
42+
});
43+
})
44+
})
45+
})
46+
47+
describe('usage note', () => {
48+
test('counterintuitive case', () => {
49+
let counter = 0;
50+
51+
const increment = anticipatedCall(() => {
52+
counter = counter + 1;
53+
});
54+
55+
increment.nextCallDuring(() => {
56+
increment();
57+
increment();
58+
increment();
59+
}).then(() => {
60+
expect(`counter value is ${counter}`).toEqual('counter value is 3');
61+
});
62+
})
63+
test('async usage', () => {
64+
let counter = 0;
65+
66+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
67+
68+
const increment = anticipatedCall(() => {
69+
counter = counter + 1;
70+
});
71+
72+
increment.nextCallDuring(async () => {
73+
increment();
74+
await delay(0);
75+
increment();
76+
await delay(0);
77+
increment();
78+
}).then(() => {
79+
expect(`counter value is ${counter}`).toEqual('counter value is 1');
80+
});
81+
})
82+
})

src/__tests__/nextCall.test.js

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const anticipatedCall = require('..');
2+
3+
describe('nextCallDuring', () => {
4+
test('should resolve when called inside callback', () => {
5+
const myFn = anticipatedCall(jest.fn());
6+
const counterFn = jest.fn();
7+
return myFn.nextCallDuring(() => {
8+
myFn();
9+
}).then(() => {
10+
expect(myFn).toHaveBeenCalledTimes(1);
11+
});
12+
});
13+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const anticipatedCall = require('..');
2+
3+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4+
5+
describe('nthCallDuring', () => {
6+
test('should resolve when called inside callback', () => {
7+
const myFn = anticipatedCall(jest.fn());
8+
return myFn.nthCallDuring(2, () => {
9+
myFn();
10+
myFn();
11+
}).then(() => {
12+
expect(myFn).toHaveBeenCalledTimes(2);
13+
});
14+
});
15+
test('should resolve when called inside async callback', () => {
16+
const myFn = anticipatedCall(jest.fn());
17+
return myFn.nthCallDuring(2, async () => {
18+
myFn();
19+
await delay(0);
20+
myFn();
21+
await delay(0);
22+
myFn();
23+
}).then(() => {
24+
expect(myFn).toHaveBeenCalledTimes(2);
25+
});
26+
});
27+
})

src/index.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,28 @@ class Anticipated extends EventEmitter {
1414
this.addListener('apply', didCall);
1515
});
1616
}
17-
17+
1818
get nextCall() {
1919
return this.nthNextCall(1);
2020
}
21-
21+
22+
nthCallDuring(n, f) {
23+
const promise = this.nthNextCall(n);
24+
f();
25+
return promise;
26+
}
27+
28+
nextCallDuring(f) {
29+
return this.nthCallDuring(1, f);
30+
}
31+
2232
get(target, name) {
2333
if (name in this) {
2434
return this[name];
2535
}
2636
return target[name];
2737
}
28-
38+
2939
apply(target, thisArg, argumentsList) {
3040
this.emit('apply', thisArg, argumentsList);
3141
return target.apply(thisArg, argumentsList);
@@ -40,4 +50,4 @@ function anticipateCall(fn = noop) {
4050

4151
module.exports = anticipateCall;
4252
module.exports.Anticipated = Anticipated;
43-
module.exports.noop = noop;
53+
module.exports.noop = noop;

0 commit comments

Comments
 (0)