[devdocsgjs/main: 640/1867] Replace AppCache with a service worker




commit 8ed1f4ace1136c9ee4f45c9063d0980fefe18ede
Author: Jasper van Merle <jaspervmerle gmail com>
Date:   Sun Jul 7 00:55:58 2019 +0200

    Replace AppCache with a service worker

 README.md                                          |  2 +-
 assets/javascripts/app/app.coffee                  | 12 ++--
 assets/javascripts/app/appcache.coffee             | 42 --------------
 assets/javascripts/app/config.coffee.erb           |  1 +
 assets/javascripts/app/serviceworker.coffee        | 55 +++++++++++++++++++
 assets/javascripts/app/update_checker.coffee       |  6 +-
 .../templates/pages/offline_tmpl.coffee            |  6 +-
 assets/javascripts/views/content/entry_page.coffee |  2 +-
 .../javascripts/views/content/settings_page.coffee |  2 -
 assets/javascripts/views/layout/resizer.coffee     |  4 +-
 assets/javascripts/views/layout/settings.coffee    | 10 +---
 lib/app.rb                                         | 10 ++--
 test/app_test.rb                                   | 52 ------------------
 views/index.erb                                    |  2 +-
 views/manifest.erb                                 | 14 -----
 views/service-worker.js.erb                        | 64 ++++++++++++++++++++++
 views/unsupported.erb                              |  4 +-
 17 files changed, 144 insertions(+), 144 deletions(-)
---
diff --git a/README.md b/README.md
index 3c652e9f..d8d92143 100644
--- a/README.md
+++ b/README.md
@@ -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
 * [GNOME Application](https://github.com/hardpixel/devdocs-desktop) GTK3 application with search integrated 
in headerbar
 * [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
 
diff --git a/assets/javascripts/app/app.coffee b/assets/javascripts/app/app.coffee
index c638e179..0d2d9814 100644
--- a/assets/javascripts/app/app.coffee
+++ b/assets/javascripts/app/app.coffee
@@ -13,7 +13,7 @@
 
     @el = $('._app')
     @localStorage = new LocalStorageStore
-    @appCache = new app.AppCache if app.AppCache.isEnabled()
+    @serviceWorker = new app.ServiceWorker if app.ServiceWorker.isEnabled()
     @settings = new app.Settings
     @db = new app.DB()
 
@@ -149,7 +149,7 @@
   saveDocs: ->
     @settings.setDocs(doc.slug for doc in @docs.all())
     @db.migrate()
-    @appCache?.updateInBackground()
+    @serviceWorker?.updateInBackground()
 
   welcomeBack: ->
     visitCount = @settings.get('count')
@@ -169,14 +169,14 @@
   reload: ->
     @docs.clearCache()
     @disabledDocs.clearCache()
-    if @appCache then @appCache.reload() else @reboot()
+    if @serviceWorker then @serviceWorker.reload() else @reboot()
     return
 
   reset: ->
     @localStorage.reset()
     @settings.reset()
     @db?.reset()
-    @appCache?.update()
+    @serviceWorker?.update()
     window.location = '/'
     return
 
@@ -195,9 +195,9 @@
     return
 
   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.
-    @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...) ->
     @trigger 'bootError'
diff --git a/assets/javascripts/app/config.coffee.erb b/assets/javascripts/app/config.coffee.erb
index ec26b697..765da0b5 100644
--- a/assets/javascripts/app/config.coffee.erb
+++ b/assets/javascripts/app/config.coffee.erb
@@ -13,3 +13,4 @@ app.config =
   version: <%= Time.now.to_i %>
   release: <%= Time.now.utc.httpdate.to_json %>
   mathml_stylesheet: '<%= App.cdn_origin %>/mathml.css'
+  service_worker_path: '/service-worker.js'
diff --git a/assets/javascripts/app/serviceworker.coffee b/assets/javascripts/app/serviceworker.coffee
new file mode 100644
index 00000000..2faab8f2
--- /dev/null
+++ b/assets/javascripts/app/serviceworker.coffee
@@ -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
diff --git a/assets/javascripts/app/update_checker.coffee b/assets/javascripts/app/update_checker.coffee
index 5630b488..b98c6563 100644
--- a/assets/javascripts/app/update_checker.coffee
+++ b/assets/javascripts/app/update_checker.coffee
@@ -3,13 +3,13 @@ class app.UpdateChecker
     @lastCheck = Date.now()
 
     $.on window, 'focus', @onFocus
-    app.appCache.on 'updateready', @onUpdateReady if app.appCache
+    app.serviceWorker.on 'updateready', @onUpdateReady if app.serviceWorker
 
     setTimeout @checkDocs, 0
 
   check: ->
-    if app.appCache
-      app.appCache.update()
+    if app.serviceWorker
+      app.serviceWorker.update()
     else
       ajax
         url: $('script[src*="application"]').getAttribute('src')
diff --git a/assets/javascripts/templates/pages/offline_tmpl.coffee 
b/assets/javascripts/templates/pages/offline_tmpl.coffee
index a9a3c21c..52705605 100644
--- a/assets/javascripts/templates/pages/offline_tmpl.coffee
+++ b/assets/javascripts/templates/pages/offline_tmpl.coffee
@@ -26,7 +26,7 @@ app.templates.offlinePage = (docs) -> """
   <dl>
     <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>
-        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?
     <dd>#{canICloseTheTab()}
     <dt>What if I don't update a documentation?
@@ -41,10 +41,10 @@ app.templates.offlinePage = (docs) -> """
 """
 
 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). """
   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). """
 
 app.templates.offlineDoc = (doc, status) ->
diff --git a/assets/javascripts/views/content/entry_page.coffee 
b/assets/javascripts/views/content/entry_page.coffee
index beae4d77..d11291a3 100644
--- a/assets/javascripts/views/content/entry_page.coffee
+++ b/assets/javascripts/views/content/entry_page.coffee
@@ -123,7 +123,7 @@ class app.views.EntryPage extends app.View
     @render @tmpl('pageLoadError')
     @resetClass()
     @addClass @constructor.errorClass
-    app.appCache?.update()
+    app.serviceWorker?.update()
     return
 
   cache: ->
diff --git a/assets/javascripts/views/content/settings_page.coffee 
b/assets/javascripts/views/content/settings_page.coffee
index c1027e9c..af2e9a9d 100644
--- a/assets/javascripts/views/content/settings_page.coffee
+++ b/assets/javascripts/views/content/settings_page.coffee
@@ -22,12 +22,10 @@ class app.views.SettingsPage extends app.View
 
   toggleDark: (enable) ->
     app.settings.set('dark', !!enable)
-    app.appCache?.updateInBackground()
     return
 
   toggleLayout: (layout, enable) ->
     app.settings.setLayout(layout, enable)
-    app.appCache?.updateInBackground()
     return
 
   toggleSmoothScroll: (enable) ->
diff --git a/assets/javascripts/views/layout/resizer.coffee b/assets/javascripts/views/layout/resizer.coffee
index 86bb46f5..8f0ce9c4 100644
--- a/assets/javascripts/views/layout/resizer.coffee
+++ b/assets/javascripts/views/layout/resizer.coffee
@@ -26,9 +26,7 @@ class app.views.Resizer extends app.View
     newSize = "#{value}px"
     @style.innerHTML = @style.innerHTML.replace(new RegExp(@size, 'g'), newSize)
     @size = newSize
-    if save
-      app.settings.setSize(value)
-      app.appCache?.updateInBackground()
+    app.settings.setSize(value) if save
     return
 
   onDragStart: (event) =>
diff --git a/assets/javascripts/views/layout/settings.coffee b/assets/javascripts/views/layout/settings.coffee
index 7888118a..6941b9cd 100644
--- a/assets/javascripts/views/layout/settings.coffee
+++ b/assets/javascripts/views/layout/settings.coffee
@@ -25,7 +25,6 @@ class app.views.Settings extends app.View
     if super
       @render()
       document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT)
-      app.appCache?.on 'progress', @onAppCacheProgress
     return
 
   deactivate: ->
@@ -33,7 +32,6 @@ class app.views.Settings extends app.View
       @resetClass()
       @docPicker.detach()
       document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT) if app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT)
-      app.appCache?.off 'progress', @onAppCacheProgress
     return
 
   render: ->
@@ -52,7 +50,7 @@ class app.views.Settings extends app.View
         docs = @docPicker.getSelectedDocs()
         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.uninstall ->
         app.db.migrate()
@@ -83,9 +81,3 @@ class app.views.Settings extends app.View
       $.stopEvent(event)
       app.router.show '/'
     return
-
-  onAppCacheProgress: (event) =>
-    if event.lengthComputable
-      percentage = Math.round event.loaded * 100 / event.total
-      @saveBtn.textContent = "Downloading\u2026 (#{percentage}%)"
-    return
diff --git a/lib/app.rb b/lib/app.rb
index 32cac31b..18dbe901 100644
--- a/lib/app.rb
+++ b/lib/app.rb
@@ -220,7 +220,7 @@ class App < Sinatra::Application
       app_theme == 'dark'
     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: '/'
       redirect '/', 302
     end
@@ -243,15 +243,15 @@ class App < Sinatra::Application
     end
   end
 
-  get '/manifest.appcache' do
-    content_type 'text/cache-manifest'
+  get '/service-worker.js' do
+    content_type 'application/javascript'
     expires 0, :'no-cache'
-    erb :manifest
+    erb :'service-worker.js'
   end
 
   get '/' do
     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
     erb :index
   end
diff --git a/test/app_test.rb b/test/app_test.rb
index 92a24acd..909eb42c 100644
--- a/test/app_test.rb
+++ b/test/app_test.rb
@@ -106,58 +106,6 @@ class AppTest < MiniTest::Spec
     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
     it "renders when the doc exists and isn't enabled" do
       set_cookie('docs=html~5')
diff --git a/views/index.erb b/views/index.erb
index 022e927f..8e42c3c7 100644
--- a/views/index.erb
+++ b/views/index.erb
@@ -1,5 +1,5 @@
 <!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>
   <meta charset="utf-8">
   <meta name="viewport" 
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no">
diff --git a/views/service-worker.js.erb b/views/service-worker.js.erb
new file mode 100644
index 00000000..74ce45b2
--- /dev/null
+++ b/views/service-worker.js.erb
@@ -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;
+        });
+    })
+  );
+});
diff --git a/views/unsupported.erb b/views/unsupported.erb
index a01b7c7e..77064160 100644
--- a/views/unsupported.erb
+++ b/views/unsupported.erb
@@ -11,9 +11,9 @@
     <p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:</p>
     <ul class="_fail-list">
       <li>Recent versions of Firefox, Chrome, or Opera</li>
-      <li>Safari 9.1+</li>
+      <li>Safari 11.1+</li>
       <li>Edge 16+</li>
-      <li>iOS 10+</li>
+      <li>iOS 11.3+</li>
     </ul>
     <p class="_fail-text">
       If you're unable to upgrade, we apologize.


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]