diff --git a/services/spelling/app/js/LearnedWordsManager.js b/services/spelling/app/js/LearnedWordsManager.js index 93d41e3139..172a098e80 100644 --- a/services/spelling/app/js/LearnedWordsManager.js +++ b/services/spelling/app/js/LearnedWordsManager.js @@ -54,6 +54,14 @@ const LearnedWordsManager = { metrics.inc('mongoCache', 0.1, { status: 'miss' }) logger.info({ userToken }, 'mongoCache miss') + LearnedWordsManager.getLearnedWordsNoCache(userToken, (err, words) => { + if (err) return callback(err) + mongoCache.set(userToken, words) + callback(null, words) + }) + }, + + getLearnedWordsNoCache(userToken, callback) { db.spellingPreferences.findOne( { token: userToken }, function (error, preferences) { @@ -68,7 +76,6 @@ const LearnedWordsManager = { (value, index, self) => self.indexOf(value) === index ) } - mongoCache.set(userToken, words) callback(null, words) } ) diff --git a/services/spelling/app/js/SpellingAPIManager.js b/services/spelling/app/js/SpellingAPIManager.js index efd19cbd4f..9232525581 100644 --- a/services/spelling/app/js/SpellingAPIManager.js +++ b/services/spelling/app/js/SpellingAPIManager.js @@ -51,7 +51,7 @@ const SpellingAPIManager = { }, getDic(token, callback) { - return LearnedWordsManager.getLearnedWords(token, callback) + return LearnedWordsManager.getLearnedWordsNoCache(token, callback) }, } @@ -67,7 +67,7 @@ const promises = { const misspellings = await ASpell.promises.checkWords(lang, wordSlice) - if (token) { + if (token && !request.skipLearnedWords) { const learnedWords = await LearnedWordsManager.promises.getLearnedWords( token ) diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index 3f76088a37..bc54d699b4 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -40,6 +40,7 @@ const Modules = require('../../infrastructure/Modules') const SplitTestV2Handler = require('../SplitTests/SplitTestV2Handler') const { getNewLogsUIVariantForUser } = require('../Helpers/NewLogsUI') const FeaturesUpdater = require('../Subscription/FeaturesUpdater') +const SpellingHandler = require('../Spelling/SpellingHandler') const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => { if (!affiliation.institution) return false @@ -691,6 +692,12 @@ const ProjectController = { ) } }, + learnedWords(cb) { + if (!userId) { + return cb(null, []) + } + SpellingHandler.getUserDictionaryWithRetries(userId, cb) + }, subscription(cb) { if (userId == null) { return cb() @@ -776,6 +783,7 @@ const ProjectController = { { project, user, + learnedWords, subscription, isTokenMember, brandVariation, @@ -927,6 +935,7 @@ const ProjectController = { isTokenMember ), languages: Settings.languages, + learnedWords, editorThemes: THEME_LIST, maxDocLength: Settings.max_doc_length, useV2History: diff --git a/services/web/app/src/Features/Spelling/SpellingHandler.js b/services/web/app/src/Features/Spelling/SpellingHandler.js index a1664fe44a..42c14c595c 100644 --- a/services/web/app/src/Features/Spelling/SpellingHandler.js +++ b/services/web/app/src/Features/Spelling/SpellingHandler.js @@ -1,10 +1,39 @@ const request = require('request') +const requestRetry = require('requestretry') const Settings = require('@overleaf/settings') const OError = require('@overleaf/o-error') const TIMEOUT = 10 * 1000 module.exports = { + getUserDictionaryWithRetries(userId, callback) { + const options = { + url: `${Settings.apis.spelling.url}/user/${userId}`, + timeout: 3 * 1000, + json: true, + retryDelay: 1, + maxAttempts: 3, + } + requestRetry(options, (error, response, body) => { + if (error) { + return callback( + OError.tag(error, 'error getting user dictionary', { error, userId }) + ) + } + + if (response.statusCode !== 200) { + return callback( + new OError( + 'Non-success code from spelling API when getting user dictionary', + { userId, statusCode: response.statusCode } + ) + ) + } + + callback(null, body) + }) + }, + getUserDictionary(userId, callback) { const url = `${Settings.apis.spelling.url}/user/${userId}` request.get({ url: url, timeout: TIMEOUT }, (error, response) => { diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 8cd5433e6b..7caede1fbe 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -123,6 +123,7 @@ block append meta meta(name="ol-project_id" content=project_id) meta(name="ol-userSettings" data-type="json" content=userSettings) meta(name="ol-user" data-type="json" content=user) + meta(name="ol-learnedWords" data-type="json" content=learnedWords) meta(name="ol-anonymous" data-type="boolean" content=anonymous) meta(name="ol-brandVariation" data-type="json" content=brandVariation) meta(name="ol-anonymousAccessToken" content=anonymousAccessToken) diff --git a/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.js b/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.js index 837e61f4b5..cdc4325d19 100644 --- a/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.js +++ b/services/web/frontend/js/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.js @@ -1,3 +1,5 @@ +import getMeta from '../../../../../utils/meta' + // eslint-disable-next-line prefer-regex-literals const BLACKLISTED_COMMAND_REGEX = new RegExp( `\ @@ -58,6 +60,8 @@ class SpellCheckManager { this.selectedHighlightContents = null + this.learnedWords = new Set(getMeta('ol-learnedWords')) + $(document).on('click', e => { // There is a bug (?) in Safari when ctrl-clicking an element, and the // the contextmenu event is preventDefault-ed. In this case, the @@ -191,6 +195,7 @@ class SpellCheckManager { this.adapter.highlightedWordManager.removeWord(highlight.word) const language = this.$scope.spellCheckLanguage this.cache.put(`${language}:${highlight.word}`, true) + this.learnedWords.add(highlight.word) } markLinesAsUpdated(change) { @@ -291,7 +296,7 @@ class SpellCheckManager { } else { this.inProgressRequest = this.apiRequest( '/check', - { language, words }, + { language, words, skipLearnedWords: true }, (error, result) => { delete this.inProgressRequest if (error != null || result == null || result.misspellings == null) { @@ -372,8 +377,10 @@ class SpellCheckManager { if (word[word.length - 1] === "'") { word = word.slice(0, -1) } - positions.push({ row: rowIdx, column: result.index }) - words.push(word) + if (!this.learnedWords.has(word)) { + positions.push({ row: rowIdx, column: result.index }) + words.push(word) + } } } return { words, positions } diff --git a/services/web/test/karma/ide/editor/aceEditor/spell-check/SpellCheckManagerTests.js b/services/web/test/karma/ide/editor/aceEditor/spell-check/SpellCheckManagerTests.js index da8a0ffa88..56597bf9b4 100644 --- a/services/web/test/karma/ide/editor/aceEditor/spell-check/SpellCheckManagerTests.js +++ b/services/web/test/karma/ide/editor/aceEditor/spell-check/SpellCheckManagerTests.js @@ -223,6 +223,7 @@ export default describe('SpellCheckManager', function () { .expect('POST', '/spelling/check', { language: this.scope.spellCheckLanguage, words: ['Lorem', 'ipsum', 'dolor'], + skipLearnedWords: true, token: window.user.id, _csrf: window.csrfToken, }) @@ -244,6 +245,7 @@ export default describe('SpellCheckManager', function () { .expect('POST', '/spelling/check', { language: this.scope.spellCheckLanguage, words: ['Lorem', 'ipsum', 'dolor'], + skipLearnedWords: true, token: window.user.id, _csrf: window.csrfToken, }) @@ -267,6 +269,7 @@ export default describe('SpellCheckManager', function () { .expect('POST', '/spelling/check', { language: this.scope.spellCheckLanguage, words: ['Lorem', 'ipsum', 'dolor'], + skipLearnedWords: true, token: window.user.id, _csrf: window.csrfToken, }) @@ -287,6 +290,7 @@ export default describe('SpellCheckManager', function () { .expect('POST', '/spelling/check', { language: this.scope.spellCheckLanguage, words: ['sit', 'amet'], + skipLearnedWords: true, token: window.user.id, _csrf: window.csrfToken, }) diff --git a/services/web/test/unit/src/Project/ProjectControllerTests.js b/services/web/test/unit/src/Project/ProjectControllerTests.js index e4c9cbb0d9..4722ff5cfa 100644 --- a/services/web/test/unit/src/Project/ProjectControllerTests.js +++ b/services/web/test/unit/src/Project/ProjectControllerTests.js @@ -193,6 +193,9 @@ describe('ProjectController', function () { hooks: { fire: sinon.stub().yields(null, []) }, }, '../Helpers/NewLogsUI': this.NewLogsUIHelper, + '../Spelling/SpellingHandler': { + getUserDictionaryWithRetries: sinon.stub().yields(null, []), + }, }, })