Combining realtime and offline. (Using Pusher in a Service Worker)

service_workers.jpg

An example of using Pusher in a Service Worker, to take realtime data offline.

Introduction

Last week we released a new version of pusher-js (3.1), which extends the JavaScript runtimes supported by the client; including Node, React Native & Service Workers.

Today we’ll take a look at using Pusher Channels in a Service Worker.

A Service Worker is a script that sits between your browser and the network, allowing you to respond to any requests made from your frontend. This can be used to serve content to users when they don’t have an active internet connection.

We’ll build an example app which will show a list of recent tweets mentioning the term ”javascript”. In browsers that support it, we’ll use Service Workers to make this page work offline.

Step one: build it all

Service Workers act as an enhancement to an existing site, so you don’t have change the way that you build your frontend (although, you do have to serve your final site over https).

So, we’re going to start by ignoring our service worker and creating the site. To do this, we’ll make a web server that provides two urls:

  • GET /json – pull the most recent tweets from a database, and serve it as json
  • GET / – give us some stuff to fetch the json and render it nicely

Here’s how we might implement it as a node.js application (full source on github):

1app.get('/json', (req, res, next) =>
2  redis
3    .lrange( 'tweets', 0, -1 )
4    .then( result => result.map( JSON.parse ) )
5    .then( tweets => res.send( tweets ) )
6    .catch( next )
7)
8
9// expose / (index.html) from file system
10app.use(express.static('public'))

(note, the database is populated by a second script)

Now we can deploy that online somewhere and we’re all done! We’ve got our page that displays the recent tweets.

pusher-channels-combine-realtime-and-offline-features.png

Taking our app offline

Our next step is to make our application available offline. We can do this by writing a Service Worker script (this will be a new script that register from our main page).

We want to request our two urls and store them in a cache, we can do this by calling cache.addAll when the service worker first starts up (firing the install event).

Once installed, we can respond to any requests from the page by hooking into a fetch event.

1self.addEventListener('install', (event) =>
2  event.waitUntil(
3    caches.open('v1').then(cache => 
4      cache.addAll([
5        '/json',
6        '/'
7      ])
8    )
9  )
10)
11
12self.addEventListener('fetch', (event) => 
13  event.respondWith(
14    caches.match(event.request)
15  )
16)

Lastly, we need to register the service worker in our frontend:

navigator.serviceWorker.register('/sw.js')

And now we’re all cool. Now we’re able to see our tweets even if we’re offline!

pusher-channels-service-worker-offline-realtime.png

Still looking the same, but now it works offline.

Keeping things up to date

We have a problem. As our /json endpoint is updated, the frontend will still be see the cached version without any new tweets.

To handle this, we can change our caching policy to include a network request. (sw-toolbox is a great way to set up caching policies.)

Though, using the new pusher-js, we have another option, we can open a connection from our service worker and update the /json cache as soon as new data comes in.

1const add = (tweet) =>
2  caches.open(NAME)
3    .then(cache =>
4      cache.match('/json')
5        .then(resp => resp.json())
6        .then(tweets => 
7          cache.put('/json', new Response(
8            JSON.stringify([tweet].concat(tweets)),
9            {headers: {
10                'Content-Type': 'application/json'
11            }}
12          ))
13        )
14      )
15
16pusher.subscribe('tweets')
17      .bind('tweet', add)

This means that our cached version will be updated, without actually making any requests to the web server – which is kinda nuts when you think about it.

We can deploy this, and now our /json endpoint will be updated with new content as it comes in.

(still the same, but now it actually works.)

Note: there are other ways of getting data into a service worker – check out background sync and push notifications. The way we’re using Pusher here is slightly different from both.

Notifying users of new data

If we’re pushing things out to the user, we might as well notify the user of there being more content. A common way of doing this is to show an “X more things” label at the top of the feed.

a display of the number of tweets ready to see

One way to get this data from the Service Wroker to the page displaying the tweets is to use postMessage – this allows you to send data between javascript contexts (handy for iframes, opened windows & worker scripts).

We can subscribe to messages in our page by adding an event listener to navigator.serviceWorker:

1navigator.serviceWorker
2.addEventListener('message',function(event) { 
3  console.log(event.data)
4})

And from the service worker, we can publish a message to all clients that are using this service worker:

1self.clients.matchAll()
2  .then( clients =>
3    clients.forEach( client =>
4      client.postMessage(message)
5    )
6  )

And we’re done.

Something interesting about this is that although our page is seeing realtime data – the only connection to pusher is within the Service Worker, meaning:

  • there won’t be any re-connects when we reload the page.
  • no matter how many windows/tabs we open – we’ll only need one connection or message to notify them all.

Way cool.

Thanks

You can see the live site deployed at pusher-tweet-list.herokuapp.com, and the source code can be found on github.com/pusher-community/tweet-list

If you’ve got any questions/comments – I’d love to hear them, give me a shout on twitter at @benjaminbenben.