### ----------------------------------------------------
### -- cro
### -- Licenses: Artistic-2.0
### -- Authors: Jonathan Worthington <jnthn@jnthn.net>
### -- File: docs/intro/spa-with-cro.md
### ----------------------------------------------------
### -- Chunk 2 of 2
 $incoming {
        supply whenever $tipsy.latest-tips -> $tip {
            emit to-json {
                WS_ACTION => True,
                action => {
                    type => 'LATEST_TIP',
                    id => $tip.id,
                    text => $tip.tip
                }
            }
        }
    }
}
```

The outer hash is the "envelope" for `redux-websocket-action`, which tells it
to pay attention to the message and dispatch its `action` property as a Redux
action.

Reload it in the browser. Add a tip. See it show up. Open a second tab with
the application. You'll see it shows the first tip. Add a second tip. Flip
back to the first tab, and you'll see that tip magically showed up there too.
We're now successfully sharing out the tips over the web socket. Hurrah! That
calls for a commit.

## Agree and disagree

We've spent a lot of time setting up the client-side infrastructure. Now it's
in place, however, adding further features is a far quicker process. Let's
round off the tutorial by implementing the Agree/Disagree feature (links to
let the user indicate agreement/disagreement with the tips), and showing the
list of top tips.

Let's start out in the very backend, by writing tests for these new features
in the business logic object. We'll add these tests to `t/tipsy.t`:

```
given $tipsy.latest-tips.head(3).list -> @tips {
    $tipsy.agree(@tips[0].id) for ^3;
    $tipsy.agree(@tips[1].id) for ^4;
    $tipsy.disagree(@tips[1].id) for ^10;
    $tipsy.agree(@tips[2].id) for ^2;
}
given $tipsy.top-tips.head(1).list[0] {
    is .[0].tip, 'Try the vanilla stout for sure',
        'Most agreeable tip first';
    is .[1].tip, 'The lamb kebabs are good!',
        'Next most agreeable tip second';
    is .[2].tip, 'Not so keen on the fish burrito!',
        'Least agreeable tip third';
}
throws-like { $tipsy.agree(99999) }, X::Tipsy::NoSuchId,
    'Correct exception on no such tip';
```

The first part just adds some agreement and disagreement on the tips (they are
sorted latest first). Next, we use the `top-tips` Supply and grab the first
thing it emits (or should emit, at least), which is the current ordering at
the point we tap the Supply. It checks that ordering is correct. A final test
checks that for unrecognized IDs, we throw an exception.

Here's the new exception type:

```
class X::Tipsy::NoSuchId is Exception {
    has $.id;
    method message() { "No tip with ID '$!id'" }
}
```

Next up, we'll give the `Tip` class some methods that produce a new version of
the `Tip` object with agreed or disagreed bumped one higher (we keep these
instances immutable, since we share them outside of our `monitor`):

```
class Tip {
    has Int $.id is required;
    has Str $.tip is required;
    has Int $.agreed = 0;
    has Int $.disagreed = 0;

    method agree() {
        self.clone(agreed => $!agreed + 1)
    }

    method disagree() {
        self.clone(disagreed => $!disagreed + 1)
    }
}
```

The `agree` and `disagree` methods are easy enough:

```
method agree(Int $tip-id --> Nil) {
    with %!tips-by-id{$tip-id} -> $tip-ref is rw {
        $tip-ref .= agree;
    }
    else {
        X::Tipsy::NoSuchId.new(id => $tip-id).throw;
    }
}

method disagree(Int $tip-id --> Nil) {
    with %!tips-by-id{$tip-id} -> $tip-ref is rw {
        $tip-ref .= disagree;
    }
    else {
        X::Tipsy::NoSuchId.new(id => $tip-id).throw;
    }
}
```

Hm, actually that's quite a bit of similarity. Let's use a private method to
factor it out:

```
method agree(Int $tip-id --> Nil) {
    self!with-tip: $tip-id, -> $tip-ref is rw {
        $tip-ref .= agree;
    }
}

method disagree(Int $tip-id --> Nil) {
    self!with-tip: $tip-id, -> $tip-ref is rw {
        $tip-ref .= disagree;
    }
}   
        
method !with-tip(Int $tip-id, &operation --> Nil) {
    with %!tips-by-id{$tip-id} -> $tip-ref is rw {
        operation($tip-ref)
    }
    else {
        X::Tipsy::NoSuchId.new(id => $tip-id).throw;
    }
}
```

Finally, here's our first attempt at the `top-tips` method:

```
method top-tips(--> Supply) { 
    my @top-tips = %!tips-by-id.values
        .sort({ .disagreed - .agreed })
        .head(50);
    supply {
        emit @top-tips;
    }
}
```

It passes the tests, and yet...something is missing. It's meant to emit an
updated sorted list of tips every time there's either a new tip or a tip is
agreed or disagreed with. Let's add some tests for that also:

```
my $new-tip-id;
react {
    whenever $tipsy.top-tips.skip(1).head(1) {
        is .[0].tip, 'Try the vanilla stout for sure',
            'After adding a tip, correct order (1)';
        is .[1].tip, 'The lamb kebabs are good!',
            'After adding a tip, correct order (2)';
        is .[2].tip, 'The pau bahji is super spicy',
            'After adding a tip, correct order (3)';
        is .[3].tip, 'Not so keen on the fish burrito!',
            'After adding a tip, correct order (4)';
        $new-tip-id = .[2].id;
    }
    $tipsy.add-tip('The pau bahji is super spicy');
}
ok $new-tip-id, 'New tip ID seen in top sorted tips';

react {
    whenever $tipsy.top-tips.skip(5).head(1) {
        is .[0].tip, 'The pau bahji is super spicy',
            'After agrees, order updated';
    }
    $tipsy.agree($new-tip-id) for ^5;
}
```

To have this update, we need a `Supplier` that we can emit on to indicate a
change to the agree/disagree counts.

```
has Supplier $!tip-change = Supplier.new;
```

And to emit on it (easy after the factoring out earlier):

```
method !with-tip(Int $tip-id, &operation --> Nil) {
    with %!tips-by-id{$tip-id} -> $tip-ref is rw {
        operation($tip-ref);
        start $!tip-change.emit($tip-ref<>);
    }
    else {
        X::Tipsy::NoSuchId.new(id => $tip-id).throw;
    }
}
```

Finally, to update the `top-tips` method. There's a few ways we could do this,
but the easiest way to be sure we're safe is to make sure the `Supply` keeps
its own local state, which it can protect (again, remember that since the
`supply` block is returned from, and lives beyond, the method, it is not going
to be protected by the monitor).

```
method top-tips(--> Supply) {
    my %initial-tips = %!tips-by-id;
    supply {
        my %current-tips = %initial-tips;
        sub emit-latest-sorted() {
            emit [%current-tips.values.sort({ .disagreed - .agreed }).head(50)]
        }
        whenever Supply.merge($!latest-tips.Supply, $!tip-change.Supply) {
            %current-tips{.id} = $_;
            emit-latest-sorted;
        }
        emit-latest-sorted;
    }
}
```

With that done, it's time to expose this functionality to the outside world by
updating `lib/Routes.rakumod`. Its job is to map the business logic onto HTTP. The
only thing we need to watch out for is to turn the "no such ID" exceptions
into the appropriate HTTP response, which is a 404 Not Found. Otherwise, we'd
send back 500 Internal Server Error, which is wrong because it's not the
server's fault that the client sent some bogus ID. Here goes:

```
post -> 'tips', Int $id, 'agree' {
    $tipsy.agree($id);
    response.status = 204;
    CATCH {
        when X::Tipsy::NoSuchId {
            not-found;
        }
    }
}

post -> 'tips', Int $id, 'disagree' {
    $tipsy.disagree($id);
    response.status = 204;
    CATCH {
        when X::Tipsy::NoSuchId {
            not-found;
        }
    }
}
```

The final step in the backend is to map the top tips out to another web
socket:

```
get -> 'top-tips' {
    web-socket -> $incoming {
        supply whenever $tipsy.top-tips -> @tips {
            emit to-json {
                WS_ACTION => True,
                action => {
                    type => 'UPDATE_TOP_TIPS',
                    tips => [@tips.map: -> $tip {
                        {
                            id => $tip.id,
                            text => $tip.tip,
                            agreed => $tip.agreed,
                            disagreed => $tip.disagreed
                        }
                    }]
                }
            }
        }
    }
}
```

Now for the frontend. In `frontend/actions.js`, we'll add three new action
type constants:

```
export const UPDATE_TOP_TIPS = 'UPDATE_TOP_TIPS';
export const AGREE = 'AGREE';
export const DISAGREE = 'DISAGREE';
```

And then two new action creators (`UPDATE_TOP_TIPS` doesn't need one, as it
comes from the server):

```
export function agree(id) {
    return dispatch => {
        $.ajax({
            url: '/tips/' + id + '/agree',
            type: 'POST',
            success: () => dispatch({ type: AGREE, id })
        });
    };
}
export function disagree(id) {
    return dispatch => {
        $.ajax({
            url: '/tips/' + id + '/disagree',
            type: 'POST',
            success: () => dispatch({ type: DISAGREE, id })
        });
    };
}
```

In the reducer, we'll tweak the initial state to have an empty set of top
tips:

```
const initialState = {
    tipText: '',
    latestTips: [],
    topTips: []
};
```

And then add an extra case statement to handle the new update action (we don't
need to do anything much on agree/disagree, though in a real application we'd
want to give some user feedback that their vote was counted):

```
case ActionTypes.UPDATE_TOP_TIPS:
    return {
        ...state,
        topTips: action.tips
    };
```

Next we need to get the second web socket connected up. That's just a little
refactor away in `frontend/index.js`:

```
['latest-tips', 'top-tips'].forEach(endpoint => {
    let host = window.location.host;
    let wsAction = new WSAction(store, 'ws://' + host + '/' + endpoint, {
        retryCount:3,
        reconnectInterval: 3
    });
    wsAction.start();
});
```

Next, we'll update the dispatch to props map, to include our new agree and
disagree actions:

```
function mapDispatch(dispatch) {
    return {
        onChangeTipText: text => dispatch(Actions.changeTipText(text)),
        onAddTip: text => dispatch(Actions.addTip()),
        onAgree: id => dispatch(Actions.agree(id)),
        onDisagree: id => dispatch(Actions.disagree(id)),
    };
}
```

Next up, we'll factor out showing a tip to a component that we can reuse in
both the latest tips and top tips, which includes links to agree or disagree:

```
var Tip = props => (
    <li>
        {props.text} [<a href="#" onClick={() => props.onAgree(props.id)}>Agree</a>]
        [<a href="#" onClick={() => props.onDisagree(props.id)}>Disagree</a>]
    </li>
);
```

And then showing a list of tips, with a heading:

```
var TipList = props => (
    <div>
        <h2>{props.heading}</h2>
        <ul>
        {props.tips.map(t => <Tip key={t.id} {...props} {...t} />)}
        </ul>
    </div>
);
```

Finally, the `App` component becomes:

```
var App = props => (
    <div>
        <SubmitTip tipText={props.tipText}
            onChangeTipText={props.onChangeTipText}
            onAddTip={props.onAddTip} />
        <TipList heading="Latest Tips" tips={props.latestTips}
            onAgree={props.onAgree} onDisagree={props.onDisagree} />
        <TipList heading="Top Tips" tips={props.topTips}
            onAgree={props.onAgree} onDisagree={props.onDisagree} />
    </div>
);
```

And there we have it. `npm run build`, refresh, and give it a spin. Clicking
agree or disagree in either list ends up with the sort order in the Top Tips
list changing to reflect the votes.

At last, we're done.

```
$ git commit -m "Add agree/disagree feature" .
[master 5d792bc] Add agree/disagree feature
 6 files changed, 188 insertions(+), 18 deletions(-)
```

## Summing up

In this tutorial we've gone from zero to reactive single page application. The
frontend is written in modern ES6 using React and Redux. The backend is in
Raku, using Cro. They communicate using both HTTP and web sockets. And both
declare their dependencies, so getting started for a new developer is just a
case of:

```
zef install --deps-only .
npm install .
npm run build
cro run
```

One again, the [code is available](https://github.com/croservices/sample-app-spa-react)
with the commit history described during this tutorial.


