Skip to content

Commit

Permalink
Local storage v3 migration (#172)
Browse files Browse the repository at this point in the history
* Add v3 migration

* Add validations for frontend/grpc url

* Improve migration logic

* move folders

* Bug fixes

* working tests

* Add locks

* Add locks test

* [in progress] move from field migrations to whole db migrations

* Update tests for new schema

* Add additional code documentation

* Improve error messaging

* move lock set inside try
  • Loading branch information
grod220 authored Aug 26, 2024
1 parent e0da705 commit c020aea
Show file tree
Hide file tree
Showing 24 changed files with 1,363 additions and 431 deletions.
2 changes: 1 addition & 1 deletion apps/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@bufbuild/protobuf": "^1.10.0",
"@connectrpc/connect": "^1.4.0",
"@connectrpc/connect-web": "^1.4.0",
"@penumbra-labs/registry": "11.0.0",
"@penumbra-labs/registry": "11.1.0",
"@penumbra-zone/bech32m": "^7.0.0",
"@penumbra-zone/client": "^18.0.0",
"@penumbra-zone/crypto-web": "^22.0.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/extension/src/state/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface PasswordSlice {

export const createPasswordSlice =
(
session: ExtensionStorage<Partial<SessionStorageState>>,
session: ExtensionStorage<SessionStorageState>,
local: ExtensionStorage<LocalStorageState>,
): SliceCreator<PasswordSlice> =>
set => {
Expand Down
45 changes: 17 additions & 28 deletions apps/extension/src/state/persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { produce } from 'immer';
import { localExtStorage } from '../storage/local';
import { LocalStorageState } from '../storage/types';
import { sessionExtStorage, SessionStorageState } from '../storage/session';
import { StorageItem } from '../storage/base';
import { walletsFromJson } from '@penumbra-zone/types/wallet';
import { AppParameters } from '@penumbra-zone/protobuf/penumbra/core/app/v1/app_pb';

Expand Down Expand Up @@ -61,79 +60,69 @@ export const customPersistImpl: Persist = f => (set, get, store) => {

function syncLocal(changes: Record<string, chrome.storage.StorageChange>, set: Setter) {
if (changes['wallets']) {
const wallets = changes['wallets'].newValue as
| StorageItem<LocalStorageState['wallets']>
| undefined;
const wallets = changes['wallets'].newValue as LocalStorageState['wallets'] | undefined;
set(
produce((state: AllSlices) => {
state.wallets.all = wallets ? walletsFromJson(wallets.value) : [];
state.wallets.all = wallets ? walletsFromJson(wallets) : [];
}),
);
}

if (changes['fullSyncHeight']) {
const stored = changes['fullSyncHeight'].newValue as
| StorageItem<LocalStorageState['fullSyncHeight']>
| LocalStorageState['fullSyncHeight']
| undefined;
set(
produce((state: AllSlices) => {
state.network.fullSyncHeight = stored?.value ?? 0;
state.network.fullSyncHeight = stored ?? 0;
}),
);
}

if (changes['grpcEndpoint']) {
const stored = changes['grpcEndpoint'].newValue as
| StorageItem<LocalStorageState['grpcEndpoint']>
| LocalStorageState['grpcEndpoint']
| undefined;
set(
produce((state: AllSlices) => {
state.network.grpcEndpoint = stored?.value ?? state.network.grpcEndpoint;
state.network.grpcEndpoint = stored ?? state.network.grpcEndpoint;
}),
);
}

if (changes['knownSites']) {
const stored = changes['knownSites'].newValue as
| StorageItem<LocalStorageState['knownSites']>
| undefined;
const stored = changes['knownSites'].newValue as LocalStorageState['knownSites'] | undefined;
set(
produce((state: AllSlices) => {
state.connectedSites.knownSites = stored?.value ?? state.connectedSites.knownSites;
state.connectedSites.knownSites = stored ?? state.connectedSites.knownSites;
}),
);
}

if (changes['frontendUrl']) {
const stored = changes['frontendUrl'].newValue as
| StorageItem<LocalStorageState['frontendUrl']>
| undefined;
const stored = changes['frontendUrl'].newValue as LocalStorageState['frontendUrl'] | undefined;
set(
produce((state: AllSlices) => {
state.defaultFrontend.url = stored?.value ?? state.defaultFrontend.url;
state.defaultFrontend.url = stored ?? state.defaultFrontend.url;
}),
);
}

if (changes['numeraires']) {
const stored = changes['numeraires'].newValue as
| StorageItem<LocalStorageState['numeraires']>
| undefined;
const stored = changes['numeraires'].newValue as LocalStorageState['numeraires'] | undefined;
set(
produce((state: AllSlices) => {
state.numeraires.selectedNumeraires = stored?.value ?? state.numeraires.selectedNumeraires;
state.numeraires.selectedNumeraires = stored ?? state.numeraires.selectedNumeraires;
}),
);
}

if (changes['params']) {
const stored = changes['params'].newValue as
| StorageItem<LocalStorageState['params']>
| undefined;
const stored = changes['params'].newValue as LocalStorageState['params'] | undefined;
set(
produce((state: AllSlices) => {
state.network.chainId = stored?.value
? AppParameters.fromJsonString(stored.value).chainId
state.network.chainId = stored
? AppParameters.fromJsonString(stored).chainId
: state.network.chainId;
}),
);
Expand All @@ -143,11 +132,11 @@ function syncLocal(changes: Record<string, chrome.storage.StorageChange>, set: S
function syncSession(changes: Record<string, chrome.storage.StorageChange>, set: Setter) {
if (changes['hashedPassword']) {
const item = changes['hashedPassword'].newValue as
| StorageItem<SessionStorageState['passwordKey']>
| SessionStorageState['passwordKey']
| undefined;
set(
produce((state: AllSlices) => {
state.password.key = item ? item.value : undefined;
state.password.key = item ? item : undefined;
}),
);
}
Expand Down
64 changes: 64 additions & 0 deletions apps/extension/src/storage/base-defaults.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { beforeEach, describe, expect, test } from 'vitest';
import { ExtensionStorage } from './base';
import { MockStorageArea } from './mock';

interface MockState {
dbVersion: number;
network: string;
seedPhrase: string | undefined;
accounts: {
label: string;
}[];
fullSyncHeight: number;
}

describe('Base storage default instantiation', () => {
let extStorage: ExtensionStorage<MockState>;

beforeEach(() => {
const storageArea = new MockStorageArea();
extStorage = new ExtensionStorage<MockState>({
storage: storageArea,
defaults: {
network: '',
seedPhrase: undefined,
accounts: [],
fullSyncHeight: 0,
},
version: { current: 1, migrations: { 0: (state: MockState) => state } },
});
});

test('first get made initializes defaults', async () => {
const networkDefault = await extStorage.get('network');
expect(networkDefault).toBe('');

const seedPhraseDefault = await extStorage.get('seedPhrase');
expect(seedPhraseDefault).toBe(undefined);

const accountsDefault = await extStorage.get('accounts');
expect(accountsDefault).toStrictEqual([]);

const syncHeightDefault = await extStorage.get('fullSyncHeight');
expect(syncHeightDefault).toBe(0);
});

test('first get made initializes version', async () => {
const version = await extStorage.get('dbVersion');
expect(version).toBe(1);
});

test('should handle concurrent initializations w/ locks', async () => {
const promise1 = extStorage.get('fullSyncHeight');
const promise2 = extStorage.set('fullSyncHeight', 123);
const promise3 = extStorage.get('fullSyncHeight');

// Both should resolve to the same migrated value
const result1 = await promise1;
await promise2;
const result3 = await promise3;

expect(result1).toBe(0);
expect(result3).toBe(123);
});
});
Loading

0 comments on commit c020aea

Please sign in to comment.