Skip to content

Commit 371a7d2

Browse files
cursoragentrekmarks
andcommitted
feat: Allow converting V2 middleware to legacy middleware
Co-authored-by: erik.marks <[email protected]>
1 parent 5441a81 commit 371a7d2

File tree

3 files changed

+165
-5
lines changed

3 files changed

+165
-5
lines changed

packages/json-rpc-engine/README.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ 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 the `asLegacyMiddleware` function to use a `JsonRpcEngineV2` or V2 middleware as middleware in a legacy `JsonRpcEngine`:
85+
86+
#### Converting a V2 engine
8687

8788
```ts
8889
import {
@@ -102,6 +103,34 @@ const v2Engine = JsonRpcEngineV2.create({
102103
legacyEngine.push(asLegacyMiddleware(v2Engine));
103104
```
104105

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

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
});

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

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import {
66
makeContext,
77
propagateToRequest,
88
} from './compatibility-utils';
9-
import type { JsonRpcEngineV2, ResultConstraint } from './JsonRpcEngineV2';
9+
import type {
10+
JsonRpcEngineV2,
11+
JsonRpcMiddleware as V2Middleware,
12+
ResultConstraint,
13+
} from './JsonRpcEngineV2';
14+
import { JsonRpcEngineV2 as EngineV2 } from './JsonRpcEngineV2';
1015
import { createAsyncMiddleware } from '..';
1116
import type { JsonRpcMiddleware as LegacyMiddleware } from '..';
1217

@@ -21,8 +26,45 @@ export function asLegacyMiddleware<
2126
Request extends JsonRpcRequest<Params>,
2227
>(
2328
engine: JsonRpcEngineV2<Request>,
29+
): LegacyMiddleware<Params, ResultConstraint<Request>>;
30+
31+
/**
32+
* Convert one or more V2 middlewares into a legacy middleware.
33+
*
34+
* @param middleware - The V2 middleware(s) to convert.
35+
* @returns The legacy middleware.
36+
*/
37+
export function asLegacyMiddleware<
38+
Params extends JsonRpcParams,
39+
Request extends JsonRpcRequest<Params>,
40+
>(
41+
...middleware: V2Middleware<Request, ResultConstraint<Request>>[]
42+
): LegacyMiddleware<Params, ResultConstraint<Request>>;
43+
44+
/**
45+
* Implementation of asLegacyMiddleware that handles all input types.
46+
*
47+
* @param engineOrMiddleware - A V2 engine, a single V2 middleware, or an array of V2 middleware.
48+
* @param rest - Additional V2 middleware when the first argument is a single middleware.
49+
* @returns The legacy middleware.
50+
*/
51+
export function asLegacyMiddleware<
52+
Params extends JsonRpcParams,
53+
Request extends JsonRpcRequest<Params>,
54+
>(
55+
engineOrMiddleware:
56+
| JsonRpcEngineV2<Request>
57+
| V2Middleware<Request, ResultConstraint<Request>>,
58+
...rest: V2Middleware<Request, ResultConstraint<Request>>[]
2459
): LegacyMiddleware<Params, ResultConstraint<Request>> {
25-
const middleware = engine.asMiddleware();
60+
// Determine the V2 middleware function from input(s)
61+
const middleware =
62+
typeof engineOrMiddleware === 'function'
63+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
64+
EngineV2.create<any>({
65+
middleware: [engineOrMiddleware, ...rest],
66+
}).asMiddleware()
67+
: engineOrMiddleware.asMiddleware();
2668
return createAsyncMiddleware(async (req, res, next) => {
2769
const request = fromLegacyRequest(req as Request);
2870
const context = makeContext(req);
@@ -32,7 +74,7 @@ export function asLegacyMiddleware<
3274
request,
3375
context,
3476
next: (finalRequest) => {
35-
modifiedRequest = finalRequest;
77+
modifiedRequest = finalRequest as Request | undefined;
3678
return Promise.resolve(undefined);
3779
},
3880
});

0 commit comments

Comments
 (0)