Skip to main content
Realtime Multi-user Spreadsheets:

Multi-User Spreadsheets with ActionCable: Part 5

Episode 141 · September 16, 2016

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



Earn a free month

The next bit, and we finally made it to the next bit, which is the most crucial piece of this, is that you don't want two users to be able to update the cell at the same time, and figure out which one you want to keep, because you're going to delete somebody's data, so if you don't have a lock on that when you're doing this concurrent work, and you're trying to modify the same variable, you can run into some really weird problems. Implementing locks, basically says: Whoever gets here first gets to lock on it, and the other person has to wait, or fails. You can set that up so that whoever gets their first does their change, and then the other person can be set up to either not be allowed to make that change, or they can go make their change after it's completed, so you can kind of wait for that. We'll take a look and see what this does, but it looks like we're going to be using some syntax here, where we're going to set up a lock. I'm not entirely sure, with this syntax for NoBrainer, how this works. But it looks like it's trying to create some sort of transaction type thing, so we get a location, and then, we're going to replace the row-- I'm not entirely sure what all this does, but that probably is in the NoBrainer documentation, to learn more about the locks, or it's in RethinkDB, so they have a bit of a description here.

The complex looking RethinkDB query in the lock_cell method, in one operation: looks up the cell, checks if it's locked, and if not locked sets the lock to the id of the user. Since we are setting the lock in the SpreadsheetCell document that all users are subscribed to, all users will receive any updates in lock statuses of cells

On the client side, we injected a monkey patch to HandsOnTable to allow us to intercept the beginEditing and finishEditing functions by setting acquireEditLock and releaseEditLock properties.

This is cool, you can select the cell and it doesn't matter, but when you hit enter, and go type into it, you begin editing, so that will lock that cell on the server side, and as you type, you can type as much as you want, take your time, no one else will modify this, and once you're done and you moved away from it, it will unlock that, which is pretty cool. This is going to call lock_cell and unlock_cell functions on the ActionCable channel, for that location, and then call these callbacks. It's pretty cool. We'll grab the code for this from the GitHub repository, just so that we have the correct version of that, and then it looks like there's a little bit of CSS that they added, so we'll be looking at this one that implements the locks and take a look at the code changes for that. So really it looks like we have to implement a couple of these lock_cell and unlock_cell methods in our The reason why this is going in here is because that is actually where we store the location that the user is currenly on, so this is the best place for us to look up their current location, so it makes sense for us to go through the user in order to lock the cell and we'll do that accordingly. We have these lock_cell and unlock_cell methods that you can use in order to delegate to the server side in order to set up the locking and the unlocking. They just set up a location, as simple as that. We are updating the, and we're setting the location as a row and a column here. We're taking the "r" and "c" from the location and turning it to just an array with two values that are the row and column in order, as opposed to having them named in like a hash. We're also going into, and underneath active_user we're setting current_user

active_user: {}

current_user: id: 'unknown'

#rest of code 

setup: () -> 
    @selected_cells = []+ 
        @selected_cells = []+
        @cell_lock_callback = {}
        #more code 

        afterRenderer: () => @render_selected_cells()

Then, we're going to add acquireEditLock and releaseEditLock in there as well, we're going to change the update cell to set the location as an object with the "R" and "C", row and column values for the array, and then we're going to call the update.lock, and then there is this Monkey patch for the HandsOnTable, basically modifies the code for it, so that it will allow us to get two new events, and those two event will be the begin editing and finish editing, so we can listen to those in order to set up our locks. I'm actually going to go and grab the code from this and we'll just paste it all in rather than trying to make our changes one by one, this will be a little bite easier for the video. We'll have all of that, and what else has changed since then? We have this big change, and we have our active user's channel now has the lock_cell and unlock_cell methods, so we have these two, paste those in (in active_users_channel.rb). This is basically going to say: This has the options in here to keep track of weather or not the lock cell was successful or not, so we have the "lock acquired", "lock refused" and the unknown result for lock, and this will allow us to keep track of that, and we'll see that in our rails logs when someone attempts to lock a cell.

Here we can try and see, we don't have to do this refactor, but we can try to see if this upsert works. This will probably work because some of that location stuff has been modified, so my guess is that this might actually be successful this time, because they´ve gone in kind of like done location as row and column, but then sometimes it´s like an object with an R and C values, so possibly, the mixture of that was causing it to fail before, so I´m curious about that one, so we´ll find out, and we can go and update these spreadsheet cells channel with that code, that cleans that up a little bit, and we can also go modify our spreadsheet cell to set the primary key on the spreadsheet cell, as well as the lock on there spreadshet_cell.rb will have those, and remember the lock is just the user id, so we know who has the lock. It's not a true or false or anything if it's locked or not, it's actually that you get that as a byproduct and having either nil or a value, but you also get to know who owns the lock by saving that value as the user id. Now we can open up the user model, and we can add in that before_destroy and lock_cell methods, and the reason that you have before_destroy is if your user is editing a cell and it has a lock on it and they close the tab before they finish, then you want to make sure that it's not locked permanently forever and no one can ever edit that ever again because that user has been deleted. That would be bad, and so you have to take care of that in order to make sure that that situation does not happen, so that's why they do that and the complicated locking code that I still don't totally understand, but we'll see how it goes, so what I'm going to do is clear the database here, so I'm going to delete these two tables, and just start from scratch, so we'll refresh the page here and we'll refresh the page here. We have some JavaScript errors, HandsOnTable is not defined. That might be application.html.erb, we might want to move our yield to the top because I believe that code is not-- This MultiEditorPatch was not actually being injected inside of the DOM ready function, so it was running as soon as it was possible to run, which was before HandsOnTable had finally loaded, so this ran quickly, and then it failed, because HandsOnTable hadn't loaded yet. If you move that inject into here, that should be good enough to keep the yield underneath your application stuff. But that's one of those situations where the order of your JavaScript and the execution of it is very crucial to how your code works, so you need to make sure that your libraries that you're overriding and modyfing are already loaded so that you can successfully override them. Or you could create your patch for it and create the inject later once everything else is loaded, and that's why these jQuery load methods are pretty common to see, because for situations like that, you want to make sure it's up and running before you execute that so that you don't get errors based upon things just running too fast. This is cool, we can say "a". Well this is not letting me edit any of these now, so that's a problem. It thinks like all of these are locked, or something, so that's not ideal. That's not working totally as expected. But we're making progress. We're almost done, we have to figure out what's up with that. We have our tables, they've been cleared out. We should have the exact same code that everything else has, so let's take a look at our rails logs and see if there was any information as to the lock. We see the lock refused to that user on cell zero, zero. The lock is being refused, and we're not being able to acquire the lock. The user.rb file. This code is not returning a successful lock for us. So active_users_channel.rb was the one to print that out. So it says lock refused, result unchanged, result inserted. The lock_cell is unsuccessful, and it was unchanged. I'm not entirely sure if I know how that works in order to kind of debug that. I think we've got all of the code necessary in here for that. It's going to be a little bit tricky to figure out what that issue is.

I did a little bit of debugging, and couldn't really figure out what was wrong with the edit stuff, so I cloned their repository, I made sure that all my files were the same cloned their repository and I got it loaded up, they've got some styles on it. We'll see if this works or not. It does let me edit, this is cool.

So when I double click on one of these, I can type the letter d, and you should have seen the user's data go over there, but you don't unfortunately. So even their example is not working correctly for me. That's interesting, but the locks do work, and that is the thing that I was having trouble with, so I'm curious as to what's missing from their example, because I'm also having the same issues with theirs. While this demo is pretty cool, you can tell that it's a little funky here and there, and a lot of the stuff that they mentioned at the bottom of their article that there are some stuff that they have added to the ruby driver and the ORM to make this seamless integration, and they're currently in a gem. The NoBrainer streams gem, and this hopefully gets merged in, but currently it's a little bit-- it's definitely an awesome situation and set up, but it's a little bit finiky here and there, and doesn't totally work consistently for me at least. I'm curious to see what happens, I'm also curious if their ActionCable actually causes issues with the random users that I was seeing pop up in my examples. I don't know what caused that. I know that the users were reporting some of the tabs were open for a while and maybe weren't interactive with or something, and maybe those went dormant, and lost their connection, and maybe the reconnect caused them to show up as a duplicate as a new user. Maybe that's what what was happening and there was a reconnect between the ActionCable server or something, which would, of course make sense, because then we would create a new user and stream from there, and so maybe that was it. I'm not entirely sure. At a high level, this is almost fully functional demo that it works as you-- It's updating now, as we're talking about it. I'm not sure what happened there. The first time that I booted this, it definitely did not work, and now it does. There you go. It is functional for me now. This may be a little bit finiky of a project, but it is really cool, and it is very interesting to see what you can do with the RethinkDB database change logs or feeds that you can get from it. I am really impressed by that, and I hope that this becomes a lot more stable, and I will probably be trying it out in the future as well.

Well, this wasn't quite the smoothest tutorial I've ever gone through, it was quite a fun one, and we built a prototype of Google Spreadsheets in like an hour and a half, so that's definitely not bad. I'm pretty impressed with this, but RethinkDB seems like a really cool database, I don't know much about it, aside from what we did here, and really the only feature that I know that is cool where you need to it, is the real time feeds. That seems like a pretty awesome feature, so yeah. That was an intro to building multiuser spreadshees. Huge thanks to Fusion for writing the tutorial, building the gem, playing with all this stuff. Publishing that, they're awesome. Thanks for the tutorial, thanks for watching. If you want to see me go through some other tutorials or something, send me links to them in the comments and I'll record some videos like this if you enjoyed them. Until next time, I will talk to you later. Peace

Transcript written by Miguel


Subscribe to the newsletter

Join 31,152+ developers who get early access to new screencasts, articles, guides, updates, and more.

    By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

    More of a social being? We're also on Twitter and YouTube.