Skip to content

Commit

Permalink
feat: composables migrations
Browse files Browse the repository at this point in the history
  • Loading branch information
peronczyk committed Oct 3, 2023
1 parent d94decb commit fc1088e
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 28 deletions.
16 changes: 14 additions & 2 deletions src/composables/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
watchUntilTruthy,
} from '@/utils';
import { ProtocolAdapterFactory } from '@/lib/ProtocolAdapterFactory';
import migrateAccountsVuexToComposable from '@/migrations/001-accounts-vuex-to-composable';
import migrateMnemonicVuexToComposable from '@/migrations/002-mnemonic-vuex-to-composable';
import { useStorageRef } from './composablesHelpers';

let isInitialized = false;
Expand All @@ -31,13 +33,23 @@ let isInitialized = false;
const mnemonic = useStorageRef<string>(
'',
STORAGE_KEYS.mnemonic,
{ backgroundSync: true },
{
backgroundSync: true,
migrations: [
migrateMnemonicVuexToComposable,
],
},
);

const accountsRaw = useStorageRef<IAccountRaw[]>(
[],
STORAGE_KEYS.accountsRaw,
{ backgroundSync: true },
{
backgroundSync: true,
migrations: [
migrateAccountsVuexToComposable,
],
},
);

const activeAccountGlobalIdx = useStorageRef<number>(
Expand Down
63 changes: 37 additions & 26 deletions src/composables/composablesHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
watch,
} from 'vue';
import { isEqual } from 'lodash-es';
import type { StorageKey } from '@/types';
import type { Migration, StorageKey } from '@/types';
import { asyncPipe } from '@/utils';
import { WalletStorage } from '@/lib/WalletStorage';
import { useConnection } from './connection';
import { useUi } from './ui';
Expand All @@ -21,9 +22,10 @@ interface ICreateStorageRefOptions<T> {
* Callbacks run on the data that will be saved and read from the browser storage.
*/
serializer?: {
read: (v: T) => any,
write: (v: T) => any,
read: (v: T) => any;
write: (v: T) => any;
};
migrations?: Migration<T>[];
}

/**
Expand All @@ -39,43 +41,52 @@ export function useStorageRef<T = string | object | any[]>(
const {
serializer,
backgroundSync = false,
migrations,
} = options;

let isRestored = false;
let watcherDisabled = false; // Avoid watcher going infinite loop
const state = ref(initialState) as Ref<T>; // https:/vuejs/core/issues/2136

function setState(val: any) {
function setLocalState(val: T | null) {
if (val) {
watcherDisabled = true;
state.value = (serializer?.read) ? serializer.read(val) : val;
setTimeout(() => { watcherDisabled = false; }, 0);
}
}

watch(state, (val, oldVal) => {
// Arrays are not compared as there is a bug which makes the new and old val always the same.
if (!watcherDisabled && (Array.isArray(initialState) || !isEqual(val, oldVal))) {
WalletStorage.set(storageKey, (serializer?.write) ? serializer.write(val) : val);
}
}, { deep: true });

/**
* Two way binding between the extension and the background
* Whenever the app saves the state to browser storage the extension background picks this
* and synchronizes own state with the change.
*/
if (backgroundSync) {
WalletStorage.watch?.(storageKey, (val) => setState(val));
function setStorageState(val: T | null) {
WalletStorage.set(storageKey, (val && serializer?.write) ? serializer.write(val) : val);
}

if (!isRestored) {
(async () => {
const restoredValue = await WalletStorage.get<T | null>(storageKey);
setState(restoredValue);
isRestored = true;
})();
}
// Restore state and run watchers
(async () => {
let restoredValue = await WalletStorage.get<T | null>(storageKey);
if (migrations?.length) {
restoredValue = await asyncPipe<T | null>(migrations)(restoredValue);
setStorageState(restoredValue);
}
setLocalState(restoredValue);

/**
* Synchronize the state value with the storage.
*/
watch(state, (val, oldVal) => {
// Arrays are not compared as there is a bug which makes the new and old val always the same.
if (!watcherDisabled && (Array.isArray(initialState) || !isEqual(val, oldVal))) {
setStorageState(val);
}
}, { deep: true });

/**
* Two way binding between the extension and the background
* Whenever the app saves the state to browser storage the extension background picks this
* and synchronizes own state with the change.
*/
if (backgroundSync) {
WalletStorage.watch?.(storageKey, (val) => setLocalState(val));
}
})();

return state;
}
Expand Down
27 changes: 27 additions & 0 deletions src/migrations/001-accounts-vuex-to-composable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { IAccountRaw, Migration } from '@/types';
import { ACCOUNT_HD_WALLET, PROTOCOLS } from '@/constants';
import { collectVuexState } from './migrationHelpers';

const migration: Migration<IAccountRaw[]> = async (restoredValue: IAccountRaw[]) => {
if (!restoredValue?.length) {
const accounts = (await collectVuexState())?.accounts?.list as any[] | undefined;
if (accounts?.length) {
return accounts.reduce(
(list: IAccountRaw[], { protocol, type }: IAccountRaw) => {
if (PROTOCOLS.includes(protocol) && type === ACCOUNT_HD_WALLET) {
list.push({
isRestored: true,
protocol,
type,
});
}
return list;
},
[],
);
}
}
return restoredValue;
};

export default migration;
15 changes: 15 additions & 0 deletions src/migrations/002-mnemonic-vuex-to-composable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { validateMnemonic } from '@aeternity/bip39';
import type { Migration } from '@/types';
import { collectVuexState } from './migrationHelpers';

const migration: Migration = async (restoredValue: string) => {
if (!restoredValue) {
const mnemonic = (await collectVuexState())?.mnemonic;
if (mnemonic && validateMnemonic(mnemonic)) {
return mnemonic;
}
}
return restoredValue;
};

export default migration;
23 changes: 23 additions & 0 deletions src/migrations/migrationHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { watchUntilTruthy } from '@/utils';
import { ref } from 'vue';

/**
* Before version 2.0.2 we were using Vuex and the whole state was kept as
* one browser/local storage entry.
*/
export const collectVuexState = (() => {
let vuexState: Record<string, any> | null;
const isCollecting = ref(false);
return async () => {
if (window?.browser) {
if (isCollecting.value) {
await watchUntilTruthy(isCollecting);
} else if (!vuexState) {
isCollecting.value = true;
vuexState = (await window.browser.storage.local.get('state'))?.state as any;
isCollecting.value = false;
}
}
return vuexState;
};
})();
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -668,3 +668,5 @@ export interface IFormSelectOption {
text: string;
value: string | number;
}

export type Migration<T = any> = (restoredValue: T | any) => Promise<T>;
10 changes: 10 additions & 0 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@ export function pipe<T = any[]>(fns: ((data: T) => T)[]) {
return (data: T) => fns.reduce((currData, func) => func(currData), data);
}

/**
* Run asynchronous callbacks one by one and pass previous returned value to the next one.
*/
export function asyncPipe<T = any[]>(fns: ((data: T) => PromiseLike<T>)[]) {
return (data: T): Promise<T> => fns.reduce(
async (currData, func) => func(await currData),
Promise.resolve(data),
);
}

export function prepareStorageKey(keys: string[]) {
return [LOCAL_STORAGE_PREFIX, ...keys].join('_');
}
Expand Down

0 comments on commit fc1088e

Please sign in to comment.