From 41daada580bb55e34c160d4ffcefeafc7702ccb1 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Wed, 11 Mar 2026 12:41:05 -0600 Subject: [PATCH 1/5] Deprecate :stateChange in favor of :stateChanged Since the `:stateChange` event was added to `BaseController`, we have added a guideline asking engineers to use past tense for all event names. This commit updates BaseController to align with that guideline. To achieve backward compatibility, we add a new event, `:stateChanged`, rather than replacing the existing event. We deprecate `:stateChange` by adding some custom ESLint rules. --- AGENTS.md | 2 +- docs/code-guidelines/controller-guidelines.md | 36 +- eslint-suppressions.json | 265 ++++++++++++ eslint.config.mjs | 47 +++ package.json | 2 +- .../src/AccountsController.test.ts | 17 +- .../RatesController/RatesController.test.ts | 14 +- packages/base-controller/CHANGELOG.md | 4 + .../src/BaseController.test.ts | 387 +++++++++--------- .../base-controller/src/BaseController.ts | 39 +- 10 files changed, 581 insertions(+), 232 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 624186be0b7..dafcb6052c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -191,7 +191,7 @@ When adding or updating controllers in packages, follow these guidelines: - Controller classes should extend `BaseController`. - Controllers should not be stateless; if a controller does not have state, it should be a service. - The controller should define a public messenger type. -- All messenger actions and events should be publicly defined. The default set should include the `:getState` action and `:stateChange` event. +- All messenger actions and events should be publicly defined. The default set should include the `:getState` action and `:stateChanged` event. - All actions and events the messenger uses from other controllers and services should also be declared in the messenger type. - Controllers should initialize state by combining default and provided state. Provided state should be optional. - The constructor should take `messenger` and `state` options at a minimum. diff --git a/docs/code-guidelines/controller-guidelines.md b/docs/code-guidelines/controller-guidelines.md index 4efb6431f65..d303efe5543 100644 --- a/docs/code-guidelines/controller-guidelines.md +++ b/docs/code-guidelines/controller-guidelines.md @@ -64,7 +64,7 @@ class FooController extends BaseController { ## Provide a default representation of state -Each controller needs a default representation in order to fully initialize itself when [receiving a partial representation of state](#accept-an-optional-partial-representation-of-state). A default representation of state is also useful when testing interactions with a controller's `*:stateChange` event. +Each controller needs a default representation in order to fully initialize itself when [receiving a partial representation of state](#accept-an-optional-partial-representation-of-state). A default representation of state is also useful when testing interactions with a controller's `*:stateChanged` event. A function which returns this default representation should be defined and exported. It should be called `getDefault${ControllerName}State`. @@ -226,7 +226,7 @@ const fooController = new FooController({ If the recipient controller uses a messenger, however, the callback pattern is unnecessary. Using the messenger not only aligns the controller with `BaseController`, but also reduces the number of options that consumers need to remember in order to use the controller: -✅ **The constructor subscribes to the `BarController:stateChange` event** +✅ **The constructor subscribes to the `BarController:stateChanged` event** ```typescript /* === This repo: packages/foo-controller/src/FooController.ts === */ @@ -247,7 +247,7 @@ class FooController extends BaseController< constructor({ messenger /*, ... */ }, { messenger: FooControllerMessenger }) { super({ messenger /* ... */ }); - messenger.subscribe('BarController:stateChange', (state) => { + messenger.subscribe('BarController:stateChanged', (state) => { // do something with the state }); } @@ -280,7 +280,7 @@ const fooControllerMessenger = new Messenger< parent: rootMessenger, }); rootMessenger.delegate({ - events: ['BarController:stateChange'], + events: ['BarController:stateChanged'], messenger: fooControllerMessenger, }); const fooController = new FooController({ @@ -541,16 +541,16 @@ type FooControllerGetStateAction = ControllerGetStateAction< >; ``` -## Define the `*:stateChange` event using the `ControllerStateChangeEvent` utility type +## Define the `*:stateChanged` event using the `ControllerStateChangedEvent` utility type -Each controller needs a type for its `*:stateChange` event. The `ControllerStateChangeEvent` utility type from the `@metamask/base-controller` package should be used to define this type. +Each controller needs a type for its `*:stateChanged` event. The `ControllerStateChangedEvent` utility type from the `@metamask/base-controller` package should be used to define this type. -The name of this type should be `${ControllerName}StateChangeEvent`. +The name of this type should be `${ControllerName}StateChangedEvent`. ```typescript -import type { ControllerStateChangeEvent } from '@metamask/base-controller'; +import type { ControllerStateChangedEvent } from '@metamask/base-controller'; -type FooControllerStateChangeEvent = ControllerStateChangeEvent< +type FooControllerStateChangedEvent = ControllerStateChangedEvent< 'FooController', FooControllerState >; @@ -890,7 +890,7 @@ This type should include: - This should always include `${controllerName}GetStateAction` - Actions imported from other controllers that the controller calls (i.e., _external actions_) - Events defined and exported by the controller that it publishes and expects consumers to subscribe to (i.e., _internal events_) - - This should always include `${controllerName}StateChangeEvent` + - This should always include `${controllerName}StateChangedEvent` - Events imported from other controllers that the controller subscribes to (i.e., _external events_) The name of this type should be `${ControllerName}Messenger`. @@ -923,7 +923,7 @@ export type AllowedActions = | ApprovalControllerAddApprovalRequestAction | ApprovalControllerAcceptApprovalRequestAction; -export type SwapsControllerStateChangeEvent = ControllerStateChangeEvent< +export type SwapsControllerStateChangedEvent = ControllerStateChangedEvent< 'SwapsController', SwapsControllerState >; @@ -934,7 +934,7 @@ export type SwapsControllerSwapCreatedEvent = { }; export type SwapsControllerEvents = - | SwapsControllerStateChangeEvent + | SwapsControllerStateChangedEvent | SwapsControllerSwapCreatedEvent; export type AllowedEvents = @@ -1045,7 +1045,7 @@ class GasFeeController extends BaseController { // ... messenger.subscribe( - 'NetworkController:stateChange', + 'NetworkController:stateChanged', (networkControllerState) => { this.#updateGasFees(networkControllerState.selectedNetworkClientId); }, @@ -1054,9 +1054,9 @@ class GasFeeController extends BaseController { } ``` -One way to fix this is to check if the other controller (the one being subscribed to) has a more suitable, granular event for the data being acted upon. For instance, `NetworkController` has a `networkDidChange` event which can be used in place of `NetworkController:stateChange` if the subscribing controller needs to know when the network has been switched: +One way to fix this is to check if the other controller (the one being subscribed to) has a more suitable, granular event for the data being acted upon. For instance, `NetworkController` has a `networkDidChange` event which can be used in place of `NetworkController:stateChanged` if the subscribing controller needs to know when the network has been switched: -✅ **`NetworkController:networkDidChange` is used instead of `NetworkController:stateChange`** +✅ **`NetworkController:networkDidChange` is used instead of `NetworkController:stateChanged`** ```typescript class GasFeeController extends BaseController { @@ -1098,7 +1098,7 @@ class TokensController extends BaseController { let selectedAccount = accountsController.internalAccounts.selectedAccount; messenger.subscribe( - 'AccountsController:stateChange', + 'AccountsController:stateChanged', (newAccountsControllerState) => { if (newAccountsControllerState.selectedAccount !== selectedAccount) { this.#updateTokens( @@ -1125,7 +1125,7 @@ class NftController extends BaseController/*<...>*/ { ); messenger.subscribe( - 'PreferencesController:stateChange', + 'PreferencesController:stateChanged', (newPreferencesControllerState) => { if ( preferencesControllerState.ipfsGateway !== newPreferencesControllerState.ipfsGateway, @@ -1190,7 +1190,7 @@ class NftController extends BaseController /*<...>*/ { // ... messenger.subscribe( - 'PreferencesController:stateChange', + 'PreferencesController:stateChanged', ({ ipfsGateway, openSeaEnabled, isIpfsGatewayEnabled }) => { this.#updateNfts(ipfsGateway, openSeaEnabled, isIpfsGatewayEnabled); }, diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 56c4e077b14..60a5d776b9b 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -19,6 +19,9 @@ }, "no-negated-condition": { "count": 3 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/account-tree-controller/src/backup-and-sync/analytics/segment.ts": { @@ -93,6 +96,19 @@ "packages/account-tree-controller/tests/mockMessenger.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 1 + }, + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/accounts-controller/src/AccountsController.test.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, + "packages/accounts-controller/src/AccountsController.ts": { + "no-restricted-syntax": { + "count": 2 } }, "packages/address-book-controller/src/AddressBookController.test.ts": { @@ -111,6 +127,11 @@ "count": 2 } }, + "packages/analytics-data-regulation-controller/src/AnalyticsDataRegulationController.test.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/approval-controller/src/ApprovalController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 22 @@ -130,6 +151,31 @@ "count": 1 } }, + "packages/assets-controller/src/AssetsController.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/assets-controller/src/__fixtures__/MockAssetControllerMessenger.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, + "packages/assets-controller/src/data-sources/RpcDataSource.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/assets-controller/src/data-sources/SnapDataSource.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/assets-controller/src/data-sources/StakedBalanceDataSource.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/assets-controllers/jest.environment.js": { "n/prefer-global/text-decoder": { "count": 1 @@ -152,6 +198,9 @@ }, "id-length": { "count": 1 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/assets-controllers/src/AssetsContractController.ts": { @@ -203,12 +252,20 @@ "count": 3 } }, + "packages/assets-controllers/src/MultichainAssetsRatesController/MultichainAssetsRatesController.test.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, "packages/assets-controllers/src/MultichainAssetsRatesController/MultichainAssetsRatesController.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 2 }, "@typescript-eslint/naming-convention": { "count": 1 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts": { @@ -233,6 +290,9 @@ }, "id-denylist": { "count": 1 + }, + "no-restricted-syntax": { + "count": 5 } }, "packages/assets-controllers/src/NftController.ts": { @@ -259,6 +319,19 @@ }, "no-param-reassign": { "count": 2 + }, + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/assets-controllers/src/NftDetectionController.test.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/assets-controllers/src/NftDetectionController.ts": { + "no-restricted-syntax": { + "count": 1 } }, "packages/assets-controllers/src/RatesController/RatesController.test.ts": { @@ -316,14 +389,50 @@ "count": 2 } }, + "packages/assets-controllers/src/TokenBalancesController.test.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, + "packages/assets-controllers/src/TokenBalancesController.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/assets-controllers/src/TokenDetectionController.test.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/assets-controllers/src/TokenDetectionController.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/assets-controllers/src/TokenListController.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/assets-controllers/src/TokenListController.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/assets-controllers/src/TokenRatesController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 4 + }, + "no-restricted-syntax": { + "count": 2 } }, "packages/assets-controllers/src/TokenRatesController.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 7 + }, + "no-restricted-syntax": { + "count": 2 } }, "packages/assets-controllers/src/TokenSearchDiscoveryDataController/TokenSearchDiscoveryDataController.test.ts": { @@ -342,6 +451,9 @@ "packages/assets-controllers/src/TokensController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 6 + }, + "no-restricted-syntax": { + "count": 4 } }, "packages/assets-controllers/src/TokensController.ts": { @@ -363,6 +475,9 @@ "no-param-reassign": { "count": 1 }, + "no-restricted-syntax": { + "count": 2 + }, "require-atomic-updates": { "count": 1 } @@ -691,6 +806,31 @@ "count": 2 } }, + "packages/composable-controller/src/ComposableController.test.ts": { + "no-restricted-syntax": { + "count": 14 + } + }, + "packages/config-registry-controller/src/ConfigRegistryController.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/config-registry-controller/src/ConfigRegistryController.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/core-backend/src/BackendWebSocketService.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/core-backend/src/BackendWebSocketService.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/delegation-controller/src/DelegationController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 3 @@ -926,6 +1066,16 @@ "count": 1 } }, + "packages/geolocation-controller/src/GeolocationController.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/keyring-controller/src/KeyringController.test.ts": { + "no-restricted-syntax": { + "count": 5 + } + }, "packages/logging-controller/src/LoggingController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 1 @@ -1038,11 +1188,21 @@ "count": 2 } }, + "packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/multichain-account-service/src/tests/accounts.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 7 } }, + "packages/multichain-account-service/src/tests/messenger.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/multichain-api-middleware/src/handlers/types.ts": { "@typescript-eslint/naming-convention": { "count": 2 @@ -1226,17 +1386,50 @@ "count": 1 } }, + "packages/network-controller/tests/NetworkController.provider.test.ts": { + "no-restricted-syntax": { + "count": 5 + } + }, + "packages/network-controller/tests/NetworkController.test.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, + "packages/network-enablement-controller/src/NetworkEnablementController.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/network-enablement-controller/src/selectors.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 3 } }, + "packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/perps-controller/src/PerpsController.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/phishing-controller/src/BulkTokenScan.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 2 }, "@typescript-eslint/naming-convention": { "count": 1 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/phishing-controller/src/CacheManager.test.ts": { @@ -1260,6 +1453,9 @@ }, "jest/unbound-method": { "count": 7 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/phishing-controller/src/PhishingController.ts": { @@ -1271,6 +1467,9 @@ }, "@typescript-eslint/prefer-nullish-coalescing": { "count": 6 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/phishing-controller/src/PhishingDetector.test.ts": { @@ -1629,9 +1828,27 @@ "count": 1 } }, + "packages/ramps-controller/src/RampsController.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/sample-controllers/src/sample-gas-prices-controller.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/sample-controllers/src/sample-gas-prices-controller.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/selected-network-controller/src/SelectedNetworkController.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 4 + }, + "no-restricted-syntax": { + "count": 2 } }, "packages/selected-network-controller/src/SelectedNetworkMiddleware.ts": { @@ -1642,6 +1859,24 @@ "packages/selected-network-controller/tests/SelectedNetworkController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 4 + }, + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/shield-controller/src/ShieldController.test.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/shield-controller/src/ShieldController.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/shield-controller/tests/mocks/messenger.ts": { + "no-restricted-syntax": { + "count": 2 } }, "packages/signature-controller/src/SignatureController.test.ts": { @@ -1703,6 +1938,36 @@ "count": 2 } }, + "packages/subscription-controller/src/SubscriptionController.test.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/transaction-controller/src/TransactionController.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, + "packages/transaction-controller/src/TransactionControllerIntegration.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/transaction-pay-controller/src/helpers/QuoteRefresher.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/transaction-pay-controller/src/strategy/bridge/bridge-submit.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/transaction-pay-controller/src/utils/transaction.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/user-operation-controller/src/UserOperationController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 6 diff --git a/eslint.config.mjs b/eslint.config.mjs index a124c6f9948..97f790b80e6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,6 +3,43 @@ import jest from '@metamask/eslint-config-jest'; import nodejs from '@metamask/eslint-config-nodejs'; import typescript from '@metamask/eslint-config-typescript'; +/** + * Extract the selector entries from all `no-restricted-syntax` rule + * configurations in a flat ESLint config array. + * + * This is needed because ESLint flat config replaces rule definitions from + * earlier configs rather than merging them. Any config block that defines + * `no-restricted-syntax` must include all entries it wants to enforce — it + * cannot just append to what a previous config defined. + * + * @param {import('eslint').Linter.Config[]} flatConfigs - A flat ESLint config array. + * @returns {object[]} The selector entries. + */ +function getNoRestrictedSyntaxEntries(flatConfigs) { + return flatConfigs.flatMap((config) => { + const rule = config.rules?.['no-restricted-syntax'] ?? []; + return rule.filter((entry) => typeof entry === 'object'); + }); +} + +/** + * ESLint rule entries that flag use of the deprecated `:stateChange` event. + */ +const DEPRECATED_STATE_CHANGE_ENTRIES = [ + { + selector: + 'CallExpression[callee.property.name="subscribe"] > Literal[value=/^.+:stateChange$/]', + message: + "Subscribing to ':stateChange' events is deprecated. Use ':stateChanged' instead.", + }, + { + selector: + 'CallExpression[callee.property.name="delegate"] Property[key.name="events"] ArrayExpression > Literal[value=/^.+:stateChange$/]', + message: + "Delegating ':stateChange' events is deprecated. Use ':stateChanged' instead.", + }, +]; + const NODE_LTS_VERSION = 22; const config = createConfig([ @@ -81,6 +118,16 @@ const config = createConfig([ // do not work very well. 'jsdoc/check-tag-names': 'off', 'jsdoc/require-jsdoc': 'off', + + // Add custom rule for deprecating `${Controller}:stateChange` in favor of + // `:stateChanged`. We must re-include the existing entries from + // `@metamask/eslint-config-typescript` here because ESLint flat config + // replaces the rule entirely rather than merging arrays. + 'no-restricted-syntax': [ + 'error', + ...getNoRestrictedSyntaxEntries(typescript), + ...DEPRECATED_STATE_CHANGE_ENTRIES, + ], }, }, { diff --git a/package.json b/package.json index 56a3ab3f777..b573d1af296 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "test:clean": "yarn workspaces foreach --all --parallel --verbose run test:clean && yarn test", "test:packages": "yarn test:verbose --silent --collectCoverage=false --reporters=jest-silent-reporter", "test:scripts": "NODE_OPTIONS=--experimental-vm-modules yarn jest --config ./jest.config.scripts.js --silent", - "test:verbose": "yarn workspaces foreach --all --parallel --verbose run test:verbose", + "test:verbose": "yarn workspaces foreach --all --no-private --verbose run test:verbose", "update-readme-content": "tsx scripts/update-readme-content.ts", "workspaces:list-versions": "./scripts/list-workspace-versions.sh" }, diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 2b56924135a..11e3d45bb1c 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -1407,10 +1407,7 @@ describe('AccountsController', () => { [], ); - // First call is 'AccountsController:stateChange' - expect(messengerSpy).toHaveBeenNthCalledWith( - // 1. AccountsController:stateChange - 2, + expect(messengerSpy).toHaveBeenCalledWith( 'AccountsController:accountAdded', MockExpectedInternalAccountBuilder.from(mockAccount2) .setExpectedLastSelectedAsAny() @@ -1728,10 +1725,7 @@ describe('AccountsController', () => { [], ); - // First call is 'AccountsController:stateChange' - expect(messengerSpy).toHaveBeenNthCalledWith( - // 1. AccountsController:stateChange - 2, + expect(messengerSpy).toHaveBeenCalledWith( 'AccountsController:accountRemoved', mockAccount3.id, ); @@ -3699,17 +3693,10 @@ describe('AccountsController', () => { accountsController.state.internalAccounts.selectedAccount, ).toStrictEqual(mockNonEvmAccount.id); - expect(messengerSpy.mock.calls).toHaveLength(2); // state change and then selectedAccountChange - expect(messengerSpy).not.toHaveBeenLastCalledWith( 'AccountsController:selectedEvmAccountChange', mockNonEvmAccount, ); - - expect(messengerSpy).toHaveBeenLastCalledWith( - 'AccountsController:selectedAccountChange', - setExpectedLastSelectedAsAny(mockNonEvmAccount), - ); }); }); diff --git a/packages/assets-controllers/src/RatesController/RatesController.test.ts b/packages/assets-controllers/src/RatesController/RatesController.test.ts index e5db90adf5c..ebd02ad5971 100644 --- a/packages/assets-controllers/src/RatesController/RatesController.test.ts +++ b/packages/assets-controllers/src/RatesController/RatesController.test.ts @@ -175,8 +175,9 @@ describe('RatesController', () => { const ratesPosUpdate = ratesController.state.rates; - // checks for the RatesController:stateChange event - expect(publishActionSpy).toHaveBeenCalledTimes(3); + // checks for the RatesController:stateChange and + // RatesController:stateChanged events + expect(publishActionSpy).toHaveBeenCalledTimes(5); expect(fetchExchangeRateStub).toHaveBeenCalled(); expect(ratesPosUpdate).toStrictEqual({ btc: { @@ -294,10 +295,9 @@ describe('RatesController', () => { await ratesController.stop(); - // check the 3rd call since the 2nd one is for the - // event stateChange + // check the 6th call since the 3rd and 4th ones are for state changes expect(publishActionSpy).toHaveBeenNthCalledWith( - 4, + 6, `${ratesControllerName}:pollingStopped`, ); @@ -307,10 +307,8 @@ describe('RatesController', () => { await ratesController.stop(); - // check if the stop method is called again, it returns early - // and no extra logic is executed expect(publishActionSpy).not.toHaveBeenNthCalledWith( - 3, + 7, `${ratesControllerName}:pollingStopped`, ); }); diff --git a/packages/base-controller/CHANGELOG.md b/packages/base-controller/CHANGELOG.md index 58b12894418..49bb1c02688 100644 --- a/packages/base-controller/CHANGELOG.md +++ b/packages/base-controller/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgrade `@metamask/utils` from `^11.8.1` to `^11.9.0` ([#7511](https://github.com/MetaMask/core/pull/7511)) +### Deprecated + +- Deprecate `${Controller}:stateChange` event in favor of `${Controller}:stateChanged` ([#8187](https://github.com/MetaMask/core/pull/8187)) + ## [9.0.0] ### Changed diff --git a/packages/base-controller/src/BaseController.test.ts b/packages/base-controller/src/BaseController.test.ts index 08008e5f12a..12be5cd52bc 100644 --- a/packages/base-controller/src/BaseController.test.ts +++ b/packages/base-controller/src/BaseController.test.ts @@ -9,6 +9,7 @@ import type { ControllerEvents, ControllerGetStateAction, ControllerStateChangeEvent, + ControllerStateChangedEvent, StatePropertyMetadata, } from './BaseController'; import { BaseController, deriveStateFromMetadata } from './BaseController'; @@ -24,10 +25,15 @@ export type CountControllerAction = ControllerGetStateAction< CountControllerState >; -export type CountControllerEvent = ControllerStateChangeEvent< - typeof countControllerName, - CountControllerState ->; +export type CountControllerEvent = + | ControllerStateChangedEvent< + typeof countControllerName, + CountControllerState + > + | ControllerStateChangeEvent< + typeof countControllerName, + CountControllerState + >; export const countControllerStateMetadata = { count: { @@ -100,7 +106,7 @@ type MessagesControllerAction = ControllerGetStateAction< MessagesControllerState >; -type MessagesControllerEvent = ControllerStateChangeEvent< +type MessagesControllerEvent = ControllerStateChangedEvent< typeof messagesControllerName, MessagesControllerState >; @@ -370,178 +376,207 @@ describe('BaseController', () => { expect(controller.state).toStrictEqual({ count: 0 }); }); - it('should inform subscribers of state changes as a result of applying patches', () => { - const messenger = getCountMessenger(); - const controller = new CountController({ - messenger, - name: 'CountController', - state: { count: 0 }, - metadata: countControllerStateMetadata, - }); - const listener1 = jest.fn(); - - messenger.subscribe('CountController:stateChange', listener1); - const { inversePatches } = controller.update(() => { - return { count: 1 }; - }); + for (const eventName of [ + 'CountController:stateChanged', + 'CountController:stateChange', + ] as const) { + const shortEventName = eventName.replace(/^(.+):(.+)$/u, '\\1'); + + it(`should inform subscribers of state changes via ${shortEventName} as a result of applying patches`, () => { + const messenger = getCountMessenger(); + const controller = new CountController({ + messenger, + name: 'CountController', + state: { count: 0 }, + metadata: countControllerStateMetadata, + }); + const listener1 = jest.fn(); - controller.applyPatches(inversePatches); + messenger.subscribe(eventName, listener1); + const { inversePatches } = controller.update(() => { + return { count: 1 }; + }); - expect(listener1).toHaveBeenCalledTimes(2); - expect(listener1.mock.calls[0]).toStrictEqual([ - { count: 1 }, - [{ op: 'replace', path: [], value: { count: 1 } }], - ]); + controller.applyPatches(inversePatches); - expect(listener1.mock.calls[1]).toStrictEqual([ - { count: 0 }, - [{ op: 'replace', path: [], value: { count: 0 } }], - ]); - }); + expect(listener1).toHaveBeenCalledTimes(2); + expect(listener1.mock.calls[0]).toStrictEqual([ + { count: 1 }, + [{ op: 'replace', path: [], value: { count: 1 } }], + ]); - it('should inform subscribers of state changes', () => { - const messenger = getCountMessenger(); - const controller = new CountController({ - messenger, - name: 'CountController', - state: { count: 0 }, - metadata: countControllerStateMetadata, + expect(listener1.mock.calls[1]).toStrictEqual([ + { count: 0 }, + [{ op: 'replace', path: [], value: { count: 0 } }], + ]); }); - const listener1 = jest.fn(); - const listener2 = jest.fn(); - messenger.subscribe('CountController:stateChange', listener1); - messenger.subscribe('CountController:stateChange', listener2); - controller.update(() => { - return { count: 1 }; - }); + it(`should inform subscribers of state changes via ${shortEventName}`, () => { + const messenger = getCountMessenger(); + const controller = new CountController({ + messenger, + name: 'CountController', + state: { count: 0 }, + metadata: countControllerStateMetadata, + }); + const listener1 = jest.fn(); + const listener2 = jest.fn(); - expect(listener1).toHaveBeenCalledTimes(1); - expect(listener1.mock.calls[0]).toStrictEqual([ - { count: 1 }, - [{ op: 'replace', path: [], value: { count: 1 } }], - ]); - expect(listener2).toHaveBeenCalledTimes(1); - expect(listener2.mock.calls[0]).toStrictEqual([ - { count: 1 }, - [{ op: 'replace', path: [], value: { count: 1 } }], - ]); - }); + messenger.subscribe(eventName, listener1); + messenger.subscribe(eventName, listener2); + controller.update(() => { + return { count: 1 }; + }); - it('should notify a subscriber with a selector of state changes', () => { - const messenger = getCountMessenger(); - const controller = new CountController({ - messenger, - name: 'CountController', - state: { count: 0 }, - metadata: countControllerStateMetadata, + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener1.mock.calls[0]).toStrictEqual([ + { count: 1 }, + [{ op: 'replace', path: [], value: { count: 1 } }], + ]); + expect(listener2).toHaveBeenCalledTimes(1); + expect(listener2.mock.calls[0]).toStrictEqual([ + { count: 1 }, + [{ op: 'replace', path: [], value: { count: 1 } }], + ]); }); - const listener = jest.fn(); - messenger.subscribe( - 'CountController:stateChange', - listener, - ({ count }) => { - // Selector rounds down to nearest multiple of 10 - return Math.floor(count / 10); - }, - ); - controller.update(() => { - return { count: 10 }; - }); + it(`should notify a subscriber with a selector of state changes via ${shortEventName}`, () => { + const messenger = getCountMessenger(); + const controller = new CountController({ + messenger, + name: 'CountController', + state: { count: 0 }, + metadata: countControllerStateMetadata, + }); + const listener = jest.fn(); + messenger.subscribe( + eventName, + listener, + ({ count }: CountControllerState) => { + // Selector rounds down to nearest multiple of 10 + return Math.floor(count / 10); + }, + ); - expect(listener).toHaveBeenCalledTimes(1); - expect(listener.mock.calls[0]).toStrictEqual([1, 0]); - }); + controller.update(() => { + return { count: 10 }; + }); - it('should not inform a subscriber of state changes if the selected value is unchanged', () => { - const messenger = getCountMessenger(); - const controller = new CountController({ - messenger, - name: 'CountController', - state: { count: 0 }, - metadata: countControllerStateMetadata, + expect(listener).toHaveBeenCalledTimes(1); + expect(listener.mock.calls[0]).toStrictEqual([1, 0]); }); - const listener = jest.fn(); - messenger.subscribe( - 'CountController:stateChange', - listener, - ({ count }) => { - // Selector rounds down to nearest multiple of 10 - return Math.floor(count / 10); - }, - ); - controller.update(() => { - // Note that this rounds down to zero, so the selected value is still zero - return { count: 1 }; - }); + it(`should not inform a subscriber of state changes via ${shortEventName} if the selected value is unchanged`, () => { + const messenger = getCountMessenger(); + const controller = new CountController({ + messenger, + name: 'CountController', + state: { count: 0 }, + metadata: countControllerStateMetadata, + }); + const listener = jest.fn(); + messenger.subscribe( + eventName, + listener, + ({ count }: CountControllerState) => { + // Selector rounds down to nearest multiple of 10 + return Math.floor(count / 10); + }, + ); - expect(listener).toHaveBeenCalledTimes(0); - }); + controller.update(() => { + // Note that this rounds down to zero, so the selected value is still zero + return { count: 1 }; + }); - it('should inform a subscriber of each state change once even after multiple subscriptions', () => { - const messenger = getCountMessenger(); - const controller = new CountController({ - messenger, - name: 'CountController', - state: { count: 0 }, - metadata: countControllerStateMetadata, + expect(listener).toHaveBeenCalledTimes(0); }); - const listener1 = jest.fn(); - messenger.subscribe('CountController:stateChange', listener1); - messenger.subscribe('CountController:stateChange', listener1); + it(`should inform a subscriber of each state change via ${shortEventName} once even after multiple subscriptions`, () => { + const messenger = getCountMessenger(); + const controller = new CountController({ + messenger, + name: 'CountController', + state: { count: 0 }, + metadata: countControllerStateMetadata, + }); + const listener1 = jest.fn(); - controller.update(() => { - return { count: 1 }; - }); + messenger.subscribe(eventName, listener1); + messenger.subscribe(eventName, listener1); - expect(listener1).toHaveBeenCalledTimes(1); - expect(listener1.mock.calls[0]).toStrictEqual([ - { count: 1 }, - [{ op: 'replace', path: [], value: { count: 1 } }], - ]); - }); + controller.update(() => { + return { count: 1 }; + }); - it('should no longer inform a subscriber about state changes after unsubscribing', () => { - const messenger = getCountMessenger(); - const controller = new CountController({ - messenger, - name: 'CountController', - state: { count: 0 }, - metadata: countControllerStateMetadata, + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener1.mock.calls[0]).toStrictEqual([ + { count: 1 }, + [{ op: 'replace', path: [], value: { count: 1 } }], + ]); }); - const listener1 = jest.fn(); - messenger.subscribe('CountController:stateChange', listener1); - messenger.unsubscribe('CountController:stateChange', listener1); - controller.update(() => { - return { count: 1 }; - }); + it(`should no longer inform a subscriber about state changes via ${shortEventName} after unsubscribing`, () => { + const messenger = getCountMessenger(); + const controller = new CountController({ + messenger, + name: 'CountController', + state: { count: 0 }, + metadata: countControllerStateMetadata, + }); + const listener1 = jest.fn(); - expect(listener1).toHaveBeenCalledTimes(0); - }); + messenger.subscribe(eventName, listener1); + messenger.unsubscribe(eventName, listener1); + controller.update(() => { + return { count: 1 }; + }); - it('should no longer inform a subscriber about state changes after unsubscribing once, even if they subscribed many times', () => { - const messenger = getCountMessenger(); - const controller = new CountController({ - messenger, - name: 'CountController', - state: { count: 0 }, - metadata: countControllerStateMetadata, + expect(listener1).toHaveBeenCalledTimes(0); }); - const listener1 = jest.fn(); - messenger.subscribe('CountController:stateChange', listener1); - messenger.subscribe('CountController:stateChange', listener1); - messenger.unsubscribe('CountController:stateChange', listener1); - controller.update(() => { - return { count: 1 }; + it(`should no longer inform a subscriber about state changes via ${shortEventName} after unsubscribing once, even if they subscribed many times`, () => { + const messenger = getCountMessenger(); + const controller = new CountController({ + messenger, + name: 'CountController', + state: { count: 0 }, + metadata: countControllerStateMetadata, + }); + const listener1 = jest.fn(); + + messenger.subscribe(eventName, listener1); + messenger.subscribe(eventName, listener1); + messenger.unsubscribe(eventName, listener1); + controller.update(() => { + return { count: 1 }; + }); + + expect(listener1).toHaveBeenCalledTimes(0); }); - expect(listener1).toHaveBeenCalledTimes(0); - }); + it(`should no longer update subscribers via ${shortEventName} after being destroyed`, () => { + const messenger = getCountMessenger(); + const controller = new CountController({ + messenger, + name: 'CountController', + state: { count: 0 }, + metadata: countControllerStateMetadata, + }); + const listener1 = jest.fn(); + const listener2 = jest.fn(); + + messenger.subscribe(eventName, listener1); + messenger.subscribe(eventName, listener2); + controller.destroy(); + controller.update(() => { + return { count: 1 }; + }); + + expect(listener1).toHaveBeenCalledTimes(0); + expect(listener2).toHaveBeenCalledTimes(0); + }); + } it('should throw when unsubscribing listener who was never subscribed', () => { const messenger = getCountMessenger(); @@ -556,30 +591,10 @@ describe('BaseController', () => { const listener1 = jest.fn(); expect(() => { - messenger.unsubscribe('CountController:stateChange', listener1); - }).toThrow('Subscription not found for event: CountController:stateChange'); - }); - - it('should no longer update subscribers after being destroyed', () => { - const messenger = getCountMessenger(); - const controller = new CountController({ - messenger, - name: 'CountController', - state: { count: 0 }, - metadata: countControllerStateMetadata, - }); - const listener1 = jest.fn(); - const listener2 = jest.fn(); - - messenger.subscribe('CountController:stateChange', listener1); - messenger.subscribe('CountController:stateChange', listener2); - controller.destroy(); - controller.update(() => { - return { count: 1 }; - }); - - expect(listener1).toHaveBeenCalledTimes(0); - expect(listener2).toHaveBeenCalledTimes(0); + messenger.unsubscribe('CountController:stateChanged', listener1); + }).toThrow( + 'Subscription not found for event: CountController:stateChanged', + ); }); describe('inter-controller communication', () => { @@ -598,15 +613,15 @@ describe('BaseController', () => { handler: () => void; }; type VisitorExternalActions = VisitorOverflowUpdateMaxAction; - type VisitorControllerActions = - | VisitorControllerClearAction - | ControllerActions; - type VisitorControllerStateChangeEvent = ControllerEvents< + type VisitorControllerStateChangedEvent = ControllerStateChangedEvent< typeof visitorName, VisitorControllerState >; - type VisitorExternalEvents = VisitorOverflowStateChangeEvent; - type VisitorControllerEvents = VisitorControllerStateChangeEvent; + type VisitorControllerActions = + | VisitorControllerClearAction + | ControllerActions; + type VisitorExternalEvents = VisitorOverflowStateChangedEvent; + type VisitorControllerEvents = VisitorControllerStateChangedEvent; const visitorControllerStateMetadata = { visitors: { @@ -671,12 +686,12 @@ describe('BaseController', () => { typeof visitorOverflowName, VisitorOverflowControllerState >; - type VisitorOverflowStateChangeEvent = ControllerEvents< + type VisitorOverflowStateChangedEvent = ControllerEvents< typeof visitorOverflowName, VisitorOverflowControllerState >; - type VisitorOverflowExternalEvents = VisitorControllerStateChangeEvent; - type VisitorOverflowControllerEvents = VisitorOverflowStateChangeEvent; + type VisitorOverflowExternalEvents = VisitorControllerStateChangedEvent; + type VisitorOverflowControllerEvents = VisitorOverflowStateChangedEvent; const visitorOverflowControllerMetadata = { maxVisitors: { @@ -711,7 +726,7 @@ describe('BaseController', () => { this.updateMax, ); - messenger.subscribe('VisitorController:stateChange', this.onVisit); + messenger.subscribe('VisitorController:stateChanged', this.onVisit); } onVisit: ({ visitors }: VisitorControllerState) => void = ({ @@ -744,24 +759,24 @@ describe('BaseController', () => { const visitorControllerMessenger = new Messenger< typeof visitorName, VisitorControllerActions | VisitorOverflowUpdateMaxAction, - VisitorControllerEvents | VisitorOverflowStateChangeEvent, + VisitorControllerEvents | VisitorOverflowStateChangedEvent, typeof rootMessenger >({ namespace: visitorName, parent: rootMessenger }); const visitorOverflowControllerMessenger = new Messenger< typeof visitorOverflowName, VisitorOverflowControllerActions | VisitorControllerClearAction, - VisitorOverflowControllerEvents | VisitorControllerStateChangeEvent, + VisitorOverflowControllerEvents | VisitorControllerStateChangedEvent, typeof rootMessenger >({ namespace: visitorOverflowName, parent: rootMessenger }); // Delegate external actions/events to controller messengers rootMessenger.delegate({ actions: ['VisitorController:clear'], - events: ['VisitorController:stateChange'], + events: ['VisitorController:stateChanged'], messenger: visitorOverflowControllerMessenger, }); rootMessenger.delegate({ actions: ['VisitorOverflowController:updateMax'], - events: ['VisitorOverflowController:stateChange'], + events: ['VisitorOverflowController:stateChanged'], messenger: visitorControllerMessenger, }); // Construct controllers diff --git a/packages/base-controller/src/BaseController.ts b/packages/base-controller/src/BaseController.ts index b3368d25189..b405800540b 100644 --- a/packages/base-controller/src/BaseController.ts +++ b/packages/base-controller/src/BaseController.ts @@ -150,6 +150,9 @@ export type ControllerGetStateAction< handler: () => ControllerState; }; +/** + * @deprecated This action is deprecated. Please use `:stateChanged` instead. + */ export type ControllerStateChangeEvent< ControllerName extends string, ControllerState extends StateConstraint, @@ -158,6 +161,14 @@ export type ControllerStateChangeEvent< payload: [ControllerState, Patch[]]; }; +export type ControllerStateChangedEvent< + ControllerName extends string, + ControllerState extends StateConstraint, +> = { + type: `${ControllerName}:stateChanged`; + payload: [ControllerState, Patch[]]; +}; + export type ControllerActions< ControllerName extends string, ControllerState extends StateConstraint, @@ -166,7 +177,9 @@ export type ControllerActions< export type ControllerEvents< ControllerName extends string, ControllerState extends StateConstraint, -> = ControllerStateChangeEvent; +> = + | ControllerStateChangeEvent + | ControllerStateChangedEvent; /** * Controller class that provides state management, subscriptions, and state metadata @@ -235,12 +248,17 @@ export class BaseController< ControllerName, ControllerState >['type'] extends MessengerActions['type'] - ? ControllerEvents< + ? ControllerStateChangeEvent< ControllerName, ControllerState >['type'] extends MessengerEvents['type'] ? ControllerMessenger - : never + : ControllerStateChangedEvent< + ControllerName, + ControllerState + >['type'] extends MessengerEvents['type'] + ? ControllerMessenger + : never : never; metadata: StateMetadata; name: ControllerName; @@ -269,6 +287,10 @@ export class BaseController< eventType: `${name}:stateChange`, getPayload: () => [this.state, []], }); + this.#messenger.registerInitialEventPayload({ + eventType: `${name}:stateChanged`, + getPayload: () => [this.state, []], + }); } /** @@ -321,6 +343,11 @@ export class BaseController< nextState, patches, ); + this.#messenger.publish( + `${this.name}:stateChanged` as const, + nextState, + patches, + ); } return { nextState, patches, inversePatches }; @@ -341,6 +368,11 @@ export class BaseController< nextState, patches, ); + this.#messenger.publish( + `${this.name}:stateChanged` as const, + nextState, + patches, + ); } /** @@ -354,6 +386,7 @@ export class BaseController< */ protected destroy(): void { this.messenger.clearEventSubscriptions(`${this.name}:stateChange`); + this.messenger.clearEventSubscriptions(`${this.name}:stateChanged`); } } From 1e8f1fd1de80a91ed6ffa065d39630ef1718a1c4 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Thu, 12 Mar 2026 13:13:42 -0600 Subject: [PATCH 2/5] Tweak this test --- .../src/RatesController/RatesController.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/RatesController/RatesController.test.ts b/packages/assets-controllers/src/RatesController/RatesController.test.ts index ebd02ad5971..4fe39814e56 100644 --- a/packages/assets-controllers/src/RatesController/RatesController.test.ts +++ b/packages/assets-controllers/src/RatesController/RatesController.test.ts @@ -295,7 +295,7 @@ describe('RatesController', () => { await ratesController.stop(); - // check the 6th call since the 3rd and 4th ones are for state changes + // Some of these calls are for state changes expect(publishActionSpy).toHaveBeenNthCalledWith( 6, `${ratesControllerName}:pollingStopped`, From 7f25d8e33f4f25153ab24474b29c4d4827c7b8d5 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Thu, 12 Mar 2026 13:16:26 -0600 Subject: [PATCH 3/5] Tweaks to changelog, BaseController --- packages/base-controller/CHANGELOG.md | 2 +- packages/base-controller/src/BaseController.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/base-controller/CHANGELOG.md b/packages/base-controller/CHANGELOG.md index 49bb1c02688..ce22bbec161 100644 --- a/packages/base-controller/CHANGELOG.md +++ b/packages/base-controller/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated -- Deprecate `${Controller}:stateChange` event in favor of `${Controller}:stateChanged` ([#8187](https://github.com/MetaMask/core/pull/8187)) +- Deprecate `${ControllerName}:stateChange` event in favor of `${ControllerName}:stateChanged` ([#8187](https://github.com/MetaMask/core/pull/8187)) ## [9.0.0] diff --git a/packages/base-controller/src/BaseController.ts b/packages/base-controller/src/BaseController.ts index b405800540b..2cc4047e45d 100644 --- a/packages/base-controller/src/BaseController.ts +++ b/packages/base-controller/src/BaseController.ts @@ -151,7 +151,8 @@ export type ControllerGetStateAction< }; /** - * @deprecated This action is deprecated. Please use `:stateChanged` instead. + * @deprecated This event type is deprecated. Please use + * `ControllerStateChangedEvent` instead. */ export type ControllerStateChangeEvent< ControllerName extends string, From d86747dbf8d9e0f26b5e98eb0e6544e6c6b00bb7 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Thu, 12 Mar 2026 13:17:41 -0600 Subject: [PATCH 4/5] Address Cursor feedback --- packages/base-controller/src/BaseController.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/base-controller/src/BaseController.test.ts b/packages/base-controller/src/BaseController.test.ts index 12be5cd52bc..b268b451763 100644 --- a/packages/base-controller/src/BaseController.test.ts +++ b/packages/base-controller/src/BaseController.test.ts @@ -380,7 +380,7 @@ describe('BaseController', () => { 'CountController:stateChanged', 'CountController:stateChange', ] as const) { - const shortEventName = eventName.replace(/^(.+):(.+)$/u, '\\1'); + const shortEventName = eventName.replace(/^(.+)(:.+)$/u, '$2'); it(`should inform subscribers of state changes via ${shortEventName} as a result of applying patches`, () => { const messenger = getCountMessenger(); From 635f6dfff47f66f9d66a6252e6a22b4cb4fa4d94 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Thu, 12 Mar 2026 14:09:33 -0600 Subject: [PATCH 5/5] Restore test:verbose script; add missing export and log in changelog --- package.json | 2 +- packages/base-controller/CHANGELOG.md | 5 +++++ packages/base-controller/src/index.ts | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b573d1af296..56a3ab3f777 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "test:clean": "yarn workspaces foreach --all --parallel --verbose run test:clean && yarn test", "test:packages": "yarn test:verbose --silent --collectCoverage=false --reporters=jest-silent-reporter", "test:scripts": "NODE_OPTIONS=--experimental-vm-modules yarn jest --config ./jest.config.scripts.js --silent", - "test:verbose": "yarn workspaces foreach --all --no-private --verbose run test:verbose", + "test:verbose": "yarn workspaces foreach --all --parallel --verbose run test:verbose", "update-readme-content": "tsx scripts/update-readme-content.ts", "workspaces:list-versions": "./scripts/list-workspace-versions.sh" }, diff --git a/packages/base-controller/CHANGELOG.md b/packages/base-controller/CHANGELOG.md index ce22bbec161..f4559635dcc 100644 --- a/packages/base-controller/CHANGELOG.md +++ b/packages/base-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `${ControllerName}:stateChanged` as alternative to `${ControllerName}:stateChange` ([#8187](https://github.com/MetaMask/core/pull/8187)) + - Add corresponding utility type, `ControllerStateChangedEvent`, as well. + ### Changed - Upgrade `@metamask/utils` from `^11.8.1` to `^11.9.0` ([#7511](https://github.com/MetaMask/core/pull/7511)) diff --git a/packages/base-controller/src/index.ts b/packages/base-controller/src/index.ts index a0b5b1ae940..796998c8b79 100644 --- a/packages/base-controller/src/index.ts +++ b/packages/base-controller/src/index.ts @@ -10,5 +10,6 @@ export type { StatePropertyMetadataConstraint, ControllerGetStateAction, ControllerStateChangeEvent, + ControllerStateChangedEvent, } from './BaseController'; export { BaseController, deriveStateFromMetadata } from './BaseController';