Skip to content
This repository has been archived by the owner on Jun 17, 2021. It is now read-only.

Rewrite middlewares to sagas #117

Conversation

mik639
Copy link
Collaborator

@mik639 mik639 commented Jul 29, 2018

Related Issue:
As was discussed in #97 I have rewrite custom middlewares with sagas, last one task.middleware should be rewritten by @superhawk610 (as he asked)

Summary:

Screenshots/GIFs:

Copy link
Owner

@joshwcomeau joshwcomeau left a comment

Choose a reason for hiding this comment

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

Hi @mik639, thanks so much for this :D

I have a concern about how it'll merge with #103 - both PRs add a different dependency.saga.js. @superhawk610's PR is based around having a queue system to group dependency installations, and so I think we can probably use that version of the file, with any changes that @mik639 (as our local Redux Saga expert) suggests?


A misc note: Could you delete import-project.middleware.js if it's no longer being used?


For the most part this code looks great, but I had a few requested tweaks. Please let me know if you have any questions, or if there's anything I can do to facilitate you and @superhawk610 working together to get both your changes merged

@@ -0,0 +1,160 @@
import { call, put, cancel, select, takeEvery } from 'redux-saga/effects';
Copy link
Owner

Choose a reason for hiding this comment

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

Would it be possible to add Flow typing to this file?

I'm not as worried about dependency.saga.js for two reasons:

If you're not comfortable/familiar with Flow, or if you just don't have the time to spare right now, feel free to just add a // TODO: Add Flow to the top of the file, and I or someone else will try to tackle it shortly after merging.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure, added

// In the future, maybe I can attach a suffix like `-copy`, but for
// now I'll just reject it outright.
const isAlredyExist = yield select(getInternalProjectById, projectId);
if (isAlredyExist) throw new Error('project-name-already-exists');
Copy link
Owner

Choose a reason for hiding this comment

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

Nit: for consistency with the rest of the codebase, can we avoid 1-line conditionals?

if (isAlreadyExist) {
  throw new Error('project-name-already-exists');
}

Copy link
Collaborator Author

@mik639 mik639 Jul 30, 2018

Choose a reason for hiding this comment

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

Sure, maybe we can use eslint for preventing mistakes like this ;)

// support... but for now, I'm prioritizing an A+ experience for
// supported project types.
const type = yield call(inferProjectType, json);
if (!type) throw new Error('unsupported-project-type');
Copy link
Owner

Choose a reason for hiding this comment

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

(same comment here about 1-line conditionals)

// Only a single path should be selected
const [path] = paths;
yield put(importExistingProjectStart(path));
}
Copy link
Owner

Choose a reason for hiding this comment

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

Good extracted function 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks

const saga = handlePathInput(paths);
expect(saga.next().value).toEqual(
put(importExistingProjectStart('path/to/first'))
);
Copy link
Owner

Choose a reason for hiding this comment

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

I'm not super familiar with saga testing - wouldn't we want to test that it's equal to a plain object?

const expectedValue = importExistingProjectStart('path/to/first');
// { type: IMPORT_EXISTING_PROJECT_START, path: 'path/to/first' }
 
expect(saga.next().value).toEqual(expectedValue);

In other words, shouldn't we check importExistingProjectStart() instead of put(importExistingProjectStart())?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we need to check that saga will dispatch action(effect put), in other words, we expect

expect(saga.next().value).toEqual(put({ type: IMPORT_EXISTING_PROJECT_START, path: 'path/to/first' }));

Copy link
Collaborator

Choose a reason for hiding this comment

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

@joshwcomeau that's one of the really cool features of redux-saga that makes testing so consistent - put(/*...*/) doesn't actually dispatch an action, it describes dispatching the action, so all you have to do when testing is check that the return from the saga matches the expected description.

It's a unique pattern but it has a handful of unique benefits - you can read more here.

Copy link
Owner

Choose a reason for hiding this comment

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

ahhh, right. Awesome!

});
});

test('showImportDialog saga', () => {
Copy link
Owner

Choose a reason for hiding this comment

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

Could we use it instead of test everywhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, we can, I didn't use it here, only because it looks not so semantic :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

it('runs showImportDialog saga correctly'?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I prefer to move it in its own describe section because it'll help to keep proper hierarchy in tests result

});

it('should import project', () => {
global.Date = { now: () => 1532809641976 };
Copy link
Owner

Choose a reason for hiding this comment

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

ah, right, the createdAt is done impurely within the saga, same as it was within the middleware.

This solution worries me, though (it won't be "unmocked" when this test file finishes, right? So this could affect subsequent tests). We could have an afterAll that restores it, but I wonder if there's a better way...

Maybe importProject could take a second optional argument, getCurrentTime, which could default to Date.now. In the test you could provide a mock function like () => 1532809641976.

What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You're right. I can replace it with:

const spyOnDate = jest.spyOn(Date, 'now');
spyOnDate.mockReturnValue(1532809641976);

// test

spyOnDate.mockRestore()

in this case mocked value will affect only one testcase

});

describe('inferProjectType helper', () => {
it('should return null if field "dependencies" not exist in json', () => {
Copy link
Owner

Choose a reason for hiding this comment

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

Thanks for checking this 👍

expect(inferProjectType(json)).toBe('create-react-app');
});

it('should return "create-react-app" if json have dependency eslint-config-react-app', () => {
Copy link
Owner

Choose a reason for hiding this comment

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

Thanks for checking this as well! Great coverage 👍

yield takeEvery(ADD_DEPENDENCY_START, addDependency);
yield takeEvery(UPDATE_DEPENDENCY_START, updateDependency);
yield takeEvery(DELETE_DEPENDENCY_START, deleteDependency);
}
Copy link
Owner

Choose a reason for hiding this comment

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

Note: I didn't give this file a thorough review since I believe #103 implements the same thing but also takes the queue into account. Would be great to get your feedback on that approach, @mik639!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ok I'll take a look

@mik639
Copy link
Collaborator Author

mik639 commented Jul 30, 2018

In the situation when we have a few pull requests with rewritten middlewares, I suggest to create a separate branch for merging and testing them, instead of merging to master. What do you think, @joshwcomeau ?

@superhawk610
Copy link
Collaborator

@mik639 That's a good idea, you should create a sagas branch or similar on the main fork and we can consolidate our codebase there. As @joshwcomeau said, I have some existing work in #103 that I'll need to integrate with yours, as well as my tasks middleware implementation in #111 (feel free to review when you get a chance).

My main goal tonight is to get some ground work done on #77, but then I'll be turning my full attention to this no later than tomorrow.

Are you developing on Mac or Windows? I'm working from Windows so I have a few Windows-specific lines throughout, I figure that's fine since we'll be merging in Windows support to master sooner or later.

First order of business once I get some progress made on #77 is to review this.

@joshwcomeau joshwcomeau mentioned this pull request Aug 1, 2018
@joshwcomeau
Copy link
Owner

@mik639 ++, like the idea of a working branch to merge and test everything 👍

@mik639 mik639 changed the base branch from master to move-middleware-to-sagas August 1, 2018 14:12
@mik639
Copy link
Collaborator Author

mik639 commented Aug 1, 2018

@superhawk610 @joshwcomeau I'm created a branch move-middleware-to-sagas and fixed comments for this pr. So let's define the order of merging #103 #111 and #117 😺

@superhawk610
Copy link
Collaborator

@mik639 I think you should merge yours (#117) first, since it contains the bulk of the work, and then I will merge #103 and #111 in that order to make sure that they conform to your structure.

I haven't written tests for task.middleware but I will do that later today or tomorrow. I'll get my review for this PR up in a few hours.

@joshwcomeau
Copy link
Owner

joshwcomeau commented Aug 1, 2018

Yay, so happy that y'all are working towards getting this stuff landed. I think we'll pen a release once this is in, since having the queue and a totally-reworked side effects layer seems like a good thing to capture in a launch :D In addition to all the great stuff folks have been merging in over the last week or two.

EDIT: Er, right, we'll also need the UI for the queue, which isn't covered in #103. I got a little ahead of myself 😅. Certainly when the UI is in, though!

@superhawk610
Copy link
Collaborator

superhawk610 commented Aug 1, 2018

@mik639 I'm working on reviewing this, but now I'm running into the issue @joshwcomeau described here. I'm going to try switching it over to yarn temporarily to see if that fixes it. Darn Windows 😤

Alright, I've got a Windows version of this PR up at superhawk610/guppy if anybody else wants to follow along. Saga functionality is 💯.

Copy link
Collaborator

@superhawk610 superhawk610 left a comment

Choose a reason for hiding this comment

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

Requesting changes for a minor spelling change. I'd also like to agree on a rule for consistency - yield call([object, 'method']) vs. yield call(object.method).

);
yield put(addDependencyFinish(projectId, dependency));
} catch (err) {
yield call([console, 'error'], 'Failed to install dependency', err);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm unfamiliar with redux-saga convention when using call with an object property. Why use

yield call([console, 'error'], /*...*/);

instead of

yield call(console.error, /*...*/);

? When implementing similar methods, I'm not sure which syntax to use (eg - ipcRenderer.send and childProcess.spawn).

switch (err.message) {
case 'project-name-already-exists': {
yield call(
dialog.showErrorBox,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here's an example of what I mentioned in my previous comment - why use dialog.showErrorBox over [dialog, 'showErrorBox']?

// In the future, maybe I can attach a suffix like `-copy`, but for
// now I'll just reject it outright.
const isAlredyExist = yield select(getInternalProjectById, projectId);
if (isAlredyExist) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should be either isAlreadyExist or alreadyExists.

Copy link
Owner

Choose a reason for hiding this comment

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

I vote for "alreadyExists", flows better IMO :)

const saga = handlePathInput(paths);
expect(saga.next().value).toEqual(
put(importExistingProjectStart('path/to/first'))
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

@joshwcomeau that's one of the really cool features of redux-saga that makes testing so consistent - put(/*...*/) doesn't actually dispatch an action, it describes dispatching the action, so all you have to do when testing is check that the return from the saga matches the expected description.

It's a unique pattern but it has a handful of unique benefits - you can read more here.

@mik639
Copy link
Collaborator Author

mik639 commented Aug 2, 2018

@superhawk610 unfortunately flow-typed redux-saga currently have the issue with describing call effect, so for avoiding flow error, we need to use call([console, console.error], error) instead of call([console, 'error'], error) or adding ignore comment in places like this 😞

@superhawk610
Copy link
Collaborator

So these three are functionally equivalent - here's a quick test:

const { call } = require('redux-saga/effects')

console.log(
  JSON.stringify(call([console, 'error'], 'foo')) + '\n' +
  JSON.stringify(call([console, console.error], 'foo')) + '\n' +
  JSON.stringify(call(console.error, 'foo'))
)

// {"@@redux-saga/IO":true,"CALL":{"context":{},"args":["foo"]}}
// {"@@redux-saga/IO":true,"CALL":{"context":{},"args":["foo"]}}
// {"@@redux-saga/IO":true,"CALL":{"context":null,"args":["foo"]}}

@mik639 None of these patterns throw an error with Flow for me

yield call([console, 'error'], 'Failed to install dependency', err);
yield call([console, console.error], 'Failed to install dependency', err);
yield call(console.error, 'Failed to install dependency', err);

flow version: 0.73.0
redux-saga version: 0.16.0

@mik639
Copy link
Collaborator Author

mik639 commented Aug 2, 2018

@superhawk610 first two - yes, there are same, but third as you can see has no context.

Redux-saga itself has no flow types, it should be typed with libdef.
In my pr I have added libdef for saga from https://github.com/flow-typed/flow-typed

@superhawk610
Copy link
Collaborator

Ah, that makes more sense. I didn't have those flow-type definitions pulled on my work machine, just at home, so I didn't catch it.

Do you want to switch all of the call() functions to use call(object, object.function) syntax then?

@mik639
Copy link
Collaborator Author

mik639 commented Aug 6, 2018

@superhawk610 @joshwcomeau Sorry for delay, I have fixed comments, and have updated pr

@superhawk610
Copy link
Collaborator

@mik639 Very nice fix with destructuring electron.remote.dialog. @joshwcomeau I'm going to go ahead and merge this into the feature branch and open a separate PR later this afternoon with my changes, if you have any further comments just leave them there and we will address them.

@superhawk610 superhawk610 merged commit a03b326 into joshwcomeau:move-middleware-to-sagas Aug 6, 2018
@joshwcomeau
Copy link
Owner

Thanks for merging @superhawk610! Took a quick glance, looks great to me!

superhawk610 pushed a commit that referenced this pull request Aug 18, 2018
* Replace middlewares with sagas

* Remove import-project middleeare from prod store configuration

* Add flow-typed redux-saga, review fixes

* review fixes
superhawk610 pushed a commit that referenced this pull request Aug 23, 2018
* Replace middlewares with sagas

* Remove import-project middleeare from prod store configuration

* Add flow-typed redux-saga, review fixes

* review fixes
superhawk610 pushed a commit that referenced this pull request Aug 30, 2018
joshwcomeau pushed a commit that referenced this pull request Aug 30, 2018
* Rewrite middlewares to sagas (#117)

* Tasks middleware to saga (#122)

* pass test suite
- fix getInternalProjectById in projects.reducer test
- move projects.reducer test up a directory
- add missing assertions in create-project.service test
- remove middlewares directory

* review tweaks

* bring in line with existing sagas
- add note about refactoring stderr handling

* update test to use new name (isDevServerFail)

* Make some small tweaks before merging

* Fix ESLint errors
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants