Skip to content

Commit

Permalink
Migrate to new CurrencyRateController (MetaMask#11005)
Browse files Browse the repository at this point in the history
The CurrencyRateController has been migrated to the BaseControllerV2
API, which includes various API changes. These changes include:
* The constructor now expects to be passed a
`RestrictedControllerMessenger`.
* State changes are subscribed to via the `ControllerMessenger` now,
rather than via a `subscribe` function.
* The state and configration are passed in as one "options" object,
rather than as two separate parameters
* The polling needs to be started explicitly by calling `start`. It
can be stopped and started on-demand now as well.
* Changing the current currency or native currency will now throw an
error if we fail to update the conversion rate.

The `ComposableObservableStore` has been updated to accomodate these
new types of controllers. The constructor has been updated to use an
options bag pattern as well, to make the addition of the new required
`controllerMessenger` parameter a bit less unweildly.
  • Loading branch information
Gudahtt committed May 20, 2021
1 parent d381f70 commit 5009cea
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 128 deletions.
52 changes: 43 additions & 9 deletions app/scripts/lib/ComposableObservableStore.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
import { ObservableStore } from '@metamask/obs-store';

/**
* @typedef {import('@metamask/controllers').ControllerMessenger} ControllerMessenger
*/

/**
* An ObservableStore that can composes a flat
* structure of child stores based on configuration
*/
export default class ComposableObservableStore extends ObservableStore {
/**
* Describes which stores are being composed. The key is the name of the
* store, and the value is either an ObserableStore, or a controller that
* extends one of the two base controllers in the `@metamask/controllers`
* package.
* @type {Record<string, Object>}
*/
config = {};

/**
* Create a new store
*
* @param {Object} [initState] - The initial store state
* @param {Object} [config] - Map of internal state keys to child stores
* @param {Object} options
* @param {Object} [options.config] - Map of internal state keys to child stores
* @param {ControllerMessenger} options.controllerMessenger - The controller
* messenger, used for subscribing to events from BaseControllerV2-based
* controllers.
* @param {Object} [options.state] - The initial store state
*/
constructor(initState, config) {
super(initState);
constructor({ config, controllerMessenger, state }) {
super(state);
this.controllerMessenger = controllerMessenger;
if (config) {
this.updateStructure(config);
}
Expand All @@ -21,15 +39,31 @@ export default class ComposableObservableStore extends ObservableStore {
/**
* Composes a new internal store subscription structure
*
* @param {Object} [config] - Map of internal state keys to child stores
* @param {Record<string, Object>} config - Describes which stores are being
* composed. The key is the name of the store, and the value is either an
* ObserableStore, or a controller that extends one of the two base
* controllers in the `@metamask/controllers` package.
*/
updateStructure(config) {
this.config = config;
this.removeAllListeners();
for (const key of Object.keys(this.config)) {
config[key].subscribe((state) => {
this.updateState({ [key]: state });
});
for (const key of Object.keys(config)) {
if (!config[key]) {
throw new Error(`Undefined '${key}'`);
}
const store = config[key];
if (store.subscribe) {
config[key].subscribe((state) => {
this.updateState({ [key]: state });
});
} else {
this.controllerMessenger.subscribe(
`${store.name}:stateChange`,
(state) => {
this.updateState({ [key]: state });
},
);
}
}
}

Expand Down
174 changes: 164 additions & 10 deletions app/scripts/lib/ComposableObservableStore.test.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,194 @@
import { strict as assert } from 'assert';
import { ObservableStore } from '@metamask/obs-store';
import {
BaseController,
BaseControllerV2,
ControllerMessenger,
} from '@metamask/controllers';
import ComposableObservableStore from './ComposableObservableStore';

class OldExampleController extends BaseController {
name = 'OldExampleController';

defaultState = {
baz: 'baz',
};

constructor() {
super();
this.initialize();
}

updateBaz(contents) {
this.update({ baz: contents });
}
}
class ExampleController extends BaseControllerV2 {
static defaultState = {
bar: 'bar',
};

static metadata = {
bar: { persist: true, anonymous: true },
};

constructor({ messenger }) {
super({
messenger,
name: 'ExampleController',
metadata: ExampleController.metadata,
state: ExampleController.defaultState,
});
}

updateBar(contents) {
this.update(() => {
return { bar: contents };
});
}
}

describe('ComposableObservableStore', function () {
it('should register initial state', function () {
const store = new ComposableObservableStore('state');
const controllerMessenger = new ControllerMessenger();
const store = new ComposableObservableStore({
controllerMessenger,
state: 'state',
});
assert.strictEqual(store.getState(), 'state');
});

it('should register initial structure', function () {
const controllerMessenger = new ControllerMessenger();
const testStore = new ObservableStore();
const store = new ComposableObservableStore(null, { TestStore: testStore });
const store = new ComposableObservableStore({
config: { TestStore: testStore },
controllerMessenger,
});
testStore.putState('state');
assert.deepEqual(store.getState(), { TestStore: 'state' });
});

it('should update structure', function () {
it('should update structure with observable store', function () {
const controllerMessenger = new ControllerMessenger();
const testStore = new ObservableStore();
const store = new ComposableObservableStore();
const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({ TestStore: testStore });
testStore.putState('state');
assert.deepEqual(store.getState(), { TestStore: 'state' });
});

it('should update structure with BaseController-based controller', function () {
const controllerMessenger = new ControllerMessenger();
const oldExampleController = new OldExampleController();
const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({ OldExample: oldExampleController });
oldExampleController.updateBaz('state');
assert.deepEqual(store.getState(), { OldExample: { baz: 'state' } });
});

it('should update structure with BaseControllerV2-based controller', function () {
const controllerMessenger = new ControllerMessenger();
const exampleController = new ExampleController({
messenger: controllerMessenger,
});
const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({ Example: exampleController });
exampleController.updateBar('state');
console.log(exampleController.state);
assert.deepEqual(store.getState(), { Example: { bar: 'state' } });
});

it('should update structure with all three types of stores', function () {
const controllerMessenger = new ControllerMessenger();
const exampleStore = new ObservableStore();
const exampleController = new ExampleController({
messenger: controllerMessenger,
});
const oldExampleController = new OldExampleController();
const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({
Example: exampleController,
OldExample: oldExampleController,
Store: exampleStore,
});
exampleStore.putState('state');
exampleController.updateBar('state');
oldExampleController.updateBaz('state');
assert.deepEqual(store.getState(), {
Example: { bar: 'state' },
OldExample: { baz: 'state' },
Store: 'state',
});
});

it('should return flattened state', function () {
const controllerMessenger = new ControllerMessenger();
const fooStore = new ObservableStore({ foo: 'foo' });
const barStore = new ObservableStore({ bar: 'bar' });
const store = new ComposableObservableStore(null, {
FooStore: fooStore,
BarStore: barStore,
const barController = new ExampleController({
messenger: controllerMessenger,
});
const bazController = new OldExampleController();
const store = new ComposableObservableStore({
config: {
FooStore: fooStore,
BarStore: barController,
BazStore: bazController,
},
controllerMessenger,
state: {
FooStore: fooStore.getState(),
BarStore: barController.state,
BazStore: bazController.state,
},
});
assert.deepEqual(store.getFlatState(), {
foo: 'foo',
bar: 'bar',
baz: 'baz',
});
assert.deepEqual(store.getFlatState(), { foo: 'foo', bar: 'bar' });
});

it('should return empty flattened state when not configured', function () {
const store = new ComposableObservableStore();
const controllerMessenger = new ControllerMessenger();
const store = new ComposableObservableStore({ controllerMessenger });
assert.deepEqual(store.getFlatState(), {});
});

it('should throw if the controller messenger is omitted and the config includes a BaseControllerV2 controller', function () {
const controllerMessenger = new ControllerMessenger();
const exampleController = new ExampleController({
messenger: controllerMessenger,
});
assert.throws(
() =>
new ComposableObservableStore({
config: {
Example: exampleController,
},
}),
);
});

it('should throw if the controller messenger is omitted and updateStructure called with a BaseControllerV2 controller', function () {
const controllerMessenger = new ControllerMessenger();
const exampleController = new ExampleController({
messenger: controllerMessenger,
});
const store = new ComposableObservableStore({});
assert.throws(() => store.updateStructure({ Example: exampleController }));
});

it('should throw if initialized with undefined config entry', function () {
const controllerMessenger = new ControllerMessenger();
assert.throws(
() =>
new ComposableObservableStore({
config: {
Example: undefined,
},
controllerMessenger,
}),
);
});
});
Loading

0 comments on commit 5009cea

Please sign in to comment.