ASpell.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. // TODO: This file was created by bulk-decaffeinate.
  2. // Sanity-check the conversion and remove this comment.
  3. /*
  4. * decaffeinate suggestions:
  5. * DS101: Remove unnecessary use of Array.from
  6. * DS102: Remove unnecessary code created because of implicit returns
  7. * DS207: Consider shorter variations of null checks
  8. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  9. */
  10. const ASpellWorkerPool = require('./ASpellWorkerPool')
  11. const LRU = require('lru-cache')
  12. const logger = require('@overleaf/logger')
  13. const fs = require('fs')
  14. const settings = require('@overleaf/settings')
  15. const Path = require('path')
  16. const { promisify } = require('util')
  17. const OError = require('@overleaf/o-error')
  18. const OneMinute = 60 * 1000
  19. const opts = { max: 10000, maxAge: OneMinute * 60 * 10 }
  20. const cache = new LRU(opts)
  21. const cacheFsPath = Path.resolve(settings.cacheDir, 'spell.cache')
  22. const cacheFsPathTmp = cacheFsPath + '.tmp'
  23. // load any existing cache
  24. try {
  25. const oldCache = fs.readFileSync(cacheFsPath)
  26. cache.load(JSON.parse(oldCache))
  27. } catch (error) {
  28. logger.debug(
  29. OError.tag(error, 'could not load the cache file', { cacheFsPath })
  30. )
  31. }
  32. // write the cache every 30 minutes
  33. const cacheDump = setInterval(function () {
  34. const dump = JSON.stringify(cache.dump())
  35. return fs.writeFile(cacheFsPathTmp, dump, function (err) {
  36. if (err != null) {
  37. logger.debug(OError.tag(err, 'error writing cache file'))
  38. fs.unlink(cacheFsPathTmp, () => {})
  39. } else {
  40. fs.rename(cacheFsPathTmp, cacheFsPath, err => {
  41. if (err) {
  42. logger.error(OError.tag(err, 'error renaming cache file'))
  43. } else {
  44. logger.debug({ len: dump.length, cacheFsPath }, 'wrote cache file')
  45. }
  46. })
  47. }
  48. })
  49. }, 30 * OneMinute)
  50. class ASpellRunner {
  51. checkWords(language, words, callback) {
  52. if (callback == null) {
  53. callback = () => {}
  54. }
  55. return this.runAspellOnWords(language, words, (error, output) => {
  56. if (error != null) {
  57. return callback(OError.tag(error))
  58. }
  59. // output = @removeAspellHeader(output)
  60. const suggestions = this.getSuggestions(language, output)
  61. const results = []
  62. let hits = 0
  63. const addToCache = {}
  64. for (let i = 0; i < words.length; i++) {
  65. const word = words[i]
  66. const key = language + ':' + word
  67. const cached = cache.get(key)
  68. if (cached != null) {
  69. hits++
  70. if (cached === true) {
  71. // valid word, no need to do anything
  72. continue
  73. } else {
  74. results.push({ index: i, suggestions: cached })
  75. }
  76. } else {
  77. if (suggestions[key] != null) {
  78. addToCache[key] = suggestions[key]
  79. results.push({ index: i, suggestions: suggestions[key] })
  80. } else {
  81. // a valid word, but uncached
  82. addToCache[key] = true
  83. }
  84. }
  85. }
  86. // update the cache after processing all words, to avoid cache
  87. // changing while we use it
  88. for (const k in addToCache) {
  89. const v = addToCache[k]
  90. cache.set(k, v)
  91. }
  92. logger.debug(
  93. {
  94. hits,
  95. total: words.length,
  96. hitrate: (hits / words.length).toFixed(2),
  97. },
  98. 'cache hit rate'
  99. )
  100. return callback(null, results)
  101. })
  102. }
  103. getSuggestions(language, output) {
  104. const lines = output.split('\n')
  105. const suggestions = {}
  106. for (const line of Array.from(lines)) {
  107. let parts, word
  108. if (line[0] === '&') {
  109. // Suggestions found
  110. parts = line.split(' ')
  111. if (parts.length > 1) {
  112. word = parts[1]
  113. const suggestionsString = line.slice(line.indexOf(':') + 2)
  114. suggestions[language + ':' + word] = suggestionsString.split(', ')
  115. }
  116. } else if (line[0] === '#') {
  117. // No suggestions
  118. parts = line.split(' ')
  119. if (parts.length > 1) {
  120. word = parts[1]
  121. suggestions[language + ':' + word] = []
  122. }
  123. }
  124. }
  125. return suggestions
  126. }
  127. // removeAspellHeader: (output) -> output.slice(1)
  128. runAspellOnWords(language, words, callback) {
  129. // send words to aspell, get back string output for those words
  130. // find a free pipe for the language (or start one)
  131. // send the words down the pipe
  132. // send an END marker that will generate a "*" line in the output
  133. // when the output pipe receives the "*" return the data sofar and reset the pipe to be available
  134. //
  135. // @open(language)
  136. // @captureOutput(callback)
  137. // @setTerseMode()
  138. // start = new Date()
  139. if (callback == null) {
  140. callback = () => {}
  141. }
  142. const newWord = {}
  143. for (const word of Array.from(words)) {
  144. if (!newWord[word] && !cache.has(language + ':' + word)) {
  145. newWord[word] = true
  146. }
  147. }
  148. words = Object.keys(newWord)
  149. if (words.length) {
  150. return WorkerPool.check(language, words, ASpell.ASPELL_TIMEOUT, callback)
  151. } else {
  152. return callback(null, '')
  153. }
  154. }
  155. }
  156. const ASpell = {
  157. // The description of how to call aspell from another program can be found here:
  158. // http://aspell.net/man-html/Through-A-Pipe.html
  159. checkWords(language, words, callback) {
  160. if (callback == null) {
  161. callback = () => {}
  162. }
  163. const runner = new ASpellRunner()
  164. return runner.checkWords(language, words, callback)
  165. },
  166. ASPELL_TIMEOUT: 10000,
  167. }
  168. const promises = {
  169. checkWords: promisify(ASpell.checkWords),
  170. }
  171. ASpell.promises = promises
  172. module.exports = ASpell
  173. const WorkerPool = new ASpellWorkerPool()
  174. module.exports.cacheDump = cacheDump