nicolium: add migrations for keys used for data storage

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-03-02 22:09:49 +01:00
parent 828a5026f8
commit 658fe4ee6d
8 changed files with 115 additions and 11 deletions

View File

@ -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<FrontendConfigAction>({
type: FRONTEND_CONFIG_REMEMBER_SUCCESS,

View File

@ -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);

View File

@ -11,6 +11,7 @@ declare global {
}
import './polyfills';
import '@/storage/migrate-legacy-data';
import React from 'react';
import { createRoot } from 'react-dom/client';

View File

@ -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']);

View File

@ -44,7 +44,7 @@ const preloadImport = (state: Record<string, any>, action: Record<string, any>)
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);
}
};

View File

@ -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 };

View File

@ -1,5 +1,7 @@
import localforage from 'localforage';
import { migrationComplete } from './migrate-legacy-data';
interface IKVStore extends LocalForage {
getItemOrError: (key: string) => Promise<any>;
}
@ -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) =>

View File

@ -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 };