Migrate to localforage to support asyncStorage mode

This commit is contained in:
Lucas Verney 2018-11-01 20:22:58 +01:00
parent 068bcdbfc6
commit 76f54932bf
11 changed files with 319 additions and 161 deletions

View File

@ -19,6 +19,7 @@
"@mdi/font": "^3.3.92", "@mdi/font": "^3.3.92",
"file-saver": "^2.0.0", "file-saver": "^2.0.0",
"gps-to-gpx": "git://github.com/phyks/gps-to-gpx.git#1cb5adf4dd382266d076d7df3cb5aa3a4d7dd27b", "gps-to-gpx": "git://github.com/phyks/gps-to-gpx.git#1cb5adf4dd382266d076d7df3cb5aa3a4d7dd27b",
"localforage": "^1.7.3",
"nosleep.js": "^0.9.0", "nosleep.js": "^0.9.0",
"ol": "^5.3.0", "ol": "^5.3.0",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",

View File

@ -82,7 +82,7 @@
<script> <script>
import runtime from 'serviceworker-webpack-plugin/lib/runtime'; import runtime from 'serviceworker-webpack-plugin/lib/runtime';
import { DELAY_BETWEEN_API_BATCH_REQUESTS } from '@/constants'; import { APP_NAME, DELAY_BETWEEN_API_BATCH_REQUESTS } from '@/constants';
import Alert from '@/components/Alert.vue'; import Alert from '@/components/Alert.vue';
import ReportIssueModal from '@/components/ReportIssueModal.vue'; import ReportIssueModal from '@/components/ReportIssueModal.vue';
@ -127,7 +127,7 @@ export default {
isSearchModalShown: false, isSearchModalShown: false,
isSendingReports: false, isSendingReports: false,
isShareMapViewModalShown: false, isShareMapViewModalShown: false,
title: "Cycl'Assist", title: APP_NAME,
}; };
}, },
methods: { methods: {

View File

@ -1,4 +1,5 @@
export const VERSION = '0.3'; export const APP_NAME = 'Cygnal';
export const VERSION = '0.4';
export const NORMAL_ICON_SCALE = 0.625; export const NORMAL_ICON_SCALE = 0.625;
export const LARGE_ICON_SCALE = 1.0; export const LARGE_ICON_SCALE = 1.0;

View File

@ -15,18 +15,21 @@ import router from '@/router';
import store from '@/store'; import store from '@/store';
import '@/vuetify'; import '@/vuetify';
// Ensure locale is correctly set from the store value
store.dispatch('setLocale', { locale: store.state.settings.locale });
Vue.config.productionTip = false; Vue.config.productionTip = false;
new Vue({ // eslint-disable-line no-new // Populate the store with settings from the storage
el: '#app', store.dispatch('populateInitialStateFromStorage').then(() => {
router, // Ensure locale is correctly set from the store value
i18n, store.dispatch('setLocale', { locale: store.state.settings.locale });
store,
components: { App }, new Vue({ // eslint-disable-line no-new
render(h) { el: '#app',
return h('App'); router,
}, i18n,
store,
components: { App },
render(h) {
return h('App');
},
});
}); });

119
src/storage/index.js Normal file
View File

@ -0,0 +1,119 @@
import localforage from 'localforage';
import { APP_NAME } from '@/constants';
// Initialize localforage
localforage.config({
// Prefer IndexedDB
driver: [localforage.INDEXEDDB, localforage.WEBSQL, localforage.LOCALSTORAGE],
name: APP_NAME,
});
export function loadDataFromStorage(table, keys) {
// If a single key is requested, get it
if (!Array.isArray(keys)) {
return localforage.getItem(`${table}.${keys}`).catch(
e => console.error(
`Unable to load data from storage using table ${table} and keys ${keys}: ${e}.`,
),
);
}
// Else, return an array of values
return Promise.all(keys.map(
key => localforage.getItem(`${table}.${key}`),
)).then((arrayValues) => {
const values = {};
arrayValues.forEach((value, i) => {
values[keys[i]] = value;
});
return values;
});
}
// Handle migrations across storage schemes
export function handleMigrations() {
return localforage.getItem('settings.version').then((localForageVersion) => {
const promises = [];
// Get (legacy) version from local storage
let localStorageVersion = null;
try {
// Try to load version from local storage if available
if (localforage.supports(localforage.LOCALSTORAGE)) {
localStorageVersion = (
localStorage.getItem('settings.version') // for versions > 0.3
|| localStorage.getItem('version') // for versions <= 0.3
);
if (localStorageVersion) {
localStorageVersion = JSON.parse(localStorageVersion);
}
}
} catch (e) {
// Pass, ignore error
}
// Pre-0.4
if (!localForageVersion) {
// First migration, to 0.1
if (!localStorageVersion) {
console.log('Migrating local storage to version 0.1...');
const preventSuspend = localStorage.getItem('preventSuspend');
if (preventSuspend !== null) {
localStorage.setItem('hasPreventSuspendPermission', JSON.stringify(preventSuspend));
}
localStorage.removeItem('preventSuspend');
localStorageVersion = '0.1';
localStorage.setItem('version', JSON.stringify(localStorageVersion));
}
// Migration to 0.4
// Migrate to localforage API and purge localStorage values.
if (localStorageVersion < '0.4') {
console.log('Migrating local storage to version 0.4...');
// Migrate settings
[
'hasGeolocationPermission',
'hasPermanentNotificationPermission',
'hasPlaySoundPermission',
'hasPreventSuspendPermission',
'hasVibratePermission',
'locale',
'shouldAutorotateMap',
'skipOnboarding',
'tileServer',
'version',
].forEach((key) => {
let value = localStorage.getItem(key);
if (value) {
// Put value in localforage
try {
value = JSON.parse(value);
} catch (e) {
// Pass, ignore error
}
promises.push(
localforage.setItem(`settings.${key}`, value),
);
// Remove value from localStorage
localStorage.removeItem(key);
}
});
// Migrate unsent reports
const unsentReports = localStorage.getItem('unsentReports');
if (unsentReports) {
// Put value in localforage
promises.push(
localforage.setItem('unsentReports.items', unsentReports),
);
// Remove value from localStorage
localStorage.removeItem('unsentReports');
}
promises.push(
localforage.setItem('settings.version', '0.4'),
);
}
}
return Promise.all(promises);
});
}

View File

@ -1,7 +1,8 @@
import * as api from '@/api'; import * as api from '@/api';
import * as constants from '@/constants'; import * as constants from '@/constants';
import i18n, { messages, getBrowserLocales } from '@/i18n';
import { handleMigrations, loadDataFromStorage } from '@/storage';
import { pointToPointDistance } from '@/tools/geometry'; import { pointToPointDistance } from '@/tools/geometry';
import i18n from '@/i18n';
import { import {
DELETE_REPORT, DELETE_REPORT,
@ -10,6 +11,7 @@ import {
INTRO_WAS_UNSEEN, INTRO_WAS_UNSEEN,
IS_DONE_LOADING, IS_DONE_LOADING,
IS_LOADING, IS_LOADING,
LOAD_UNSENT_REPORTS,
PUSH_REPORT, PUSH_REPORT,
PUSH_UNSENT_REPORT, PUSH_UNSENT_REPORT,
REMOVE_UNSENT_REPORT, REMOVE_UNSENT_REPORT,
@ -24,6 +26,135 @@ import {
STORE_REPORTS, STORE_REPORTS,
} from './mutations-types'; } from './mutations-types';
export function populateInitialStateFromStorage({ commit }) {
return handleMigrations().then(() => {
// Load unsent reports from storage
const unsentReportsPromise = loadDataFromStorage(
'unsentReports', 'items',
).then(
unsentReports => commit(LOAD_UNSENT_REPORTS, {
unsentReports: unsentReports || [],
}),
);
// Load settings from storage
const settingsPromise = loadDataFromStorage('settings', [
'hasGeolocationPermission',
'hasPermanentNotificationPermission',
'hasPlaySoundPermission',
'hasPreventSuspendPermission',
'hasVibratePermission',
'locale',
'shouldAutorotateMap',
'skipOnboarding',
'tileCachingDuration',
'tileServer',
], null).then((dbSettings) => {
const settings = dbSettings || {};
if (!(settings.locale in messages)) {
settings.locale = null;
}
if (!settings.locale) {
// Get best matching locale from browser
const locales = getBrowserLocales();
for (let i = 0; i < locales.length; i += 1) {
if (messages[locales[i]]) {
settings.locale = locales[i];
break; // Break at first matching locale
}
}
}
if (
settings.tileCachingDuration !== null
&& !Number.isInteger(settings.tileCachingDuration)
) {
settings.tileCachingDuration = null;
}
if (
settings.tileServer
&& !constants.TILE_SERVERS[settings.tileServer]
&& !settings.tileServer.startsWith('custom:')
) {
settings.tileServer = null;
}
commit(SET_SETTING, {
setting: 'locale',
value: settings.locale || 'en',
});
commit(SET_SETTING, {
setting: 'hasGeolocationPermission',
value: (
settings.hasGeolocationPermission !== null
? settings.hasGeolocationPermission
: true
),
});
commit(SET_SETTING, {
setting: 'hasPermanentNotificationPermission',
value: (
settings.hasPermanentNotificationPermission !== null
? settings.hasPermanentNotificationPermission
: true
),
});
commit(SET_SETTING, {
setting: 'hasPlaySoundPermission',
value: (
settings.hasPlaySoundPermission !== null
? settings.hasPlaySoundPermission
: true
),
});
commit(SET_SETTING, {
setting: 'hasPreventSuspendPermission',
value: (
settings.hasPreventSuspendPermission !== null
? settings.hasPreventSuspendPermission
: true
),
});
commit(SET_SETTING, {
setting: 'hasVibratePermission',
value: (
settings.hasVibratePermission !== null
? settings.hasVibratePermission
: true
),
});
commit(SET_SETTING, {
setting: 'shouldAutorotateMap',
value: (
settings.shouldAutorotateMap !== null
? settings.shouldAutorotateMap
: false
),
});
commit(SET_SETTING, {
setting: 'skipOnboarding',
value: settings.skipOnboarding || false,
});
commit(SET_SETTING, {
setting: 'tileCachingDuration',
value: (
settings.tileCachingDuration !== null
? settings.tileCachingDuration
: constants.DEFAULT_TILE_CACHING_DURATION
),
});
commit(SET_SETTING, {
setting: 'tileServer',
value: settings.tileServer || constants.DEFAULT_TILE_SERVER,
});
});
return Promise.all(
[settingsPromise, unsentReportsPromise],
);
});
}
export function fetchReports({ commit, state }) { export function fetchReports({ commit, state }) {
commit(IS_LOADING); commit(IS_LOADING);
return api.getActiveReports() return api.getActiveReports()

View File

@ -4,6 +4,7 @@ export const INTRO_WAS_SEEN = 'INTRO_WAS_SEEN';
export const INTRO_WAS_UNSEEN = 'INTRO_WAS_UNSEEN'; export const INTRO_WAS_UNSEEN = 'INTRO_WAS_UNSEEN';
export const IS_DONE_LOADING = 'IS_DONE_LOADING'; export const IS_DONE_LOADING = 'IS_DONE_LOADING';
export const IS_LOADING = 'IS_LOADING'; export const IS_LOADING = 'IS_LOADING';
export const LOAD_UNSENT_REPORTS = 'LOAD_UNSENT_REPORTS';
export const PUSH_REPORT = 'PUSH_REPORT'; export const PUSH_REPORT = 'PUSH_REPORT';
export const PUSH_UNSENT_REPORT = 'PUSH_UNSENT_REPORT'; export const PUSH_UNSENT_REPORT = 'PUSH_UNSENT_REPORT';
export const REMOVE_UNSENT_REPORT = 'REMOVE_UNSENT_REPORT'; export const REMOVE_UNSENT_REPORT = 'REMOVE_UNSENT_REPORT';

View File

@ -1,99 +1,13 @@
import Vue from 'vue'; import Vue from 'vue';
import localforage from 'localforage';
import { messages, getBrowserLocales } from '@/i18n';
import { storageAvailable } from '@/tools';
import { import {
DEFAULT_TILE_CACHING_DURATION, DEFAULT_TILE_CACHING_DURATION,
DEFAULT_TILE_SERVER, DEFAULT_TILE_SERVER,
TILE_SERVERS,
VERSION,
} from '@/constants'; } from '@/constants';
import * as types from './mutations-types'; import * as types from './mutations-types';
function loadDataFromStorage(name) { // Handle the required migrations
try {
const value = localStorage.getItem(name);
if (value) {
return JSON.parse(value);
}
return null;
} catch (e) {
console.error(`Unable to load data from storage using key ${name}: ${e}.`);
return null;
}
}
function handleMigrations() {
if (!storageAvailable('localStorage')) {
return;
}
const version = loadDataFromStorage('version');
// Migration from pre-0.1 to 0.1
if (version === null) {
const preventSuspend = loadDataFromStorage('preventSuspend');
if (preventSuspend !== null) {
localStorage.setItem('hasPreventSuspendPermission', JSON.stringify(preventSuspend));
}
localStorage.removeItem('preventSuspend');
localStorage.setItem('version', JSON.stringify(VERSION));
}
}
// Load unsent reports from storage
let unsentReports = null;
if (storageAvailable('localStorage')) {
unsentReports = loadDataFromStorage('unsentReports');
}
// Load settings from storage
let locale = null;
let hasGeolocationPermission = null;
let hasPermanentNotificationPermission = null;
let hasPlaySoundPermission = null;
let hasPreventSuspendPermission = null;
let hasVibratePermission = null;
let shouldAutorotateMap = null;
let skipOnboarding = null;
let tileCachingDuration = null;
let tileServer = null;
if (storageAvailable('localStorage')) {
handleMigrations();
hasGeolocationPermission = loadDataFromStorage('hasGeolocationPermission');
hasPermanentNotificationPermission = loadDataFromStorage('hasPermanentNotificationPermission');
hasPlaySoundPermission = loadDataFromStorage('hasPlaySoundPermission');
hasPreventSuspendPermission = loadDataFromStorage('hasPreventSuspendPermission');
hasVibratePermission = loadDataFromStorage('hasVibratePermission');
shouldAutorotateMap = loadDataFromStorage('shouldAutorotateMap');
skipOnboarding = loadDataFromStorage('skipOnboarding');
tileServer = loadDataFromStorage('tileServer');
if (tileServer && !TILE_SERVERS[tileServer] && !tileServer.startsWith('custom:')) {
tileServer = null;
}
tileCachingDuration = loadDataFromStorage('tileCachingDuration');
if (tileCachingDuration !== null && !Number.isInteger(tileCachingDuration)) {
tileCachingDuration = null;
}
locale = loadDataFromStorage('locale');
if (!(locale in messages)) {
locale = null;
}
if (!locale) {
// Get best matching locale from browser
const locales = getBrowserLocales();
for (let i = 0; i < locales.length; i += 1) {
if (messages[locales[i]]) {
locale = locales[i];
break; // Break at first matching locale
}
}
}
}
export const initialState = { export const initialState = {
hasGoneThroughIntro: false, hasGoneThroughIntro: false,
hasVibratedOnce: false, hasVibratedOnce: false,
@ -114,30 +28,18 @@ export const initialState = {
userAsked: null, userAsked: null,
}, },
reports: [], reports: [],
unsentReports: unsentReports || [], unsentReports: [],
settings: { settings: {
locale: locale || 'en', locale: 'en',
hasGeolocationPermission: ( hasGeolocationPermission: true,
hasGeolocationPermission !== null ? hasGeolocationPermission : true hasPermanentNotificationPermission: true,
), hasPlaySoundPermission: true,
hasPermanentNotificationPermission: ( hasPreventSuspendPermission: true,
hasPermanentNotificationPermission !== null ? hasPermanentNotificationPermission : true hasVibratePermission: true,
), shouldAutorotateMap: false,
hasPlaySoundPermission: ( skipOnboarding: false,
hasPlaySoundPermission !== null ? hasPlaySoundPermission : true tileCachingDuration: DEFAULT_TILE_CACHING_DURATION,
), tileServer: DEFAULT_TILE_SERVER,
hasPreventSuspendPermission: (
hasPreventSuspendPermission !== null ? hasPreventSuspendPermission : true
),
hasVibratePermission: (
hasVibratePermission !== null ? hasVibratePermission : true
),
shouldAutorotateMap: shouldAutorotateMap !== null ? shouldAutorotateMap : false,
skipOnboarding: skipOnboarding || false,
tileCachingDuration: (
tileCachingDuration !== null ? tileCachingDuration : DEFAULT_TILE_CACHING_DURATION
),
tileServer: tileServer || DEFAULT_TILE_SERVER,
}, },
}; };
@ -163,6 +65,9 @@ export const mutations = {
[types.IS_LOADING](state) { [types.IS_LOADING](state) {
state.isLoading = true; state.isLoading = true;
}, },
[types.LOAD_UNSENT_REPORTS](state, { unsentReports }) {
state.unsentReports = unsentReports;
},
[types.PUSH_REPORT](state, { report }) { [types.PUSH_REPORT](state, { report }) {
const reportIndex = state.reports.findIndex(item => item.id === report.id); const reportIndex = state.reports.findIndex(item => item.id === report.id);
if (reportIndex === -1) { if (reportIndex === -1) {
@ -173,15 +78,11 @@ export const mutations = {
}, },
[types.PUSH_UNSENT_REPORT](state, { report }) { [types.PUSH_UNSENT_REPORT](state, { report }) {
state.unsentReports.push(report); state.unsentReports.push(report);
if (storageAvailable('localStorage')) { localforage.setItem('unsentReports.items', state.unsentReports);
localStorage.setItem('unsentReports', JSON.stringify(state.unsentReports));
}
}, },
[types.REMOVE_UNSENT_REPORT](state, { index }) { [types.REMOVE_UNSENT_REPORT](state, { index }) {
state.unsentReports.splice(index, 1); state.unsentReports.splice(index, 1);
if (storageAvailable('localStorage')) { localforage.setItem('unsentReports.items', state.unsentReports);
localStorage.setItem('unsentReports', JSON.stringify(state.unsentReports));
}
}, },
[types.SET_CURRENT_MAP_CENTER](state, { center }) { [types.SET_CURRENT_MAP_CENTER](state, { center }) {
Vue.set(state.map, 'center', center); Vue.set(state.map, 'center', center);
@ -202,9 +103,7 @@ export const mutations = {
Vue.set(state.location, 'watcherID', id); Vue.set(state.location, 'watcherID', id);
}, },
[types.SET_SETTING](state, { setting, value }) { [types.SET_SETTING](state, { setting, value }) {
if (storageAvailable('localStorage')) { localforage.setItem(`settings.${setting}`, value);
localStorage.setItem(setting, JSON.stringify(value));
}
state.settings[setting] = value; state.settings[setting] = value;
}, },
[types.SHOW_REPORT_DETAILS](state, { id, userAsked }) { [types.SHOW_REPORT_DETAILS](state, { id, userAsked }) {

View File

@ -71,31 +71,6 @@ export function mockLocation(setPosition) {
); );
} }
export function storageAvailable(type) {
let storage;
try {
storage = window[type];
const x = '__storage_test__';
storage.setItem(x, x);
storage.removeItem(x);
return true;
} catch (e) {
return e instanceof DOMException && (
// everything except Firefox
e.code === 22
// Firefox
|| e.code === 1014
// test name field too, because code might not be present
// everything except Firefox
|| e.name === 'QuotaExceededError'
// Firefox
|| e.name === 'NS_ERROR_DOM_QUOTA_REACHED'
)
// acknowledge QuotaExceededError only if there's something already stored
&& storage.length !== 0;
}
}
export function capitalize(string) { export function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
} }

View File

@ -34,6 +34,7 @@
item-value="duration" item-value="duration"
:label="$t('settings.tileCachingDuration')" :label="$t('settings.tileCachingDuration')"
required required
v-if="tileCachingEnabled"
></v-select> ></v-select>
<v-text-field <v-text-field
@ -58,6 +59,8 @@
</template> </template>
<script> <script>
import localforage from 'localforage';
import { TILE_SERVERS } from '@/constants'; import { TILE_SERVERS } from '@/constants';
import { AVAILABLE_LOCALES } from '@/i18n'; import { AVAILABLE_LOCALES } from '@/i18n';
import { capitalize } from '@/tools'; import { capitalize } from '@/tools';
@ -169,6 +172,11 @@ export default {
})); }));
i18nItems.sort((a, b) => a.iso > b.iso); i18nItems.sort((a, b) => a.iso > b.iso);
// Only IndexedDB backend can be used in service workers
const tileCachingEnabled = (
localforage.driver() === localforage.INDEXEDDB
);
return { return {
i18nItems, i18nItems,
orientationModes: [ orientationModes: [
@ -181,6 +189,7 @@ export default {
value: true, value: true,
}, },
], ],
tileCachingEnabled,
}; };
}, },
}; };

View File

@ -4340,6 +4340,11 @@ imagemin@^5.3.1:
pify "^2.3.0" pify "^2.3.0"
replace-ext "^1.0.0" replace-ext "^1.0.0"
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
import-cwd@^2.0.0: import-cwd@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@ -4991,6 +4996,13 @@ levn@^0.3.0, levn@~0.3.0:
prelude-ls "~1.1.2" prelude-ls "~1.1.2"
type-check "~0.3.2" type-check "~0.3.2"
lie@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=
dependencies:
immediate "~3.0.5"
load-bmfont@^1.2.3: load-bmfont@^1.2.3:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/load-bmfont/-/load-bmfont-1.3.0.tgz#bb7e7c710de6bcafcb13cb3b8c81e0c0131ecbc9" resolved "https://registry.yarnpkg.com/load-bmfont/-/load-bmfont-1.3.0.tgz#bb7e7c710de6bcafcb13cb3b8c81e0c0131ecbc9"
@ -5059,6 +5071,13 @@ loader-utils@^1.2.1:
emojis-list "^2.0.0" emojis-list "^2.0.0"
json5 "^1.0.1" json5 "^1.0.1"
localforage@^1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.7.3.tgz#0082b3ca9734679e1bd534995bdd3b24cf10f204"
integrity sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==
dependencies:
lie "3.1.1"
locate-path@^2.0.0: locate-path@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"