Time to start another 3 hour chunk of focused work and deliberate practice on learning new things! I’m picking up where I left off yesterday on my mob coding app.
See all my posts and video blogs for this project here.
Goals for today
- Learn how to edit a gist with the GitHub API
- Get my mob coding app to create a new gist for the first connected user and then create a fork of that gist for every subsequent user when their turn is over, committing the latest version of the code to the forked gist.
- Try an alternate implementation where the server handles all of this instead of the clients.
Testing out the GitHub Gists API
I think I’ll start by repeating yesterday’s experiment but with the “gist” scope, just to confirm that it doesn’t work. I’m pretty sure it won’t work, but I want to double check now that I realized I was using the wrong URL the whole time yesterday. (I felt so stupid!)
I’ll make a temporary personal access token on GitHub with only the “gist” scope and retry the cURL command that worked for me yesterday:
curl -i -H ‘Authorization: token MY-TOKEN-WAS-HERE’ -d ‘{“description”: “a brand new test gist!”, “public”: true, “files”: {“newtest.md”: {“content”: “This is ANOTHER test gist made with the GitHub Gists API via a client-side POST request using AJAX!”}}}’ https://api.github.com/gists
It worked!!! OK, now I feel really stupid. Oh well, haha. Hopefully I’ll feel less stupid today. That’s the definition of progress, isn’t it?
Next up: how do I edit a Gist? (And does that automatically make a new commit to it that shows up in the revisions tab?) Here’s what I’ll try, following the API docs:
curl -i -H ‘Authorization: token MY-TOKEN-WAS-HERE’ -d ‘{“description”: “a brand new test gist!”, “public”: true, “files”: {“newtest.md”: {“content”: “Here is some updated content added to the beginning of this gist. This is ANOTHER test gist made with the GitHub Gists API via a client-side POST request using AJAX!”}}}’ https://api.github.com/gists/3b673bd17ec758312ae60e213564fd06
Success again! Today is off to a much better start so far, haha.
Next: how do I fork a gist? Time to test it out:
curl -i -H ‘Authorization: token MY-TOKEN-WAS-HERE’ -d ‘{“description”: “a brand new test gist!”, “public”: true, “files”: {“newtest.md”: {“content”: “Here is some updated content added to the beginning of this gist. This is ANOTHER test gist made with the GitHub Gists API via a client-side POST request using AJAX!”}}}’ https://api.github.com/gists/3b673bd17ec758312ae60e213564fd06/forks
Haha, I received message: "You cannot fork your own gist."
– oops! I’ll test it with a random public gist. OK, that worked just fine. I even edited the gist that I forked and that worked too! The edit shows up in the list of revisions, on top of the gist creator’s own revisions. Very cool.
Side note: Before I forget, I just wanted to mention that I realized one more very important thing to consider in deciding whether to do the GitHub API stuff on the clients or on the server: speed and bandwidth! Imagine if I do get to make a multiplexed version of this app that can handle multiple game rooms running concurrently, each with multiple users. That’s a lot for the server to handle!
Questions about making API requests from the client versus the server:
-
Does the client-side versus server-side implementation here make any difference in what I’d have to pay for bandwidth? (I’m pretty sure it matters for server processing power at least!)
-
Does every authenticated user have a separate rate limit for the GitHub API, or is the limit based on the URL from which GitHub receives the API requests?
Anyway, back to coding. Now that I’ve confirmed how to create, fork, and edit gists, all I have to do is integrate those API calls into my existing app! The tricky part might be making sure I get the logic flow right here. But it seems pretty straightforward now. In fact, it seems too easy now, which makes me feel very weary… Maybe I’m missing something?
Right now, my code creates a new gist for every user as soon as they log into the app. That’s not what I want. Instead, only the first user to log in for a given game session should create a brand new gist. (And later, this would be part of another workflow to let users choose from a list of suggested programming challenges, or maybe create their own, or maybe just fork off a previous session’s code.)
Here’s the problem: how would a client know if they’re the first client? Only the server has access to the list of all connected clients and the full state of the game. I guess I need to create yet another event, this time for the server to notify the client to create a new gist because they’re the first user to connect to this game.
Oh wait! But maybe this should happen at the end of their first turn, when they’ve presumably written some code already. Hmm… I feel like I need to get out some paper and sketch a flowchart for this. I’m too sleepy to see it in my head.
Drawing break!
I’m back now. That helped a bit. First I figured it would help me out later to write out the current workflow.
Current logic flow of the mob coding app
-
Client connects to server, establishes WebSocket connection. (Note: for now, client generates initial GitHub login link after getting the app’s client ID from the server via AJAX.)
-
User clicks login link, gets redirected to GitHub, and accepts permissions to authorize the app.
-
GitHub redirects user to
/github-auth
route with an initial code as a URL parameter. -
Server makes a POST request to GitHub to exchange that code (along with the client ID and client secret) for an access token.
-
As soon as it receives the response form GitHub, the server redirects the client back to the app homepage with the access token as a URL parameter (will replace this with headers later if possible).
- As soon as the client hits the homepage again, if the access token is present, the client:
- saves the access token locally (currently just as a global variable for now, even though that’s bad!),
- and makes an AJAX request to GitHub to get username and avatar.
- Upon recieving the response from GitHub, the client:
- updates the views,
- and emits the “loggedIn” event to pass the GitHub user data to the server.
- Upon receiving the “loggedIn” event, the server:
- adds the user’s data to its list of current players,
- sends the current game state to the new user (with events “editorChange”, “changeScroll”, and “changeCursor”),
- starts the turn timer (only if it wasn’t already running),
- and sends the events “turnChange” and “playerListChange” (in that particular order!) to every connected client.
- Upon receiving the events “editorChange”, “changeScroll”, “changeCursor”, “turnChange”, or “playerListChange”:
- the clients update the views and event listeners accordingly,
- and the server updates the game state and broadcasts the changes to all other clients.
- When a player disconnects, if that player was logged in, the server:
- updates the list of current players,
- resets the game and turns off the turn timer if no logged-in players are left,
- updates the turn to pass control to the next player if the current player was disconnected,
- and sends the events “turnChange” and “playerListChange” to all other clients.
New events and logic flow for saving version history
I think I probably need to add the following new events:
-
“createNewGist” - Sent from the server when starting the turn timer (first round of the game)
-
“newGistLink” - Sent from the client upon receiving a response from GitHub. The server should save the gist link or ID internally and also broadcast the “newGistLink” event back out to update the other clients, so they can update their views.
-
“editFirstGist” - Sent from the server when the first round of the game has ended. Upon receiving this event, the client should commit the code from round 1 into the empty gist and send another “newGistLink” event to the server.
-
“forkAndEditGist” - Sent from the server at the end of every subsequent round of the game. Upon receiving this event, the client should fork the current gist, commit the latest changes into it, and send another “newGistLink” event to the server.
Now it’s finally pretty clear where and when I need to fire off these events. (I may prove myself wrong once I start writing the actual code, though!)
Next question: how should I check if the turn that’s ending was the first turn? I have a couple options: I could start tracking how many rounds have been played. That could be a nice extra little feature to display, actually. Or I could just store a Boolean to serve as a flag for this. Or I could save the previous gist ID in addition to the current gist ID, and that way I could remove one of these events…
Oh! I think this will work: I’ll only send one event from the server at the end of every turn (I’ll just call it “forkGist”), and I’ll have the client save the current gist ID locally. Then upon receives the “forkGist” event, the client can check if the new gist ID matches the old gist ID and act accordingly.
I also just realized that there was one scenario – an edge case – that I had forgotten about! What if the current player repeats their turn again, like if they’re the only player in the game? I’d encounter an error with the GitHub API because a user can’t fork their own gist. The fork only needs to happen if the gist ID has changed (which only happens after forks, not after a user edits their own gist).
OK, sounds like problem solved! I’ll implement it that way and then test it.
New bug: I forgot that my timer function doesn’t have access to the socket object, so my server crashed!
Oh wait… before I address that bug, forget what I wrote in the previous couple of paragraphs. I can’t use the gist ID to check if it’s new or not. I had things out of order. I need to check if the player ID has changed between turns! That makes way more sense.
I also just realized that I shouldn’t need any of those extra events because I can just use the existing “turnChange” event for this! Saving version history is directly linked to the turn change mechanic, so it’s better to just use one event for all of that stuff.
Another new bug: On the first round of the game, my “turnChange” event is sending “gist” as a property within the turn object, but it has an undefined value, which is causing the client to throw an error. I thought I could just add an extra check to see if that property isn’t null, but apparently I don’t understand this as well as I thought I did! I’m going to test it real quick in my browser console…
OK, that was just another typo. It does work the way I thought, but I typed if (turnData.gist !== null)
when I meant to use the !=
operator instead, which checks for both null
and undefined
.
Anyway, it seems to be working now! But here’s another little issue: in order to properly test the forking feature, I need to log in to an alternate GitHub account! Hrmm.
Question: Is there a way to test the GitHub API with a fake user account, or even better, multiple fake user accounts? How do other developers streamline or automate testing a multi-user app that needs to interface with third-party APIs?
Maybe one quick search wouldn’t hurt…
Interesting! There are indeed lots of tools and libraries for creating fake APIs to test with client-side apps. Not sure how well a fake API can help me test the GitHub API, but I guess it’s better than nothing! Here’s a very popular one that I want to look at later: https://github.com/typicode/json-server
For now, I’ll just quickly make a second GitHub account and test it in another browser. Oh, even better! I realized I already have a second GitHub account that I haven’t used in ages. But I was able to reset my password and log in again.
Now let’s test this app with two separate GitHub users! Crossing my fingers…
It sort of works, but I’m getting this 422 error from GitHub, “Unprocessable Entity”. Not sure what that’s all about. But it’s lunch time or nap time or both, and this seems like a good stopping point, because I’m so close to finishing this feature, and this way I’ll know exactly what to search for when I return to this project again tomorrow. (I’m going to try my best not to use this project as an excuse to procrastinate on the other things I should be working on. As it is, I’ve already spent more time on this today than I planned to!)