Add ability to cache complete documntations in IndexedDB

pull/165/head
Thibaut 10 years ago
parent bc5488faa2
commit 25f844da9b

@ -61,7 +61,7 @@
bootOne: ->
@doc = new app.models.Doc @DOC
@docs.reset [@doc]
@doc.load @start.bind(@), @onBootError.bind(@), readCache: true
@doc.load @bootDB.bind(@), @onBootError.bind(@), readCache: true
new app.views.Notice 'singleDoc', @doc
delete @DOC
return
@ -72,20 +72,25 @@
(if docs.indexOf(doc.slug) >= 0 then @docs else @disabledDocs).add(doc)
@docs.sort()
@disabledDocs.sort()
@docs.load @start.bind(@), @onBootError.bind(@), readCache: true, writeCache: true
@docs.load @bootDB.bind(@), @onBootError.bind(@), readCache: true, writeCache: true
delete @DOCS
return
start: ->
bootDB: ->
for doc in @docs.all()
@entries.add doc.toEntry()
@entries.add type.toEntry() for type in doc.types.all()
@entries.add doc.entries.all()
@db = new app.DB()
@db.init(@start.bind(@))
return
start: ->
@trigger 'ready'
@router.start()
@hideLoading()
@welcomeBack() unless @doc
@removeEvent 'ready bootError'
return

@ -8,3 +8,4 @@ app.config =
production_host: 'devdocs.io'
search_param: 'q'
sentry_dsn: '<%= App.sentry_dsn %>'
version: '<%= Time.now.to_i %>'

@ -0,0 +1,115 @@
class app.DB
NAME = 'docs'
constructor: ->
@useIndexedDB = @useIndexedDB()
init: (@_callback) ->
if @useIndexedDB
@initIndexedDB()
else
@callback()
return
initIndexedDB: ->
try
req = indexedDB.open(NAME, @indexedDBVersion())
req.onerror = @callback
req.onsuccess = @onOpenSuccess
req.onupgradeneeded = @onUpgradeNeeded
catch
@callback()
return
isEnabled: ->
!!@db
callback: =>
@_callback?()
@_callback = null
return
onOpenSuccess: (event) =>
try
@db = event.target.result
@db.transaction(['docs', app.docs.all()[0].slug], 'readwrite').abort() # https://bugs.webkit.org/show_bug.cgi?id=136937
catch
@db = null
@callback()
return
onUpgradeNeeded: (event) =>
db = event.target.result
unless db.objectStoreNames.contains('docs')
db.createObjectStore('docs')
for doc in app.docs.all() when not db.objectStoreNames.contains(doc.slug)
db.createObjectStore(doc.slug)
for doc in app.disabledDocs.all() when not db.objectStoreNames.contains(doc.slug)
db.createObjectStore(doc.slug)
return
store: (doc, data, onSuccess, onError) ->
txn = @db.transaction ['docs', doc.slug], 'readwrite'
txn.oncomplete = -> if txn.error then onError() else onSuccess()
store = txn.objectStore(doc.slug)
store.clear()
store.add(content, path) for path, content of data
store = txn.objectStore('docs')
store.put(doc.mtime, doc.slug)
return
unstore: (doc, onSuccess, onError) ->
txn = @db.transaction ['docs', doc.slug], 'readwrite'
txn.oncomplete = -> if txn.error then onError() else onSuccess()
store = txn.objectStore(doc.slug)
store.clear()
store = txn.objectStore('docs')
store.delete(doc.slug)
return
version: (doc, callback) ->
txn = @db.transaction ['docs'], 'readonly'
store = txn.objectStore('docs')
req = store.get(doc.slug)
req.onsuccess = -> callback(!!req.result)
req.onerror = -> callback(false)
return
load: (entry, onSuccess, onError) ->
if @isEnabled()
onError = @loadWithXHR.bind(@, entry, onSuccess, onError)
@loadWithIDB(entry, onSuccess, onError)
else
@loadWithXHR(entry, onSuccess, onError)
loadWithXHR: (entry, onSuccess, onError) ->
ajax
url: entry.fileUrl()
dataType: 'html'
success: onSuccess
error: onError
loadWithIDB: (entry, onSuccess, onError) ->
txn = @db.transaction [entry.doc.slug], 'readonly'
store = txn.objectStore(entry.doc.slug)
req = store.get(entry.path)
req.onsuccess = -> if req.result then onSuccess(req.result) else onError()
req.onerror = onError
txn
useIndexedDB: ->
!app.isSingleDoc() and !!window.indexedDB
indexedDBVersion: ->
if app.config.env is 'production' then app.config.version else Date.now() / 1000

@ -4,6 +4,7 @@ class app.Router
@routes: [
['*', 'before' ]
['/', 'root' ]
['/offline', 'offline' ]
['/about', 'about' ]
['/news', 'news' ]
['/help', 'help' ]
@ -75,6 +76,10 @@ class app.Router
@triggerRoute 'root'
return
offline: ->
@triggerRoute 'offline'
return
about: (context) ->
context.page = 'about'
@triggerRoute 'page'

@ -27,6 +27,9 @@ class app.models.Doc extends app.Model
fileUrl: (path) ->
"#{app.config.docs_host}#{@fullPath(path)}"
dbUrl: ->
"#{app.config.docs_host}/#{@db_path}?#{@mtime}"
indexUrl: ->
"#{app.indexHost()}/#{@index_path}?#{@mtime}"
@ -83,3 +86,39 @@ class app.models.Doc extends app.Model
_setCache: (data) ->
app.store.set @slug, [@mtime, data]
return
download: (onSuccess, onError) ->
return if @downloading
@downloading = true
error = =>
@downloading = null
onError()
success = (data) =>
@downloading = null
app.db.store @, data, onSuccess, error
ajax
url: @dbUrl()
success: success
error: error
return
undownload: (onSuccess, onError) ->
return if @downloading
@downloading = true
success = =>
@downloading = null
onSuccess()
error = =>
@downloading = null
onError()
app.db.unstore @, success, error
getDownloadStatus: (callback) ->
app.db.version @, (value) ->
callback downloaded: !!value, version: value

@ -41,8 +41,4 @@ class app.models.Entry extends app.Model
@doc.types.findBy 'name', @type
loadFile: (onSuccess, onError) ->
ajax
url: @fileUrl()
dataType: 'html'
success: onSuccess
error: onError
app.db.load(@, onSuccess, onError)

@ -0,0 +1,16 @@
app.templates.offlinePage = ->
""" <h1 class="_lined-heading">Offline</h1>
<table class="_docs">
#{app.templates.render 'offlineDoc', app.docs.all()}
</table> """
app.templates.offlineDoc = (doc) ->
"""<tr data-slug="#{doc.slug}"></tr>"""
app.templates.offlineDocContent = (doc, status) ->
html = """<th class="_icon-#{doc.slug}">#{doc.name}</th>"""
html += if status.downloaded
"""<td><a data-del>Delete</a></td>"""
else
"""<td><a data-dl>Download</a></td>"""
html

@ -25,6 +25,7 @@ class app.views.Content extends app.View
@rootPage = new app.views.RootPage
@staticPage = new app.views.StaticPage
@offlinePage = new app.views.OfflinePage
@typePage = new app.views.TypePage
@entryPage = new app.views.EntryPage
@ -137,6 +138,8 @@ class app.views.Content extends app.View
@show @entryPage
when 'type'
@show @typePage
when 'offline'
@show @offlinePage
else
@show @staticPage

@ -0,0 +1,63 @@
class app.views.OfflinePage extends app.View
@className: '_static'
@events:
click: 'onClick'
@elements:
list: '_._docs'
deactivate: ->
if super
@empty()
return
render: ->
@html @tmpl('offlinePage')
@refreshElements()
app.docs.each(@renderDoc)
return
renderDoc: (doc) =>
doc.getDownloadStatus (status) =>
html = app.templates.render('offlineDocContent', doc, status)
el = @docEl(doc)
el.className = ''
el.innerHTML = html
return
getTitle: ->
'Offline'
getDoc: (el) ->
el = el.parentNode until slug = el.getAttribute('data-slug')
app.docs.findBy('slug', slug)
docEl: (doc) ->
@find("[data-slug='#{doc.slug}']")
onRoute: ->
@render()
return
onClick: (event) =>
if event.target.hasAttribute('data-dl')
action = 'download'
else if event.target.hasAttribute('data-del')
action = 'undownload'
if action
$.stopEvent(event)
doc = @getDoc(event.target)
doc[action](@onDownloadSuccess.bind(@, doc), @onDownloadError.bind(@, doc))
@docEl(doc).classList.add("#{action}ing")
return
onDownloadSuccess: (doc) ->
@renderDoc(doc)
return
onDownloadError: (doc) ->
el = @docEl(doc)
el.className = ''
el.classList.add('error')

@ -242,6 +242,51 @@
td:first-child, td:last-child { white-space: nowrap; }
}
//
// Doc table
//
._docs {
width: 100%;
line-height: 1.5rem;
th {
max-width: 0;
padding-left: .5rem;
padding-right: .5rem;
white-space: nowrap;
font-weight: normal;
&:before {
float: left;
margin: .25rem .5rem .25rem 0;
@extend %icon;
}
}
td:last-child { text-align: right; }
td > a { cursor: pointer; }
tr.downloading > td:last-child {
> a { display: none; }
&:before { content: 'Downloading…' }
}
tr.undownloading > td:last-child {
> a { display: none; }
&:before { content: 'Deleting…' }
}
tr.error > td:last-child {
> a { display: none; }
&:before {
content: 'Error';
color: red;
}
}
}
//
// News
//

@ -122,7 +122,7 @@ class App < Sinatra::Application
erb :index
end
%w(about news help).each do |page|
%w(offline about news help).each do |page|
get "/#{page}" do
redirect "/#/#{page}", 302
end

Loading…
Cancel
Save