History

Replace AppCache with a service worker

Jasper van Merle committed 7 years ago

Commit 8ed1f4a1 parent 5edbb1617 files · +144 −144Browse files

Unified · Split

17 files
  1. README.md +1 −1
  2. assets/javascripts/app/app.coffee +6 −6
  3. assets/javascripts/app/appcache.coffee +0 −42
  4. assets/javascripts/app/config.coffee.erb +1 −0
  5. assets/javascripts/app/serviceworker.coffee +55 −0
  6. assets/javascripts/app/update_checker.coffee +3 −3
  7. assets/javascripts/templates/pages/offline_tmpl.coffee +3 −3
  8. assets/javascripts/views/content/entry_page.coffee +1 −1
  9. assets/javascripts/views/content/settings_page.coffee +0 −2
  10. assets/javascripts/views/layout/resizer.coffee +1 −3
  11. assets/javascripts/views/layout/settings.coffee +1 −9
  12. lib/app.rb +5 −5
  13. test/app_test.rb +0 −52
  14. views/index.erb +1 −1
  15. views/manifest.erb +0 −14
  16. views/service-worker.js.erb +64 −0
  17. views/unsupported.erb +2 −2
README.md+1−1
@@ -156,7 +156,7 @@ Contributions are welcome. Please read the [contributing guidelines](./.github/C · 155 unmodified lines
156* [Doc Browser](https://github.com/qwfy/doc-browser) is a native Linux app that supports DevDocs docsets156* [Doc Browser](https://github.com/qwfy/doc-browser) is a native Linux app that supports DevDocs docsets
157* [GNOME Application](https://github.com/hardpixel/devdocs-desktop) GTK3 application with search integrated in headerbar157* [GNOME Application](https://github.com/hardpixel/devdocs-desktop) GTK3 application with search integrated in headerbar
158* [macOS Application](https://github.com/dteoh/devdocs-macos)158* [macOS Application](https://github.com/dteoh/devdocs-macos)
159* [Android Application](https://github.com/Merith-TK/devdocs_webapp_kotlin) is a fully working, advanced WebView with AppCache enabled159* [Android Application](https://github.com/Merith-TK/devdocs_webapp_kotlin) is a fully working, advanced WebView
160 160
161## Copyright / License161## Copyright / License
162 162
assets/javascripts/app/app.coffee+6−6
@@ -13,7 +13,7 @@ · 12 unmodified lines
13 13
14 @el = $('._app')14 @el = $('._app')
15 @localStorage = new LocalStorageStore15 @localStorage = new LocalStorageStore
16 @appCache = new app.AppCache if app.AppCache.isEnabled()16 @serviceWorker = new app.ServiceWorker if app.ServiceWorker.isEnabled()
17 @settings = new app.Settings17 @settings = new app.Settings
18 @db = new app.DB()18 @db = new app.DB()
19 19
@@ -149,7 +149,7 @@ · 129 unmodified lines
149 saveDocs: ->149 saveDocs: ->
150 @settings.setDocs(doc.slug for doc in @docs.all())150 @settings.setDocs(doc.slug for doc in @docs.all())
151 @db.migrate()151 @db.migrate()
152 @appCache?.updateInBackground()152 @serviceWorker?.updateInBackground()
153 153
154 welcomeBack: ->154 welcomeBack: ->
155 visitCount = @settings.get('count')155 visitCount = @settings.get('count')
@@ -169,14 +169,14 @@ · 13 unmodified lines
169 reload: ->169 reload: ->
170 @docs.clearCache()170 @docs.clearCache()
171 @disabledDocs.clearCache()171 @disabledDocs.clearCache()
172 if @appCache then @appCache.reload() else @reboot()172 if @serviceWorker then @serviceWorker.reload() else @reboot()
173 return173 return
174 174
175 reset: ->175 reset: ->
176 @localStorage.reset()176 @localStorage.reset()
177 @settings.reset()177 @settings.reset()
178 @db?.reset()178 @db?.reset()
179 @appCache?.update()179 @serviceWorker?.update()
180 window.location = '/'180 window.location = '/'
181 return181 return
182 182
@@ -195,9 +195,9 @@ · 12 unmodified lines
195 return195 return
196 196
197 indexHost: ->197 indexHost: ->
198 # Can't load the index files from the host/CDN when applicationCache is198 # Can't load the index files from the host/CDN when service worker is
199 # enabled because it doesn't support caching URLs that use CORS.199 # enabled because it doesn't support caching URLs that use CORS.
200 @config[if @appCache and @settings.hasDocs() then 'index_path' else 'docs_origin']200 @config[if @serviceWorker and @settings.hasDocs() then 'index_path' else 'docs_origin']
201 201
202 onBootError: (args...) ->202 onBootError: (args...) ->
203 @trigger 'bootError'203 @trigger 'bootError'
assets/javascripts/app/appcache.coffee+0−42
@@ -1,42 +0,0 @@
1class app.AppCache
2 $.extend @prototype, Events
3
4 @isEnabled: ->
5 try
6 applicationCache and applicationCache.status isnt applicationCache.UNCACHED
7 catch
8
9 constructor: ->
10 @cache = applicationCache
11 @notifyUpdate = true
12 @onUpdateReady() if @cache.status is @cache.UPDATEREADY
13
14 $.on @cache, 'progress', @onProgress
15 $.on @cache, 'updateready', @onUpdateReady
16
17 update: ->
18 @notifyUpdate = true
19 @notifyProgress = true
20 try @cache.update() catch
21 return
22
23 updateInBackground: ->
24 @notifyUpdate = false
25 @notifyProgress = false
26 try @cache.update() catch
27 return
28
29 reload: ->
30 $.on @cache, 'updateready noupdate error', -> app.reboot()
31 @notifyUpdate = false
32 @notifyProgress = true
33 try @cache.update() catch
34 return
35
36 onProgress: (event) =>
37 @trigger 'progress', event if @notifyProgress
38 return
39
40 onUpdateReady: =>
41 @trigger 'updateready' if @notifyUpdate
42 return
assets/javascripts/app/config.coffee.erb+1−0
@@ -13,3 +13,4 @@ app.config = · 12 unmodified lines
13 version: <%= Time.now.to_i %>13 version: <%= Time.now.to_i %>
14 release: <%= Time.now.utc.httpdate.to_json %>14 release: <%= Time.now.utc.httpdate.to_json %>
15 mathml_stylesheet: '<%= App.cdn_origin %>/mathml.css'15 mathml_stylesheet: '<%= App.cdn_origin %>/mathml.css'
16 service_worker_path: '/service-worker.js'
assets/javascripts/app/serviceworker.coffee+55−0
@@ -0,0 +1,55 @@
1class app.ServiceWorker
2 $.extend @prototype, Events
3
4 @isEnabled: ->
5 !!navigator.serviceWorker
6
7 constructor: ->
8 @registration = null
9 @installingRegistration = null
10 @notifyUpdate = true
11
12 navigator.serviceWorker.register(app.config.service_worker_path, {scope: '/'})
13 .then((registration) => @updateRegistration(registration))
14 .catch((error) -> console.error 'Could not register service worker:', error)
15
16 update: ->
17 return unless @registration
18 @notifyUpdate = true
19 return @doUpdate()
20
21 updateInBackground: ->
22 return unless @registration
23 @notifyUpdate = false
24 return @doUpdate()
25
26 reload: ->
27 return @updateInBackground().then(() -> app.reboot())
28
29 doUpdate: ->
30 return @registration.update().catch(->)
31
32 updateRegistration: (registration) ->
33 $.off @registration, 'updatefound', @onUpdateFound if @registration
34 $.off @installingRegistration, 'statechange', @onStateChange if @installingRegistration
35
36 @registration = registration
37 @installingRegistration = null
38
39 $.on @registration, 'updatefound', @onUpdateFound
40 return
41
42 onUpdateFound: () =>
43 @installingRegistration = @registration.installing
44 $.on @installingRegistration, 'statechange', @onStateChange
45 return
46
47 onStateChange: () =>
48 if @installingRegistration.state == 'installed' and navigator.serviceWorker.controller
49 @updateRegistration(@installingRegistration)
50 @onUpdateReady()
51 return
52
53 onUpdateReady: ->
54 @trigger 'updateready' if @notifyUpdate
55 return
assets/javascripts/app/update_checker.coffee+3−3
@@ -3,13 +3,13 @@ class app.UpdateChecker · 2 unmodified lines
3 @lastCheck = Date.now()3 @lastCheck = Date.now()
4 4
5 $.on window, 'focus', @onFocus5 $.on window, 'focus', @onFocus
6 app.appCache.on 'updateready', @onUpdateReady if app.appCache6 app.serviceWorker.on 'updateready', @onUpdateReady if app.serviceWorker
7 7
8 setTimeout @checkDocs, 08 setTimeout @checkDocs, 0
9 9
10 check: ->10 check: ->
11 if app.appCache11 if app.serviceWorker
12 app.appCache.update()12 app.serviceWorker.update()
13 else13 else
14 ajax14 ajax
15 url: $('script[src*="application"]').getAttribute('src')15 url: $('script[src*="application"]').getAttribute('src')
assets/javascripts/templates/pages/offline_tmpl.coffee+3−3
@@ -26,7 +26,7 @@ app.templates.offlinePage = (docs) -> """ · 25 unmodified lines
26 <dl>26 <dl>
27 <dt>How does this work?27 <dt>How does this work?
28 <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>28 <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>
29 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.29 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.
30 <dt>Can I close the tab/browser?30 <dt>Can I close the tab/browser?
31 <dd>#{canICloseTheTab()}31 <dd>#{canICloseTheTab()}
32 <dt>What if I don't update a documentation?32 <dt>What if I don't update a documentation?
@@ -41,10 +41,10 @@ app.templates.offlinePage = (docs) -> """ · 8 unmodified lines
41"""41"""
42 42
43canICloseTheTab = ->43canICloseTheTab = ->
44 if app.AppCache.isEnabled()44 if app.ServiceWorker.isEnabled()
45 """ 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). """45 """ 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). """
46 else46 else
47 """ 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>47 """ 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>
48 The current tab will continue to function even when you go offline (provided you installed all the documentations beforehand). """48 The current tab will continue to function even when you go offline (provided you installed all the documentations beforehand). """
49 49
50app.templates.offlineDoc = (doc, status) ->50app.templates.offlineDoc = (doc, status) ->
assets/javascripts/views/content/entry_page.coffee+1−1
@@ -123,7 +123,7 @@ class app.views.EntryPage extends app.View · 122 unmodified lines
123 @render @tmpl('pageLoadError')123 @render @tmpl('pageLoadError')
124 @resetClass()124 @resetClass()
125 @addClass @constructor.errorClass125 @addClass @constructor.errorClass
126 app.appCache?.update()126 app.serviceWorker?.update()
127 return127 return
128 128
129 cache: ->129 cache: ->
assets/javascripts/views/content/settings_page.coffee+0−2
@@ -22,12 +22,10 @@ class app.views.SettingsPage extends app.View · 21 unmodified lines
22 22
23 toggleDark: (enable) ->23 toggleDark: (enable) ->
24 app.settings.set('dark', !!enable)24 app.settings.set('dark', !!enable)
25 app.appCache?.updateInBackground()
26 return25 return
27 26
28 toggleLayout: (layout, enable) ->27 toggleLayout: (layout, enable) ->
29 app.settings.setLayout(layout, enable)28 app.settings.setLayout(layout, enable)
30 app.appCache?.updateInBackground()
31 return29 return
32 30
33 toggleSmoothScroll: (enable) ->31 toggleSmoothScroll: (enable) ->
assets/javascripts/views/layout/resizer.coffee+1−3
@@ -26,9 +26,7 @@ class app.views.Resizer extends app.View · 25 unmodified lines
26 newSize = "#{value}px"26 newSize = "#{value}px"
27 @style.innerHTML = @style.innerHTML.replace(new RegExp(@size, 'g'), newSize)27 @style.innerHTML = @style.innerHTML.replace(new RegExp(@size, 'g'), newSize)
28 @size = newSize28 @size = newSize
29 if save29 app.settings.setSize(value) if save
30 app.settings.setSize(value)
31 app.appCache?.updateInBackground()
32 return30 return
33 31
34 onDragStart: (event) =>32 onDragStart: (event) =>
assets/javascripts/views/layout/settings.coffee+1−9
@@ -25,7 +25,6 @@ class app.views.Settings extends app.View · 24 unmodified lines
25 if super25 if super
26 @render()26 @render()
27 document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT)27 document.body.classList.remove(SIDEBAR_HIDDEN_LAYOUT)
28 app.appCache?.on 'progress', @onAppCacheProgress
29 return28 return
30 29
31 deactivate: ->30 deactivate: ->
@@ -33,7 +32,6 @@ class app.views.Settings extends app.View · 1 unmodified line
33 @resetClass()32 @resetClass()
34 @docPicker.detach()33 @docPicker.detach()
35 document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT) if app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT)34 document.body.classList.add(SIDEBAR_HIDDEN_LAYOUT) if app.settings.hasLayout(SIDEBAR_HIDDEN_LAYOUT)
36 app.appCache?.off 'progress', @onAppCacheProgress
37 return35 return
38 36
39 render: ->37 render: ->
@@ -52,7 +50,7 @@ class app.views.Settings extends app.View · 12 unmodified lines
52 docs = @docPicker.getSelectedDocs()50 docs = @docPicker.getSelectedDocs()
53 app.settings.setDocs(docs)51 app.settings.setDocs(docs)
54 52
55 @saveBtn.textContent = if app.appCache then 'Downloading\u2026' else 'Saving\u2026'53 @saveBtn.textContent = 'Saving\u2026'
56 disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1)54 disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1)
57 disabledDocs.uninstall ->55 disabledDocs.uninstall ->
58 app.db.migrate()56 app.db.migrate()
@@ -83,9 +81,3 @@ class app.views.Settings extends app.View · 24 unmodified lines
83 $.stopEvent(event)81 $.stopEvent(event)
84 app.router.show '/'82 app.router.show '/'
85 return83 return
86
87 onAppCacheProgress: (event) =>
88 if event.lengthComputable
89 percentage = Math.round event.loaded * 100 / event.total
90 @saveBtn.textContent = "Downloading\u2026 (#{percentage}%)"
91 return
lib/app.rb+5−5
@@ -220,7 +220,7 @@ class App < Sinatra::Application · 219 unmodified lines
220 app_theme == 'dark'220 app_theme == 'dark'
221 end221 end
222 222
223 def redirect_via_js(path) # courtesy of HTML5 App Cache223 def redirect_via_js(path)
224 response.set_cookie :initial_path, value: path, expires: Time.now + 15, path: '/'224 response.set_cookie :initial_path, value: path, expires: Time.now + 15, path: '/'
225 redirect '/', 302225 redirect '/', 302
226 end226 end
@@ -243,15 +243,15 @@ class App < Sinatra::Application · 16 unmodified lines
243 end243 end
244 end244 end
245 245
246 get '/manifest.appcache' do246 get '/service-worker.js' do
247 content_type 'text/cache-manifest'247 content_type 'application/javascript'
248 expires 0, :'no-cache'248 expires 0, :'no-cache'
249 erb :manifest249 erb :'service-worker.js'
250 end250 end
251 251
252 get '/' do252 get '/' do
253 return redirect "/#q=#{params[:q]}" if params[:q]253 return redirect "/#q=#{params[:q]}" if params[:q]
254 return redirect '/' unless request.query_string.empty? # courtesy of HTML5 App Cache254 return redirect '/' unless request.query_string.empty?
255 response.headers['Content-Security-Policy'] = settings.csp if settings.csp255 response.headers['Content-Security-Policy'] = settings.csp if settings.csp
256 erb :index256 erb :index
257 end257 end
test/app_test.rb+0−52
@@ -106,58 +106,6 @@ class AppTest < MiniTest::Spec · 105 unmodified lines
106 end106 end
107 end107 end
108 108
109 describe "/manifest.appcache" do
110 it "works" do
111 get '/manifest.appcache'
112 assert last_response.ok?
113 end
114
115 it "works with cookie" do
116 set_cookie('docs=css/html~5')
117 get '/manifest.appcache'
118 assert last_response.ok?
119 assert_includes last_response.body, '/css/index.json?1420139788'
120 assert_includes last_response.body, '/html~5/index.json?1420139791'
121 end
122
123 it "ignores invalid docs in the cookie" do
124 set_cookie('docs=foo')
125 get '/manifest.appcache'
126 assert last_response.ok?
127 refute_includes last_response.body, 'foo'
128 end
129
130 it "has the word 'default' when no 'dark' cookie is set" do
131 get '/manifest.appcache'
132 assert_includes last_response.body, '# default'
133 refute_includes last_response.body, '# dark'
134 end
135
136 it "has the word 'dark' when the cookie is set" do
137 set_cookie('dark=1')
138 get '/manifest.appcache'
139 assert_includes last_response.body, '# dark'
140 refute_includes last_response.body, '# default'
141 end
142
143 it "sets default size" do
144 get '/manifest.appcache'
145 assert_includes last_response.body, '20rem'
146 end
147
148 it "sets size from cookie" do
149 set_cookie('size=42')
150 get '/manifest.appcache'
151 assert_includes last_response.body, '42px'
152 end
153
154 it "sets layout from cookie" do
155 set_cookie('layout=foo_layout')
156 get '/manifest.appcache'
157 assert_includes last_response.body, 'foo_layout'
158 end
159 end
160
161 describe "/[doc]" do109 describe "/[doc]" do
162 it "renders when the doc exists and isn't enabled" do110 it "renders when the doc exists and isn't enabled" do
163 set_cookie('docs=html~5')111 set_cookie('docs=html~5')
views/index.erb+1−1
@@ -1,5 +1,5 @@
1<!DOCTYPE html>1<!DOCTYPE html>
2<html<%= ' manifest="/manifest.appcache"' if App.production? %> prefix="og: http://ogp.me/ns#" lang="en" class="_booting _theme-<%= app_theme %>">2<html prefix="og: http://ogp.me/ns#" lang="en" class="_booting _theme-<%= app_theme %>">
3<head>3<head>
4 <meta charset="utf-8">4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no">5 <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no">
views/manifest.erb+0−14
@@ -1,14 +0,0 @@
1CACHE MANIFEST
2# <%= app_theme %> <%= app_size %> <%= app_layout %>
3
4CACHE:
5/
6<%= manifest_asset_urls.join "\n" %>
7<%= doc_index_urls.join "\n" %>
8
9NETWORK:
10/s/
11*
12
13FALLBACK:
14/ /
views/service-worker.js.erb+64−0
@@ -0,0 +1,64 @@
1<%# Use the hash of the application.js file as cache name, or 'app' if not running in production %>
2<%# This ensures that the cache is always updated if the hash of the application.js file changes %>
3const cacheName = '<%= javascript_path('application', asset_host: false).scan(/application-([^\.]+)\.js/).last&.first || 'app' %>';
4
5<%# Paths to cache when the service worker is installed %>
6const cachePaths = [
7 '/',
8 '/favicon.ico',
9 '/manifest.json',
10 '/images/webapp-icon-32.png',
11 '/images/webapp-icon-60.png',
12 '/images/webapp-icon-80.png',
13 '/images/webapp-icon-128.png',
14 '/images/webapp-icon-256.png',
15 '/images/webapp-icon-512.png',
16 '<%= manifest_asset_urls.join "',\n '" %>',
17 '<%= doc_index_urls.join "',\n '" %>',
18];
19
20<%# Set-up the cache %>
21self.addEventListener('install', event => {
22 self.skipWaiting();
23
24 event.waitUntil(
25 caches.open(cacheName).then(cache => cache.addAll(cachePaths)),
26 );
27});
28
29<%# Remove old caches %>
30self.addEventListener('activate', event => {
31 event.waitUntil(
32 caches.keys().then(keys => Promise.all(
33 keys.map(key => {
34 if (key !== cacheName) {
35 return caches.delete(key);
36 }
37 })
38 ))
39 );
40});
41
42<%# Handle HTTP requests %>
43self.addEventListener('fetch', event => {
44 event.respondWith(
45 caches.match(event.request).then(response => {
46 if (response) {
47 return response;
48 }
49
50 return fetch(event.request)
51 .catch(err => {
52 const url = new URL(event.request.url);
53
54 <%# Return the index page from the cache if the user is visiting a url like devdocs.io/javascript/global_objects/array/find %>
55 <%# The index page will make sure the correct documentation or a proper offline page is shown %>
56 if (url.origin === location.origin && !url.pathname.includes('.')) {
57 return caches.match('/').then(response => response || err);
58 }
59
60 return err;
61 });
62 })
63 );
64});
views/unsupported.erb+2−2
@@ -11,9 +11,9 @@ · 10 unmodified lines
11 <p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:</p>11 <p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:</p>
12 <ul class="_fail-list">12 <ul class="_fail-list">
13 <li>Recent versions of Firefox, Chrome, or Opera</li>13 <li>Recent versions of Firefox, Chrome, or Opera</li>
14 <li>Safari 9.1+</li>14 <li>Safari 11.1+</li>
15 <li>Edge 16+</li>15 <li>Edge 16+</li>
16 <li>iOS 10+</li>16 <li>iOS 11.3+</li>
17 </ul>17 </ul>
18 <p class="_fail-text">18 <p class="_fail-text">
19 If you're unable to upgrade, we apologize.19 If you're unable to upgrade, we apologize.