/* MIT License Copyright (c) 2022 Viresh Ratnakar See the full Exet license notice in exet.js. Current version: v1.01, November 14, 2025 */ /** * Code related to managing localStorage and IndexedDB for Exet. * localStorage is stored for puzzle revisions state and IndexedDB * is used for custom lexicons (word lists). */ function ExetRev(id, title, revNum, revType, timestamp, details="") { this.id = id; this.title = title this.revNum = revNum; this.revType = revType; this.timestamp = timestamp; this.details = details; // prefix, suffix, exolve should be set directly. }; function ExetRevManager() { this.REV_LOADED_FROM_FILE = 1; this.REV_CREATED_BLANK = 2; this.REV_CREATED_AUTOBLOCK = 3; this.REV_JUMPED_TO_REV = 10; this.REV_GRID_CHANGE = 20; this.REV_LIGHT_REVERSAL = 24; this.REV_AUTOFILL_GRIDFILL_CHANGE = 28; this.REV_GRIDFILL_CHANGE = 30; this.REV_ENUM_CHANGE = 40; this.REV_CLUE_CHANGE = 50; this.REV_METADATA_CHANGE = 60; this.REV_FILL_OPTIONS_CHANGE = 65; this.REV_PREFLEX_CHANGE = 70; this.REV_OPTIONS_CHANGE = 80; this.REV_RESAVE = 90; this.revMsgs = {}; this.revMsgs[this.REV_LOADED_FROM_FILE] = "Loaded from a file"; this.revMsgs[this.REV_CREATED_BLANK] = "Created a blank grid"; this.revMsgs[this.REV_CREATED_AUTOBLOCK] = "Created a blank grid " + "with automagic blocks"; this.revMsgs[this.REV_JUMPED_TO_REV] = "Jumped to a previous revision"; this.revMsgs[this.REV_GRID_CHANGE] = "Grid change"; this.revMsgs[this.REV_LIGHT_REVERSAL] = "Light reversal"; this.revMsgs[this.REV_AUTOFILL_GRIDFILL_CHANGE] = "Autofilled grid-fill " + "change"; this.revMsgs[this.REV_GRIDFILL_CHANGE] = "Grid-fill change"; this.revMsgs[this.REV_ENUM_CHANGE] = "Enum change"; this.revMsgs[this.REV_CLUE_CHANGE] = "Clue or anno change"; this.revMsgs[this.REV_METADATA_CHANGE] = "Metadata change"; this.revMsgs[this.REV_FILL_OPTIONS_CHANGE] = "Change in options " + "for suggested fills"; this.revMsgs[this.REV_PREFLEX_CHANGE] = "Change in the list of or options " + "for preferred words"; this.revMsgs[this.REV_OPTIONS_CHANGE] = "Crossword options change"; this.revMsgs[this.REV_RESAVE] = "Resaved (to be safe) with local storage freed up"; /* State for throttled revision-saving */ this.throttleRevTimer = null; this.saveLagMS = 5000 this.throttlingLastRev = 0; /** * Special localStorage key for storing preferences and state, * qualified by non-default lexicon properties. */ this.SPECIAL_KEY_PREFIX = '42-exet-42'; this.SPECIAL_KEY = this.SPECIAL_KEY_PREFIX; if ('en' != exetLexicon.language || 'Latin' != exetLexicon.script || 1 != exetLexicon.maxCharCodes) { this.SPECIAL_KEY += `-${exetLexicon.language}-${exetLexicon.script}` + `-${exetLexicon.maxCharCodes}`; } /* Id for previews */ this.previewId = `exet-preview-${Math.random().toString(36).substring(2, 8)}`; }; ExetRevManager.prototype.skippableKey = function(id) { return (id.startsWith(this.SPECIAL_KEY_PREFIX) || id.startsWith('xlvstate:') || id == '42-xlvp-player-state'); } ExetRevManager.prototype.sizeOfPrefUnpref = function(id) { let sz = 0; for (isPref of [true, false]) { const key = this.keyPrefUnpref(id, isPref); let data = window.localStorage.getItem(key); if (data) { sz += data.length; } } return sz; } /** * params object should have these fields (missing = false/null): * forStorage: true/false * onlyPuz: null/current-puzzle (if not null, only used changing rev) * startId: null or can be specified when onlyPuz is null, for starting id. * elt: the element in which to render * callback: null or fn. to call after selection, passing rev * sortBy: null or 'timestamp'/'title'/'space'/'id' * sortOrder: null or 'increasing'/'decreasing' * sortBy/sortOrder are ignored when onlyPuz is not null. */ ExetRevManager.prototype.choosePuzRev = function(params) { this.params = params; let choices = []; if (params.onlyPuz) { let lsUsed = 0; let stored = window.localStorage.getItem(params.onlyPuz.id); if (lsUsed) { lsUsed = stored.length + this.sizeOfPrefUnpref(params.onlyPuz.id); } choices = [{id: params.onlyPuz.id, title: params.onlyPuz.title, space: lsUsed, timestamp: 0}]; } else { for (let idx = 0; idx < window.localStorage.length; idx++) { const id = window.localStorage.key(idx); if (this.skippableKey(id)) { continue; } const storedJson = window.localStorage.getItem(id); let lsUsed = storedJson.length + this.sizeOfPrefUnpref(id); let stored = ''; try { stored = JSON.parse(storedJson); } catch (err) { console.log('Unparseable stored item for id [' + id + ']:' + storedJson); continue; } if (!stored || !stored["id"] || !stored["revs"] || !stored["maxRevNum"]) { console.log('Weird stored item for id [' + id + ']:' + storedJson); continue; } let title = ''; let timestamp = 0; if (stored.revs.length > 0) { const lastRev = stored.revs[stored.revs.length - 1]; title = lastRev.title; timestamp = lastRev.timestamp; } choices.push({id: stored.id, title: title, space: lsUsed, timestamp: timestamp}); } } if (params.sortBy) { const cmp = (a, b) => { const mult = (params.sortOrder == 'increasing' ? 1 : -1); if (a < b) { return -1 * mult; } else if (b < a) { return 1 * mult; } else { return 0; } }; const sorter = (c1, c2) => { return cmp(c1[params.sortBy], c2[params.sortBy]); }; choices.sort(sorter); } exet.checkStorage(); let sortingHTML = ''; if (!params.onlyPuz) { const sorters = ['timestamp', 'title', 'id', 'space']; sortingHTML = ', sort order: '; } let html = `
Select puzzle${sortingHTML}: Select revision:
` for (let i = 0; i < choices.length; i++) { html = html + ` ` } html = html + `
${choices[i].id} ${choices[i].timestamp > 0 ? '
Last change: ' + (new Date(choices[i].timestamp)).toLocaleString() + '' : ''}
${choices[i].title} ${exet.inMB(choices[i].space)} MB
Space used = ${exet.lsUsedSpan.innerText} MB, Space available ≈ ${exet.lsFreeSpan.innerText} MB
`; params.elt.innerHTML = html; const idChoicesBox = document.getElementById('xet-choose-id'); idChoicesBox.style.width = '385px'; idChoicesBox.style.height = '200px'; this.idChoices = document.getElementById('xet-id-choices'); const revChoicesBox = document.getElementById('xet-choose-rev'); revChoicesBox.style.width = '385px'; revChoicesBox.style.height = '200px'; this.revChoices = document.getElementById('xet-rev-choices'); this.preview = document.getElementById('xet-preview'); this.idChoice = ''; this.revChoice = -1; this.puzDeleter = document.getElementById('xet-puz-deleter'); this.puzPriorDeleter = document.getElementById('xet-puz-prior-deleter'); this.puzRevDeleter = document.getElementById('xet-puz-rev-deleter'); this.puzRevSelector = document.getElementById('xet-puz-rev-selector'); this.sortByElt = document.getElementById('xet-choose-puz-sort-by'); this.sortOrderElt = document.getElementById('xet-choose-puz-sort-order'); if (this.sortByElt) { const resorter = (e => { const m = exetRevManager; const params = m.params; params.sortBy = m.sortByElt.value; params.sortOrder = m.sortOrderElt.value; params.startId = m.idChoice; m.choosePuzRev(params); }); this.sortByElt.addEventListener('change', resorter); this.sortOrderElt.addEventListener('change', resorter); } if (params.forStorage) { this.puzDeleter.style.display = ''; this.puzPriorDeleter.style.display = ''; this.puzRevDeleter.style.display = ''; this.puzDeleter.disabled = true; this.puzPriorDeleter.disabled = true; this.puzRevDeleter.disabled = true; const deleter = (types, e) => { let newRevsNotAll = []; if (types != 'all') { if (this.revChoice < 0 || !this.storedRevs || this.storedRevs.revs.length == 0 || this.revChoice >= this.storedRevs.revs.length) { console.log('Weird, did not find revChoice/storedRevs to delete from'); return; } let lastToDelete = this.revChoice; if (types == 'prior') lastToDelete--; let numToDelete = (types == 'prior' ? lastToDelete + 1 : 1); let keptRevs = []; if (lastToDelete - numToDelete >= 0) { keptRevs = this.storedRevs.revs.slice( 0, lastToDelete - numToDelete + 1); } newRevsNotAll = keptRevs.concat( this.storedRevs.revs.slice(lastToDelete + 1)); if (newRevsNotAll.length == 0) { /** The user has indirectly chosen all revisions to be deleted. */ types = 'all'; } } if (!confirm('Are you sure you want to delete ' + types + ' revision(s)?')) { return; } this.idChoices.className = 'xet-choices'; this.revChoices.className = 'xet-choices'; if (types == 'all') { window.localStorage.removeItem(this.idChoice); window.localStorage.removeItem( this.keyPrefUnpref(this.idChoice, true)); window.localStorage.removeItem( this.keyPrefUnpref(this.idChoice, false)); this.idChoice = ''; } else { this.storedRevs.revs = newRevsNotAll; this.saveLocal(this.idChoice, JSON.stringify(this.storedRevs)); this.savePrefUnpref(this.idChoice, this.storedRevs.revs, true); } this.params.startId = this.idChoice; this.choosePuzRev(this.params); e.stopPropagation(); } this.puzDeleter.addEventListener('click', deleter.bind(this, 'all')); this.puzPriorDeleter.addEventListener('click', deleter.bind(this, 'prior')); this.puzRevDeleter.addEventListener('click', deleter.bind(this, 'this')); } else { this.puzRevSelector.style.display = ''; this.puzRevSelector.disabled = true; this.puzRevSelector.addEventListener('click', e => { if (this.revChoice < 0 || !this.storedRevs || this.storedRevs.revs.length == 0 || this.revChoice >= this.storedRevs.revs.length) { console.log('Hmm: bad selection! Check ExetRevManager:'); console.log(this); return; } exetModals.hide(); this.idSelectors = []; this.revSelectors = []; this.preview.innerHTML = ''; if (exolvePuzzles[this.previewId]) { exolvePuzzles[this.previewId].destroy(); } if (params.callback) { params.callback(this.storedRevs.revs[this.revChoice]); } }) } this.idSelectors = []; this.revSelectors = []; this.storedRevs = null; if (params.onlyPuz) { this.idChoice = params.onlyPuz.id; document.getElementById("xet-id-choice-0").classList.add('xet-chosen'); this.chooseRev(); return; } const selectIndex = (idx => { this.preview.innerHTML = ''; if (this.previewId && exolvePuzzles[this.previewId]) { exolvePuzzles[this.previewId].destroy(); } this.puzDeleter.disabled = true; this.puzPriorDeleter.disabled = true; this.puzRevDeleter.disabled = true; this.revChoices.innerHTML = ''; this.revChoice = -1; this.revSelectors = []; this.storedRevs = null; this.puzRevSelector.disabled = true; if (choices[idx].id == this.idChoice) { this.idChoice = ''; this.idSelectors[idx].classList.remove('xet-chosen'); } else { for (let j = 0; j < choices.length; j++) { if (j != idx) { this.idSelectors[j].className = ''; } } this.idChoice = choices[idx].id; this.puzDeleter.disabled = false; this.idSelectors[idx].classList.add('xet-chosen'); this.chooseRev(); } }); let startIdIndex = -1; for (let i = 0; i < choices.length; i++) { let selector = document.getElementById(`xet-id-choice-${i}`); this.idSelectors.push(selector); const id = choices[i].id; if (id == params.startId) { startIdIndex = i; } selector.addEventListener('click', e => selectIndex(i)); } if (startIdIndex >= 0) { selectIndex(startIdIndex); } }; ExetRevManager.prototype.chooseRev = function() { let stored = window.localStorage.getItem(this.idChoice); if (!stored) { return; } this.storedRevs = JSON.parse(stored); let html = ''; for (let idx = this.storedRevs.revs.length - 1; idx >= 0; idx--) { let rev = this.storedRevs.revs[idx]; let revTime = new Date(rev.timestamp); html = html + ` ${rev.title} #${rev.revNum} ${revTime.toLocaleString()} ${exetRevManager.revMsgs[rev.revType]} ${rev.details} `; } this.revChoices.innerHTML = html; this.revSelectors = []; this.revChoice = -1; for (let i = 0; i < this.storedRevs.revs.length; i++) { let selector = document.getElementById(`xet-rev-choice-${i}`); this.revSelectors.push(selector); selector.addEventListener('click', e => { if (!this.storedRevs) { return; } this.puzPriorDeleter.disabled = true; this.puzRevDeleter.disabled = true; this.puzRevSelector.disabled = true; this.preview.innerHTML = ''; if (exolvePuzzles[this.previewId]) { exolvePuzzles[this.previewId].destroy(); } if (i == this.revChoice) { this.revChoice = -1; selector.classList.remove('xet-chosen'); } else { for (let j = 0; j < this.revSelectors.length; j++) { if (j != i) { this.revSelectors[j].className = ''; } } this.revChoice = i; selector.classList.add('xet-chosen'); let exolve = this.storedRevs.revs[i].exolve.replace( /exolve-id:[^\n]*/, `exolve-id: ${this.previewId}`); exet.renderPreview(exolve, "xet-preview"); this.puzPriorDeleter.disabled = (i <= 0); this.puzRevDeleter.disabled = false; this.puzRevSelector.disabled = false; } }); } }; ExetRevManager.prototype.saveLocal = function(k, v) { try { window.localStorage.setItem(k, v); } catch (err) { this.checkStorage(); alert('No available local storage left. Please use the ' + '"Manage local storage" menu option to free up some space.'); console.log('Could not save value of length ' + v.length + ' for key: ' + k) return false; } return true; } ExetRevManager.prototype.keyPrefUnpref = function(id, isPref) { return this.SPECIAL_KEY_PREFIX + (isPref ? '-preflex-' : '-unpreflex-') + id; } ExetRevManager.prototype.hashPrefUnpref = function(arr) { if (!arr || arr.length == 0) { return null; } return exetLexicon.javaHash(JSON.stringify(arr)); } ExetRevManager.prototype.savePrefUnpref = function(id, revs, doGC=false) { const isCurrPuz = (exet.puz && exet.puz.id && exet.puz.id == id); console.assert(doGC || isCurrPuz, doGC, isCurrPuz); for (isPref of [true, false]) { const hashName = isPref ? 'preflexHash' : 'unpreflexHash'; if (!doGC && isCurrPuz && !exet[hashName]) { continue; } let changed = false; const key = this.keyPrefUnpref(id, isPref); let data = window.localStorage.getItem(key); if (data) { data = JSON.parse(data); } else { data = {}; } if (isCurrPuz && exet[hashName] && !data.hasOwnProperty(exet[hashName])) { data[exet[hashName]] = isPref ? exet.preflex : exet.unpreflex; changed = true; } if (doGC) { /** Don't use a set for usedHashes to avoid int/str issue! */ const usedHashes = {}; if (isCurrPuz && exet[hashName]) { usedHashes[exet[hashName]] = true; } for (const rev of revs) { if (rev[hashName]) { usedHashes[rev[hashName]] = true; } } for (const hash in data) { if (!usedHashes.hasOwnProperty(hash)) { delete data[hash]; changed = true; } } } if (changed) { if (Object.keys(data).length == 0) { window.localStorage.removeItem(key); } else { this.saveLocal(key, JSON.stringify(data)); } } } } ExetRevManager.prototype.retrievePrefUnpref = function(rev) { for (isPref of [true, false]) { const hashName = isPref ? 'preflexHash' : 'unpreflexHash'; const objName = isPref ? 'preflex' : 'unpreflex'; let obj = null; if (rev[objName]) { /** old way */ obj = rev[objName]; if (!isPref) { const pList = Object.keys(obj); obj = []; for (const p of pList) { if (p < 0 || p >= exetLexicon.lexicon.length) { console.log('Skipping out-of-bounds old "unpreflex" entry: ' + p); continue; } obj.push(exetLexicon.getLex(p)); } } } else if (rev[hashName]) { const key = this.keyPrefUnpref(rev.id, isPref); let data = window.localStorage.getItem(key); if (data) { data = JSON.parse(data); } else { data = {}; } obj = data[rev[hashName]] || null; if (!obj) { console.log('Missing data for ' + hashName + ' = ' + rev[hashName]); } } if (!obj) { obj = []; } if (isPref) { exet.setPreflex(obj); } else { exet.setUnpreflex(obj); } } } /** * Compare exolve format strings after removing timestamps. */ ExetRevManager.prototype.equalExolves = function(x1, x2) { const r = /Timestamp: .*/g; const x1mod = x1.replaceAll(r, ''); const x2mod = x2.replaceAll(r, ''); return x1mod == x2mod; } ExetRevManager.prototype.saveRev = function(revType, details="") { if (!exet || !exet.puz || !exet.puz.id) { console.log('Cannot save revision when there is no puzzle!'); return; } let stored = window.localStorage.getItem(exet.puz.id); if (!stored) { stored = { id: exet.puz.id, maxRevNum: 0, revs: [] }; } else { stored = JSON.parse(stored); } /** * Do garbage collection of unused pref/unpref data when saving, * to catch potentially unsaved versions or when saving preflex * data updates. */ const doPrefUnprefGC = (revType == this.REV_RESAVE || revType == this.REV_PREFLEX_CHANGE); this.savePrefUnpref(exet.puz.id, stored.revs, doPrefUnprefGC); const exolve = exet.getExolve(); if (stored.revs.length > 0) { const lastRev = stored.revs[stored.revs.length - 1]; if (this.equalExolves(lastRev.exolve, exolve) && lastRev.prefix == exet.prefix && lastRev.suffix == exet.suffix && lastRev.scratchPad == exet.puz.scratchPad.value && lastRev.hasOwnProperty('preflexHash') && lastRev.preflexHash == exet.preflexHash && lastRev.hasOwnProperty('unpreflexHash') && lastRev.unpreflexHash == exet.unpreflexHash && lastRev.noProperNouns == exet.noProperNouns && lastRev.noStemDupes == exet.noStemDupes && lastRev.tryReversals == exet.tryReversals && lastRev.minpop == exet.minpop && lastRev.hasOwnProperty('requireEnums') && lastRev.requireEnums == exet.requireEnums && lastRev.hasOwnProperty('lightRegexps') && JSON.stringify(lastRev.lightRegexps) == JSON.stringify(exet.lightRegexps) && lastRev.asymOK == exet.asymOK) { return; } } stored.maxRevNum++; let exetRev = new ExetRev(exet.puz.id, (exet.puz.title ? exet.puz.title : ''), stored.maxRevNum, revType, Date.now(), details); exetRev.maxRevNum = stored.maxRevNum; exetRev.prefix = exet.prefix; exetRev.suffix = exet.suffix; exetRev.exolve = exolve; exetRev.scratchPad = exet.puz.scratchPad.value; exetRev.navState = [exet.puz.currDir, exet.puz.currRow, exet.puz.currCol]; exetRev.preflexHash = exet.preflexHash; exetRev.unpreflexHash = exet.unpreflexHash; exetRev.noProperNouns = exet.noProperNouns; exetRev.noStemDupes = exet.noStemDupes; exetRev.asymOK = exet.asymOK; exetRev.tryReversals = exet.tryReversals; exetRev.minpop = exet.minpop; exetRev.requireEnums = exet.requireEnums; exetRev.lightRegexps = exet.lightRegexps; stored.revs.push(exetRev); this.saveLocal(exet.puz.id, JSON.stringify(stored)); } ExetRevManager.prototype.throttledSaveRev = function(revType, details="") { let urgent = revType <= 10; if (this.throttleRevTimer) { clearTimeout(this.throttleRevTimer); if (this.throttlingRevType > 0 && revType < this.throttlingRevType) { urgent = true } } this.throttleRevTimer = null; this.throttlingRevType = 0; if (urgent) { this.saveRev(revType, details) return } this.throttlingRevType = revType; this.throttleRevTimer = setTimeout(() => { this.saveRev(revType, details) this.throttleRevTimer = null; this.throttlingRevType = 0; }, this.saveLagMS); } ExetRevManager.prototype.saveAllRevisions = function() { const storage = {}; for (let idx = 0; idx < window.localStorage.length; idx++) { const id = window.localStorage.key(idx); if (this.skippableKey(id)) { continue; } const storedJson = window.localStorage.getItem(id); let storedRevs = null; try { storedRevs = JSON.parse(storedJson); } catch (err) { console.log('Unparseable stored item for id [' + id + ']:' + storedJson); continue; } if (!storedRevs || !storedRevs['revs']) { console.log('Weird stored item for id [' + id + ']:' + storedJson); continue; } storage[id] = storedRevs; this.savePrefUnpref(id, storedRevs.revs, true); for (isPref of [true, false]) { const key = this.keyPrefUnpref(id, isPref); const data = window.localStorage.getItem(key); if (data) { storage[key] = JSON.parse(data); } } } const json = JSON.stringify(storage, null, 2); const filename = `exet-backup-${(new Date()).toISOString()}.json`; Exolve.prototype.fileDownload(json, "text/json", filename); exetState.lastBackup = Date.now(); exetRevManager.saveLocal(exetRevManager.SPECIAL_KEY, JSON.stringify(exetState)); exet.checkStorage(); exetModals.hide(); } ExetRevManager.prototype.mergeRevisionsFile = function() { exetModals.hide(); let fr = new FileReader(); fr.onload = function(){ let allSavedRevs = {} try { allSavedRevs = JSON.parse(fr.result); } catch (err) { alert('Could not parse the saved revisions file'); return; } existingRevs = {}; for (let idx = 0; idx < window.localStorage.length; idx++) { const id = window.localStorage.key(idx); if (exetRevManager.skippableKey(id)) { continue; } const storedJson = window.localStorage.getItem(id); let storedRevs = null; try { storedRevs = JSON.parse(storedJson); } catch (err) { console.log('Unparseable stored item for id [' + id + ']:' + storedJson); continue; } if (!storedRevs || !storedRevs['revs']) { console.log('Weird stored item for id [' + id + ']:' + storedJson); continue; } for (rev of storedRevs['revs']) { const revHash = exetLexicon.javaHash(JSON.stringify(rev)); existingRevs[revHash] = true; } } let numRevs = 0; let numRevsMerged = 0; let numDupRevs = 0; let numNonLatest = 0; const mergeOnlyLatest = document.getElementById( 'xet-merge-only-latest-revs').checked ? true : false; for (let id in allSavedRevs) { if (id.startsWith(this.SPECIAL_KEY_PREFIX)) { continue; } savedRevs = allSavedRevs[id]['revs']; if (!savedRevs || savedRevs.length == 0) { continue; } const start = mergeOnlyLatest ? savedRevs.length - 1 : 0; if (mergeOnlyLatest) { numNonLatest += savedRevs.length - 1; } revsToSplice = []; for (let i = start; i < savedRevs.length; i++) { numRevs++; const rev = savedRevs[i]; const revHash = exetLexicon.javaHash(JSON.stringify(rev)); if (existingRevs[revHash]) { numDupRevs++; continue; } revsToSplice.push(rev); } if (revsToSplice.length == 0) { continue; } let stored = window.localStorage.getItem(id); const prefKey = exetRevManager.keyPrefUnpref(id, true); let storedPref = window.localStorage.getItem(prefKey); const unprefKey = exetRevManager.keyPrefUnpref(id, true); let storedUnpref = window.localStorage.getItem(unprefKey); try { if (stored) { stored = JSON.parse(stored); } if (storedPref) { storedPref = JSON.parse(storedPref); } if (storedUnpref) { storedUnpref = JSON.parse(storedUnpref); } } catch (err) { console.log('Skipped id in merging as JSON.parse() failed, id: ' + id); continue; } if (!stored) { stored = { id: id, maxRevNum: 0, revs: [] }; } if (!storedPref) { storedPref = {}; } if (!storedUnpref) { storedUnpref = {}; } let addedPref = false; let addedUnpref = false; for (rev of revsToSplice) { stored['revs'].push(rev); if (rev.preflexHash && allSavedRevs[prefKey] && allSavedRevs[prefKey][rev.preflexHash]) { storedPref[rev.preflexHash] = allSavedRevs[prefKey][rev.preflexHash]; addedPref = true; } if (rev.unpreflexHash && allSavedRevs[unprefKey] && allSavedRevs[unprefKey][rev.unpreflexHash]) { storedUnpref[rev.unpreflexHash] = allSavedRevs[unprefKey][rev.unpreflexHash]; addedUnpref = true; } } stored['revs'].sort((r1, r2) => r1.timestamp - r2.timestamp); for (rev of stored['revs']) { if (rev.revNum > stored.maxRevNum) { stored.maxRevNum = rev.revNum; } } if (!exetRevManager.saveLocal(id, JSON.stringify(stored))) { break; } if (addedPref && !exetRevManager.saveLocal(prefKey, JSON.stringify(storedPref))) { break; } if (addedUnpref && !exetRevManager.saveLocal(unprefKey, JSON.stringify(storedUnpref))) { break; } numRevsMerged += revsToSplice.length; } const ignored = (numNonLatest > 0) ? `Ignored ${numNonLatest} non-latest revisions.` : ''; alert(`From ${numRevs} revisions considered across ` + `${Object.keys(allSavedRevs).length} crosswords, merged ` + `${numRevsMerged} revisions. There were ${numDupRevs} revisions ` + `that already existed. ${ignored}`); } const f = document.getElementById('xet-merge-revs-file').files[0]; fr.readAsText(f); } ExetRevManager.prototype.autofree = function() { this.saveAllRevisions(); const SAVE_LAST_THESE_MANY = 25; const SAVE_LAST_THESE_MANY_HOURS = 1; const tsCutoffMillis = Date.now() - (SAVE_LAST_THESE_MANY_HOURS * 60 * 60 * 1000); let bytesUsed = 0; let itemsPurged = 0; for (let idx = 0; idx < window.localStorage.length; idx++) { const id = window.localStorage.key(idx); const storedJson = window.localStorage.getItem(id); bytesUsed += storedJson.length; if (this.skippableKey(id)) { continue; } let stored = ''; try { stored = JSON.parse(storedJson); } catch (err) { console.log('Unparseable stored item for id [' + id + ']:' + storedJson); continue; } if (!stored || !stored["id"] || !stored["revs"] || !stored["maxRevNum"]) { console.log('Weird stored item for id [' + id + ']:' + storedJson); continue; } const revs = stored.revs; if (revs.length <= 0) { console.log('No revisions in crossword with id ' + id + ': should be deleted'); continue; } const limit = revs.length - SAVE_LAST_THESE_MANY; const revsToKeep = []; for (let r = 0; r < revs.length; r++) { const rev = revs[r]; revsToKeep.push(rev); if ((r < limit) && ((r % 2) == 1) && (rev.timestamp < tsCutoffMillis)) { revsToKeep.pop(); itemsPurged++; } } if (revsToKeep.length < revs.length) { stored.revs = revsToKeep; this.saveLocal(id, JSON.stringify(stored)); this.savePrefUnpref(id, stored.revs, true); } } /** * Call checkStorage() to update displayed numbers/warnings, and to * get the return value from checkLocalStorage(). */ const ampleLeft = exet.checkStorage(); if (itemsPurged == 0 && !ampleLeft) { alert('Auto-Free could not find any old revisions to purge, and you ' + 'are running very low on available storage. This probably means ' + 'that you have excessively many active crosswords. Use the ' + '"Manage local storage" menu option to manually delete some old ' + 'crosswords, perhaps.'); } }