Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Refactor extension storage classes #24016

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

brad-decker
Copy link
Contributor

@brad-decker brad-decker commented Apr 12, 2024

Description

adds a new class that models the shared surface of the two storage solutions we utilize, and then creates two new classes designed to extend from that one. The goal of this Pull request is to consolidate some of the operations of state management that were previously being implemented in background.js (the process of creating a new state tree). We relied upon a bullish value being returned from the .get() method to know to generate a new first time state object fro the user. This logic has been moved into the storage class so that determining whether to use a new state tree is handled within the storage system itself.

This is a step towards providing a fail safe for state corruption. The new class will be utilized for this additional functionality.

Related issues

Fixes:

Manual testing steps

No functional changes should have occurred. Manual regression cases to test include:

  • make some changes such as a adding a network or changing settings. Close the browser. Re-open the browser and open metamask. The data should be unchanged.
  • Install an old version... such as 11.10.0. Make some changes. Upgrade to the build generated from this branch. Verify that the state of the wallet, and its data, is as expected.

Screenshots/Recordings

Before

After

Pre-merge author checklist

  • I’ve followed MetaMask Coding Standards.
  • I've completed the PR template to the best of my ability
  • I’ve included tests if applicable
  • I’ve documented my code using JSDoc format if applicable
  • I’ve applied the right labels on the PR (see labeling guidelines). Not required for external contributors.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

@brad-decker brad-decker changed the base branch from develop to feat/new-onboarding-type April 12, 2024 20:55
@brad-decker brad-decker changed the title Feat/refactor state classes Refactor(storage): Refactor extension storage classes Apr 12, 2024
@brad-decker brad-decker marked this pull request as ready for review April 15, 2024 15:30
@brad-decker brad-decker requested a review from a team as a code owner April 15, 2024 15:30
@brad-decker brad-decker force-pushed the feat/new-onboarding-type branch from 9e06f1d to b451ae0 Compare April 15, 2024 15:35
@metamaskbot
Copy link
Collaborator

Builds ready [9837883]
Page Load Metrics (1288 ± 593 ms)
PlatformPageMetricMin (ms)Max (ms)Average (ms)StandardDeviation (ms)MarginOfError (ms)
ChromeHomefirstPaint813471416632
domContentLoaded159931199
load68318812881235593
domInteractive159931199
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: -1.85 KiB (-0.06%)
  • ui: 587 Bytes (0.01%)
  • common: 6.78 KiB (0.13%)

Base automatically changed from feat/new-onboarding-type to develop April 23, 2024 17:22
@brad-decker brad-decker force-pushed the feat/refactor-state-classes branch from 9837883 to fa740a8 Compare April 24, 2024 17:45
Copy link

codecov bot commented Apr 25, 2024

Codecov Report

Attention: Patch coverage is 44.44444% with 65 lines in your changes are missing coverage. Please review.

Project coverage is 67.46%. Comparing base (cfcbd1e) to head (91b92a6).

Files Patch % Lines
shared/modules/Storage/ReadOnlyNetworkStore.ts 0.00% 43 Missing ⚠️
shared/modules/Storage/ExtensionStore.ts 70.77% 19 Missing ⚠️
app/scripts/lib/setup-initial-state-hooks.js 0.00% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop   #24016      +/-   ##
===========================================
- Coverage    67.48%   67.46%   -0.02%     
===========================================
  Files         1288     1289       +1     
  Lines        50153    50181      +28     
  Branches     13023    13027       +4     
===========================================
+ Hits         33842    33852      +10     
- Misses       16311    16329      +18     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@metamaskbot
Copy link
Collaborator

Builds ready [be80104]
Page Load Metrics (882 ± 603 ms)
PlatformPageMetricMin (ms)Max (ms)Average (ms)StandardDeviation (ms)MarginOfError (ms)
ChromeHomefirstPaint57152902713
domContentLoaded9371473
load4530498821256603
domInteractive9371473
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: -1.85 KiB (-0.05%)
  • ui: 0 Bytes (0.00%)
  • common: 6.78 KiB (0.11%)

// we use this flag to avoid flooding sentry with a ton of errors:
// once data persistence fails once and it flips true we don't send further
// data persistence errors to sentry
this.dataPersistenceFailing = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

DDDDDanica
DDDDDanica previously approved these changes Apr 29, 2024
@metamaskbot
Copy link
Collaborator

Builds ready [8e43959]
Page Load Metrics (648 ± 569 ms)
PlatformPageMetricMin (ms)Max (ms)Average (ms)StandardDeviation (ms)MarginOfError (ms)
ChromeHomefirstPaint54151822713
domContentLoaded75515115
load4333276481185569
domInteractive75515115
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: -1.85 KiB (-0.05%)
  • ui: 0 Bytes (0.00%)
  • common: 6.57 KiB (0.11%)

@@ -81,12 +80,14 @@ import DesktopManager from '@metamask/desktop/dist/desktop-manager';

// Setup global hook for improved Sentry state snapshots during initialization
const inTest = process.env.IN_TEST;
const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore();
const migrator = new Migrator({ migrations });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously the new Migrator({ migrations }) call was inside loadStateFromPersistence which was inside the try block of initialize, the catch of which uses rejectInitialization coming from the deferredPromise() call. It seems unlikely that the Migrator constructor could throw an error, but if it did somehow, it would no longer be handled by rejectInitialization. But again, maybe it is not really possible for the Migrator constructor to error?

Also, the timing of creating the migrator has changed slightly: it used to await a localStore.get call, but now will happen immediately on the browser loading the background file. This probably has no consequence, but I thought I'd point it out

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine and that these changes don't have any negative impact.

// this should never happen but new error reporting suggests that it has
// for a small number of users
// https://github.com/metamask/metamask-extension/issues/3919
if (versionedData && !versionedData.data) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this check for !versionedData.data, or an alternative to it, present in the new ExtensionStore.ts? I can't find it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed by 70e6191

// if the object is empty, treat it as undefined
if (isEmpty(result)) {
this.mostRecentRetrievedState = null;
this.stateCorruptionDetected = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing it, but I don't see this.stateCorruptionDetected being used anywhere. I see it being set to true and false, but then not used otherwise

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now used in unit tests as of 70e6191

Will be used in application code in a following PR

this.#state = await response.json();
}
} catch (error) {
if (isErrorWithMessage(error)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we ever have an error without a message here? If so, why would we ignore it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in ada46a4

this.#initialized = true;
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line we used to have in background.js that was replaced by a call to isFirstTimeInstall was
const storeAlreadyExisted = Boolean(await localStore.get());

In turn, that get call had:

    if (!this._initialized) {
      await this._initializing;
    }

So previously, in background.js, when the localStore was a ReadOnlyNetworkStore, the onInstall() function was waiting for init() to resolve, but now it is not awaiting that. Is that intentional?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should const result = this.#state; be const result = await this.get();?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e391c77

Copy link
Contributor

@danjm danjm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have completed my first review on this. Left some questions and comments. Nice work overall, a very good improvement to the codebase

* A read-only network-based storage wrapper
*/
export default class ReadOnlyNetworkStore extends BaseStorage {
#initialized: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QQ: why are we using # instead of private keyword here ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that background.js is still just javascript, the private javascript field ensures that these properties will not be misused or unintentionally modified.

this.stateCorruptionDetected = false;
this.dataPersistenceFailing = false;
this.migrator = migrator;
this.firstTimeInstall = false;
Copy link
Contributor

@salimtb salimtb May 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given that our constructor doesn't rely on a parameter-based variable for initialization, why do we initiate it there? Could we consider an alternative approach like the following?

export default class ReadOnlyNetworkStore extends BaseStorage {
  #initialized: boolean = false;
  #initialized = false;
  #promiseToInitialize?: Promise<void>;
  #state = null;
   mostRecentRetrievedState = null;
   stateCorruptionDetected = false;
   dataPersistenceFailing = false;
   migrator:migrator;
   firstTimeInstall = false;
    
 constructor({ migrator }: { migrator: Migrator }) {
    super();
    this.migrator = migrator;
    this.promiseToInitialize = this.init();
  }
.....

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you say more about this? What are the benefits you see from this approach?

@danjm danjm changed the title Refactor(storage): Refactor extension storage classes refactor (storage): Refactor extension storage classes Nov 15, 2024
@danjm
Copy link
Contributor

danjm commented Nov 15, 2024

I need to fix the failing tests...

app/scripts/lib/Stores/ExtensionStore.ts Outdated Show resolved Hide resolved
*/
#set(obj: {
data: IntermediaryStateType;
meta: { version: number };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Changing to metadata as it is referred as this.metadata across the store

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would require a state migration, which I'd like to avoid for the sake of a renaming. We could consider renaming this.metadata to this.meta

app/scripts/lib/Stores/ExtensionStore.test.ts Outdated Show resolved Hide resolved
app/scripts/lib/Stores/ExtensionStore.ts Outdated Show resolved Hide resolved
app/scripts/lib/Stores/ExtensionStore.test.ts Outdated Show resolved Hide resolved
app/scripts/lib/Stores/ExtensionStore.ts Show resolved Hide resolved
app/scripts/lib/Stores/ExtensionStore.ts Outdated Show resolved Hide resolved
app/scripts/background.js Outdated Show resolved Hide resolved
@danjm danjm changed the title refactor (storage): Refactor extension storage classes refactor: Refactor extension storage classes Dec 9, 2024
Copy link
Contributor

@danjm danjm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Unit tests for ReadOnlyNetworkStore
  • delete the existing store files

@danjm danjm force-pushed the feat/refactor-state-classes branch from 381a058 to 82d2650 Compare December 9, 2024 18:42
@metamaskbot
Copy link
Collaborator

Builds ready [f0c377f]
Page Load Metrics (2104 ± 187 ms)
PlatformPageMetricMin (ms)Max (ms)Average (ms)StandardDeviation (ms)MarginOfError (ms)
ChromeHomefirstPaint39234042018543261
domContentLoaded158033882084390187
load162734062104390187
domInteractive247641157
backgroundConnect13112262311
firstReactRender16372152
getState792431333215
initialActions01000
loadScripts122222621605259124
setupStore7201042
uiStartup182940182440485233
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: -298.99 KiB (-5.69%)
  • ui: 0 Bytes (0.00%)
  • common: 303.41 KiB (3.77%)

@DDDDDanica
Copy link
Contributor

LGTM !


async isFirstTimeInstall(): Promise<boolean> {
const result = await this.#get();
return Boolean(isEmpty(result));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this Boolean is redundant

const FIXTURE_SERVER_PORT = 12345;
const FIXTURE_SERVER_URL = `http://${FIXTURE_SERVER_HOST}:${FIXTURE_SERVER_PORT}/state.json`;

/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment could be more descriptive

* metadata key of this class is set on the 'meta' key of the underlying
* storage implementation (e.g. chrome.storage.local).
*/
set metadata(metadata: { version: number }) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider how to ensure:

  • This is always set prior to persistence
  • This is correctly loaded from persisted state (or backup persisted state) prior to migrations running
  • This cannot be set to the incorrect value (e.g. guard where/how it's set)

// has changed using migrations to adapt to backwards incompatible changes
await this.#set({ data: state, meta: this.metadata });
if (this.dataPersistenceFailing) {
this.dataPersistenceFailing = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this makes me think we're mixing concerns here. This logic is helpful for our "primary store" no matter what storage backend we're using. But it's implemented in a storage-backend-specific class.

Perhaps instead what we want is a single "PersistedStore" class, and then separate "BaseStorage", "ExtensionStorage", and "NetworkStorage" classes that are used by the "PersistedStore". The "PersistedStore" would handle metadata/version management, fallback logic, tracking "failure" state, etc, while the storage classes would just get/set, and could be easily switched around without any other logic changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Needs more work from the author
Development

Successfully merging this pull request may close these issues.

8 participants