WorryFree Computers   »   [go: up one dir, main page]

On Building Software

Old School Workarounds: Keeping the Screen Up to Date

Ka Wai Cheung
On Building Software
8 min readMay 10, 2024

--

Years ago, it was common assumption that when a page loaded, nothing about it would change until you manually refreshed the page.

That’s not the case with SaaS products anymore. Today, if you’re on a dashboard, list screen, or detail page, when someone has updated something on the screen you’re looking at, you expect to see the update automatically, even if you let a page sit quietly in a separate tab that you bounce back to hours later.

DoneDone is an old app (15 years and running!) built in a time before this expectation was ubiquitous. And though we released an entirely new version in 2018, the infrastructure started as a copy-over from the original version.

So, when it came time to add auto-updating, I wanted to do it in the least invasive way possible. Here’s my approach.

Note: DoneDone has two main features: Projects (where tasks are tracked) and mailboxes (where email conversations are tracked). For the purposes of this post, they’re nearly identical concepts, so I’ll just talk about the projects side of DoneDone.

Where auto-updates should happen

I wanted to have three key areas of the app auto-update whenever new information is available.

(1) The Dashboard

The dashboard shows your projects as cards. I wanted the “last updated” times, a count of active tickets, and a count of tickets assigned to you auto-updated whenever they changed.

Project cards on the DoneDone Dashboard

(2) Project landing pages

Within a project’s landing page, I wanted tasks to auto-update whenever a meaningful change was made to any one of them. DoneDone projects have both a listing view and kanban board view, but ostensibly they show the same information.

Project list and kanban board views

(3) Task detail pages

I wanted the individual details to auto-update whenever a meaningful update was made to them.

The Approach

Here’s the approach I used to create the auto-update behavior starting with the data we need to detect updates.

Storing the last updated dates in the database

The linchpin to the approach starts in the relational database. I track a lastUpdated date field on the tables whose records I care to know got updated.

In my case, it’s just two tables:

  • ActionableItems—All task tickets are stored in this table and have a related foreign key to their specific project in the Projects table.
  • Projects—All base information for projects (like their name and access level) are stored in this table.

Theoretically, I don’t need the lastUpdated column on Projectsbecause in practice, this column only updates when a task within a project is updated (At the current moment, I don’t consider events like changing the name of a project or adding members to it as an update I care to broadcast to idle screens).

This means I could always find the last updated date for a project by finding out which task has the most recent updated date. Something like SELECT TOP(1) LastUpdated FROM ActionableItems WHERE RelatedProjectID = @project_id_in_question ORDER BY LastUpdated DESC .

But, I’m worried about performance here (even with an index on RelatedProjectID), especially as our ActionableItems table grows into the tens or hundreds of millions of records. So, I’d rather track the lastUpdated value for a project in its own column directly on Projects .

In code, whenever any updates happen to a task, DoneDone will also update the lastUpdated column to the current moment (now) for that ticket’s ActionableItems record in the same transaction.

This line of code is sprinkled in 13 places in code, for database actions like adding a comment to a task, updating priority, changing status, modifying due date, re-assigning a task…you get the idea. Any action that meaningfully changes the task.

Similarly, when any updates happen to a task, I update the lastUpdatedfield for its project once the task update has transacted.

Here, a priority level for a task is updated first (which in turn, updates the task’s lastUpdated field). Then, the lastUpdated field of the project it’s associated to is updated.

This sets all the data I need in the database:

  • For any given task, I can find out when it was last updated.
  • For any given project, I can find out when the last task update was made.

Pulling back the current timestamp when an updatable screen renders

When one of these three areas (the dashboard, the list views, or the task detail) is accessed, the app makes a few calls to the DoneDone API to hydrate the components of the screen with the data it needs.

I happen to use Vue.js on the front end so calls are generally executed on Vue’s created() event callback method of the main components’ lifecycle.

In each case, one of these API calls (namely, the one that’s pulling back data related to tasks) also passes back a numeric property called accessEpochTime. This is just the current date/time converted to UNIX epoch time in milliseconds.

The API calls when navigating to the dashboard, project listing, or task detail also return the current UNIX epoch time (accessEpochTime).

Since the data related to tasks is pulled on the same request as the accessEpochTime value, I can be certain (to a degree of accuracy I’m willing to accept) that nothing else has changed to the data as of the accessEpochTime timestamp.

The value of accessEpochTime is stored on the Vue component side. It’s what I use to compare against to determine if the screen’s data has gotten stale.

Pinging the API for timestamp updates

For each of these three screens, at the end of the created() method, I leverage trusty old setInterval() to make periodic calls to the DoneDone API to see if updates to tasks exist. I happen to set the interval calls to every 19 seconds.

Now I could simply call the same API methods on these intervals as I do on the created()method. But the majority of those interval calls would just return the exact same data each time. DoneDone isn’t an app where transactions to a specific account are happening every second of every day. So, I’d like to minimize calling those heftier queries whenever I can.

Instead, these intervals each call an API method that only return the relevant last updated date (returned, again, as UNIX epoch time in milliseconds).

Periodic pings to the API for the relevant last updated date with setInterval(). I could’ve used something like web sockets here too but, I’m keeping it old school.
  • The task detail screen’s setInterval calls the API for the last update on the specific task in question.
  • The project listing/kanban board screens’ setInterval calls the API for the last update on the specific project in question.
  • The dashboard screens’ setInterval calls the API for the most recent last update amongst all projects the user belongs to.

Each of these calls is lightweight.

The task detail call is grabbing the lastUpdated value from the database by filtering directly against its task’s primary key (id). Fast!

The project listing and kanban board calls are similarly grabbing the lastUpdated value from the database by filtering directly against its project’s primary key (id). (Hence why I decided to add the extra column in the first place).

The dashboard’s call is also lightweight—on the backend I’m just selecting the TOP(1) value of all lastUpdated project values filtered from a list of the user’s accessible project IDs.

Refetching the data if necessary

Once the call is completed, I check whether the stored accessEpochTime is less than the lastUpdated value returned. If it is, I can assume a data update has been made and then make that heavier data call immediately.

The one exception is with the task detail. Rather than automatically refresh the task’s information on the page, I surface a little “updated” notification on the lower-right corner of the page and only refresh when clicked. This avoids the scenario where you’re reading a long comment only to see it update magically before your eyes.

On the “task detail” screen, a notification pops up when the task has been updated since data was last fetched.

In all scenarios, I then set accessEpochTime on the client to the newly fetched value for the future interval calls.

Finally, when a user leaves the screen, I use clearInterval to ensure pings aren’t continually being made. In Vue, I detect this through beforeDestroy() .

Things I like and things I can live with

As with any technical implementation, there are imperfections (and also happy little accidents).

One nice side effect of this strategy is that if I open multiple tabs to the same screen, each tab will refresh on its own time—because each one is pulling back its own unique accessEpochTime to start.

There’s also a gotcha though. For the project listing and dashboard scenarios, it’s very possible that even though there had been an update, the data refresh wouldn’t show anything new.

For instance, it could be that the project listing had a filter against just tasks with high priority and instead, an update was made to a task of medium priority. On the dashboard, it could be that none of the tasks related to the user in question were updated, so their counts would remain the same. In both scenarios, we’d be fetching new data and have nothing to show for it.

I’m OK with this false positive. There’s no inherent harm in reloading the same data (save wasting data transfer bytes). And, as mentioned earlier, transactions aren’t occurring at a blazing rate.

So, I still get most of the benefit of saving data transfer on the big reads even if occasionally the reads being made aren’t necessary.

The alternative would be to have a more granular fabric of lastUpdated scenarios—the permutations of which would grow pretty fast. For another app, this might be worth it, just not this one.

I’m a stickler for simple “old school” solutions. But getting to the best version of simple is always about weighing trade-offs. And trade-offs are not only app-specific, but can often be a very subjective thing.

If you liked this one, here’s another I wrote recently on interstitial states:

Thanks for reading!

--

--

Ka Wai Cheung
On Building Software

I write about software, design, fatherhood, and nostalgia usually. Dad to a boy and a girl. Creator of donedone.com. More at kawaicheung.io.