Replace AppCache with a service worker

pull/1022/head
Jasper van Merle 6 years ago
parent 5edbb16c1b
commit 8ed1f4ace1

@ -156,7 +156,7 @@ Contributions are welcome. Please read the [contributing guidelines](./.github/C
* [Doc Browser](https://github.com/qwfy/doc-browser) is a native Linux app that supports DevDocs docsets * [Doc Browser](https://github.com/qwfy/doc-browser) is a native Linux app that supports DevDocs docsets
* [GNOME Application](https://github.com/hardpixel/devdocs-desktop) GTK3 application with search integrated in headerbar * [GNOME Application](https://github.com/hardpixel/devdocs-desktop) GTK3 application with search integrated in headerbar
* [macOS Application](https://github.com/dteoh/devdocs-macos) * [macOS Application](https://github.com/dteoh/devdocs-macos)
* [Android Application](https://github.com/Merith-TK/devdocs_webapp_kotlin) is a fully working, advanced WebView with AppCache enabled * [Android Application](https://github.com/Merith-TK/devdocs_webapp_kotlin) is a fully working, advanced WebView
## Copyright / License ## Copyright / License

@ -13,7 +13,7 @@
@el = $('._app') @el = $('._app')
@localStorage = new LocalStorageStore @localStorage = new LocalStorageStore
@appCache = new app.AppCache if app.AppCache.isEnabled() @serviceWorker = new app.ServiceWorker if app.ServiceWorker.isEnabled()
@settings = new app.Settings @settings = new app.Settings
@db = new app.DB() @db = new app.DB()
@ -149,7 +149,7 @@
saveDocs: -> saveDocs: ->
@settings.setDocs(doc.slug for doc in @docs.all()) @settings.setDocs(doc.slug for doc in @docs.all())
@db.migrate() @db.migrate()
@appCache?.updateInBackground() @serviceWorker?.updateInBackground()
welcomeBack: -> welcomeBack: ->
visitCount = @settings.get('count') visitCount = @settings.get('count')
@ -169,14 +169,14 @@
reload: -> reload: ->
@docs.clearCache() @docs.clearCache()
@disabledDocs.clearCache() @disabledDocs.clearCache()
if @appCache then @appCache.reload() else @reboot() if @serviceWorker then @serviceWorker.reload() else @reboot()
return return
reset: -> reset: ->
@localStorage.reset() @localStorage.reset()
@settings.reset() @settings.reset()
@db?.reset() @db?.reset()
@appCache?.update() @serviceWorker?.update()
window.location = '/' window.location = '/'
return return
@ -195,9 +195,9 @@
return return
indexHost: -> indexHost: ->
# Can't load the index files from the host/CDN when applicationCache is # Can't load the index files from the host/CDN when service worker is
# enabled because it doesn't support caching URLs that use CORS. # enabled because it doesn't support caching URLs that use CORS.
@config[if @appCache and @settings.hasDocs() then 'index_path' else 'docs_origin'] @config[if @serviceWorker and @settings.hasDocs() then 'index_path' else 'docs_origin']
onBootError: (args...) -> onBootError: (args...) ->
@trigger 'bootError' @trigger 'bootError'

@ -1,42 +0,0 @@
class app.AppCache
$.extend @prototype, Events
@isEnabled: ->
try
applicationCache and applicationCache.status isnt applicationCache.UNCACHED
catch
constructor: ->
@cache = applicationCache
@notifyUpdate = true
@onUpdateReady() if @cache.status is @cache.UPDATEREADY
$.on @cache, 'progress', @onProgress
$.on @cache, 'updateready', @onUpdateReady
update: ->
@notifyUpdate = true
@notifyProgress = true
try @cache.update() catch
return
updateInBackground: ->
@notifyUpdate = false
@notifyProgress = false
try @cache.update() catch
return
reload: ->
$.on @cache, 'updateready noupdate error', -> app.reboot()
@notifyUpdate = false
@notifyProgress = true
try @cache.update() catch
return
onProgress: (event) =>
@trigger 'progress', event if @notifyProgress
return
onUpdateReady: =>
@trigger 'updateready' if @notifyUpdate
return

@ -13,3 +13,4 @@ app.config =
version: <%= Time.now.to_i %> version: <%= Time.now.to_i %>
release: <%= Time.now.utc.httpdate.to_json %> release: <%= Time.now.utc.httpdate.to_json %>
mathml_stylesheet: '<%= App.cdn_origin %>/mathml.css' mathml_stylesheet: '<%= App.cdn_origin %>/mathml.css'
service_worker_path: '/service-worker.js'

@ -0,0 +1,55 @@
class app.ServiceWorker
$.extend @prototype, Events
@isEnabled: ->
!!navigator.serviceWorker
constructor: ->
@registration = null
@installingRegistration = null
@notifyUpdate = true
navigator.serviceWorker.register(app.config.service_worker_path, {scope: '/'})
.then((registration) => @updateRegistration(registration))
.catch((error) -> console.error 'Could not register service worker:', error)
update: ->
return unless @registration
@notifyUpdate = true
return @doUpdate()
updateInBackground: ->
return unless @registration
@notifyUpdate = false
return @doUpdate()
reload: ->
return @updateInBackground().then(() -> app.reboot())
doUpdate: ->
return @registration.update().catch(->)
updateRegistration: (registration) ->
$.off @registration, 'updatefound', @onUpdateFound if @registration
$.off @installingRegistration, 'statechange', @onStateChange if @installingRegistration
@registration = registration
@installingRegistration = null
$.on @registration, 'updatefound', @onUpdateFound
return
onUpdateFound: () =>
@installingRegistration = @registration.installing
$.on @installingRegistration, 'statechange', @onStateChange
return
onStateChange: () =>
if @installingRegistration.state == 'installed' and navigator.serviceWorker.controller
@updateRegistration(@installingRegistration)
@onUpdateReady()
return
onUpdateReady: ->
@trigger 'updateready' if @notifyUpdate
return

@ -3,13 +3,13 @@ class app.UpdateChecker
@lastCheck = Date.now() @lastCheck = Date.now()
$.on window, 'focus', @onFocus $.on window, 'focus', @onFocus
app.appCache.on 'updateready', @onUpdateReady if app.appCache app.serviceWorker.on 'updateready', @onUpdateReady if app.serviceWorker
setTimeout @checkDocs, 0 setTimeout @checkDocs, 0
check: -> check: ->
if app.appCache if app.serviceWorker
app.appCache.update() app.serviceWorker.update()
else else
ajax ajax
url: $('script[src*="application"]').getAttribute('src') url: $('script[src*="application"]').getAttribute('src')

@ -26,7 +26,7 @@ app.templates.offlinePage = (docs) -> """
<dl> <dl>
<dt>How does this work? <dt>How does this work?
<dd>Each page is cached as a key-value pair in <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a> (downloaded from a single file).<br> <dd>Each page is cached as a key-value pair in <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a> (downloaded from a single file).<br>
The app also uses <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache">AppCache</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API">localStorage</a> to cache the assets and index files. The app also uses <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers">Service Workers</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API">localStorage</a> to cache the assets and index files.
<dt>Can I close the tab/browser? <dt>Can I close the tab/browser?
<dd>#{canICloseTheTab()} <dd>#{canICloseTheTab()}
<dt>What if I don't update a documentation? <dt>What if I don't update a documentation?
@ -41,10 +41,10 @@ app.templates.offlinePage = (docs) -> """
""" """
canICloseTheTab = -> canICloseTheTab = ->
if app.AppCache.isEnabled() if app.ServiceWorker.isEnabled()
""" Yes! Even offline, you can open a new tab, go to <a href="//devdocs.io">devdocs.io</a>, and everything will work as if you were online (provided you installed all the documentations you want to use beforehand). """ """ Yes! Even offline, you can open a new tab, go to <a href="//devdocs.io">devdocs.io</a>, and everything will work as if you were online (provided you installed all the documentations you want to use beforehand). """
else else
""" No. AppCache isn't available in your browser (or is disabled), so loading <a href="//devdocs.io">devdocs.io</a> offline won't work.<br> """ No. Service Workers aren't available in your browser (or are disabled), so loading <a href="//devdocs.io">devdocs.io</a> offline won't work.<br>
The current tab will continue to function even when you go offline (provided you installed all the documentations beforehand). """ The current tab will continue to function even when you go offline (provided you installed all the documentations beforehand). """
app.templates.offlineDoc = (doc, status) -> app.templates.offlineDoc = (doc, status) ->

@ -123,7 +123,7 @@ class app.views.EntryPage extends app.View
@render @tmpl('pageLoadError') @render @tmpl('pageLoadError')
@resetClass() @resetClass()
@addClass @constructor.errorClass @addClass @constructor.errorClass
app.appCache?.update() app.serviceWorker?.update()
return return
cache: -> cache: ->

@ -22,12 +22,10 @@ class app.views.SettingsPage extends app.View
toggleDark: (enable) -> toggleDark: (enable) ->
app.settings.set('dark', !!enable) app.settings.set('dark', !!enable)
app.appCache?.updateInBackground()
return return
toggleLayout: (layout, enable) -> toggleLayout: (layout, enable) ->
app.settings.setLayout(layout, enable) app.settings.setLayout(layout, enable)
app.appCache?.updateInBackground()
return return
toggleSmoothScroll: (enable) -> toggleSmoothScroll: (enable) ->

@ -26,9 +26,7 @@ class app.views.Resizer extends app.View
newSize = "#{value}px" newSize = "#{value}px"
@style.innerHTML = @style.innerHTML.replace(new RegExp(@size, 'g'), newSize) @style.innerHTML = @style.innerHTML.replace(new RegExp(@size, 'g'), newSize)
@size = newSize @size = newSize
if save app.settings.setSize(value) if save
app.settings.setSize(value)
app.appCache?.updateInBackground()
return return
onDragStart: (event) => onDragStart: (event) =>

@ -25,7 +25,6 @@ class app.views.Settings extends app.View
if super if super
@render() @render()
document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT) document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT)
app.appCache?.on 'progress', @onAppCacheProgress
return return
deactivate: -> deactivate: ->
@ -33,7 +32,6 @@ class app.views.Settings extends app.View
@resetClass() @resetClass()
@docPicker.detach() @docPicker.detach()
document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT) if app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT) document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT) if app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT)
app.appCache?.off 'progress', @onAppCacheProgress
return return
render: -> render: ->
@ -52,7 +50,7 @@ class app.views.Settings extends app.View
docs = @docPicker.getSelectedDocs() docs = @docPicker.getSelectedDocs()
app.settings.setDocs(docs) app.settings.setDocs(docs)
@saveBtn.textContent = if app.appCache then 'Downloading\u2026' else 'Saving\u2026' @saveBtn.textContent = 'Saving\u2026'
disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1) disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1)
disabledDocs.uninstall -> disabledDocs.uninstall ->
app.db.migrate() app.db.migrate()
@ -83,9 +81,3 @@ class app.views.Settings extends app.View
$.stopEvent(event) $.stopEvent(event)
app.router.show '/' app.router.show '/'
return return
onAppCacheProgress: (event) =>
if event.lengthComputable
percentage = Math.round event.loaded * 100 / event.total
@saveBtn.textContent = "Downloading\u2026 (#{percentage}%)"
return

@ -220,7 +220,7 @@ class App < Sinatra::Application
app_theme == 'dark' app_theme == 'dark'
end end
def redirect_via_js(path) # courtesy of HTML5 App Cache def redirect_via_js(path)
response.set_cookie :initial_path, value: path, expires: Time.now + 15, path: '/' response.set_cookie :initial_path, value: path, expires: Time.now + 15, path: '/'
redirect '/', 302 redirect '/', 302
end end
@ -243,15 +243,15 @@ class App < Sinatra::Application
end end
end end
get '/manifest.appcache' do get '/service-worker.js' do
content_type 'text/cache-manifest' content_type 'application/javascript'
expires 0, :'no-cache' expires 0, :'no-cache'
erb :manifest erb :'service-worker.js'
end end
get '/' do get '/' do
return redirect "/#q=#{params[:q]}" if params[:q] return redirect "/#q=#{params[:q]}" if params[:q]
return redirect '/' unless request.query_string.empty? # courtesy of HTML5 App Cache return redirect '/' unless request.query_string.empty?
response.headers['Content-Security-Policy'] = settings.csp if settings.csp response.headers['Content-Security-Policy'] = settings.csp if settings.csp
erb :index erb :index
end end

@ -106,58 +106,6 @@ class AppTest < MiniTest::Spec
end end
end end
describe "/manifest.appcache" do
it "works" do
get '/manifest.appcache'
assert last_response.ok?
end
it "works with cookie" do
set_cookie('docs=css/html~5')
get '/manifest.appcache'
assert last_response.ok?
assert_includes last_response.body, '/css/index.json?1420139788'
assert_includes last_response.body, '/html~5/index.json?1420139791'
end
it "ignores invalid docs in the cookie" do
set_cookie('docs=foo')
get '/manifest.appcache'
assert last_response.ok?
refute_includes last_response.body, 'foo'
end
it "has the word 'default' when no 'dark' cookie is set" do
get '/manifest.appcache'
assert_includes last_response.body, '# default'
refute_includes last_response.body, '# dark'
end
it "has the word 'dark' when the cookie is set" do
set_cookie('dark=1')
get '/manifest.appcache'
assert_includes last_response.body, '# dark'
refute_includes last_response.body, '# default'
end
it "sets default size" do
get '/manifest.appcache'
assert_includes last_response.body, '20rem'
end
it "sets size from cookie" do
set_cookie('size=42')
get '/manifest.appcache'
assert_includes last_response.body, '42px'
end
it "sets layout from cookie" do
set_cookie('layout=foo_layout')
get '/manifest.appcache'
assert_includes last_response.body, 'foo_layout'
end
end
describe "/[doc]" do describe "/[doc]" do
it "renders when the doc exists and isn't enabled" do it "renders when the doc exists and isn't enabled" do
set_cookie('docs=html~5') set_cookie('docs=html~5')

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html<%= ' manifest="/manifest.appcache"' if App.production? %> prefix="og: http://ogp.me/ns#" lang="en" class="_booting _theme-<%= app_theme %>"> <html prefix="og: http://ogp.me/ns#" lang="en" class="_booting _theme-<%= app_theme %>">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no"> <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no">

@ -1,14 +0,0 @@
CACHE MANIFEST
# <%= app_theme %> <%= app_size %> <%= app_layout %>
CACHE:
/
<%= manifest_asset_urls.join "\n" %>
<%= doc_index_urls.join "\n" %>
NETWORK:
/s/
*
FALLBACK:
/ /

@ -0,0 +1,64 @@
<%# Use the hash of the application.js file as cache name, or 'app' if not running in production %>
<%# This ensures that the cache is always updated if the hash of the application.js file changes %>
const cacheName = '<%= javascript_path('application', asset_host: false).scan(/application-([^\.]+)\.js/).last&.first || 'app' %>';
<%# Paths to cache when the service worker is installed %>
const cachePaths = [
'/',
'/favicon.ico',
'/manifest.json',
'/images/webapp-icon-32.png',
'/images/webapp-icon-60.png',
'/images/webapp-icon-80.png',
'/images/webapp-icon-128.png',
'/images/webapp-icon-256.png',
'/images/webapp-icon-512.png',
'<%= manifest_asset_urls.join "',\n '" %>',
'<%= doc_index_urls.join "',\n '" %>',
];
<%# Set-up the cache %>
self.addEventListener('install', event => {
self.skipWaiting();
event.waitUntil(
caches.open(cacheName).then(cache => cache.addAll(cachePaths)),
);
});
<%# Remove old caches %>
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.map(key => {
if (key !== cacheName) {
return caches.delete(key);
}
})
))
);
});
<%# Handle HTTP requests %>
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
return response;
}
return fetch(event.request)
.catch(err => {
const url = new URL(event.request.url);
<%# Return the index page from the cache if the user is visiting a url like devdocs.io/javascript/global_objects/array/find %>
<%# The index page will make sure the correct documentation or a proper offline page is shown %>
if (url.origin === location.origin && !url.pathname.includes('.')) {
return caches.match('/').then(response => response || err);
}
return err;
});
})
);
});

@ -11,9 +11,9 @@
<p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:</p> <p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:</p>
<ul class="_fail-list"> <ul class="_fail-list">
<li>Recent versions of Firefox, Chrome, or Opera</li> <li>Recent versions of Firefox, Chrome, or Opera</li>
<li>Safari 9.1+</li> <li>Safari 11.1+</li>
<li>Edge 16+</li> <li>Edge 16+</li>
<li>iOS 10+</li> <li>iOS 11.3+</li>
</ul> </ul>
<p class="_fail-text"> <p class="_fail-text">
If you're unable to upgrade, we apologize. If you're unable to upgrade, we apologize.

Loading…
Cancel
Save