Introducing Code-Replay, and how we built it
Logicboard is a collaborative code editor for conducting technical interviews. We believe the most important aspect of a technical interview is to understand a candidate’s thought process. Having a working solution to a problem during an interview would be ideal, but there’s never really a single solution to any problem.
At the end of an interview, what I usually take away is some code that a candidate wrote and a few key points that I remember. But it’s hard to convey a candidate’s thought process to the rest of the team based on a few lines of code.
But what if I could replay the entire coding session to the rest of the team? We thought that would give everyone a better context and help understand the thought process behind a solution. This is in fact one of the top feedback we got from our customers.
Today we’re rolling out Code-Replay, a feature that lets you replay the entire interview session line-by-line. It’s like having the ability to undo/redo even after the interview has ended.
Here’s Code-Replay in action:
You can also check out the live demo here
Let’s take look at how we built this
Logicboard uses Firebase+Firepad to power the code editor. Firepad uses Firebase Realtime Database to keep a versioned history of a document.
Firebase
Let’s deep dive in to how this history is stored in Firebase. Here’s what gets stored in Firebase when I type The
A0
,A1
andA2
are the revisions of document created each time I edit content.User ID
is the ID of the user that created this revision. This is helpful when multiple users are collaborating.o
is for operation, it contains the actual information about changes made in this version.Index
is the position of the text added/deleted in the document.Content
is the actual edited content. If it is an insert, it contains the inserted text and in case of deletion this will be a negative integer indicating the number of characters deleted.Timestamp
is the timestamp for this revision.
This structure is sequential and each revision stores just the delta. To restore a specific revision all you need to do is apply changes from A0
through An
. This is conceptually similar to git, you can think of each revision as a git commit.
TextOperation
Each revision in Firebase is represented by a TextOperation
in Firepad.
You can create a TextOperation from operation (o) of a Firebase revision:
const operation = Firepad.TextOperation.fromJSON(history.A0.o)
TextOperation provides a handy compose
function that combines the changes of two operations in to a new operation. This new operation can be further composed with another operation, so on and so forth.
// "T"
const a0 = Firepad.TextOperation.fromJSON(history.A0.o)
// "Th"
const a1 = a0.compose(Firepad.TextOperation.fromJSON(history.A1.o))
// "The"
const final = a1.compose(Firepad.TextOperation.fromJSON(history.A2.o))
You can think of compose like cherry-pick in git.
Putting it together
If we start with the operation for first revision and recursively compose subsequent operations, we’ll end up with an operation that contains the final state of the document. So to restore a specific version An
just compose operations A0…An
.
With this in mind we can break down the entire process in to:
1. Fetching revisions from Firebase
Fetching history from firebase is pretty straightforward:
const fetchRevisions = async (firebaseRef, callback) => {
firebaseRef.child('history').once("value", function (snapshot) {
const revisions = snapshot.val()
}
2. Wrap each revision in to a TextOperation
Once we have revisions from Firebase, we create a TextOperation for each revision.
Also to make it easy to restore revision for a given timestamp, we generate a dictionary of {timestamp : TextOperation}
:
const fetchRevisions = async (firebaseRef, callback) => {
firebaseRef.child('history').once("value", function (snapshot) {
const revisions = snapshot.val()
var revisionsByTimestamp = {}
const Firepad = require('firepad')for (key in revisions) {
const revision = revisions[key]
const operation = Firepad.TextOperation.fromJSON(revision.o)
revisionsByTimestamp[`${revision.t}`] = operation
}
callback(revisionsByTimestamp)
})
}
3. Recursively compose and get final text
If we recursively compose all the TextOperations
from A0
to An
where An
is the specified revision, we’ll end up with a TextOperation
that combines all changes to the document up until An
:
for (var key of keys) {
const operation = revisions[key]
document = document.compose(operation)
if (key === revision) {
break
}
}
Next, we need the actual text content of the finalTextOperation
, to do this we just use TextOperation
’s toJSON
function. This function returns an array whose first element is the resulting text:
document.ops.length ? document.toJSON().slice(-1).pop() : null
TL;DR
const fetchRevisions = async (firebaseRef, callback) => {firebaseRef.child('history').once("value", function (snapshot) {
const revisions = snapshot.val()
// Map revisions by their timestamp to make it easy to restore
var revisionsByTimestamp = {}
const Firepad = require('firepad')
for (key in revisions) {
const revision = revisions[key]
const operation = Firepad.TextOperation.fromJSON(revision.o)
revisionsByTimestamp[`${revision.t}`] = operation
}callback(revisionsByTimestamp)
})
}const textForRevision = (revision, revisions) => {
const Firepad = require('firepad')
var document = new Firepad.TextOperation()
const keys = Object.keys(revisions).sort()
for (var key of keys) {
const operation = revisions[key]
document = document.compose(operation)
if (key === revision) {
break
}
}return document.ops.length ? document.toJSON().slice(-1).pop() : null
}module.exports = {
fetchRevisions,
textForRevision,
}
Usage:
const Firepad = require('firepad')
const Firebase = require('firebase')
const firebaseRef = Firebase.database().ref().child('<child location>')firepad.on('ready', function () {
fetchRevisions(firebaseRef, revisionInfo => {
const timestamp = // eg: "1601406184335"
const text = textForRevision(timestamp, revisionInfo)codeMirror.setValue(text)
})
})
Final thoughts
We think our Firebase-based replay implementation proved to be a faster and much cleaner solution. It is super lightweight, a typical hour-long coding session takes up only a few hundred kb.
Please don’t forget to check out Logicboard.com, we support over 28 languages with built-in Video Calling and Chat. To experience all of this in action, check out our live demo.