| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- 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')
|