// TODO: This file was created by bulk-decaffeinate. // Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const ASpellWorkerPool = require('./ASpellWorkerPool') const LRU = require('lru-cache') const logger = require('@overleaf/logger') const fs = require('fs') const settings = require('@overleaf/settings') const Path = require('path') const { promisify } = require('util') const OError = require('@overleaf/o-error') const OneMinute = 60 * 1000 const opts = { max: 10000, maxAge: OneMinute * 60 * 10 } const cache = new LRU(opts) const cacheFsPath = Path.resolve(settings.cacheDir, 'spell.cache') const cacheFsPathTmp = cacheFsPath + '.tmp' // load any existing cache try { const oldCache = fs.readFileSync(cacheFsPath) cache.load(JSON.parse(oldCache)) } catch (error) { logger.debug( OError.tag(error, 'could not load the cache file', { cacheFsPath }) ) } // write the cache every 30 minutes const cacheDump = setInterval(function () { const dump = JSON.stringify(cache.dump()) return fs.writeFile(cacheFsPathTmp, dump, function (err) { if (err != null) { logger.debug(OError.tag(err, 'error writing cache file')) fs.unlink(cacheFsPathTmp, () => {}) } else { fs.rename(cacheFsPathTmp, cacheFsPath, err => { if (err) { logger.error(OError.tag(err, 'error renaming cache file')) } else { logger.debug({ len: dump.length, cacheFsPath }, 'wrote cache file') } }) } }) }, 30 * OneMinute) class ASpellRunner { checkWords(language, words, callback) { if (callback == null) { callback = () => {} } return this.runAspellOnWords(language, words, (error, output) => { if (error != null) { return callback(OError.tag(error)) } // output = @removeAspellHeader(output) const suggestions = this.getSuggestions(language, output) const results = [] let hits = 0 const addToCache = {} for (let i = 0; i < words.length; i++) { const word = words[i] const key = language + ':' + word const cached = cache.get(key) if (cached != null) { hits++ if (cached === true) { // valid word, no need to do anything continue } else { results.push({ index: i, suggestions: cached }) } } else { if (suggestions[key] != null) { addToCache[key] = suggestions[key] results.push({ index: i, suggestions: suggestions[key] }) } else { // a valid word, but uncached addToCache[key] = true } } } // update the cache after processing all words, to avoid cache // changing while we use it for (const k in addToCache) { const v = addToCache[k] cache.set(k, v) } logger.debug( { hits, total: words.length, hitrate: (hits / words.length).toFixed(2), }, 'cache hit rate' ) return callback(null, results) }) } getSuggestions(language, output) { const lines = output.split('\n') const suggestions = {} for (const line of Array.from(lines)) { let parts, word if (line[0] === '&') { // Suggestions found parts = line.split(' ') if (parts.length > 1) { word = parts[1] const suggestionsString = line.slice(line.indexOf(':') + 2) suggestions[language + ':' + word] = suggestionsString.split(', ') } } else if (line[0] === '#') { // No suggestions parts = line.split(' ') if (parts.length > 1) { word = parts[1] suggestions[language + ':' + word] = [] } } } return suggestions } // removeAspellHeader: (output) -> output.slice(1) runAspellOnWords(language, words, callback) { // send words to aspell, get back string output for those words // find a free pipe for the language (or start one) // send the words down the pipe // send an END marker that will generate a "*" line in the output // when the output pipe receives the "*" return the data sofar and reset the pipe to be available // // @open(language) // @captureOutput(callback) // @setTerseMode() // start = new Date() if (callback == null) { callback = () => {} } const newWord = {} for (const word of Array.from(words)) { if (!newWord[word] && !cache.has(language + ':' + word)) { newWord[word] = true } } words = Object.keys(newWord) if (words.length) { return WorkerPool.check(language, words, ASpell.ASPELL_TIMEOUT, callback) } else { return callback(null, '') } } } const ASpell = { // The description of how to call aspell from another program can be found here: // http://aspell.net/man-html/Through-A-Pipe.html checkWords(language, words, callback) { if (callback == null) { callback = () => {} } const runner = new ASpellRunner() return runner.checkWords(language, words, callback) }, ASPELL_TIMEOUT: 10000, } const promises = { checkWords: promisify(ASpell.checkWords), } ASpell.promises = promises module.exports = ASpell const WorkerPool = new ASpellWorkerPool() module.exports.cacheDump = cacheDump