Skip to main content

2 Multi-User Spreadsheets with ActionCable: Part 4

Episode 140 · September 15, 2016

Build a reactive multi-user spreadsheet web app with ActionCable and RethinkDB



Now we're actually going to be transmitting the value, so if you type in the letter A into one of these, we want to be able to transmit that text over to everybody else so your text is kept in sync, and then the next thing is we'll be doing locking on the right, so we'll be basically setting it up so that if someone types, whoever does that first will lock it temporarily in order for nobody to also try to update that same cell and things getting out of sync. Whoever types there first will have the lock, and they will be able to be the one whou can write in that cell, and when they're done, it will unlock the cell and let someone else type in there, but in order to prevent conflicts and not having to deal with the resolution of that, like Git commit merges, when those fail, that can be very painful, and so it's the same situation here, but we're just simply not going to allow it. That's like being able to, if you were writing in Git, if you could never write a commit that would be out of date with someone else's code. That's kind of what the locks will be doing, and we'll just be implementing those in here. We're just going to be creating a new model for each spreadsheet cell, and we'll just store the value in the spreadsheet cell record, as well as the location, so we won't actually store the entire spreadsheet, we're only going to be storing individual cells, and the display of it will come down to the JavaScript, and so if yo have giant one with a hundred columns or a thousand rows or something like that, then this is only going to save the data where you've actually typed it, it won't store a thousand times a hundred records, and have empty values in all of those. This will only store the ones that actually have data in them, which is really good for efficiency, because there's no reason for you to create all those records if they're just empty anyways. Let's go create this file, app/models/spreadsheet_cell.rb, and we'll create a new channel called app/channels/spreadsheet_cells_channels.rb, and I've changed the name of that a little bit so spreadsheet is one word, no problem, we'll just do that. This is going to monitor the spreadsheets cells table, it's going to load up everything that we've got when you initialize it, so that's going to make sure that you always have when you start you always get all of the data in the spreadsheet, and then you have the set cell value which is going to create, and it has an upsert method, which is to update or insert the record, so basically if it exists, we want to update the record, if it doesn't exist we want to insert the record, and so upsert basically handles both situations for us, and is a nice little method. It's like doing in ActiveRecord a where, and then saying: first or create, or first or initialize and then calling save on that. That's effectively what this is doing, so it's looking it up, and then it's initializing a new one and then it's making the changes, and then it's always saving it weather or not it's a new record or an old one. This is all we need from the channel side on the server, and then we're also going to be creating our JavaScript version of this, which will go into app/assets/javascripts/channels/ so we'll paste that in there, and this is really just going to say: Any time we get spreadsheet cell data over, we want to update that cells value with the new value, and we have the set cell value, so that when you type into the spreadsheet, we will take the callback from HandsOnTable, we will grab the value, and then we will send it over to the server, which will then go save it to the database and then broadcast it back to everybody, and then they will receive it here, which will go update their HandsOnTable, and so we get this full whole cycle so that everybody stays in sync. That is our client side stuff for that piece, and then we need to also update the spreadsheet file again, and I'm going to go into the ActionCable demo. We're going to take a look at the commit log here and see what we've got. We did implementing communicating cell selection, so we need to do the cell values now. Let's make sure that we have the matching code for all of this. This, this time, selected cells on the @platform here on the active users. Let's change that. It looks like that's one of the cases where the tutorial wasn't totally up to date at that point in time, so they made that change, so that changed a little bit. We have our lines 58, don't quite match up here, because there's comments at the top, so let's grab this file all in it's entirety, I did not copy those before, but this will be helpful just so we can match up line numbers. Let's go back and take a look at what we've got, so this after change is new. This afterChange is new, so when one of the cells data changes, we're going to get the changes and the source, and if the source was not remote, and there are changes, we're going through each of those changes and setting that server side. We're going to go send it to the server, our spreadsheet is already up to date, and active, so we don't have to do anything, except for sending it to the server in order to get saved permanently and rethink and broadcast out to everybody else. This is as simple as that is, I don't actually know what source != 'remote' is, I'm guessing that that is to help us keep track of which ones were done by other people, and not ourselves, so this is making sure that it's only our changes that we made, but that's just a guess, it sounds right, but we'll see. That will just go call our spreadsheet cells, channel, which we did right here, which will send the location and the value over to the server for set cell value, and we'll go loop through each of those changes and make sure that it has all of those. This is instead of the location, well the location is just a hash with an R and a C, which is row and column, and it will send over those two values, and then the actual content, the value itself, and that's that. That's pretty simple, and it looks like we also have an update cell method now, and that method is down here, and so when we receive one of those new cells, we grab the location, we grab the value and we tell that the JavaScript HandsOnTable to set the data at that cell, and we set the source as remote, so as I speculated, this is data coming in from outside, but that's the trick. If you type into a cell, you are changing the data, and it knows, and it says: Let's call the after change call back, and that's good, but when data comes in from the server and websockets update that cell, that's also technically a change, but you didn't do it. Someone else did it, and so we don't actually want to send that back over to the server because you get this infinite loop, and that would be awful, because you would be sending the data over constantly and it would never stop and that would not be good. We're checking that source in order to keep track of who did it, so we know when to broadcast and when not to. We only want to broadcast when we made the change, and not when the change came from the remote. That is why you were doing the remote there, in order to save that. Here's where our change comes back in the active users channel. We made that change for-- all of this stuff is going back to the user.update, so it calls selected cells now because our JavaScript got updated to set the selected cells, and we now have a new spreadsheets cell channel.

in app/channels/spread_sheet_cells_channel.rb, set_cell_value here is totally different here than it was there, so let's grab this version, because that looks a lot better than what we've got. That's probably more correct than what we have, when we set the cell value, we are doing a look-- Oh, it's doing the same thing, so it's basically saying: Let's do the upsert that we had before. We're just going to go look up the location, and this example here is just doing the stuff manually, and this is probably getting refactored the upsert later in a commit? Is my guess. Because this is just saying: If we find one, if we look up the first record, and there is one, then we'll update it, and if there's not, then we'll just go create one. My guess is that upsert is what they permanently use, and we can find out by taking a look at their next commit here. There's a refactor, and let's look for upsert here. There we go, there's the upsert, we'll leave it as the upsert and we won't use that one, but let's hop back into the tutorial and take a look at what we've got. Let's take a look at the actual code here. I've refreshed this page, and now we have some things have gotten a little bit out of sync with users again, so I think that it's that the disconnect or unsubscribe isn't firing appropriately for the ActionCable channels, so we're getting these users that are sort of duplicates, that didn't stick around. That's one of those issues that we may have to figure out, where is that a bug at and how do we handle that. Do they just drop off like if a user disconnected, will eventually ActionCable pick up that that websocket connection is dead and then call unsubscribe? It appears to be a bug somewhere in there that we may or may not be able to fix with what we're doing, but we'll have to find out.

This looks to be like it's working reasonably well. We can type into the cells, and we should be able to see that once we hit enter, we should see that that data comes across, and it did not, but let's see what happens when I type in here, and we are looking at the websocket connection, so let's do this. Let's open this up, we're now connected to the two channels, let's type into this box, let's see that the spreadsheet cell channel sets the value, and the value is A, so that worked correctly, and let's see what this one sees, and let's set it to B, and see if this updates. So it did not update, it did not update across the pages, so that's unfortunate, so we still have some bug in here that's not working appropriately

Bug gets fixed off camera

Bug came from me changing the "S" on spreadsheet cells channel, and the names of course did not match up, which meant that the client was not properly connecting to the server side aspect of everything, which should, if we refresh our pages now, we should be able to get this to work in theory... It didn't quite work, but that certainly was one of those things because I did make that spreadsheet_cells_channels.rb and this text should match exactly with this one. We need to make sure that those are kept accordingly, and this shouldn't matter as much, it's just a variable that you set on the client side that you can access that stuff with, so you don't need to change this in order to keep it in sync with that, it's the text here that needs to stay there. What we see is that now we do confirm the subscription, which is good, but we actually didn't receive any of the initial data, which should be saved in the database, so we should see this character here, but we're not seeing that, so the spreadsheets cells themselves may not be saving correctly or something in the database, so we should be able to try to figure out what the SpreadsheetCell.all is doing. What I'm going to try to do is open up rails console here and see if we have any records in our database, and we do not, so that's interesting, we should have some spreadsheet cells, because they were supposedly inserting those records into the database, and maybe they have not been. It looks like when-- let's go ahead and type into a cell, and let's say: b should be there. Let's make sure that something gets sent over, so we sent over data on the spreadsheets cells channels, the value b and the row and column of 1, 1. So you go zero, one, zero, one and the value of b, and that will get sent over to the spreadsheet cells channel. The green ones are messages that you send I believe, and so this other tab should have received that. We won't be able to look that up, so we'll open up the ActionCable stuff here. When it initializes, it should receive the spreadsheet cells value, so it initally gets the current user value, confirms the subscriptions, and then gets the active user's channel initial value, but we're not getting any initial values from the client. That's interesting, and that seems to point to me that we're not getting any writes to the database when the spreadsheet cells are sent over, so that's pretty interesting, that that is not working, so we now have kind of narrowed this down, and here we go. *Could not execute command from command, message, spreadsheets data. Run time error, could not find a uniqueness validator for the following keys location and value. There's something here that we're running into, and it looks to me that this is what is failing, and it's not saving this to the database. That appears to be our issue, and let's just go and do the opposite of the upsert, and let's just save it as the manual version of this, so let's flip this around to their example, and this is in the spreadsheet channel, so I'll replace that, not do the upsert, we'll do it the manual way for now, and see what happens. If I type the letter "a" here, we should see no errors down here. "Could not execute command" again, though. I believe that that is "Unable to load autoload constant SpreadsheetCellsChannel", and that would be because I did the lower case thing there, so that will fix that, and we can check our logs for anymore errors. This time we'll get the yellow, which means that it was a create, and the green, looks to have done our query on it. That does seem to have work now, and if we type the three into this one, we can go into the other one, and we can see that those characters are now showing up. What we ran into was some weird thing with the upsert, and validations, but I'm not entirely sure, because we didn't write any validations, but this manual way, instead of doing upsert is working, what we'll do is we'll take a look at their code that they refactored and see if what we get then works at all. As you can see, the more that I keep doing this, the more random disconnects that fail to remove the user, so this isn't quite the best situation that you can have. You don't want to have all these failed users to still exist in the database, but that's kind of just the issue with what we've got right now

Transcript written by Miguel