1
- import type { AddApprovalRequest } from '@metamask/approval-controller' ;
2
1
import type {
3
2
ControllerGetStateAction ,
4
3
ControllerStateChangeEvent ,
5
4
RestrictedControllerMessenger ,
6
5
} from '@metamask/base-controller' ;
7
6
import { BaseController } from '@metamask/base-controller' ;
8
- import { ApprovalType } from '@metamask/controller-utils' ;
9
7
import type {
10
- NetworkControllerGetNetworkConfigurationByNetworkClientId ,
11
8
NetworkControllerGetStateAction ,
12
9
NetworkControllerSetActiveNetworkAction ,
13
10
} from '@metamask/network-controller' ;
11
+ import type { SelectedNetworkControllerGetNetworkClientIdForDomainAction } from '@metamask/selected-network-controller' ;
12
+ import { createDeferredPromise } from '@metamask/utils' ;
14
13
15
14
import type { QueuedRequestMiddlewareJsonRpcRequest } from './types' ;
16
15
@@ -36,6 +35,7 @@ export type QueuedRequestControllerEnqueueRequestAction = {
36
35
} ;
37
36
38
37
export const QueuedRequestControllerEventTypes = {
38
+ networkSwitched : `${ controllerName } :networkSwitched` as const ,
39
39
stateChange : `${ controllerName } :stateChange` as const ,
40
40
} ;
41
41
@@ -45,8 +45,14 @@ export type QueuedRequestControllerStateChangeEvent =
45
45
QueuedRequestControllerState
46
46
> ;
47
47
48
+ export type QueuedRequestControllerNetworkSwitched = {
49
+ type : typeof QueuedRequestControllerEventTypes . networkSwitched ;
50
+ payload : [ string ] ;
51
+ } ;
52
+
48
53
export type QueuedRequestControllerEvents =
49
- QueuedRequestControllerStateChangeEvent ;
54
+ | QueuedRequestControllerStateChangeEvent
55
+ | QueuedRequestControllerNetworkSwitched ;
50
56
51
57
export type QueuedRequestControllerActions =
52
58
| QueuedRequestControllerGetStateAction
@@ -55,8 +61,7 @@ export type QueuedRequestControllerActions =
55
61
export type AllowedActions =
56
62
| NetworkControllerGetStateAction
57
63
| NetworkControllerSetActiveNetworkAction
58
- | NetworkControllerGetNetworkConfigurationByNetworkClientId
59
- | AddApprovalRequest ;
64
+ | SelectedNetworkControllerGetNetworkClientIdForDomainAction ;
60
65
61
66
export type QueuedRequestControllerMessenger = RestrictedControllerMessenger <
62
67
typeof controllerName ,
@@ -71,28 +76,62 @@ export type QueuedRequestControllerOptions = {
71
76
} ;
72
77
73
78
/**
74
- * Controller for request queueing. The QueuedRequestController manages the orderly execution of enqueued requests
75
- * to prevent concurrency issues and ensure proper handling of asynchronous operations.
79
+ * A queued request.
80
+ */
81
+ type QueuedRequest = {
82
+ /**
83
+ * The origin of the queued request.
84
+ */
85
+ origin : string ;
86
+ /**
87
+ * A callback used to continue processing the request, called when the request is dequeued.
88
+ */
89
+ processRequest : ( error : unknown ) => void ;
90
+ } ;
91
+
92
+ /**
93
+ * Queue requests for processing in batches, by request origin.
76
94
*
77
- * @param options - The controller options, including the restricted controller messenger for the QueuedRequestController.
78
- * @param options.messenger - The restricted controller messenger that facilitates communication with the QueuedRequestController.
95
+ * Processing requests in batches allows us to completely separate sets of requests that originate
96
+ * from different origins. This ensures that our UI will not display those requests as a set, which
97
+ * could mislead users into thinking they are related.
79
98
*
80
- * The QueuedRequestController maintains a count of enqueued requests, allowing you to monitor the queue's workload.
81
- * It processes requests sequentially, ensuring that each request is executed one after the other. The class offers
82
- * an `enqueueRequest` method for adding requests to the queue. The controller initializes with a count of zero and
83
- * registers message handlers for request enqueuing. It also publishes count changes to inform external observers.
99
+ * Queuing requests in batches also allows us to ensure the globally selected network matches the
100
+ * dapp-selected network, before the confirmation UI is rendered. This is important because the
101
+ * data shown on some confirmation screens is only collected for the globally selected network.
102
+ *
103
+ * Requests get processed in order of insertion, even across batches. All requests get processed
104
+ * even in the event of preceding requests failing.
84
105
*/
85
106
export class QueuedRequestController extends BaseController <
86
107
typeof controllerName ,
87
108
QueuedRequestControllerState ,
88
109
QueuedRequestControllerMessenger
89
110
> {
90
- private currentRequest : Promise < unknown > = Promise . resolve ( ) ;
111
+ /**
112
+ * The origin of the current batch of requests being processed, or `undefined` if there are no
113
+ * requests currently being processed.
114
+ */
115
+ #originOfCurrentBatch: string | undefined ;
91
116
92
117
/**
93
- * Constructs a QueuedRequestController, responsible for managing and processing enqueued requests sequentially.
94
- * @param options - The controller options, including the restricted controller messenger for the QueuedRequestController.
95
- * @param options.messenger - The restricted controller messenger that facilitates communication with the QueuedRequestController.
118
+ * The list of all queued requests, in chronological order.
119
+ */
120
+ #requestQueue: QueuedRequest [ ] = [ ] ;
121
+
122
+ /**
123
+ * The number of requests currently being processed.
124
+ *
125
+ * Note that this does not include queued requests, just those being actively processed (i.e.
126
+ * those in the "current batch").
127
+ */
128
+ #processingRequestCount = 0 ;
129
+
130
+ /**
131
+ * Construct a QueuedRequestController.
132
+ *
133
+ * @param options - Controller options.
134
+ * @param options.messenger - The restricted controller messenger that facilitates communication with other controllers.
96
135
*/
97
136
constructor ( { messenger } : QueuedRequestControllerOptions ) {
98
137
super ( {
@@ -111,118 +150,160 @@ export class QueuedRequestController extends BaseController<
111
150
112
151
#registerMessageHandlers( ) : void {
113
152
this . messagingSystem . registerActionHandler (
114
- QueuedRequestControllerActionTypes . enqueueRequest ,
153
+ ` ${ controllerName } : enqueueRequest` ,
115
154
this . enqueueRequest . bind ( this ) ,
116
155
) ;
117
156
}
118
157
119
158
/**
120
- * Switch the current globally selected network if necessary for processing the given
121
- * request.
159
+ * Process the next batch of requests.
160
+ *
161
+ * This will trigger the next batch of requests with matching origins to be processed. Each
162
+ * request in the batch is dequeued one at a time, in chronological order, but they all get
163
+ * processed in parallel.
164
+ *
165
+ * This should be called after a batch of requests has finished processing, if the queue is non-
166
+ * empty.
167
+ */
168
+ async #processNextBatch( ) {
169
+ const firstRequest = this . #requestQueue. shift ( ) as QueuedRequest ;
170
+ this . #originOfCurrentBatch = firstRequest . origin ;
171
+ const batch = [ firstRequest . processRequest ] ;
172
+ while ( this . #requestQueue[ 0 ] ?. origin === this . #originOfCurrentBatch) {
173
+ const nextEntry = this . #requestQueue. shift ( ) as QueuedRequest ;
174
+ batch . push ( nextEntry . processRequest ) ;
175
+ }
176
+
177
+ // If globally selected network is different from origin selected network,
178
+ // switch network before processing batch
179
+ let networkSwitchError : unknown ;
180
+ try {
181
+ await this . #switchNetworkIfNecessary( ) ;
182
+ } catch ( error : unknown ) {
183
+ networkSwitchError = error ;
184
+ }
185
+
186
+ for ( const processRequest of batch ) {
187
+ processRequest ( networkSwitchError ) ;
188
+ }
189
+ this . #updateQueuedRequestCount( ) ;
190
+ }
191
+
192
+ /**
193
+ * Switch the globally selected network client to match the network
194
+ * client of the current batch.
122
195
*
123
- * @param request - The request currently being processed.
124
196
* @throws Throws an error if the current selected `networkClientId` or the
125
197
* `networkClientId` on the request are invalid.
126
198
*/
127
- async #switchNetworkIfNecessary(
128
- request : QueuedRequestMiddlewareJsonRpcRequest ,
129
- ) {
199
+ async #switchNetworkIfNecessary( ) {
200
+ // This branch is unreachable; it's just here for type reasons.
201
+ /* istanbul ignore next */
202
+ if ( ! this . #originOfCurrentBatch) {
203
+ throw new Error ( 'Current batch origin must be initialized first' ) ;
204
+ }
205
+ const originNetworkClientId = this . messagingSystem . call (
206
+ 'SelectedNetworkController:getNetworkClientIdForDomain' ,
207
+ this . #originOfCurrentBatch,
208
+ ) ;
130
209
const { selectedNetworkClientId } = this . messagingSystem . call (
131
210
'NetworkController:getState' ,
132
211
) ;
133
- if ( request . networkClientId === selectedNetworkClientId ) {
212
+ if ( originNetworkClientId === selectedNetworkClientId ) {
134
213
return ;
135
214
}
136
215
137
- const toNetworkConfiguration = this . messagingSystem . call (
138
- 'NetworkController:getNetworkConfigurationByNetworkClientId' ,
139
- request . networkClientId ,
140
- ) ;
141
- const fromNetworkConfiguration = this . messagingSystem . call (
142
- 'NetworkController:getNetworkConfigurationByNetworkClientId' ,
143
- selectedNetworkClientId ,
144
- ) ;
145
- if ( ! toNetworkConfiguration ) {
146
- throw new Error (
147
- `Missing network configuration for ${ request . networkClientId } ` ,
148
- ) ;
149
- } else if ( ! fromNetworkConfiguration ) {
150
- throw new Error (
151
- `Missing network configuration for ${ selectedNetworkClientId } ` ,
152
- ) ;
153
- }
154
-
155
- const requestData = {
156
- toNetworkConfiguration,
157
- fromNetworkConfiguration,
158
- } ;
159
216
await this . messagingSystem . call (
160
- 'ApprovalController:addRequest' ,
161
- {
162
- origin : request . origin ,
163
- type : ApprovalType . SwitchEthereumChain ,
164
- requestData,
165
- } ,
166
- true ,
217
+ 'NetworkController:setActiveNetwork' ,
218
+ originNetworkClientId ,
167
219
) ;
168
220
169
- await this . messagingSystem . call (
170
- 'NetworkController:setActiveNetwork ' ,
171
- request . networkClientId ,
221
+ this . messagingSystem . publish (
222
+ 'QueuedRequestController:networkSwitched ' ,
223
+ originNetworkClientId ,
172
224
) ;
173
225
}
174
226
175
- #updateCount( change : - 1 | 1 ) {
227
+ /**
228
+ * Update the queued request count.
229
+ */
230
+ #updateQueuedRequestCount( ) {
176
231
this . update ( ( state ) => {
177
- state . queuedRequestCount += change ;
232
+ state . queuedRequestCount = this . #requestQueue . length ;
178
233
} ) ;
179
234
}
180
235
181
236
/**
182
- * Enqueues a new request for sequential processing in the request queue. This function manages the order of
183
- * requests, ensuring they are executed one after the other to prevent concurrency issues and maintain proper
184
- * execution flow.
237
+ * Enqueue a request to be processed in a batch with other requests from the same origin.
238
+ *
239
+ * We process requests one origin at a time, so that requests from different origins do not get
240
+ * interwoven, and so that we can ensure that the globally selected network matches the dapp-
241
+ * selected network.
242
+ *
243
+ * Requests get processed in order of insertion, even across origins/batches. All requests get
244
+ * processed even in the event of preceding requests failing.
185
245
*
186
246
* @param request - The JSON-RPC request to process.
187
- * @param requestNext - A function representing the next steps for processing this request. It returns a promise that
188
- * resolves when the request is complete.
189
- * @returns A promise that resolves when the enqueued request and any subsequent asynchronous
190
- * operations are fully processed. This allows you to await the completion of the enqueued request before continuing
191
- * with additional actions. If there are multiple enqueued requests, this function ensures they are processed in
192
- * the order they were enqueued, guaranteeing sequential execution.
247
+ * @param requestNext - A function representing the next steps for processing this request.
248
+ * @returns A promise that resolves when the given request has been fully processed.
193
249
*/
194
250
async enqueueRequest (
195
251
request : QueuedRequestMiddlewareJsonRpcRequest ,
196
252
requestNext : ( ) => Promise < void > ,
197
- ) {
198
- this . #updateCount( 1 ) ;
199
- if ( this . state . queuedRequestCount > 1 ) {
200
- try {
201
- await this . currentRequest ;
202
- } catch ( _error ) {
203
- // error ignored - this is handled in the middleware instead
204
- this . #updateCount( - 1 ) ;
205
- }
253
+ ) : Promise < void > {
254
+ if ( this . #originOfCurrentBatch === undefined ) {
255
+ this . #originOfCurrentBatch = request . origin ;
206
256
}
207
257
208
- const processCurrentRequest = async ( ) => {
209
- try {
210
- if (
211
- request . method !== 'wallet_switchEthereumChain' &&
212
- request . method !== 'wallet_addEthereumChain'
213
- ) {
214
- await this . #switchNetworkIfNecessary( request ) ;
215
- }
258
+ try {
259
+ // Queue request for later processing
260
+ // Network switch is handled when this batch is processed
261
+ if (
262
+ this . state . queuedRequestCount > 0 ||
263
+ this . #originOfCurrentBatch !== request . origin
264
+ ) {
265
+ const {
266
+ promise : waitForDequeue ,
267
+ reject,
268
+ resolve,
269
+ } = createDeferredPromise ( {
270
+ suppressUnhandledRejection : true ,
271
+ } ) ;
272
+ this . #requestQueue. push ( {
273
+ origin : request . origin ,
274
+ processRequest : ( error : unknown ) => {
275
+ if ( error ) {
276
+ reject ( error ) ;
277
+ } else {
278
+ resolve ( ) ;
279
+ }
280
+ } ,
281
+ } ) ;
282
+ this . #updateQueuedRequestCount( ) ;
216
283
284
+ await waitForDequeue ;
285
+ } else {
286
+ // Process request immediately
287
+ // Requires switching network now if necessary
288
+ await this . #switchNetworkIfNecessary( ) ;
289
+ }
290
+ this . #processingRequestCount += 1 ;
291
+ try {
217
292
await requestNext ( ) ;
218
293
} finally {
219
- // The count is updated as part of the request processing to ensure
220
- // that it has been updated before the next request is run.
221
- this . #updateCount( - 1 ) ;
294
+ this . #processingRequestCount -= 1 ;
222
295
}
223
- } ;
224
-
225
- this . currentRequest = processCurrentRequest ( ) ;
226
- await this . currentRequest ;
296
+ return undefined ;
297
+ } finally {
298
+ if ( this . #processingRequestCount === 0 ) {
299
+ this . #originOfCurrentBatch = undefined ;
300
+ if ( this . #requestQueue. length > 0 ) {
301
+ // The next batch is triggered here. We intentionally omit the `await` because we don't
302
+ // want the next batch to block resolution of the current request.
303
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
304
+ this . #processNextBatch( ) ;
305
+ }
306
+ }
307
+ }
227
308
}
228
309
}
0 commit comments