db.coffee 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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. try
  22. db = event.target.result
  23. unless @checkedBuggyIDB
  24. @idbTransaction(db, stores: ['docs', app.docs.all()[0].slug], mode: 'readwrite').abort() # https://bugs.webkit.org/show_bug.cgi?id=136937
  25. @checkedBuggyIDB = true
  26. catch
  27. try db.close()
  28. @reason = 'apple'
  29. @onOpenError()
  30. return
  31. @runCallbacks(db)
  32. @open = false
  33. db.close()
  34. return
  35. onOpenError: (event) =>
  36. event?.preventDefault()
  37. @open = false
  38. if event?.target?.error?.name is 'QuotaExceededError'
  39. @reset()
  40. @db()
  41. app.onQuotaExceeded()
  42. else
  43. @useIndexedDB = false
  44. @reason or= 'cant_open'
  45. @runCallbacks()
  46. return
  47. runCallbacks: (db) ->
  48. fn(db) while fn = @callbacks.shift()
  49. return
  50. onUpgradeNeeded: (event) ->
  51. return unless db = event.target.result
  52. objectStoreNames = $.makeArray(db.objectStoreNames)
  53. unless $.arrayDelete(objectStoreNames, 'docs')
  54. db.createObjectStore('docs')
  55. for doc in app.docs.all() when not $.arrayDelete(objectStoreNames, doc.slug)
  56. try db.createObjectStore(doc.slug)
  57. for name in objectStoreNames
  58. try db.deleteObjectStore(name)
  59. return
  60. store: (doc, data, onSuccess, onError) ->
  61. @db (db) =>
  62. unless db
  63. onError()
  64. return
  65. txn = @idbTransaction db, stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false
  66. txn.oncomplete = =>
  67. @cachedDocs?[doc.slug] = doc.mtime
  68. onSuccess()
  69. return
  70. txn.onerror = (event) ->
  71. event.preventDefault()
  72. onError(event)
  73. return
  74. store = txn.objectStore(doc.slug)
  75. store.clear()
  76. store.add(content, path) for path, content of data
  77. store = txn.objectStore('docs')
  78. store.put(doc.mtime, doc.slug)
  79. return
  80. return
  81. unstore: (doc, onSuccess, onError) ->
  82. @db (db) =>
  83. unless db
  84. onError()
  85. return
  86. txn = @idbTransaction db, stores: ['docs', doc.slug], mode: 'readwrite', ignoreError: false
  87. txn.oncomplete = =>
  88. delete @cachedDocs?[doc.slug]
  89. onSuccess()
  90. return
  91. txn.onerror = (event) ->
  92. event.preventDefault()
  93. onError(event)
  94. return
  95. store = txn.objectStore(doc.slug)
  96. store.clear()
  97. store = txn.objectStore('docs')
  98. store.delete(doc.slug)
  99. return
  100. return
  101. version: (doc, fn) ->
  102. if (version = @cachedVersion(doc))?
  103. fn(version)
  104. return
  105. @db (db) =>
  106. unless db
  107. fn(false)
  108. return
  109. txn = @idbTransaction db, stores: ['docs'], mode: 'readonly'
  110. store = txn.objectStore('docs')
  111. req = store.get(doc.slug)
  112. req.onsuccess = ->
  113. fn(req.result)
  114. return
  115. req.onerror = (event) ->
  116. event.preventDefault()
  117. fn(false)
  118. return
  119. return
  120. return
  121. cachedVersion: (doc) ->
  122. return unless @cachedDocs
  123. @cachedDocs[doc.slug] or false
  124. versions: (docs, fn) ->
  125. if versions = @cachedVersions(docs)
  126. fn(versions)
  127. return
  128. @db (db) =>
  129. unless db
  130. fn(false)
  131. return
  132. txn = @idbTransaction db, stores: ['docs'], mode: 'readonly'
  133. txn.oncomplete = ->
  134. fn(result)
  135. return
  136. store = txn.objectStore('docs')
  137. result = {}
  138. docs.forEach (doc) ->
  139. req = store.get(doc.slug)
  140. req.onsuccess = ->
  141. result[doc.slug] = req.result
  142. return
  143. req.onerror = (event) ->
  144. event.preventDefault()
  145. result[doc.slug] = false
  146. return
  147. return
  148. return
  149. cachedVersions: (docs) ->
  150. return unless @cachedDocs
  151. result = {}
  152. result[doc.slug] = @cachedVersion(doc) for doc in docs
  153. result
  154. load: (entry, onSuccess, onError) ->
  155. if @shouldLoadWithIDB(entry)
  156. onError = @loadWithXHR.bind(@, entry, onSuccess, onError)
  157. @loadWithIDB entry, onSuccess, onError
  158. else
  159. @loadWithXHR entry, onSuccess, onError
  160. loadWithXHR: (entry, onSuccess, onError) ->
  161. ajax
  162. url: entry.fileUrl()
  163. dataType: 'html'
  164. success: onSuccess
  165. error: onError
  166. loadWithIDB: (entry, onSuccess, onError) ->
  167. @db (db) =>
  168. unless db
  169. onError()
  170. return
  171. unless db.objectStoreNames.contains(entry.doc.slug)
  172. onError()
  173. @loadDocsCache(db)
  174. return
  175. txn = @idbTransaction db, stores: [entry.doc.slug], mode: 'readonly'
  176. store = txn.objectStore(entry.doc.slug)
  177. req = store.get(entry.dbPath())
  178. req.onsuccess = ->
  179. if req.result then onSuccess(req.result) else onError()
  180. return
  181. req.onerror = (event) ->
  182. event.preventDefault()
  183. onError()
  184. return
  185. @loadDocsCache(db)
  186. return
  187. loadDocsCache: (db) ->
  188. return if @cachedDocs
  189. @cachedDocs = {}
  190. txn = @idbTransaction db, stores: ['docs'], mode: 'readonly'
  191. store = txn.objectStore('docs')
  192. req = store.openCursor()
  193. req.onsuccess = (event) =>
  194. return unless cursor = event.target.result
  195. @cachedDocs[cursor.key] = cursor.value
  196. cursor.continue()
  197. return
  198. req.onerror = (event) ->
  199. event.preventDefault()
  200. return
  201. return
  202. shouldLoadWithIDB: (entry) ->
  203. @useIndexedDB and (not @cachedDocs or @cachedDocs[entry.doc.slug])
  204. idbTransaction: (db, options) ->
  205. txn = db.transaction(options.stores, options.mode)
  206. unless options.ignoreError is false
  207. txn.onerror = (event) ->
  208. event.preventDefault()
  209. return
  210. unless options.ignoreAbort is false
  211. txn.onabort = (event) ->
  212. event.preventDefault()
  213. return
  214. txn
  215. reset: ->
  216. try indexedDB?.deleteDatabase(NAME) catch
  217. return
  218. useIndexedDB: ->
  219. try
  220. if !app.isSingleDoc() and window.indexedDB
  221. true
  222. else
  223. @reason = 'not_supported'
  224. false
  225. catch
  226. false
  227. migrate: ->
  228. app.settings.set('schema', @userVersion() + 1)
  229. return
  230. schemaVersion: ->
  231. @appVersion * 10 + @userVersion()
  232. userVersion: ->
  233. app.settings.get('schema')
  234. appVersion: ->
  235. if app.config.env is 'production' then app.config.version else Math.floor(Date.now() / 1000)