blob: d229a5064ad08bf43983e50a6031366a24d62524 [file] [log] [blame]
#
# A service worker that bootstraps the board agenda application
#
# *) Replace calls to fetch any agenda page with calls to fetch a bootstrap
# page. This page will reconstruct the page requested using cached
# data and then request fresh data. If the server doesn't respond
# with 0.5 seconds or fails, return a cached version of the bootstrap
# page.
#
# *) When a bootstrap.html page is loaded, a scan is made for references
# to javascripts and stylesheets, and if such a page is not present in
# the cache, it is fetched and the results are cached. This is because
# browsers will sometimes try to request these pages -- even when marked
# as immutable -- when offline.
#
# *) Requests for javascript and stylesheets are cached and used to
# respond to fetches that fail. Once a new response is received,
# old responses (with different query strings) are deleted.
#
# *) Inform clients of the need to reload if a slow server caused
# pages to be loaded with stale scripts and/or stylesheets
#
# *) when requested to do so by a client, preload additional pages. This
# is for the initial installation, as the pages will have already been
# loaded by the browser.
#
timeout = 500
# install immediately
self.addEventListener :install do
return self.skipWaiting()
end
# take over responsibility for existing clients
# https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim
self.addEventListener :activate do |event|
event.waitUntil self.clients.claim();
end
# insert or replace a response into the cache. Delete other responses
# with the same path (ignoring the query string).
def cache_replace(cache, request, response)
path = request.url.split('?')[0]
cache.keys!().then do |keys|
keys.each do |key|
if key.url.split('?')[0] == path and key.url != path
cache.delete(key).then {}
end
end
end
cache.put(request, response)
end
# broadcast a message to all clients
def broadcast(message)
clients.matchAll().then do |clients|
clients.each do |client|
client.postMessage(message)
end
end
end
# look for css and js files and in HTML response ensure that each are cached
def preload(cache, base, text, toolate)
pattern = Regexp.new('"[-.\w+/]+\.(css|js)\?\d+"', 'g')
count = 0
changed = false
while (match = pattern.exec(text))
count += 1
path = match[0].split('"')[1]
request = Request.new(URL.new(path, base))
cache.match(request).then do |response|
if response
count -= 1
else
changed = true
fetch(request).then do |response|
cache_replace(cache, request, response) if response.ok
count -= 1
broadcast(type: 'reload') if toolate and changed and count == 0
end
end
end
end
end
# fetch from cache with a network fallback
def fetch_from_cache(event)
return caches.open('board/agenda').then do |cache|
return cache.match(event.request).then do |response|
return response || fetch(event.request).then do |response|
cache_replace(cache, event.request, response.clone()) if response.ok
return response
end
end
end
end
# Return latest bootstrap page from the cache; then update the bootstrap
# from the server. If the body has changed, broadcast that information to
# all the browser window clients.
def latest(event)
return Promise.new do |fulfill, reject|
caches.open('board/agenda').then do |cache|
cache.matchAll().then do |responses|
match = nil
responses.each do |response|
if response.url.end_with? '/bootstrap.html'
match = response if not match or match.url < response.url
end
end
if match
match.clone().text().then do |before|
fulfill(match)
request = Request.new(match.url, cache: "no-store")
fetch(request).then do |response|
if response.ok
response.clone().text().then do |after|
cache.put request, response
broadcast(type: 'latest', body: after) # if after != before
end
end
end
end
else
fetch(event.request).then(fulfill, reject)
end
end
end
end
end
# Return a bootstrap.html page within 0.5 seconds. If the network responds
# in time, go with that response, otherwise respond with a cached version.
def bootstrap(event, request)
return Promise.new do |fulfill, reject|
timeoutId = nil
caches.open('board/agenda').then do |cache|
# common logic to reply from cache
replyFromCache = lambda do |refetch|
cache.match(request).then do |response|
clearTimeout timeoutId
if response
fulfill response
timeoutId = nil
elsif refetch
fetch(event.request).then(fulfill, reject)
end
end
end
# respond from cache if the server isn't fast enough
timeoutId = setTimeout timeout do
replyFromCache(false)
end
# attempt to fetch bootstrap.html from the network
fetch(request).then {|response|
# cache the response if OK, fulfill the response if not timed out
if response.ok
cache.put(request, response.clone())
# preload stylesheets and javascripts
if request.url =~ /bootstrap\.html$/
response.clone().text().then do |text|
toolate = !timeoutId
setTimeout(toolate ? 0 : 3_000) do
preload(cache, request.url, text, toolate)
end
end
end
if timeoutId
clearTimeout timeoutId
fulfill response
end
else
# bad response: use cache instead
replyFromCache(true)
end
}.catch {|failure|
# no response: use cache instead
replyFromCache(true)
}
end
end
end
# intercept selected pages
self.addEventListener :fetch do |event|
scope = self.registration.scope
url = event.request.url
url = url.slice(scope.length) if url.start_with? scope
if event.request.method == 'GET'
# determine what url to fetch (if any)
if url.end_with? '/bootstrap.html'
return
elsif url =~ %r{^\d\d\d\d-\d\d-\d\d/(\w+/)?[-\w]*$}
# substitute bootstrap.html for html pages
date = url.split('/')[0]
bootstrap_url = "#{scope}#{date}/bootstrap.html"
request = Request.new(bootstrap_url, cache: "no-store")
# produce response
event.respondWith(bootstrap(event, request))
elsif url =~ %r{\.(js|css)\?\d+$}
# cache and respond to js and css requests
event.respondWith(fetch_from_cache(event))
elsif url == ''
# event.respondWith(Response.redirect('latest/'))
elsif url == 'latest/'
event.respondWith(latest(event))
end
end
end
# watch for preload requests
self.addEventListener :message do |event|
if event.data.type == :preload
caches.open('board/agenda').then do |cache|
request = Request.new(event.data.url, cache: "no-store")
cache.match(request).then do |response|
unless response
fetch(request).then do |response|
if response.ok
response.text().then do |text|
preload(cache, request.url, text, false)
end
end
end
end
end
end
end
end