db.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. class app.DB
  2. NAME = 'docs'
  3. VERSION = 15
  4. constructor: ->
  5. @versionMultipler = if $.isIE() then 1e5 else 1e9
  6. @useIndexedDB = @useIndexedDB()
  7. @callbacks = []
  8. db: (fn) ->
  9. return fn() unless @useIndexedDB
  10. @callbacks.push(fn) if fn
  11. return if @open
  12. try
  13. @open = true
  14. req = indexedDB.open(NAME, VERSION * @versionMultipler + @userVersion())
  15. req.onsuccess = @onOpenSuccess
  16. req.onerror = @onOpenError
  17. req.onupgradeneeded = @onUpgradeNeeded
  18. catch error
  19. @fail 'exception', error
  20. return
  21. onOpenSuccess: (event) =>
  22. db = event.target.result
  23. if db.objectStoreNames.length is 0
  24. try db.close()
  25. @open = false
  26. @fail 'empty'
  27. else if error = @buggyIDB(db)
  28. try db.close()
  29. @open = false
  30. @fail 'buggy', error
  31. else
  32. @runCallbacks(db)
  33. @open = false
  34. db.close()
  35. return
  36. onOpenError: (event) =>
  37. event.preventDefault()
  38. @open = false
  39. error = event.target.error
  40. switch error.name
  41. when 'QuotaExceededError'
  42. @onQuotaExceededError()
  43. when 'VersionError'
  44. @onVersionError()
  45. when 'InvalidStateError'
  46. @fail 'private_mode'
  47. else
  48. @fail 'cant_open', error
  49. return
  50. fail: (reason, error) ->
  51. @cachedDocs = null
  52. @useIndexedDB = false
  53. @reason or= reason
  54. @error or= error
  55. console.error? 'IDB error', error if error
  56. @runCallbacks()
  57. if error and reason is 'cant_open'
  58. Raven.captureMessage "#{error.name}: #{error.message}", level: 'warning', fingerprint: [error.name]
  59. return
  60. onQuotaExceededError: ->
  61. @reset()
  62. @db()
  63. app.onQuotaExceeded()
  64. Raven.captureMessage 'QuotaExceededError', level: 'warning'
  65. return
  66. onVersionError: ->
  67. req = indexedDB.open(NAME)
  68. req.onsuccess = (event) =>
  69. @handleVersionMismatch event.target.result.version
  70. req.onerror = (event) ->
  71. event.preventDefault()
  72. @fail 'cant_open', error
  73. return
  74. handleVersionMismatch: (actualVersion) ->
  75. if Math.floor(actualVersion / @versionMultipler) isnt VERSION
  76. @fail 'version'
  77. else
  78. @setUserVersion actualVersion - VERSION * @versionMultipler
  79. @db()
  80. return
  81. buggyIDB: (db) ->
  82. return if @checkedBuggyIDB
  83. @checkedBuggyIDB = true
  84. try
  85. @idbTransaction(db, stores: $.makeArray(db.objectStoreNames)[0..1], mode: 'readwrite').abort() # https://bugs.webkit.org/show_bug.cgi?id=136937
  86. return
  87. catch error
  88. return error
  89. runCallbacks: (db) ->
  90. fn(db) while fn = @callbacks.shift()
  91. return
  92. onUpgradeNeeded: (event) ->
  93. return unless db = event.target.result
  94. objectStoreNames = $.makeArray(db.objectStoreNames)
  95. unless $.arrayDelete(objectStoreNames, 'docs')
  96. try db.createObjectStore('docs')
  97. for doc in app.docs.all() when not $.arrayDelete(objectStoreNames, doc.slug)
  98. try db.createObjectStore(doc.slug)
  99. for name in objectStoreNames
  100. try db.deleteObjectStore(name)
  101. return
  102. store: (doc, data, onSuccess, onError, _retry = true) ->
  103. @db (db) =>
  104. unless db
  105. onError()
  106. return
  107. txn = @idbTransaction db, stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false
  108. txn.oncomplete = =>
  109. @cachedDocs?[doc.slug] = doc.mtime
  110. onSuccess()
  111. return
  112. txn.onerror = (event) =>
  113. event.preventDefault()
  114. if txn.error?.name is 'NotFoundError' and _retry
  115. @migrate()
  116. setTimeout =>
  117. @store(doc, data, onSuccess, onError, false)
  118. , 0
  119. else
  120. onError(event)
  121. return
  122. store = txn.objectStore(doc.slug)
  123. store.clear()
  124. store.add(content, path) for path, content of data
  125. store = txn.objectStore('docs')
  126. store.put(doc.mtime, doc.slug)
  127. return
  128. return
  129. unstore: (doc, onSuccess, onError, _retry = true) ->
  130. @db (db) =>
  131. unless db
  132. onError()
  133. return
  134. txn = @idbTransaction db, stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false
  135. txn.oncomplete = =>
  136. delete @cachedDocs?[doc.slug]
  137. onSuccess()
  138. return
  139. txn.onerror = (event) ->
  140. event.preventDefault()
  141. if txn.error?.name is 'NotFoundError' and _retry
  142. @migrate()
  143. setTimeout =>
  144. @unstore(doc, onSuccess, onError, false)
  145. , 0
  146. else
  147. onError(event)
  148. return
  149. store = txn.objectStore('docs')
  150. store.delete(doc.slug)
  151. store = txn.objectStore(doc.slug)
  152. store.clear()
  153. return
  154. return
  155. version: (doc, fn) ->
  156. if (version = @cachedVersion(doc))?
  157. fn(version)
  158. return
  159. @db (db) =>
  160. unless db
  161. fn(false)
  162. return
  163. txn = @idbTransaction db, stores: ['docs'], mode: 'readonly'
  164. store = txn.objectStore('docs')
  165. req = store.get(doc.slug)
  166. req.onsuccess = ->
  167. fn(req.result)
  168. return
  169. req.onerror = (event) ->
  170. event.preventDefault()
  171. fn(false)
  172. return
  173. return
  174. return
  175. cachedVersion: (doc) ->
  176. return unless @cachedDocs
  177. @cachedDocs[doc.slug] or false
  178. versions: (docs, fn) ->
  179. if versions = @cachedVersions(docs)
  180. fn(versions)
  181. return
  182. @db (db) =>
  183. unless db
  184. fn(false)
  185. return
  186. txn = @idbTransaction db, stores: ['docs'], mode: 'readonly'
  187. txn.oncomplete = ->
  188. fn(result)
  189. return
  190. store = txn.objectStore('docs')
  191. result = {}
  192. docs.forEach (doc) ->
  193. req = store.get(doc.slug)
  194. req.onsuccess = ->
  195. result[doc.slug] = req.result
  196. return
  197. req.onerror = (event) ->
  198. event.preventDefault()
  199. result[doc.slug] = false
  200. return
  201. return
  202. return
  203. cachedVersions: (docs) ->
  204. return unless @cachedDocs
  205. result = {}
  206. result[doc.slug] = @cachedVersion(doc) for doc in docs
  207. result
  208. load: (entry, onSuccess, onError) ->
  209. if @shouldLoadWithIDB(entry)
  210. onError = @loadWithXHR.bind(@, entry, onSuccess, onError)
  211. @loadWithIDB entry, onSuccess, onError
  212. else
  213. @loadWithXHR entry, onSuccess, onError
  214. loadWithXHR: (entry, onSuccess, onError) ->
  215. ajax
  216. url: entry.fileUrl()
  217. dataType: 'html'
  218. success: onSuccess
  219. error: onError
  220. loadWithIDB: (entry, onSuccess, onError) ->
  221. @db (db) =>
  222. unless db
  223. onError()
  224. return
  225. unless db.objectStoreNames.contains(entry.doc.slug)
  226. onError()
  227. @loadDocsCache(db)
  228. return
  229. txn = @idbTransaction db, stores: [entry.doc.slug], mode: 'readonly'
  230. store = txn.objectStore(entry.doc.slug)
  231. req = store.get(entry.dbPath())
  232. req.onsuccess = ->
  233. if req.result then onSuccess(req.result) else onError()
  234. return
  235. req.onerror = (event) ->
  236. event.preventDefault()
  237. onError()
  238. return
  239. @loadDocsCache(db)
  240. return
  241. loadDocsCache: (db) ->
  242. return if @cachedDocs
  243. @cachedDocs = {}
  244. txn = @idbTransaction db, stores: ['docs'], mode: 'readonly'
  245. txn.oncomplete = =>
  246. setTimeout(@checkForCorruptedDocs, 50)
  247. return
  248. req = txn.objectStore('docs').openCursor()
  249. req.onsuccess = (event) =>
  250. return unless cursor = event.target.result
  251. @cachedDocs[cursor.key] = cursor.value
  252. cursor.continue()
  253. return
  254. req.onerror = (event) ->
  255. event.preventDefault()
  256. return
  257. return
  258. checkForCorruptedDocs: =>
  259. @db (db) =>
  260. @corruptedDocs = []
  261. docs = (key for key, value of @cachedDocs when value)
  262. return if docs.length is 0
  263. for slug in docs when not app.docs.findBy('slug', slug)
  264. @corruptedDocs.push(slug)
  265. for slug in @corruptedDocs
  266. $.arrayDelete(docs, slug)
  267. if docs.length is 0
  268. setTimeout(@deleteCorruptedDocs, 0)
  269. return
  270. txn = @idbTransaction(db, stores: docs, mode: 'readonly', ignoreError: false)
  271. txn.oncomplete = =>
  272. setTimeout(@deleteCorruptedDocs, 0) if @corruptedDocs.length > 0
  273. return
  274. for doc in docs
  275. txn.objectStore(doc).get('index').onsuccess = (event) =>
  276. @corruptedDocs.push(event.target.source.name) unless event.target.result
  277. return
  278. return
  279. return
  280. deleteCorruptedDocs: =>
  281. @db (db) =>
  282. txn = @idbTransaction(db, stores: ['docs'], mode: 'readwrite', ignoreError: false)
  283. store = txn.objectStore('docs')
  284. while doc = @corruptedDocs.pop()
  285. @cachedDocs[doc] = false
  286. store.delete(doc)
  287. return
  288. Raven.captureMessage 'corruptedDocs', level: 'info', extra: { docs: @corruptedDocs.join(',') }
  289. return
  290. shouldLoadWithIDB: (entry) ->
  291. @useIndexedDB and (not @cachedDocs or @cachedDocs[entry.doc.slug])
  292. idbTransaction: (db, options) ->
  293. app.lastIDBTransaction = [options.stores, options.mode]
  294. txn = db.transaction(options.stores, options.mode)
  295. unless options.ignoreError is false
  296. txn.onerror = (event) ->
  297. event.preventDefault()
  298. return
  299. unless options.ignoreAbort is false
  300. txn.onabort = (event) ->
  301. event.preventDefault()
  302. return
  303. txn
  304. reset: ->
  305. try indexedDB?.deleteDatabase(NAME) catch
  306. return
  307. useIndexedDB: ->
  308. try
  309. if !app.isSingleDoc() and window.indexedDB
  310. true
  311. else
  312. @reason = 'not_supported'
  313. false
  314. catch
  315. false
  316. migrate: ->
  317. app.settings.set('schema', @userVersion() + 1)
  318. return
  319. setUserVersion: (version) ->
  320. app.settings.set('schema', version)
  321. return
  322. userVersion: ->
  323. app.settings.get('schema')