diff --git a/packages/nicolium/src/actions/frontend-config.ts b/packages/nicolium/src/actions/frontend-config.ts index e1ce3794c..2e0bd7b2d 100644 --- a/packages/nicolium/src/actions/frontend-config.ts +++ b/packages/nicolium/src/actions/frontend-config.ts @@ -12,7 +12,7 @@ const FRONTEND_CONFIG_REQUEST_FAIL = 'FRONTEND_CONFIG_REQUEST_FAIL' as const; const FRONTEND_CONFIG_REMEMBER_SUCCESS = 'FRONTEND_CONFIG_REMEMBER_SUCCESS' as const; const rememberFrontendConfig = (host: string | null) => (dispatch: AppDispatch) => - KVStore.getItemOrError(`plfe_config:${host}`) + KVStore.getItemOrError(`frontendConfig:${host}`) .then((frontendConfig) => { dispatch({ type: FRONTEND_CONFIG_REMEMBER_SUCCESS, diff --git a/packages/nicolium/src/actions/push-notifications/registerer.ts b/packages/nicolium/src/actions/push-notifications/registerer.ts index 8709df9b6..9c40d443f 100644 --- a/packages/nicolium/src/actions/push-notifications/registerer.ts +++ b/packages/nicolium/src/actions/push-notifications/registerer.ts @@ -1,5 +1,5 @@ import { createPushSubscription } from '@/actions/push-subscriptions'; -import { pushNotificationsSetting } from '@/settings'; +import { pushNotificationsSettings } from '@/settings'; import { getVapidKey } from '@/utils/auth'; import { decode as decodeBase64 } from '@/utils/base64'; @@ -55,7 +55,7 @@ const sendSubscriptionToBackend = const params = { subscription, data: { alerts } }; if (me) { - const data = pushNotificationsSetting.get(me); + const data = pushNotificationsSettings.get(me); if (data) { params.data = data; } @@ -124,7 +124,7 @@ const register = () => (dispatch: AppDispatch, getState: () => RootState) => { if (!(subscription instanceof PushSubscription)) { dispatch(setSubscription(subscription)); if (me) { - pushNotificationsSetting.set(me, { alerts: subscription.alerts }); + pushNotificationsSettings.set(me, { alerts: subscription.alerts }); } } }) @@ -141,7 +141,7 @@ const register = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(clearSubscription()); if (me) { - pushNotificationsSetting.remove(me); + pushNotificationsSettings.remove(me); } return getRegistration().then(getPushSubscription).then(unsubscribe); diff --git a/packages/nicolium/src/main.tsx b/packages/nicolium/src/main.tsx index 4ea609b24..c05eb7814 100644 --- a/packages/nicolium/src/main.tsx +++ b/packages/nicolium/src/main.tsx @@ -11,6 +11,7 @@ declare global { } import './polyfills'; +import '@/storage/migrate-legacy-data'; import React from 'react'; import { createRoot } from 'react-dom/client'; diff --git a/packages/nicolium/src/reducers/auth.ts b/packages/nicolium/src/reducers/auth.ts index 02e2e2056..9993da90f 100644 --- a/packages/nicolium/src/reducers/auth.ts +++ b/packages/nicolium/src/reducers/auth.ts @@ -97,8 +97,8 @@ const buildKey = (parts: string[]) => parts.join(':'); // For subdirectory support const NAMESPACE = trim(BuildConfig.FE_SUBDIRECTORY, '/') - ? `pl-fe@${BuildConfig.FE_SUBDIRECTORY}` - : 'pl-fe'; + ? `nicolium@${BuildConfig.FE_SUBDIRECTORY}` + : 'nicolium'; const STORAGE_KEY = buildKey([NAMESPACE, 'auth']); diff --git a/packages/nicolium/src/reducers/frontend-config.ts b/packages/nicolium/src/reducers/frontend-config.ts index 0b36dce24..84911b9a6 100644 --- a/packages/nicolium/src/reducers/frontend-config.ts +++ b/packages/nicolium/src/reducers/frontend-config.ts @@ -44,7 +44,7 @@ const preloadImport = (state: Record, action: Record) const persistFrontendConfig = (frontendConfig: PartialFrontendConfig, host: string) => { if (host) { - KVStore.setItem(`plfe_config:${host}`, frontendConfig).catch(console.error); + KVStore.setItem(`frontendConfig:${host}`, frontendConfig).catch(console.error); } }; diff --git a/packages/nicolium/src/settings.ts b/packages/nicolium/src/settings.ts index fddb32bed..96189ccb6 100644 --- a/packages/nicolium/src/settings.ts +++ b/packages/nicolium/src/settings.ts @@ -45,6 +45,6 @@ class Settings { } /** Remember push notification settings. */ -const pushNotificationsSetting = new Settings('plfe_push_notification_data'); +const pushNotificationsSettings = new Settings('nicolium:pushNotificationSettings'); -export { pushNotificationsSetting }; +export { pushNotificationsSettings }; diff --git a/packages/nicolium/src/storage/kv-store.ts b/packages/nicolium/src/storage/kv-store.ts index d7f866c81..5f2d8b5f4 100644 --- a/packages/nicolium/src/storage/kv-store.ts +++ b/packages/nicolium/src/storage/kv-store.ts @@ -1,5 +1,7 @@ import localforage from 'localforage'; +import { migrationComplete } from './migrate-legacy-data'; + interface IKVStore extends LocalForage { getItemOrError: (key: string) => Promise; } @@ -7,12 +9,18 @@ interface IKVStore extends LocalForage { // localForage // https://localforage.github.io/localForage/#settings-api-config const KVStore = localforage.createInstance({ - name: 'pl-fe', + name: 'nicolium', description: 'Nicolium offline data store', driver: localforage.INDEXEDDB, storeName: 'keyvaluepairs', }) as IKVStore; +const originalGetItem = KVStore.getItem.bind(KVStore); +KVStore.getItem = async (...args) => { + await migrationComplete; + return originalGetItem(...args); +}; + // localForage returns 'null' when a key isn't found. // In the Redux action flow, we want it to fail harder. KVStore.getItemOrError = (key: string) => diff --git a/packages/nicolium/src/storage/migrate-legacy-data.ts b/packages/nicolium/src/storage/migrate-legacy-data.ts new file mode 100644 index 000000000..4fffe4892 --- /dev/null +++ b/packages/nicolium/src/storage/migrate-legacy-data.ts @@ -0,0 +1,95 @@ +/** + * Migrate data from the legacy `pl-fe` namespaces to the new ones using `nicolium`. + * Includes synchronous migrations for localStorage (ran on import) and async for IndexedDB. + * Made a hack in @/storage/kv-store to delay getItem calls until the async migration is complete to avoid race conditions. + * Will remove this migration handling in like a month or so. + */ + +import localforage from 'localforage'; +import trim from 'lodash/trim'; + +import { FE_SUBDIRECTORY } from '@/build-config'; + +// synchronous localStorage migrations + +const migrateLocalStorageKey = (oldKey: string, newKey: string) => { + const value = localStorage.getItem(oldKey); + if (value !== null && localStorage.getItem(newKey) === null) { + localStorage.setItem(newKey, value); + localStorage.removeItem(oldKey); + } +}; + +const migrateAuthStorage = () => { + // subdirectory support, see @/reducers/auth + const subdir = trim(FE_SUBDIRECTORY, '/'); + const oldNamespace = subdir ? `pl-fe@${FE_SUBDIRECTORY}` : 'pl-fe'; + const newNamespace = subdir ? `nicolium@${FE_SUBDIRECTORY}` : 'nicolium'; + + migrateLocalStorageKey(`${oldNamespace}:auth`, `${newNamespace}:auth`); +}; + +const migratePushNotificationSettings = () => { + const keysToMigrate: string[] = []; + + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key?.startsWith('plfe_push_notification_data')) { + keysToMigrate.push(key); + } + } + + for (const oldKey of keysToMigrate) { + const newKey = oldKey.replace( + 'plfe_push_notification_data', + 'nicolium:pushNotificationSettings', + ); + migrateLocalStorageKey(oldKey, newKey); + } +}; + +migrateAuthStorage(); +migratePushNotificationSettings(); + +// async migrations + +const migrateIndexedDB = async () => { + const oldStore = localforage.createInstance({ + name: 'pl-fe', + driver: localforage.INDEXEDDB, + storeName: 'keyvaluepairs', + }); + + const newStore = localforage.createInstance({ + name: 'nicolium', + driver: localforage.INDEXEDDB, + storeName: 'keyvaluepairs', + }); + + try { + const oldKeys = await oldStore.keys(); + const newKeys = await newStore.keys(); + + if (oldKeys.length === 0 || newKeys.length > 0) { + await oldStore.dropInstance({ name: 'pl-fe' }); + return; + } + + for (const oldKey of oldKeys) { + const value = await oldStore.getItem(oldKey); + + const newKey = oldKey.startsWith('plfe_config:') + ? oldKey.replace('plfe_config:', 'frontendConfig:') + : oldKey; + await newStore.setItem(newKey, value); + } + + await oldStore.dropInstance({ name: 'pl-fe' }); + } catch (e) { + console.error('Failed to migrate IndexedDB data', e); + } +}; + +const migrationComplete = migrateIndexedDB(); + +export { migrationComplete };