@metamask-previews/json-rpc-engine 10.1.1-preview-63ea58af → 10.1.1-preview-0458fe94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +647 -124
- package/dist/JsonRpcEngine.cjs +11 -13
- package/dist/JsonRpcEngine.cjs.map +1 -1
- package/dist/JsonRpcEngine.d.cts +18 -0
- package/dist/JsonRpcEngine.d.cts.map +1 -1
- package/dist/JsonRpcEngine.d.mts +18 -0
- package/dist/JsonRpcEngine.d.mts.map +1 -1
- package/dist/JsonRpcEngine.mjs +11 -13
- package/dist/JsonRpcEngine.mjs.map +1 -1
- package/dist/asV2Middleware.cjs +48 -0
- package/dist/asV2Middleware.cjs.map +1 -0
- package/dist/asV2Middleware.d.cts +11 -0
- package/dist/asV2Middleware.d.cts.map +1 -0
- package/dist/asV2Middleware.d.mts +11 -0
- package/dist/asV2Middleware.d.mts.map +1 -0
- package/dist/asV2Middleware.mjs +44 -0
- package/dist/asV2Middleware.mjs.map +1 -0
- package/dist/createAsyncMiddleware.cjs +1 -0
- package/dist/createAsyncMiddleware.cjs.map +1 -1
- package/dist/createAsyncMiddleware.d.cts +1 -0
- package/dist/createAsyncMiddleware.d.cts.map +1 -1
- package/dist/createAsyncMiddleware.d.mts +1 -0
- package/dist/createAsyncMiddleware.d.mts.map +1 -1
- package/dist/createAsyncMiddleware.mjs +1 -0
- package/dist/createAsyncMiddleware.mjs.map +1 -1
- package/dist/createScaffoldMiddleware.cjs +1 -0
- package/dist/createScaffoldMiddleware.cjs.map +1 -1
- package/dist/createScaffoldMiddleware.d.cts +1 -0
- package/dist/createScaffoldMiddleware.d.cts.map +1 -1
- package/dist/createScaffoldMiddleware.d.mts +1 -0
- package/dist/createScaffoldMiddleware.d.mts.map +1 -1
- package/dist/createScaffoldMiddleware.mjs +1 -0
- package/dist/createScaffoldMiddleware.mjs.map +1 -1
- package/dist/idRemapMiddleware.cjs +1 -0
- package/dist/idRemapMiddleware.cjs.map +1 -1
- package/dist/idRemapMiddleware.d.cts +1 -0
- package/dist/idRemapMiddleware.d.cts.map +1 -1
- package/dist/idRemapMiddleware.d.mts +1 -0
- package/dist/idRemapMiddleware.d.mts.map +1 -1
- package/dist/idRemapMiddleware.mjs +1 -0
- package/dist/idRemapMiddleware.mjs.map +1 -1
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/dist/mergeMiddleware.cjs +1 -0
- package/dist/mergeMiddleware.cjs.map +1 -1
- package/dist/mergeMiddleware.d.cts +1 -0
- package/dist/mergeMiddleware.d.cts.map +1 -1
- package/dist/mergeMiddleware.d.mts +1 -0
- package/dist/mergeMiddleware.d.mts.map +1 -1
- package/dist/mergeMiddleware.mjs +1 -0
- package/dist/mergeMiddleware.mjs.map +1 -1
- package/dist/v2/JsonRpcEngineV2.cjs +213 -0
- package/dist/v2/JsonRpcEngineV2.cjs.map +1 -0
- package/dist/v2/JsonRpcEngineV2.d.cts +122 -0
- package/dist/v2/JsonRpcEngineV2.d.cts.map +1 -0
- package/dist/v2/JsonRpcEngineV2.d.mts +122 -0
- package/dist/v2/JsonRpcEngineV2.d.mts.map +1 -0
- package/dist/v2/JsonRpcEngineV2.mjs +213 -0
- package/dist/v2/JsonRpcEngineV2.mjs.map +1 -0
- package/dist/v2/JsonRpcServer.cjs +162 -0
- package/dist/v2/JsonRpcServer.cjs.map +1 -0
- package/dist/v2/JsonRpcServer.d.cts +85 -0
- package/dist/v2/JsonRpcServer.d.cts.map +1 -0
- package/dist/v2/JsonRpcServer.d.mts +85 -0
- package/dist/v2/JsonRpcServer.d.mts.map +1 -0
- package/dist/v2/JsonRpcServer.mjs +158 -0
- package/dist/v2/JsonRpcServer.mjs.map +1 -0
- package/dist/v2/MiddlewareContext.cjs +66 -0
- package/dist/v2/MiddlewareContext.cjs.map +1 -0
- package/dist/v2/MiddlewareContext.d.cts +95 -0
- package/dist/v2/MiddlewareContext.d.cts.map +1 -0
- package/dist/v2/MiddlewareContext.d.mts +95 -0
- package/dist/v2/MiddlewareContext.d.mts.map +1 -0
- package/dist/v2/MiddlewareContext.mjs +62 -0
- package/dist/v2/MiddlewareContext.mjs.map +1 -0
- package/dist/v2/asLegacyMiddleware.cjs +39 -0
- package/dist/v2/asLegacyMiddleware.cjs.map +1 -0
- package/dist/v2/asLegacyMiddleware.d.cts +11 -0
- package/dist/v2/asLegacyMiddleware.d.cts.map +1 -0
- package/dist/v2/asLegacyMiddleware.d.mts +11 -0
- package/dist/v2/asLegacyMiddleware.d.mts.map +1 -0
- package/dist/v2/asLegacyMiddleware.mjs +35 -0
- package/dist/v2/asLegacyMiddleware.mjs.map +1 -0
- package/dist/v2/compatibility-utils.cjs +151 -0
- package/dist/v2/compatibility-utils.cjs.map +1 -0
- package/dist/v2/compatibility-utils.d.cts +75 -0
- package/dist/v2/compatibility-utils.d.cts.map +1 -0
- package/dist/v2/compatibility-utils.d.mts +75 -0
- package/dist/v2/compatibility-utils.d.mts.map +1 -0
- package/dist/v2/compatibility-utils.mjs +142 -0
- package/dist/v2/compatibility-utils.mjs.map +1 -0
- package/dist/v2/index.cjs +29 -0
- package/dist/v2/index.cjs.map +1 -0
- package/dist/v2/index.d.cts +8 -0
- package/dist/v2/index.d.cts.map +1 -0
- package/dist/v2/index.d.mts +8 -0
- package/dist/v2/index.d.mts.map +1 -0
- package/dist/v2/index.mjs +6 -0
- package/dist/v2/index.mjs.map +1 -0
- package/dist/v2/utils.cjs +41 -0
- package/dist/v2/utils.cjs.map +1 -0
- package/dist/v2/utils.d.cts +35 -0
- package/dist/v2/utils.d.cts.map +1 -0
- package/dist/v2/utils.d.mts +35 -0
- package/dist/v2/utils.d.mts.map +1 -0
- package/dist/v2/utils.mjs +34 -0
- package/dist/v2/utils.mjs.map +1 -0
- package/package.json +17 -3
- package/v2.js +3 -0
package/README.md
CHANGED
|
@@ -12,193 +12,716 @@ or
|
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
> [!NOTE]
|
|
16
|
+
> For the legacy `JsonRpcEngine`, see [its readme](./src/README.md).
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2';
|
|
20
|
+
import type {
|
|
21
|
+
Json,
|
|
22
|
+
JsonRpcMiddleware,
|
|
23
|
+
MiddlewareContext,
|
|
24
|
+
} from '@metamask/json-rpc-engine/v2';
|
|
25
|
+
|
|
26
|
+
type Middleware = JsonRpcMiddleware<
|
|
27
|
+
JsonRpcRequest,
|
|
28
|
+
Json,
|
|
29
|
+
MiddlewareContext<{ hello: string }>
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
// Engines are instantiated using the `create()` factory method as opposed to
|
|
33
|
+
// the constructor, which is private.
|
|
34
|
+
const engine = JsonRpcEngineV2.create<Middleware>({
|
|
35
|
+
middleware: [
|
|
36
|
+
({ request, next, context }) => {
|
|
37
|
+
if (request.method === 'hello') {
|
|
38
|
+
context.set('hello', 'world');
|
|
39
|
+
return next();
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
},
|
|
43
|
+
({ context }) => context.assertGet('hello'),
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requests are handled asynchronously, stepping down the middleware stack until complete.
|
|
17
49
|
|
|
18
|
-
|
|
50
|
+
```ts
|
|
51
|
+
const request = { id: '1', jsonrpc: '2.0', method: 'hello' };
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = await engine.handle(request);
|
|
55
|
+
// Do something with the result
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// Handle the error
|
|
58
|
+
}
|
|
19
59
|
```
|
|
20
60
|
|
|
21
|
-
|
|
61
|
+
Alternatively, pass the engine to a `JsonRpcServer`, which coerces raw request
|
|
62
|
+
objects into well-formed requests, and handles error serialization:
|
|
22
63
|
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
64
|
+
```ts
|
|
65
|
+
const server = new JsonRpcServer({ engine, onError });
|
|
66
|
+
const request = { id: '1', jsonrpc: '2.0', method: 'hello' };
|
|
67
|
+
|
|
68
|
+
// server.handle() never throws
|
|
69
|
+
const response = await server.handle(request);
|
|
70
|
+
if ('result' in response) {
|
|
71
|
+
// Handle result
|
|
72
|
+
} else {
|
|
73
|
+
// Handle error
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const notification = { jsonrpc: '2.0', method: 'hello' };
|
|
77
|
+
|
|
78
|
+
// Always returns undefined for notifications
|
|
79
|
+
await server.handle(notification);
|
|
28
80
|
```
|
|
29
81
|
|
|
30
|
-
|
|
82
|
+
### Legacy compatibility
|
|
83
|
+
|
|
84
|
+
Use the `asLegacyMiddleware` function to use a `JsonRpcEngineV2` as a
|
|
85
|
+
middleware in a legacy `JsonRpcEngine`:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import {
|
|
89
|
+
asLegacyMiddleware,
|
|
90
|
+
JsonRpcEngineV2,
|
|
91
|
+
} from '@metamask/json-rpc-engine/v2';
|
|
92
|
+
import { JsonRpcEngine } from '@metamask/json-rpc-engine';
|
|
31
93
|
|
|
32
|
-
|
|
33
|
-
const request = { id: 1, jsonrpc: '2.0', method: 'hello' };
|
|
94
|
+
const legacyEngine = new JsonRpcEngine();
|
|
34
95
|
|
|
35
|
-
|
|
36
|
-
|
|
96
|
+
const v2Engine = JsonRpcEngineV2.create({
|
|
97
|
+
middleware: [
|
|
98
|
+
// ...
|
|
99
|
+
],
|
|
37
100
|
});
|
|
38
101
|
|
|
39
|
-
|
|
40
|
-
const response = await engine.handle(request);
|
|
102
|
+
legacyEngine.push(asLegacyMiddleware(v2Engine));
|
|
41
103
|
```
|
|
42
104
|
|
|
43
|
-
|
|
44
|
-
|
|
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._
|
|
108
|
+
|
|
109
|
+
### Middleware
|
|
110
|
+
|
|
111
|
+
Middleware functions can be sync or async.
|
|
112
|
+
They receive a `MiddlewareParams` object containing:
|
|
113
|
+
|
|
114
|
+
- `request`
|
|
115
|
+
- The JSON-RPC request or notification (readonly)
|
|
116
|
+
- `context`
|
|
117
|
+
- An append-only `Map` for passing data between middleware
|
|
118
|
+
- `next`
|
|
119
|
+
- Function that calls the next middleware in the stack and returns its result (if any)
|
|
120
|
+
|
|
121
|
+
Here's a basic example:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const engine = JsonRpcEngineV2.create({
|
|
125
|
+
middleware: [
|
|
126
|
+
({ next, context }) => {
|
|
127
|
+
context.set('foo', 'bar');
|
|
128
|
+
// Proceed to the next middleware and return its result
|
|
129
|
+
return next();
|
|
130
|
+
},
|
|
131
|
+
async ({ request, context }) => {
|
|
132
|
+
await doSomething(request, context.get('foo'));
|
|
133
|
+
// Return a result wihout calling next() to end the request
|
|
134
|
+
return 42;
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
In practice, middleware functions are often defined apart from the engine in which
|
|
141
|
+
they are used. Middleware defined in this manner must use the `JsonRpcMiddleware` type:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
export const permissionMiddleware: JsonRpcMiddleware<
|
|
145
|
+
JsonRpcRequest,
|
|
146
|
+
Json, // The result
|
|
147
|
+
MiddlewareContext<{ user: User; permissions: Permissions }>
|
|
148
|
+
> = async ({ request, context, next }) => {
|
|
149
|
+
const user = context.assertGet('user');
|
|
150
|
+
const permissions = await getUserPermissions(user.id);
|
|
151
|
+
context.set('permissions', permissions);
|
|
152
|
+
return next();
|
|
153
|
+
};
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Middleware can specify a return type, however `next()` always returns the widest possible
|
|
157
|
+
type based on the type of the `request`. See [Requests vs. notifications](#requests-vs-notifications)
|
|
158
|
+
for more details.
|
|
159
|
+
|
|
160
|
+
Creating a useful `JsonRpcEngineV2` requires composing differently typed middleware together.
|
|
161
|
+
See [Engine composition](#engine-composition) for how to
|
|
162
|
+
accomplish this in the same or a set of composed engines.
|
|
163
|
+
|
|
164
|
+
### Requests vs. notifications
|
|
165
|
+
|
|
166
|
+
JSON-RPC requests come in two flavors:
|
|
167
|
+
|
|
168
|
+
- [Requests](https://www.jsonrpc.org/specification#request_object), i.e. request objects _with_ an `id`
|
|
169
|
+
- [Notifications](https://www.jsonrpc.org/specification#notification), i.e. request objects _without_ an `id`
|
|
45
170
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
171
|
+
`next()` returns `Json` for requests, `void` for notifications, and `Json | void` if the type of the request
|
|
172
|
+
object is not known.
|
|
173
|
+
|
|
174
|
+
For requests, one of the engine's middleware must "end" the request by returning a non-`undefined` result, or `.handle()`
|
|
175
|
+
will throw an error:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const engine = JsonRpcEngineV2.create({
|
|
179
|
+
middleware: [
|
|
180
|
+
() => {
|
|
181
|
+
if (Math.random() > 0.5) {
|
|
182
|
+
return 42;
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
},
|
|
186
|
+
],
|
|
51
187
|
});
|
|
188
|
+
|
|
189
|
+
const request = { jsonrpc: '2.0', id: '1', method: 'hello' };
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const result = await engine.handle(request);
|
|
193
|
+
console.log(result); // 42
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error(error); // Nothing ended request: { ... }
|
|
196
|
+
}
|
|
52
197
|
```
|
|
53
198
|
|
|
54
|
-
|
|
199
|
+
For notifications, on the other hand, one of the engine's middleware must return `undefined` to end the request,
|
|
200
|
+
and any non-`undefined` return values will cause an error to be thrown:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const notification = { jsonrpc: '2.0', method: 'hello' };
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const result = await engine.handle(notification);
|
|
207
|
+
console.log(result); // undefined
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error(error); // Result returned for notification: { ... }
|
|
210
|
+
}
|
|
211
|
+
```
|
|
55
212
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
213
|
+
If your middleware may be passed both requests and notifications,
|
|
214
|
+
use the `isRequest` or `isNotification` utilities to determine what to do:
|
|
215
|
+
|
|
216
|
+
> [!NOTE]
|
|
217
|
+
> Middleware that handle both requests and notifications—i.e. the `JsonRpcCall` type—
|
|
218
|
+
> must ensure that their return values are valid for incoming requests at runtime.
|
|
219
|
+
> There is no compile time type error if such a middleware returns e.g. a string
|
|
220
|
+
> for a notification.
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import {
|
|
224
|
+
isRequest,
|
|
225
|
+
isNotification,
|
|
226
|
+
JsonRpcEngineV2,
|
|
227
|
+
} from '@metamask/json-rpc-engine/v2';
|
|
228
|
+
|
|
229
|
+
const engine = JsonRpcEngineV2.create({
|
|
230
|
+
middleware: [
|
|
231
|
+
async ({ request, next }) => {
|
|
232
|
+
if (isRequest(request) && request.method === 'everything') {
|
|
233
|
+
return 42;
|
|
234
|
+
}
|
|
235
|
+
return next();
|
|
236
|
+
},
|
|
237
|
+
({ request }) => {
|
|
238
|
+
if (isNotification(request)) {
|
|
239
|
+
console.log(`Received notification: ${request.method}`);
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
},
|
|
244
|
+
],
|
|
61
245
|
});
|
|
62
246
|
```
|
|
63
247
|
|
|
64
|
-
|
|
248
|
+
### Request modification
|
|
249
|
+
|
|
250
|
+
The `request` object is immutable.
|
|
251
|
+
Attempting to directly modify it will throw an error.
|
|
252
|
+
Middleware can modify the `method` and `params` properties
|
|
253
|
+
by passing a new request object to `next()`:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const engine = JsonRpcEngineV2.create({
|
|
257
|
+
middleware: [
|
|
258
|
+
({ request, next }) => {
|
|
259
|
+
// Modify the request for subsequent middleware
|
|
260
|
+
// The new request object will be deeply frozen
|
|
261
|
+
return next({
|
|
262
|
+
...request,
|
|
263
|
+
method: 'modified_method',
|
|
264
|
+
params: [1, 2, 3],
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
({ request }) => {
|
|
268
|
+
// This middleware receives the modified request
|
|
269
|
+
return request.params[0];
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
});
|
|
273
|
+
```
|
|
65
274
|
|
|
66
|
-
|
|
67
|
-
|
|
275
|
+
Modifying the `jsonrpc` or `id` properties is not allowed, and will cause
|
|
276
|
+
an error:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
const engine = JsonRpcEngineV2.create({
|
|
280
|
+
middleware: [
|
|
281
|
+
({ request, next }) => {
|
|
282
|
+
return next({
|
|
283
|
+
...request,
|
|
284
|
+
// Modifying either property will cause an error
|
|
285
|
+
jsonrpc: '3.0',
|
|
286
|
+
id: 'foo',
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
() => 42,
|
|
290
|
+
],
|
|
291
|
+
});
|
|
68
292
|
|
|
69
|
-
//
|
|
70
|
-
|
|
293
|
+
// Error: Middleware attempted to modify readonly property...
|
|
294
|
+
await engine.handle(anyRequest);
|
|
295
|
+
```
|
|
71
296
|
|
|
72
|
-
|
|
73
|
-
|
|
297
|
+
### Result handling
|
|
298
|
+
|
|
299
|
+
Middleware can observe the result by awaiting `next()`:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
const engine = JsonRpcEngineV2.create({
|
|
303
|
+
middleware: [
|
|
304
|
+
async ({ request, next }) => {
|
|
305
|
+
const startTime = Date.now();
|
|
306
|
+
const result = await next();
|
|
307
|
+
const duration = Date.now() - startTime;
|
|
308
|
+
|
|
309
|
+
// Log the request duration
|
|
310
|
+
console.log(
|
|
311
|
+
`Request ${request.method} producing ${result} took ${duration}ms`,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// By returning `undefined`, the result will be forwarded unmodified to earlier
|
|
315
|
+
// middleware.
|
|
316
|
+
},
|
|
317
|
+
({ request }) => {
|
|
318
|
+
return 'Hello, World!';
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
});
|
|
74
322
|
```
|
|
75
323
|
|
|
76
|
-
|
|
324
|
+
Like the `request`, the `result` is also immutable.
|
|
325
|
+
Middleware can update the result by returning a new one.
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
const engine = JsonRpcEngineV2.create({
|
|
329
|
+
middleware: [
|
|
330
|
+
async ({ request, next }) => {
|
|
331
|
+
const result = await next();
|
|
332
|
+
|
|
333
|
+
// Add metadata to the result
|
|
334
|
+
if (result && typeof result === 'object') {
|
|
335
|
+
// The new result will also be deeply frozen
|
|
336
|
+
return {
|
|
337
|
+
...result,
|
|
338
|
+
metadata: {
|
|
339
|
+
processedAt: new Date().toISOString(),
|
|
340
|
+
requestId: request.id,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Returning the unmodified result is equivalent to returning `undefined`
|
|
346
|
+
return result;
|
|
347
|
+
},
|
|
348
|
+
({ request }) => {
|
|
349
|
+
// Initial result
|
|
350
|
+
return { message: 'Hello, World!' };
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
});
|
|
77
354
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
355
|
+
const result = await engine.handle({
|
|
356
|
+
id: '1',
|
|
357
|
+
jsonrpc: '2.0',
|
|
358
|
+
method: 'hello',
|
|
359
|
+
});
|
|
360
|
+
console.log(result);
|
|
361
|
+
// {
|
|
362
|
+
// message: 'Hello, World!',
|
|
363
|
+
// metadata: {
|
|
364
|
+
// processedAt: '2024-01-01T12:00:00.000Z',
|
|
365
|
+
// requestId: 1
|
|
366
|
+
// }
|
|
367
|
+
// }
|
|
82
368
|
```
|
|
83
369
|
|
|
84
|
-
### `
|
|
370
|
+
### The `MiddlewareContext`
|
|
371
|
+
|
|
372
|
+
Use the `context` to share data between middleware:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
const engine = JsonRpcEngineV2.create({
|
|
376
|
+
middleware: [
|
|
377
|
+
async ({ context, next }) => {
|
|
378
|
+
context.set('user', { id: '123', name: 'Alice' });
|
|
379
|
+
return next();
|
|
380
|
+
},
|
|
381
|
+
async ({ context, next }) => {
|
|
382
|
+
// context.assertGet() throws if the value does not exist
|
|
383
|
+
const user = context.assertGet('user') as { id: string; name: string };
|
|
384
|
+
context.set('permissions', await getUserPermissions(user.id));
|
|
385
|
+
return next();
|
|
386
|
+
},
|
|
387
|
+
({ context }) => {
|
|
388
|
+
const user = context.get('user');
|
|
389
|
+
const permissions = context.get('permissions');
|
|
390
|
+
return { user, permissions };
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
});
|
|
394
|
+
```
|
|
85
395
|
|
|
86
|
-
|
|
396
|
+
The `context` supports `PropertyKey` keys, i.e. strings, numbers, and symbols.
|
|
397
|
+
To prevent accidental naming collisions, existing keys must be deleted before they can be
|
|
398
|
+
overwritten via `set()`.
|
|
399
|
+
Context values are not frozen, and objects can be mutated as normal:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
const engine = JsonRpcEngineV2.create({
|
|
403
|
+
middleware: [
|
|
404
|
+
async ({ context, next }) => {
|
|
405
|
+
context.set('user', { id: '123', name: 'Alice' });
|
|
406
|
+
return next();
|
|
407
|
+
},
|
|
408
|
+
async ({ context, next }) => {
|
|
409
|
+
const user = context.assertGet<{ id: string; name: string }>('user');
|
|
410
|
+
user.name = 'Bob';
|
|
411
|
+
return next();
|
|
412
|
+
},
|
|
413
|
+
// ...
|
|
414
|
+
],
|
|
415
|
+
});
|
|
416
|
+
```
|
|
87
417
|
|
|
88
|
-
|
|
89
|
-
const { createAsyncMiddleware } = require('@metamask/json-rpc-engine');
|
|
418
|
+
#### Constraining context keys and values
|
|
90
419
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
);
|
|
420
|
+
The context exposes a generic parameter `KeyValues`, which determines the keys and values
|
|
421
|
+
a context instance supports:
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
const context = new MiddlewareContext();
|
|
425
|
+
context.set('foo', 'bar');
|
|
426
|
+
context.get('foo'); // 'bar'
|
|
427
|
+
context.get('fizz'); // undefined
|
|
98
428
|
```
|
|
99
429
|
|
|
100
|
-
`
|
|
101
|
-
|
|
430
|
+
By default, `KeyValues` is `Record<PropertyKey, unknown>`. However, any object type can be
|
|
431
|
+
specified, effectively turning the context into a strongly typed `Map`:
|
|
102
432
|
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
/* The request will end when this returns */
|
|
108
|
-
}),
|
|
109
|
-
);
|
|
433
|
+
```ts
|
|
434
|
+
const context = new MiddlewareContext<{ foo: string }>([['foo', 'bar']]);
|
|
435
|
+
context.get('foo'); // 'bar'
|
|
436
|
+
context.get('fizz'); // Type error
|
|
110
437
|
```
|
|
111
438
|
|
|
112
|
-
The
|
|
113
|
-
|
|
114
|
-
|
|
439
|
+
The context is itself exposed as the third generic parameter of the `JsonRpcMiddleware` type.
|
|
440
|
+
See [Instrumenting middleware pipelines](#instrumenting-middleware-pipelines) for how to
|
|
441
|
+
compose different context types together.
|
|
442
|
+
|
|
443
|
+
### Error handling
|
|
444
|
+
|
|
445
|
+
Errors in middleware are propagated up the call stack:
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
const engine = JsonRpcEngineV2.create({
|
|
449
|
+
middleware: [
|
|
450
|
+
({ next }) => {
|
|
451
|
+
return next();
|
|
452
|
+
},
|
|
453
|
+
({ request, next }) => {
|
|
454
|
+
if (request.method === 'restricted') {
|
|
455
|
+
throw new Error('Method not allowed');
|
|
456
|
+
}
|
|
457
|
+
return 'Success';
|
|
458
|
+
},
|
|
459
|
+
],
|
|
460
|
+
});
|
|
115
461
|
|
|
116
|
-
|
|
117
|
-
engine.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
/* Your return handler logic goes here */
|
|
122
|
-
addToMetrics(res);
|
|
123
|
-
}),
|
|
124
|
-
);
|
|
462
|
+
try {
|
|
463
|
+
await engine.handle({ id: '1', jsonrpc: '2.0', method: 'restricted' });
|
|
464
|
+
} catch (error) {
|
|
465
|
+
console.error('Request failed:', error.message);
|
|
466
|
+
}
|
|
125
467
|
```
|
|
126
468
|
|
|
127
|
-
|
|
469
|
+
If your middleware awaits `next()`, it can handle errors using `try`/`catch`:
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
const engine = JsonRpcEngineV2.create({
|
|
473
|
+
middleware: [
|
|
474
|
+
({ request, next }) => {
|
|
475
|
+
try {
|
|
476
|
+
return await next();
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error(`Request ${request.method} errored:`, error);
|
|
479
|
+
return 42;
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
({ request }) => {
|
|
483
|
+
if (!isValid(request)) {
|
|
484
|
+
throw new Error('Invalid request');
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
});
|
|
128
489
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
insertIntoCache(res, cb);
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
res.result = getResultFromCache(req);
|
|
137
|
-
end();
|
|
490
|
+
const result = await engine.handle({
|
|
491
|
+
id: '1',
|
|
492
|
+
jsonrpc: '2.0',
|
|
493
|
+
method: 'hello',
|
|
138
494
|
});
|
|
495
|
+
console.log('Result:', result);
|
|
496
|
+
// Request hello errored: Error: Invalid request
|
|
497
|
+
// Result: 42
|
|
498
|
+
```
|
|
139
499
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
500
|
+
#### Internal errors
|
|
501
|
+
|
|
502
|
+
The engine throws `JsonRpcEngineError` values when its invariants are violated, e.g. a middleware returns
|
|
503
|
+
a result value for a notification.
|
|
504
|
+
If you want to reliably detect these cases, use `JsonRpcEngineError.isInstance(error)`, which works across
|
|
505
|
+
versions of this package in the same realm.
|
|
506
|
+
|
|
507
|
+
### Engine composition
|
|
508
|
+
|
|
509
|
+
#### Instrumenting middleware pipelines
|
|
510
|
+
|
|
511
|
+
As discussed in the [Middleware](#middleware) section, middleware are often defined apart from the
|
|
512
|
+
engine in which they are used. To be used within the same engine, a set of middleware must have
|
|
513
|
+
compatible types. Specifically, all middleware must:
|
|
514
|
+
|
|
515
|
+
- Handle either `JsonRpcRequest`, `JsonRpcNotification`, or both (i.e. `JsonRpcCall`)
|
|
516
|
+
- It is okay to mix `JsonRpcCall` middleware with either `JsonRpcRequest` or `JsonRpcNotification`
|
|
517
|
+
middleware, as long as the latter two are not mixed together.
|
|
518
|
+
- Return valid results for the overall request type
|
|
519
|
+
- Specify mutually inclusive context types
|
|
520
|
+
- The context types may be the same, partially intersecting, or completely disjoint
|
|
521
|
+
so long as they are not mutually exclusive.
|
|
522
|
+
|
|
523
|
+
For example, the following middleware are compatible:
|
|
524
|
+
|
|
525
|
+
```ts
|
|
526
|
+
const middleware1: JsonRpcMiddleware<
|
|
527
|
+
JsonRpcRequest,
|
|
528
|
+
Json,
|
|
529
|
+
MiddlewareContext<{ foo: string }>
|
|
530
|
+
> = /* ... */;
|
|
531
|
+
|
|
532
|
+
const middleware2: JsonRpcMiddleware<
|
|
533
|
+
JsonRpcRequest,
|
|
534
|
+
Json,
|
|
535
|
+
MiddlewareContext<{ bar: string }>
|
|
536
|
+
> = /* ... */;
|
|
537
|
+
|
|
538
|
+
const middleware3: JsonRpcMiddleware<
|
|
539
|
+
JsonRpcRequest,
|
|
540
|
+
{ foo: string; bar: string },
|
|
541
|
+
MiddlewareContext<{ foo: string; bar: string; baz: number }>
|
|
542
|
+
> = /* ... */;
|
|
543
|
+
|
|
544
|
+
// ✅ OK
|
|
545
|
+
const engine = JsonRpcEngineV2.create<Middleware>({
|
|
546
|
+
middleware: [middleware1, middleware2, middleware3],
|
|
547
|
+
});
|
|
147
548
|
```
|
|
148
549
|
|
|
149
|
-
|
|
550
|
+
The following middleware are incompatible due to mismatched request types:
|
|
150
551
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
552
|
+
> [!WARNING]
|
|
553
|
+
> Providing `JsonRpcRequest`- and `JsonRpcNotification`-only middleware to the same engine is
|
|
554
|
+
> unsound and should be avoided. However, doing so will **not** cause a type error, and it
|
|
555
|
+
> is the programmer's responsibility to prevent it from happening.
|
|
154
556
|
|
|
155
|
-
```
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
557
|
+
```ts
|
|
558
|
+
const middleware1: JsonRpcMiddleware<JsonRpcNotification> = /* ... */;
|
|
559
|
+
|
|
560
|
+
const middleware2: JsonRpcMiddleware<JsonRpcRequest> = /* ... */;
|
|
561
|
+
|
|
562
|
+
// ⚠️ Attempting to call engine.handle() will NOT cause a type error, but it
|
|
563
|
+
// may cause errors at runtime and should be avoided.
|
|
564
|
+
const engine = JsonRpcEngineV2.create<Middleware>({
|
|
565
|
+
middleware: [middleware1, middleware2],
|
|
566
|
+
});
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Finally, these middleware are incompatible due to mismatched context types:
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
const middleware1: JsonRpcMiddleware<
|
|
573
|
+
JsonRpcRequest,
|
|
574
|
+
Json,
|
|
575
|
+
MiddlewareContext<{ foo: string }>
|
|
576
|
+
> = /* ... */;
|
|
577
|
+
|
|
578
|
+
const middleware2: JsonRpcMiddleware<
|
|
579
|
+
JsonRpcRequest,
|
|
580
|
+
Json,
|
|
581
|
+
MiddlewareContext<{ foo: number }>
|
|
582
|
+
> = /* ... */;
|
|
583
|
+
|
|
584
|
+
// ❌ The type of the engine is `never`; accessing any property will cause a type error
|
|
585
|
+
const engine = JsonRpcEngineV2.create<Middleware>({
|
|
586
|
+
middleware: [middleware1, middleware2],
|
|
587
|
+
});
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
#### `asMiddleware()`
|
|
591
|
+
|
|
592
|
+
Engines can be nested by converting them to middleware using `asMiddleware()`:
|
|
593
|
+
|
|
594
|
+
```ts
|
|
595
|
+
const subEngine = JsonRpcEngineV2.create({
|
|
596
|
+
middleware: [
|
|
597
|
+
({ request }) => {
|
|
598
|
+
return 'Sub-engine result';
|
|
599
|
+
},
|
|
600
|
+
],
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
const mainEngine = JsonRpcEngineV2.create({
|
|
604
|
+
middleware: [
|
|
605
|
+
subEngine.asMiddleware(),
|
|
606
|
+
({ request, next }) => {
|
|
607
|
+
const subResult = await next();
|
|
608
|
+
return `Main engine processed: ${subResult}`;
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
});
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
Engines used as middleware may return `undefined` for requests, but only when
|
|
615
|
+
used as middleware:
|
|
162
616
|
|
|
163
|
-
|
|
164
|
-
|
|
617
|
+
```ts
|
|
618
|
+
const loggingEngine = JsonRpcEngineV2.create({
|
|
619
|
+
middleware: [
|
|
620
|
+
({ request, next }) => {
|
|
621
|
+
console.log('Observed request:', request.method);
|
|
622
|
+
},
|
|
623
|
+
],
|
|
624
|
+
});
|
|
165
625
|
|
|
166
|
-
|
|
626
|
+
const mainEngine = JsonRpcEngineV2.create({
|
|
627
|
+
middleware: [
|
|
628
|
+
loggingEngine.asMiddleware(),
|
|
629
|
+
({ request }) => {
|
|
630
|
+
return 'success';
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
});
|
|
167
634
|
|
|
168
|
-
|
|
169
|
-
|
|
635
|
+
const request = { id: '1', jsonrpc: '2.0', method: 'hello' };
|
|
636
|
+
const result = await mainEngine.handle(request);
|
|
637
|
+
console.log('Result:', result);
|
|
638
|
+
// Observed request: hello
|
|
639
|
+
// Result: success
|
|
170
640
|
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
engine.handle(req);
|
|
641
|
+
// ATTN: This will throw "Nothing ended request"
|
|
642
|
+
const result2 = await loggingEngine.handle(request);
|
|
174
643
|
```
|
|
175
644
|
|
|
176
|
-
|
|
645
|
+
#### Calling `handle()` in a middleware
|
|
646
|
+
|
|
647
|
+
You can also compose different engines together by calling `handle(request, context)`
|
|
648
|
+
on a different engine in a middleware. Keep in mind that, unlike when using `asMiddleware()`,
|
|
649
|
+
these "sub"-engines must return results for requests.
|
|
177
650
|
|
|
178
|
-
|
|
651
|
+
This method of composition can be useful to instrument request- and notification-only
|
|
652
|
+
middleware pipelines:
|
|
653
|
+
|
|
654
|
+
```ts
|
|
655
|
+
const requestEngine = JsonRpcEngineV2.create({
|
|
656
|
+
middleware: [
|
|
657
|
+
/* Request-only middleware */
|
|
658
|
+
],
|
|
659
|
+
});
|
|
179
660
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
661
|
+
const notificationEngine = JsonRpcEngineV2.create({
|
|
662
|
+
middleware: [
|
|
663
|
+
/* Notification-only middleware */
|
|
664
|
+
],
|
|
184
665
|
});
|
|
185
666
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
667
|
+
const orchestratorEngine = JsonRpcEngineV2.create({
|
|
668
|
+
middleware: [
|
|
669
|
+
({ request, context }) =>
|
|
670
|
+
isRequest(request)
|
|
671
|
+
? requestEngine.handle(request, { context })
|
|
672
|
+
: notificationEngine.handle(request as JsonRpcNotification, {
|
|
673
|
+
context,
|
|
674
|
+
}),
|
|
675
|
+
],
|
|
189
676
|
});
|
|
190
677
|
```
|
|
191
678
|
|
|
192
|
-
|
|
193
|
-
`end(res.error)` to be called.
|
|
679
|
+
### `JsonRpcServer`
|
|
194
680
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
681
|
+
The `JsonRpcServer` wraps a `JsonRpcEngineV2` to provide JSON-RPC 2.0 compliance and error handling. It coerces raw request objects into well-formed requests and handles error serialization.
|
|
682
|
+
|
|
683
|
+
```ts
|
|
684
|
+
import { JsonRpcEngineV2, JsonRpcServer } from '@metamask/json-rpc-engine/v2';
|
|
685
|
+
|
|
686
|
+
const engine = new JsonRpcEngine({ middleware });
|
|
687
|
+
|
|
688
|
+
const server = new JsonRpcServer({
|
|
689
|
+
engine,
|
|
690
|
+
// onError receives the raw error, before it is coerced into a JSON-RPC error.
|
|
691
|
+
onError: (error) => console.error('Server error:', error),
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// server.handle() never throws - all errors are handled by onError
|
|
695
|
+
const response = await server.handle({
|
|
696
|
+
id: '1',
|
|
697
|
+
jsonrpc: '2.0',
|
|
698
|
+
method: 'hello',
|
|
199
699
|
});
|
|
700
|
+
if ('result' in response) {
|
|
701
|
+
// Handle successful response
|
|
702
|
+
} else {
|
|
703
|
+
// Handle error response
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Notifications always return undefined
|
|
707
|
+
const notification = { jsonrpc: '2.0', method: 'hello' };
|
|
708
|
+
await server.handle(notification); // Returns undefined
|
|
200
709
|
```
|
|
201
710
|
|
|
711
|
+
The server accepts any object with a `method` property, coercing it into a request or notification
|
|
712
|
+
depending on the presence or absence of the `id` property, respectively.
|
|
713
|
+
Except for the `id`, all present JSON-RPC 2.0 fields are validated for spec conformance.
|
|
714
|
+
The `id` is replaced during request processing with an internal, trusted value, although the
|
|
715
|
+
original `id` is attached to the response before it is returned.
|
|
716
|
+
|
|
717
|
+
Response objects are returned for requests, and contain
|
|
718
|
+
the `result` in case of success and `error` in case of failure.
|
|
719
|
+
`undefined` is always returned for notifications.
|
|
720
|
+
|
|
721
|
+
Errors thrown by the underlying engine are always passed to `onError` unmodified.
|
|
722
|
+
If the request is not a notification, the error is subsequently serialized and attached
|
|
723
|
+
to the response object via the `error` property.
|
|
724
|
+
|
|
202
725
|
## Contributing
|
|
203
726
|
|
|
204
727
|
This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme).
|