mirror of
https://github.com/freeCodeCamp/devdocs
synced 2024-11-16 19:48:10 +01:00
382 lines
9.4 KiB
CoffeeScript
382 lines
9.4 KiB
CoffeeScript
class app.DB
|
|
NAME = 'docs'
|
|
VERSION = 15
|
|
|
|
constructor: ->
|
|
@versionMultipler = if $.isIE() then 1e5 else 1e9
|
|
@useIndexedDB = @useIndexedDB()
|
|
@callbacks = []
|
|
|
|
db: (fn) ->
|
|
return fn() unless @useIndexedDB
|
|
@callbacks.push(fn) if fn
|
|
return if @open
|
|
|
|
try
|
|
@open = true
|
|
req = indexedDB.open(NAME, VERSION * @versionMultipler + @userVersion())
|
|
req.onsuccess = @onOpenSuccess
|
|
req.onerror = @onOpenError
|
|
req.onupgradeneeded = @onUpgradeNeeded
|
|
catch error
|
|
@fail 'exception', error
|
|
return
|
|
|
|
onOpenSuccess: (event) =>
|
|
db = event.target.result
|
|
|
|
if db.objectStoreNames.length is 0
|
|
try db.close()
|
|
@open = false
|
|
@fail 'empty'
|
|
else if error = @buggyIDB(db)
|
|
try db.close()
|
|
@open = false
|
|
@fail 'buggy', error
|
|
else
|
|
@runCallbacks(db)
|
|
@open = false
|
|
db.close()
|
|
return
|
|
|
|
onOpenError: (event) =>
|
|
event.preventDefault()
|
|
@open = false
|
|
error = event.target.error
|
|
|
|
switch error.name
|
|
when 'QuotaExceededError'
|
|
@onQuotaExceededError()
|
|
when 'VersionError'
|
|
@onVersionError()
|
|
when 'InvalidStateError'
|
|
@fail 'private_mode'
|
|
else
|
|
@fail 'cant_open', error
|
|
return
|
|
|
|
fail: (reason, error) ->
|
|
@cachedDocs = null
|
|
@useIndexedDB = false
|
|
@reason or= reason
|
|
@error or= error
|
|
console.error? 'IDB error', error if error
|
|
@runCallbacks()
|
|
if error and reason is 'cant_open'
|
|
Raven.captureMessage "#{error.name}: #{error.message}", level: 'warning', fingerprint: [error.name]
|
|
return
|
|
|
|
onQuotaExceededError: ->
|
|
@reset()
|
|
@db()
|
|
app.onQuotaExceeded()
|
|
Raven.captureMessage 'QuotaExceededError', level: 'warning'
|
|
return
|
|
|
|
onVersionError: ->
|
|
req = indexedDB.open(NAME)
|
|
req.onsuccess = (event) =>
|
|
@handleVersionMismatch event.target.result.version
|
|
req.onerror = (event) ->
|
|
event.preventDefault()
|
|
@fail 'cant_open', error
|
|
return
|
|
|
|
handleVersionMismatch: (actualVersion) ->
|
|
if Math.floor(actualVersion / @versionMultipler) isnt VERSION
|
|
@fail 'version'
|
|
else
|
|
@setUserVersion actualVersion - VERSION * @versionMultipler
|
|
@db()
|
|
return
|
|
|
|
buggyIDB: (db) ->
|
|
return if @checkedBuggyIDB
|
|
@checkedBuggyIDB = true
|
|
try
|
|
@idbTransaction(db, stores: $.makeArray(db.objectStoreNames)[0..1], mode: 'readwrite').abort() # https://bugs.webkit.org/show_bug.cgi?id=136937
|
|
return
|
|
catch error
|
|
return error
|
|
|
|
runCallbacks: (db) ->
|
|
fn(db) while fn = @callbacks.shift()
|
|
return
|
|
|
|
onUpgradeNeeded: (event) ->
|
|
return unless db = event.target.result
|
|
|
|
objectStoreNames = $.makeArray(db.objectStoreNames)
|
|
|
|
unless $.arrayDelete(objectStoreNames, 'docs')
|
|
try db.createObjectStore('docs')
|
|
|
|
for doc in app.docs.all() when not $.arrayDelete(objectStoreNames, doc.slug)
|
|
try db.createObjectStore(doc.slug)
|
|
|
|
for name in objectStoreNames
|
|
try db.deleteObjectStore(name)
|
|
return
|
|
|
|
store: (doc, data, onSuccess, onError, _retry = true) ->
|
|
@db (db) =>
|
|
unless db
|
|
onError()
|
|
return
|
|
|
|
txn = @idbTransaction db, stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false
|
|
txn.oncomplete = =>
|
|
@cachedDocs?[doc.slug] = doc.mtime
|
|
onSuccess()
|
|
return
|
|
txn.onerror = (event) =>
|
|
event.preventDefault()
|
|
if txn.error?.name is 'NotFoundError' and _retry
|
|
@migrate()
|
|
setTimeout =>
|
|
@store(doc, data, onSuccess, onError, false)
|
|
, 0
|
|
else
|
|
onError(event)
|
|
return
|
|
|
|
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
|
|
return
|
|
|
|
unstore: (doc, onSuccess, onError, _retry = true) ->
|
|
@db (db) =>
|
|
unless db
|
|
onError()
|
|
return
|
|
|
|
txn = @idbTransaction db, stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false
|
|
txn.oncomplete = =>
|
|
delete @cachedDocs?[doc.slug]
|
|
onSuccess()
|
|
return
|
|
txn.onerror = (event) ->
|
|
event.preventDefault()
|
|
if txn.error?.name is 'NotFoundError' and _retry
|
|
@migrate()
|
|
setTimeout =>
|
|
@unstore(doc, onSuccess, onError, false)
|
|
, 0
|
|
else
|
|
onError(event)
|
|
return
|
|
|
|
store = txn.objectStore('docs')
|
|
store.delete(doc.slug)
|
|
|
|
store = txn.objectStore(doc.slug)
|
|
store.clear()
|
|
return
|
|
return
|
|
|
|
version: (doc, fn) ->
|
|
if (version = @cachedVersion(doc))?
|
|
fn(version)
|
|
return
|
|
|
|
@db (db) =>
|
|
unless db
|
|
fn(false)
|
|
return
|
|
|
|
txn = @idbTransaction db, stores: ['docs'], mode: 'readonly'
|
|
store = txn.objectStore('docs')
|
|
|
|
req = store.get(doc.slug)
|
|
req.onsuccess = ->
|
|
fn(req.result)
|
|
return
|
|
req.onerror = (event) ->
|
|
event.preventDefault()
|
|
fn(false)
|
|
return
|
|
return
|
|
return
|
|
|
|
cachedVersion: (doc) ->
|
|
return unless @cachedDocs
|
|
@cachedDocs[doc.slug] or false
|
|
|
|
versions: (docs, fn) ->
|
|
if versions = @cachedVersions(docs)
|
|
fn(versions)
|
|
return
|
|
|
|
@db (db) =>
|
|
unless db
|
|
fn(false)
|
|
return
|
|
|
|
txn = @idbTransaction db, stores: ['docs'], mode: 'readonly'
|
|
txn.oncomplete = ->
|
|
fn(result)
|
|
return
|
|
store = txn.objectStore('docs')
|
|
result = {}
|
|
|
|
docs.forEach (doc) ->
|
|
req = store.get(doc.slug)
|
|
req.onsuccess = ->
|
|
result[doc.slug] = req.result
|
|
return
|
|
req.onerror = (event) ->
|
|
event.preventDefault()
|
|
result[doc.slug] = false
|
|
return
|
|
return
|
|
return
|
|
|
|
cachedVersions: (docs) ->
|
|
return unless @cachedDocs
|
|
result = {}
|
|
result[doc.slug] = @cachedVersion(doc) for doc in docs
|
|
result
|
|
|
|
load: (entry, onSuccess, onError) ->
|
|
if @shouldLoadWithIDB(entry)
|
|
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) ->
|
|
@db (db) =>
|
|
unless db
|
|
onError()
|
|
return
|
|
|
|
unless db.objectStoreNames.contains(entry.doc.slug)
|
|
onError()
|
|
@loadDocsCache(db)
|
|
return
|
|
|
|
txn = @idbTransaction db, stores: [entry.doc.slug], mode: 'readonly'
|
|
store = txn.objectStore(entry.doc.slug)
|
|
|
|
req = store.get(entry.dbPath())
|
|
req.onsuccess = ->
|
|
if req.result then onSuccess(req.result) else onError()
|
|
return
|
|
req.onerror = (event) ->
|
|
event.preventDefault()
|
|
onError()
|
|
return
|
|
@loadDocsCache(db)
|
|
return
|
|
|
|
loadDocsCache: (db) ->
|
|
return if @cachedDocs
|
|
@cachedDocs = {}
|
|
|
|
txn = @idbTransaction db, stores: ['docs'], mode: 'readonly'
|
|
txn.oncomplete = =>
|
|
setTimeout(@checkForCorruptedDocs, 50)
|
|
return
|
|
|
|
req = txn.objectStore('docs').openCursor()
|
|
req.onsuccess = (event) =>
|
|
return unless cursor = event.target.result
|
|
@cachedDocs[cursor.key] = cursor.value
|
|
cursor.continue()
|
|
return
|
|
req.onerror = (event) ->
|
|
event.preventDefault()
|
|
return
|
|
return
|
|
|
|
checkForCorruptedDocs: =>
|
|
@db (db) =>
|
|
@corruptedDocs = []
|
|
docs = (key for key, value of @cachedDocs when value)
|
|
return if docs.length is 0
|
|
|
|
for slug in docs when not app.docs.findBy('slug', slug)
|
|
@corruptedDocs.push(slug)
|
|
|
|
for slug in @corruptedDocs
|
|
$.arrayDelete(docs, slug)
|
|
|
|
if docs.length is 0
|
|
setTimeout(@deleteCorruptedDocs, 0)
|
|
return
|
|
|
|
txn = @idbTransaction(db, stores: docs, mode: 'readonly', ignoreError: false)
|
|
txn.oncomplete = =>
|
|
setTimeout(@deleteCorruptedDocs, 0) if @corruptedDocs.length > 0
|
|
return
|
|
|
|
for doc in docs
|
|
txn.objectStore(doc).get('index').onsuccess = (event) =>
|
|
@corruptedDocs.push(event.target.source.name) unless event.target.result
|
|
return
|
|
return
|
|
return
|
|
|
|
deleteCorruptedDocs: =>
|
|
@db (db) =>
|
|
txn = @idbTransaction(db, stores: ['docs'], mode: 'readwrite', ignoreError: false)
|
|
store = txn.objectStore('docs')
|
|
while doc = @corruptedDocs.pop()
|
|
@cachedDocs[doc] = false
|
|
store.delete(doc)
|
|
return
|
|
Raven.captureMessage 'corruptedDocs', level: 'info', extra: { docs: @corruptedDocs.join(',') }
|
|
return
|
|
|
|
shouldLoadWithIDB: (entry) ->
|
|
@useIndexedDB and (not @cachedDocs or @cachedDocs[entry.doc.slug])
|
|
|
|
idbTransaction: (db, options) ->
|
|
app.lastIDBTransaction = [options.stores, options.mode]
|
|
txn = db.transaction(options.stores, options.mode)
|
|
unless options.ignoreError is false
|
|
txn.onerror = (event) ->
|
|
event.preventDefault()
|
|
return
|
|
unless options.ignoreAbort is false
|
|
txn.onabort = (event) ->
|
|
event.preventDefault()
|
|
return
|
|
txn
|
|
|
|
reset: ->
|
|
try indexedDB?.deleteDatabase(NAME) catch
|
|
return
|
|
|
|
useIndexedDB: ->
|
|
try
|
|
if !app.isSingleDoc() and window.indexedDB
|
|
true
|
|
else
|
|
@reason = 'not_supported'
|
|
false
|
|
catch
|
|
false
|
|
|
|
migrate: ->
|
|
app.settings.set('schema', @userVersion() + 1)
|
|
return
|
|
|
|
setUserVersion: (version) ->
|
|
app.settings.set('schema', version)
|
|
return
|
|
|
|
userVersion: ->
|
|
app.settings.get('schema')
|