db.coffee 8.4 KB

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