-
Notifications
You must be signed in to change notification settings - Fork 5k
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
base: main
Are you sure you want to change the base?
Conversation
9e06f1d
to
b451ae0
Compare
Builds ready [9837883]
Page Load Metrics (1288 ± 593 ms)
Bundle size diffs [🚨 Warning! Bundle size has increased!]
|
9837883
to
fa740a8
Compare
Codecov ReportAttention: Patch coverage is
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. |
Builds ready [be80104]
Page Load Metrics (882 ± 603 ms)
Bundle size diffs [🚨 Warning! Bundle size has increased!]
|
// 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯
Builds ready [8e43959]
Page Load Metrics (648 ± 569 ms)
Bundle size diffs [🚨 Warning! Bundle size has increased!]
|
app/scripts/background.js
Outdated
@@ -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 }); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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();
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in e391c77
There was a problem hiding this 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; |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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();
}
.....
There was a problem hiding this comment.
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?
I need to fix the failing tests... |
*/ | ||
#set(obj: { | ||
data: IntermediaryStateType; | ||
meta: { version: number }; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this 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
…ore possible empty/corrupt vault types
…as an error without a message
Co-authored-by: Danica Shen <[email protected]>
381a058
to
82d2650
Compare
Builds ready [f0c377f]
Page Load Metrics (2104 ± 187 ms)
Bundle size diffs [🚨 Warning! Bundle size has increased!]
|
LGTM ! |
|
||
async isFirstTimeInstall(): Promise<boolean> { | ||
const result = await this.#get(); | ||
return Boolean(isEmpty(result)); |
There was a problem hiding this comment.
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`; | ||
|
||
/** |
There was a problem hiding this comment.
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 }) { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
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:
Screenshots/Recordings
Before
After
Pre-merge author checklist
Pre-merge reviewer checklist