Skip to content

Commit e5897c7

Browse files
feat(json-rpc-engine): Permit passing middleware functions to compatibility adapters (#6991)
## Explanation As a convenience, permit passing one or more middleware functions or an engine to `asV2Middleware` and `asLegacyMiddleware`. ## References N/A ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Enable `asV2Middleware` and `asLegacyMiddleware` to accept one or multiple middleware functions (in addition to engines), with docs, tests, and changelog updates. > > - **API (compatibility adapters)** > - `asV2Middleware(...)`: add overloads to accept single/multiple legacy `JsonRpcEngine` middlewares; composes via `mergeMiddleware`; existing engine support unchanged. > - `asLegacyMiddleware(...)`: add overloads to accept single/multiple V2 `JsonRpcMiddleware`; wraps them in a temporary V2 engine internally. > - **Behavior & Tests** > - Verify composing multiple middlewares, forwarding results/errors, and continuing pipeline when middleware doesn’t end. > - Confirm context/request propagation between legacy and V2 paths. > - **Docs** > - Update `README.md` and `src/README.md` with new usage examples (engine vs. middleware conversion) and context propagation notes. > - **Changelog** > - Append PR reference to `JsonRpcEngineV2` entry in `CHANGELOG.md`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d1fe897. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor Agent <[email protected]>
1 parent 5295c05 commit e5897c7

File tree

7 files changed

+306
-18
lines changed

7 files changed

+306
-18
lines changed

packages/json-rpc-engine/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- `JsonRpcEngineV2` ([#6176](https://github.com/MetaMask/core/pull/6176), [#6971](https://github.com/MetaMask/core/pull/6971), [#6975](https://github.com/MetaMask/core/pull/6975), [#6990](https://github.com/MetaMask/core/pull/6990))
12+
- `JsonRpcEngineV2` ([#6176](https://github.com/MetaMask/core/pull/6176), [#6971](https://github.com/MetaMask/core/pull/6971), [#6975](https://github.com/MetaMask/core/pull/6975), [#6990](https://github.com/MetaMask/core/pull/6990), [#6991](https://github.com/MetaMask/core/pull/6991))
1313
- This is a complete rewrite of `JsonRpcEngine`, intended to replace the original implementation.
1414
See the readme for details.
1515

packages/json-rpc-engine/README.md

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,15 @@ await server.handle(notification);
8181

8282
### Legacy compatibility
8383

84-
Use the `asLegacyMiddleware` function to use a `JsonRpcEngineV2` as a
85-
middleware in a legacy `JsonRpcEngine`:
84+
Use `asLegacyMiddleware()` to convert a `JsonRpcEngineV2` or one or more V2 middleware into a legacy middleware.
85+
86+
#### Context propagation
87+
88+
In keeping with the conventions of the legacy engine, non-JSON-RPC string properties of the `context` will be
89+
copied over to the request once the V2 engine is done with the request. _Note that **only `string` keys** of
90+
the `context` will be copied over._
91+
92+
#### Converting a V2 engine
8693

8794
```ts
8895
import {
@@ -102,9 +109,31 @@ const v2Engine = JsonRpcEngineV2.create({
102109
legacyEngine.push(asLegacyMiddleware(v2Engine));
103110
```
104111

105-
In keeping with the conventions of the legacy engine, non-JSON-RPC string properties of the `context` will be
106-
copied over to the request once the V2 engine is done with the request. _Note that **only `string` keys** of
107-
the `context` will be copied over._
112+
#### Converting V2 middleware
113+
114+
```ts
115+
import {
116+
asLegacyMiddleware,
117+
type JsonRpcMiddleware,
118+
} from '@metamask/json-rpc-engine/v2';
119+
import { JsonRpcEngine } from '@metamask/json-rpc-engine';
120+
121+
// Convert a single V2 middleware
122+
const middleware1: JsonRpcMiddleware<JsonRpcRequest> = ({ request }) => {
123+
/* ... */
124+
};
125+
126+
const legacyEngine = new JsonRpcEngine();
127+
legacyEngine.push(asLegacyMiddleware(middleware1));
128+
129+
// Convert multiple V2 middlewares at once
130+
const middleware2: JsonRpcMiddleware<JsonRpcRequest> = ({ context, next }) => {
131+
/* ... */
132+
};
133+
134+
const legacyEngine2 = new JsonRpcEngine();
135+
legacyEngine2.push(asLegacyMiddleware(middleware1, middleware2));
136+
```
108137

109138
### Middleware
110139

packages/json-rpc-engine/src/README.md

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,15 @@ engine.push(function (req, res, next, end) {
2323

2424
### V2 compatibility
2525

26-
Use the `asV2Middleware` function to use a `JsonRpcEngine` as a middleware in a
27-
`JsonRpcEngineV2`:
26+
Use `asV2Middleware()` to convert a `JsonRpcEngine` or one or more legacy middleware into a V2 middleware.
27+
28+
#### Context propagation
29+
30+
Non-JSON-RPC string properties on the request object will be copied over to the V2 engine's `context` object
31+
once the legacy engine is done with the request, _unless_ they already exist on the `context`, in which case
32+
they will be ignored.
33+
34+
#### Converting a legacy engine
2835

2936
```ts
3037
import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2';
@@ -38,9 +45,32 @@ const v2Engine = JsonRpcEngineV2.create({
3845
});
3946
```
4047

41-
Non-JSON-RPC string properties on the request object will be copied over to the V2 engine's `context` object
42-
once the legacy engine is done with the request, _unless_ they already exist on the `context`, in which case
43-
they will be ignored.
48+
#### Converting legacy middleware
49+
50+
You can also directly convert one or more legacy middlewares without creating an engine:
51+
52+
```ts
53+
import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2';
54+
import { asV2Middleware } from '@metamask/json-rpc-engine';
55+
56+
// Convert a single legacy middleware
57+
const middleware1 = (req, res, next, end) => {
58+
/* ... */
59+
};
60+
61+
const v2Engine = JsonRpcEngineV2.create({
62+
middleware: [asV2Middleware(middleware1)],
63+
});
64+
65+
// Convert multiple legacy middlewares at once
66+
const middleware2 = (req, res, next, end) => {
67+
/* ... */
68+
};
69+
70+
const v2Engine2 = JsonRpcEngineV2.create({
71+
middleware: [asV2Middleware(middleware1, middleware2)],
72+
});
73+
```
4474

4575
### Middleware
4676

packages/json-rpc-engine/src/asV2Middleware.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,72 @@ describe('asV2Middleware', () => {
117117
await v2Engine.handle(makeRequest());
118118
expect(observedContextValues).toStrictEqual([1, 2]);
119119
});
120+
121+
describe('with legacy middleware', () => {
122+
it('accepts a single legacy middleware', async () => {
123+
const legacyMiddleware = jest.fn((_req, res, _next, end) => {
124+
res.result = 'test-result';
125+
end();
126+
});
127+
128+
const v2Engine = JsonRpcEngineV2.create({
129+
middleware: [asV2Middleware(legacyMiddleware)],
130+
});
131+
132+
const result = await v2Engine.handle(makeRequest());
133+
expect(result).toBe('test-result');
134+
expect(legacyMiddleware).toHaveBeenCalledTimes(1);
135+
});
136+
137+
it('accepts multiple legacy middlewares via rest params', async () => {
138+
const middleware1 = jest.fn((req, _res, next) => {
139+
req.visited1 = true;
140+
next();
141+
});
142+
143+
const middleware2 = jest.fn((req, res, _next, end) => {
144+
expect(req.visited1).toBe(true);
145+
res.result = 'composed-result';
146+
end();
147+
});
148+
149+
const v2Engine = JsonRpcEngineV2.create({
150+
middleware: [asV2Middleware(middleware1, middleware2)],
151+
});
152+
153+
const result = await v2Engine.handle(makeRequest());
154+
expect(result).toBe('composed-result');
155+
expect(middleware1).toHaveBeenCalledTimes(1);
156+
expect(middleware2).toHaveBeenCalledTimes(1);
157+
});
158+
159+
it('forwards errors from legacy middleware', async () => {
160+
const legacyMiddleware = jest.fn((_req, res, _next, end) => {
161+
res.error = rpcErrors.internal('legacy-error');
162+
end();
163+
});
164+
165+
const v2Engine = JsonRpcEngineV2.create({
166+
middleware: [asV2Middleware(legacyMiddleware)],
167+
});
168+
169+
await expect(v2Engine.handle(makeRequest())).rejects.toThrow(
170+
rpcErrors.internal('legacy-error'),
171+
);
172+
});
173+
174+
it('allows v2 engine to continue when legacy middleware does not end', async () => {
175+
const legacyMiddleware = jest.fn((_req, _res, next) => {
176+
next();
177+
});
178+
179+
const v2Engine = JsonRpcEngineV2.create({
180+
middleware: [asV2Middleware(legacyMiddleware), makeNullMiddleware()],
181+
});
182+
183+
const result = await v2Engine.handle(makeRequest());
184+
expect(result).toBeNull();
185+
expect(legacyMiddleware).toHaveBeenCalledTimes(1);
186+
});
187+
});
120188
});

packages/json-rpc-engine/src/asV2Middleware.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { serializeError } from '@metamask/rpc-errors';
22
import type { JsonRpcFailure, JsonRpcResponse } from '@metamask/utils';
33
import {
44
hasProperty,
5+
type Json,
56
type JsonRpcParams,
67
type JsonRpcRequest,
78
} from '@metamask/utils';
@@ -11,6 +12,8 @@ import type {
1112
JsonRpcEngineEndCallback,
1213
JsonRpcEngineNextCallback,
1314
} from './JsonRpcEngine';
15+
import { type JsonRpcMiddleware as LegacyMiddleware } from './JsonRpcEngine';
16+
import { mergeMiddleware } from './mergeMiddleware';
1417
import {
1518
deepClone,
1619
fromLegacyRequest,
@@ -19,7 +22,7 @@ import {
1922
unserializeError,
2023
} from './v2/compatibility-utils';
2124
import type {
22-
// JsonRpcEngineV2 is used in docs.
25+
// Used in docs.
2326
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2427
JsonRpcEngineV2,
2528
JsonRpcMiddleware,
@@ -35,8 +38,40 @@ import type {
3538
export function asV2Middleware<
3639
Params extends JsonRpcParams,
3740
Request extends JsonRpcRequest<Params>,
38-
>(engine: JsonRpcEngine): JsonRpcMiddleware<Request> {
39-
const middleware = engine.asMiddleware();
41+
>(engine: JsonRpcEngine): JsonRpcMiddleware<Request>;
42+
43+
/**
44+
* Convert one or more legacy middleware into a {@link JsonRpcEngineV2} middleware.
45+
*
46+
* @param middleware - The legacy middleware to convert.
47+
* @returns The {@link JsonRpcEngineV2} middleware.
48+
*/
49+
export function asV2Middleware<
50+
Params extends JsonRpcParams,
51+
Request extends JsonRpcRequest<Params>,
52+
>(
53+
...middleware: LegacyMiddleware<JsonRpcParams, Json>[]
54+
): JsonRpcMiddleware<Request>;
55+
56+
/**
57+
* The asV2Middleware implementation.
58+
*
59+
* @param engineOrMiddleware - A legacy engine or legacy middleware.
60+
* @param rest - Any additional legacy middleware when the first argument is a middleware.
61+
* @returns The {@link JsonRpcEngineV2} middleware.
62+
*/
63+
export function asV2Middleware<
64+
Params extends JsonRpcParams,
65+
Request extends JsonRpcRequest<Params>,
66+
>(
67+
engineOrMiddleware: JsonRpcEngine | LegacyMiddleware<JsonRpcParams, Json>,
68+
...rest: LegacyMiddleware<JsonRpcParams, Json>[]
69+
): JsonRpcMiddleware<Request> {
70+
const legacyMiddleware =
71+
typeof engineOrMiddleware === 'function'
72+
? mergeMiddleware([engineOrMiddleware, ...rest])
73+
: engineOrMiddleware.asMiddleware();
74+
4075
return async ({ request, context, next }) => {
4176
const req = deepClone(request) as JsonRpcRequest<Params>;
4277
propagateToRequest(req, context);
@@ -62,7 +97,7 @@ export function asV2Middleware<
6297
const legacyNext = ((cb: JsonRpcEngineEndCallback) =>
6398
cb(end)) as JsonRpcEngineNextCallback;
6499

65-
middleware(req, res, legacyNext, end);
100+
legacyMiddleware(req, res, legacyNext, end);
66101
});
67102
propagateToContext(req, context);
68103

packages/json-rpc-engine/src/v2/asLegacyMiddleware.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,93 @@ describe('asLegacyMiddleware', () => {
190190
await legacyEngine.handle(makeRequest());
191191
expect(observedContextValues).toStrictEqual([1, 2]);
192192
});
193+
194+
describe('with V2 middleware', () => {
195+
it('accepts a single V2 middleware', async () => {
196+
const v2Middleware: JsonRpcMiddleware<JsonRpcRequest> = jest.fn(
197+
() => 'test-result',
198+
);
199+
200+
const legacyEngine = new JsonRpcEngine();
201+
legacyEngine.push(asLegacyMiddleware(v2Middleware));
202+
203+
const response = (await legacyEngine.handle(
204+
makeRequest(),
205+
)) as JsonRpcSuccess;
206+
207+
expect(response.result).toBe('test-result');
208+
expect(v2Middleware).toHaveBeenCalledTimes(1);
209+
});
210+
211+
it('accepts multiple V2 middlewares via rest params', async () => {
212+
const middleware1: JsonRpcMiddleware<JsonRpcRequest> = jest.fn(
213+
({ context, next }) => {
214+
context.set('visited1', true);
215+
return next();
216+
},
217+
);
218+
219+
const middleware2: JsonRpcMiddleware<JsonRpcRequest> = jest.fn(
220+
({ context }) => {
221+
expect(context.get('visited1')).toBe(true);
222+
return 'composed-result';
223+
},
224+
);
225+
226+
const legacyEngine = new JsonRpcEngine();
227+
legacyEngine.push(asLegacyMiddleware(middleware1, middleware2));
228+
229+
const response = (await legacyEngine.handle(
230+
makeRequest(),
231+
)) as JsonRpcSuccess;
232+
233+
expect(response.result).toBe('composed-result');
234+
expect(middleware1).toHaveBeenCalledTimes(1);
235+
expect(middleware2).toHaveBeenCalledTimes(1);
236+
});
237+
238+
it('forwards errors from V2 middleware', async () => {
239+
const v2Middleware: JsonRpcMiddleware<JsonRpcRequest> = jest.fn(() => {
240+
throw new Error('v2-error');
241+
});
242+
243+
const legacyEngine = new JsonRpcEngine();
244+
legacyEngine.push(asLegacyMiddleware(v2Middleware));
245+
246+
const response = (await legacyEngine.handle(
247+
makeRequest(),
248+
)) as JsonRpcFailure;
249+
250+
expect(response.error).toStrictEqual({
251+
message: 'v2-error',
252+
code: -32603,
253+
data: {
254+
cause: {
255+
message: 'v2-error',
256+
stack: expect.any(String),
257+
},
258+
},
259+
});
260+
});
261+
262+
it('allows legacy engine to continue when V2 middleware does not end', async () => {
263+
const v2Middleware: JsonRpcMiddleware<JsonRpcRequest> = jest.fn(
264+
({ next }) => next(),
265+
);
266+
267+
const legacyEngine = new JsonRpcEngine();
268+
legacyEngine.push(asLegacyMiddleware(v2Middleware));
269+
legacyEngine.push((_req, res, _next, end) => {
270+
res.result = 'continued';
271+
end();
272+
});
273+
274+
const response = (await legacyEngine.handle(
275+
makeRequest(),
276+
)) as JsonRpcSuccess;
277+
278+
expect(response.result).toBe('continued');
279+
expect(v2Middleware).toHaveBeenCalledTimes(1);
280+
});
281+
});
193282
});

0 commit comments

Comments
 (0)