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 372fb67fe31..8bb9a66a647 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 546bda917aa..5f8752c845b 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": { @@ -42,6 +45,11 @@ "count": 1 } }, + "packages/account-tree-controller/src/backup-and-sync/syncing/group.test.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, "packages/account-tree-controller/src/backup-and-sync/syncing/metadata.ts": { "@typescript-eslint/naming-convention": { "count": 1 @@ -93,6 +101,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/accounts-controller/src/index.ts": { @@ -116,11 +137,21 @@ "count": 2 } }, + "packages/analytics-controller/src/AnalyticsController.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/analytics-controller/src/index.ts": { "no-restricted-syntax": { "count": 1 } }, + "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 @@ -137,9 +168,42 @@ "count": 6 } }, + "packages/assets-controller/src/AssetsController.ts": { + "no-restricted-syntax": { + "count": 6 + } + }, "packages/assets-controller/src/__fixtures__/MockAssetControllerMessenger.ts": { "import-x/no-relative-packages": { "count": 1 + }, + "no-restricted-syntax": { + "count": 4 + } + }, + "packages/assets-controller/src/data-sources/AccountsApiDataSource.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/assets-controller/src/data-sources/PriceDataSource.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "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": 2 + } + }, + "packages/assets-controller/src/data-sources/StakedBalanceDataSource.ts": { + "no-restricted-syntax": { + "count": 2 } }, "packages/assets-controller/src/index.ts": { @@ -147,6 +211,16 @@ "count": 9 } }, + "packages/assets-controller/src/selectors/balance.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/assets-controller/src/utils/formatStateForTransactionPay.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/assets-controllers/jest.environment.js": { "n/prefer-global/text-decoder": { "count": 1 @@ -169,11 +243,17 @@ }, "id-length": { "count": 1 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/assets-controllers/src/AssetsContractController.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 5 + }, + "no-restricted-syntax": { + "count": 2 } }, "packages/assets-controllers/src/CurrencyRateController.test.ts": { @@ -220,12 +300,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": { @@ -242,6 +330,9 @@ }, "@typescript-eslint/prefer-nullish-coalescing": { "count": 1 + }, + "no-restricted-syntax": { + "count": 2 } }, "packages/assets-controllers/src/NftController.test.ts": { @@ -250,6 +341,9 @@ }, "id-denylist": { "count": 1 + }, + "no-restricted-syntax": { + "count": 5 } }, "packages/assets-controllers/src/NftController.ts": { @@ -276,6 +370,19 @@ }, "no-param-reassign": { "count": 2 + }, + "no-restricted-syntax": { + "count": 6 + } + }, + "packages/assets-controllers/src/NftDetectionController.test.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/assets-controllers/src/NftDetectionController.ts": { + "no-restricted-syntax": { + "count": 2 } }, "packages/assets-controllers/src/RatesController/RatesController.test.ts": { @@ -333,14 +440,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": 3 + } + }, + "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": { @@ -359,6 +502,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": { @@ -380,6 +526,9 @@ "no-param-reassign": { "count": 1 }, + "no-restricted-syntax": { + "count": 2 + }, "require-atomic-updates": { "count": 1 } @@ -441,6 +590,11 @@ "count": 4 } }, + "packages/assets-controllers/src/multi-chain-accounts-service/api-balance-fetcher.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/assets-controllers/src/multi-chain-accounts-service/multi-chain-accounts.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 3 @@ -462,6 +616,9 @@ "packages/assets-controllers/src/multicall.ts": { "id-length": { "count": 2 + }, + "no-restricted-syntax": { + "count": 2 } }, "packages/assets-controllers/src/rpc-service/rpc-balance-fetcher.test.ts": { @@ -490,6 +647,11 @@ "count": 1 } }, + "packages/assets-controllers/src/token-prices-service/codefi-v2.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/assets-controllers/src/utils/formatters.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 9 @@ -569,6 +731,11 @@ "count": 2 } }, + "packages/bridge-controller/src/utils/quote.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "packages/bridge-controller/src/utils/slippage.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 1 @@ -587,6 +754,9 @@ "packages/bridge-controller/src/utils/trade-utils.ts": { "no-restricted-globals": { "count": 1 + }, + "no-restricted-syntax": { + "count": 5 } }, "packages/bridge-controller/src/utils/validators.ts": { @@ -602,11 +772,21 @@ "count": 2 } }, + "packages/bridge-status-controller/src/utils/snaps.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, "packages/bridge-status-controller/src/utils/swap-received-amount.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 3 } }, + "packages/bridge-status-controller/src/utils/transaction.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, "packages/chain-agnostic-permission/src/caip25Permission.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 11 @@ -671,11 +851,66 @@ "count": 2 } }, + "packages/claims-controller/src/utils.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/composable-controller/src/ComposableController.test.ts": { + "no-restricted-syntax": { + "count": 14 + } + }, + "packages/composable-controller/src/ComposableController.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, + "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/config-registry-controller/src/index.ts": { "no-restricted-syntax": { "count": 2 } }, + "packages/controller-utils/src/create-service-policy.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/controller-utils/src/util.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/core-backend/src/AccountActivityService.test.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/core-backend/src/BackendWebSocketService.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/core-backend/src/BackendWebSocketService.ts": { + "no-restricted-syntax": { + "count": 5 + } + }, + "packages/core-backend/src/api/shared-types.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/core-backend/src/index.ts": { "no-restricted-syntax": { "count": 2 @@ -820,6 +1055,31 @@ "count": 6 } }, + "packages/eth-block-tracker/tests/withBlockTracker.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, + "packages/eth-json-rpc-middleware/src/fetch.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/eth-json-rpc-middleware/src/inflight-cache.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/eth-json-rpc-provider/src/internal-provider.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/eth-json-rpc-provider/src/internal-provider.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/foundryup/src/cli.ts": { "no-restricted-globals": { "count": 1 @@ -876,6 +1136,9 @@ }, "id-denylist": { "count": 2 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/foundryup/types/unzipper.d.ts": { @@ -916,6 +1179,46 @@ "count": 1 } }, + "packages/geolocation-controller/src/GeolocationController.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/json-rpc-engine/src/JsonRpcEngine.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/json-rpc-engine/src/v2/JsonRpcEngineV2.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/json-rpc-engine/src/v2/MiddlewareContext.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/json-rpc-engine/src/v2/compatibility-utils.test.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/json-rpc-engine/src/v2/utils.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/keyring-controller/src/KeyringController.test.ts": { + "no-restricted-syntax": { + "count": 5 + } + }, + "packages/keyring-controller/src/KeyringController.ts": { + "no-restricted-syntax": { + "count": 5 + } + }, "packages/logging-controller/src/LoggingController.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 1 @@ -927,6 +1230,9 @@ "packages/logging-controller/src/LoggingController.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 2 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/message-manager/src/AbstractMessageManager.test.ts": { @@ -975,6 +1281,9 @@ "packages/multichain-account-service/src/MultichainAccountWallet.test.ts": { "id-length": { "count": 2 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/multichain-account-service/src/MultichainAccountWallet.ts": { @@ -1025,11 +1334,21 @@ "count": 2 } }, + "packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, "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 @@ -1091,6 +1410,9 @@ }, "no-negated-condition": { "count": 1 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/multichain-api-middleware/src/middlewares/MultichainMiddlewareManager.ts": { @@ -1119,6 +1441,9 @@ }, "id-length": { "count": 4 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/multichain-transactions-controller/src/MultichainTransactionsController.test.ts": { @@ -1132,6 +1457,9 @@ }, "@typescript-eslint/no-misused-promises": { "count": 2 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/name-controller/src/NameController.ts": { @@ -1213,9 +1541,122 @@ "count": 1 } }, + "packages/network-controller/src/NetworkController.ts": { + "no-restricted-syntax": { + "count": 6 + } + }, + "packages/network-controller/src/create-auto-managed-network-client.test.ts": { + "no-restricted-syntax": { + "count": 23 + } + }, + "packages/network-controller/src/create-auto-managed-network-client.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, + "packages/network-controller/src/create-network-client.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/network-controller/src/rpc-service/rpc-service.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, + "packages/network-controller/tests/NetworkController.provider.test.ts": { + "no-restricted-syntax": { + "count": 5 + } + }, + "packages/network-controller/tests/NetworkController.test.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, + "packages/network-controller/tests/network-client/helpers.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, + "packages/network-enablement-controller/src/NetworkEnablementController.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/network-enablement-controller/src/NetworkEnablementController.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/network-enablement-controller/src/selectors.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 3 + }, + "no-restricted-syntax": { + "count": 2 + } + }, + "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/perps-controller/src/providers/HyperLiquidProvider.ts": { + "no-restricted-syntax": { + "count": 12 + } + }, + "packages/perps-controller/src/services/HyperLiquidSubscriptionService.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, + "packages/perps-controller/src/services/TradingService.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/perps-controller/src/types/index.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/perps-controller/src/types/transactionTypes.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, + "packages/perps-controller/src/utils/errorUtils.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/perps-controller/src/utils/hyperLiquidAdapter.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/perps-controller/src/utils/hyperLiquidValidation.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/perps-controller/src/utils/marketDataTransform.ts": { + "no-restricted-syntax": { + "count": 1 } }, "packages/perps-controller/src/utils/myxAdapter.ts": { @@ -1223,12 +1664,20 @@ "count": 2 } }, + "packages/perps-controller/tests/defer-eligibility.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "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": { @@ -1249,6 +1698,9 @@ "packages/phishing-controller/src/PhishingController.test.ts": { "jest/unbound-method": { "count": 7 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/phishing-controller/src/PhishingController.ts": { @@ -1260,6 +1712,9 @@ }, "@typescript-eslint/prefer-nullish-coalescing": { "count": 6 + }, + "no-restricted-syntax": { + "count": 10 } }, "packages/phishing-controller/src/PhishingDetector.test.ts": { @@ -1291,6 +1746,9 @@ }, "@typescript-eslint/prefer-nullish-coalescing": { "count": 1 + }, + "no-restricted-syntax": { + "count": 4 } }, "packages/polling-controller/src/AbstractPollingController.ts": { @@ -1490,6 +1948,11 @@ "count": 1 } }, + "packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/services.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, "packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/types.ts": { "@typescript-eslint/naming-convention": { "count": 5 @@ -1520,6 +1983,9 @@ "packages/profile-sync-controller/src/sdk/errors.ts": { "id-length": { "count": 1 + }, + "no-restricted-syntax": { + "count": 1 } }, "packages/profile-sync-controller/src/sdk/mocks/userstorage.ts": { @@ -1618,6 +2084,21 @@ "count": 1 } }, + "packages/ramps-controller/src/RampsController.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/ramps-controller/src/RampsController.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/ramps-controller/src/TransakService.test.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, "packages/ramps-controller/src/index.ts": { "no-restricted-syntax": { "count": 1 @@ -1628,9 +2109,52 @@ "count": 1 } }, + "packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/remote-feature-flag-controller/src/utils/version.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/seedless-onboarding-controller/src/SecretMetadata.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/seedless-onboarding-controller/src/assertions.ts": { + "no-restricted-syntax": { + "count": 10 + } + }, + "packages/seedless-onboarding-controller/src/errors.ts": { + "no-restricted-syntax": { + "count": 4 + } + }, + "packages/seedless-onboarding-controller/tests/mocks/vaultEncryptor.ts": { + "no-restricted-syntax": { + "count": 5 + } + }, "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": { @@ -1641,6 +2165,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": { @@ -1669,6 +2211,11 @@ "count": 1 } }, + "packages/signature-controller/src/utils/delegations.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/signature-controller/src/utils/normalize.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 2 @@ -1702,16 +2249,71 @@ "count": 2 } }, + "packages/subscription-controller/src/SubscriptionController.test.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/subscription-controller/src/errors.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/subscription-controller/src/index.ts": { "no-restricted-syntax": { "count": 2 } }, + "packages/transaction-controller/src/TransactionController.ts": { + "no-restricted-syntax": { + "count": 5 + } + }, + "packages/transaction-controller/src/TransactionControllerIntegration.test.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/transaction-controller/src/utils/retry.ts": { + "no-restricted-syntax": { + "count": 3 + } + }, + "packages/transaction-controller/src/utils/utils.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/across/across-actions.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "packages/transaction-pay-controller/src/strategy/bridge/bridge-submit.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, + "packages/transaction-pay-controller/src/strategy/relay/hyperliquid-withdraw.ts": { + "no-restricted-syntax": { + "count": 1 + } + }, "packages/transaction-pay-controller/src/utils/source-amounts.ts": { "import-x/no-relative-packages": { "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 @@ -1815,5 +2417,20 @@ "@typescript-eslint/naming-convention": { "count": 1 } + }, + "tests/fake-provider.ts": { + "no-restricted-syntax": { + "count": 7 + } + }, + "tests/helpers.ts": { + "no-restricted-syntax": { + "count": 2 + } + }, + "tests/mock-network.ts": { + "no-restricted-syntax": { + "count": 10 + } } } diff --git a/eslint.config.mjs b/eslint.config.mjs index def3343abc8..c9231401fd5 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -107,6 +107,28 @@ 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`. + 'no-restricted-syntax': [ + 'error', + ...collectExistingRuleOptions('no-restricted-syntax', [ + base, + typescript, + ]), + { + 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.", + }, + ], }, }, { diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 2b56924135a..0ebbd275568 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( + expect(messengerSpy).not.toHaveBeenCalledWith( '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..4fe39814e56 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 + // Some of these calls 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 9935747a394..f7a084da974 100644 --- a/packages/base-controller/CHANGELOG.md +++ b/packages/base-controller/CHANGELOG.md @@ -7,6 +7,15 @@ 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. + +### Deprecated + +- Deprecate `${ControllerName}:stateChange` event in favor of `${ControllerName}:stateChanged` ([#8187](https://github.com/MetaMask/core/pull/8187)) + ## [9.0.1] ### Changed diff --git a/packages/base-controller/src/BaseController.test.ts b/packages/base-controller/src/BaseController.test.ts index 08008e5f12a..b268b451763 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, '$2'); + + 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..2cc4047e45d 100644 --- a/packages/base-controller/src/BaseController.ts +++ b/packages/base-controller/src/BaseController.ts @@ -150,6 +150,10 @@ export type ControllerGetStateAction< handler: () => ControllerState; }; +/** + * @deprecated This event type is deprecated. Please use + * `ControllerStateChangedEvent` instead. + */ export type ControllerStateChangeEvent< ControllerName extends string, ControllerState extends StateConstraint, @@ -158,6 +162,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 +178,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 +249,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 +288,10 @@ export class BaseController< eventType: `${name}:stateChange`, getPayload: () => [this.state, []], }); + this.#messenger.registerInitialEventPayload({ + eventType: `${name}:stateChanged`, + getPayload: () => [this.state, []], + }); } /** @@ -321,6 +344,11 @@ export class BaseController< nextState, patches, ); + this.#messenger.publish( + `${this.name}:stateChanged` as const, + nextState, + patches, + ); } return { nextState, patches, inversePatches }; @@ -341,6 +369,11 @@ export class BaseController< nextState, patches, ); + this.#messenger.publish( + `${this.name}:stateChanged` as const, + nextState, + patches, + ); } /** @@ -354,6 +387,7 @@ export class BaseController< */ protected destroy(): void { this.messenger.clearEventSubscriptions(`${this.name}:stateChange`); + this.messenger.clearEventSubscriptions(`${this.name}:stateChanged`); } } 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';