@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.
Files changed (116) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +647 -124
  3. package/dist/JsonRpcEngine.cjs +11 -13
  4. package/dist/JsonRpcEngine.cjs.map +1 -1
  5. package/dist/JsonRpcEngine.d.cts +18 -0
  6. package/dist/JsonRpcEngine.d.cts.map +1 -1
  7. package/dist/JsonRpcEngine.d.mts +18 -0
  8. package/dist/JsonRpcEngine.d.mts.map +1 -1
  9. package/dist/JsonRpcEngine.mjs +11 -13
  10. package/dist/JsonRpcEngine.mjs.map +1 -1
  11. package/dist/asV2Middleware.cjs +48 -0
  12. package/dist/asV2Middleware.cjs.map +1 -0
  13. package/dist/asV2Middleware.d.cts +11 -0
  14. package/dist/asV2Middleware.d.cts.map +1 -0
  15. package/dist/asV2Middleware.d.mts +11 -0
  16. package/dist/asV2Middleware.d.mts.map +1 -0
  17. package/dist/asV2Middleware.mjs +44 -0
  18. package/dist/asV2Middleware.mjs.map +1 -0
  19. package/dist/createAsyncMiddleware.cjs +1 -0
  20. package/dist/createAsyncMiddleware.cjs.map +1 -1
  21. package/dist/createAsyncMiddleware.d.cts +1 -0
  22. package/dist/createAsyncMiddleware.d.cts.map +1 -1
  23. package/dist/createAsyncMiddleware.d.mts +1 -0
  24. package/dist/createAsyncMiddleware.d.mts.map +1 -1
  25. package/dist/createAsyncMiddleware.mjs +1 -0
  26. package/dist/createAsyncMiddleware.mjs.map +1 -1
  27. package/dist/createScaffoldMiddleware.cjs +1 -0
  28. package/dist/createScaffoldMiddleware.cjs.map +1 -1
  29. package/dist/createScaffoldMiddleware.d.cts +1 -0
  30. package/dist/createScaffoldMiddleware.d.cts.map +1 -1
  31. package/dist/createScaffoldMiddleware.d.mts +1 -0
  32. package/dist/createScaffoldMiddleware.d.mts.map +1 -1
  33. package/dist/createScaffoldMiddleware.mjs +1 -0
  34. package/dist/createScaffoldMiddleware.mjs.map +1 -1
  35. package/dist/idRemapMiddleware.cjs +1 -0
  36. package/dist/idRemapMiddleware.cjs.map +1 -1
  37. package/dist/idRemapMiddleware.d.cts +1 -0
  38. package/dist/idRemapMiddleware.d.cts.map +1 -1
  39. package/dist/idRemapMiddleware.d.mts +1 -0
  40. package/dist/idRemapMiddleware.d.mts.map +1 -1
  41. package/dist/idRemapMiddleware.mjs +1 -0
  42. package/dist/idRemapMiddleware.mjs.map +1 -1
  43. package/dist/index.cjs +3 -1
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.cts +1 -0
  46. package/dist/index.d.cts.map +1 -1
  47. package/dist/index.d.mts +1 -0
  48. package/dist/index.d.mts.map +1 -1
  49. package/dist/index.mjs +1 -0
  50. package/dist/index.mjs.map +1 -1
  51. package/dist/mergeMiddleware.cjs +1 -0
  52. package/dist/mergeMiddleware.cjs.map +1 -1
  53. package/dist/mergeMiddleware.d.cts +1 -0
  54. package/dist/mergeMiddleware.d.cts.map +1 -1
  55. package/dist/mergeMiddleware.d.mts +1 -0
  56. package/dist/mergeMiddleware.d.mts.map +1 -1
  57. package/dist/mergeMiddleware.mjs +1 -0
  58. package/dist/mergeMiddleware.mjs.map +1 -1
  59. package/dist/v2/JsonRpcEngineV2.cjs +213 -0
  60. package/dist/v2/JsonRpcEngineV2.cjs.map +1 -0
  61. package/dist/v2/JsonRpcEngineV2.d.cts +122 -0
  62. package/dist/v2/JsonRpcEngineV2.d.cts.map +1 -0
  63. package/dist/v2/JsonRpcEngineV2.d.mts +122 -0
  64. package/dist/v2/JsonRpcEngineV2.d.mts.map +1 -0
  65. package/dist/v2/JsonRpcEngineV2.mjs +213 -0
  66. package/dist/v2/JsonRpcEngineV2.mjs.map +1 -0
  67. package/dist/v2/JsonRpcServer.cjs +162 -0
  68. package/dist/v2/JsonRpcServer.cjs.map +1 -0
  69. package/dist/v2/JsonRpcServer.d.cts +85 -0
  70. package/dist/v2/JsonRpcServer.d.cts.map +1 -0
  71. package/dist/v2/JsonRpcServer.d.mts +85 -0
  72. package/dist/v2/JsonRpcServer.d.mts.map +1 -0
  73. package/dist/v2/JsonRpcServer.mjs +158 -0
  74. package/dist/v2/JsonRpcServer.mjs.map +1 -0
  75. package/dist/v2/MiddlewareContext.cjs +66 -0
  76. package/dist/v2/MiddlewareContext.cjs.map +1 -0
  77. package/dist/v2/MiddlewareContext.d.cts +95 -0
  78. package/dist/v2/MiddlewareContext.d.cts.map +1 -0
  79. package/dist/v2/MiddlewareContext.d.mts +95 -0
  80. package/dist/v2/MiddlewareContext.d.mts.map +1 -0
  81. package/dist/v2/MiddlewareContext.mjs +62 -0
  82. package/dist/v2/MiddlewareContext.mjs.map +1 -0
  83. package/dist/v2/asLegacyMiddleware.cjs +39 -0
  84. package/dist/v2/asLegacyMiddleware.cjs.map +1 -0
  85. package/dist/v2/asLegacyMiddleware.d.cts +11 -0
  86. package/dist/v2/asLegacyMiddleware.d.cts.map +1 -0
  87. package/dist/v2/asLegacyMiddleware.d.mts +11 -0
  88. package/dist/v2/asLegacyMiddleware.d.mts.map +1 -0
  89. package/dist/v2/asLegacyMiddleware.mjs +35 -0
  90. package/dist/v2/asLegacyMiddleware.mjs.map +1 -0
  91. package/dist/v2/compatibility-utils.cjs +151 -0
  92. package/dist/v2/compatibility-utils.cjs.map +1 -0
  93. package/dist/v2/compatibility-utils.d.cts +75 -0
  94. package/dist/v2/compatibility-utils.d.cts.map +1 -0
  95. package/dist/v2/compatibility-utils.d.mts +75 -0
  96. package/dist/v2/compatibility-utils.d.mts.map +1 -0
  97. package/dist/v2/compatibility-utils.mjs +142 -0
  98. package/dist/v2/compatibility-utils.mjs.map +1 -0
  99. package/dist/v2/index.cjs +29 -0
  100. package/dist/v2/index.cjs.map +1 -0
  101. package/dist/v2/index.d.cts +8 -0
  102. package/dist/v2/index.d.cts.map +1 -0
  103. package/dist/v2/index.d.mts +8 -0
  104. package/dist/v2/index.d.mts.map +1 -0
  105. package/dist/v2/index.mjs +6 -0
  106. package/dist/v2/index.mjs.map +1 -0
  107. package/dist/v2/utils.cjs +41 -0
  108. package/dist/v2/utils.cjs.map +1 -0
  109. package/dist/v2/utils.d.cts +35 -0
  110. package/dist/v2/utils.d.cts.map +1 -0
  111. package/dist/v2/utils.d.mts +35 -0
  112. package/dist/v2/utils.d.mts.map +1 -0
  113. package/dist/v2/utils.mjs +34 -0
  114. package/dist/v2/utils.mjs.map +1 -0
  115. package/package.json +17 -3
  116. package/v2.js +3 -0
package/README.md CHANGED
@@ -12,193 +12,716 @@ or
12
12
 
13
13
  ## Usage
14
14
 
15
- ```js
16
- const { JsonRpcEngine } = require('@metamask/json-rpc-engine');
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
- const engine = new JsonRpcEngine();
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
- Build a stack of JSON-RPC processors by pushing middleware to the engine.
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
- ```js
24
- engine.push(function (req, res, next, end) {
25
- res.result = 42;
26
- end();
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
- Requests are handled asynchronously, stepping down the stack until complete.
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
- ```js
33
- const request = { id: 1, jsonrpc: '2.0', method: 'hello' };
94
+ const legacyEngine = new JsonRpcEngine();
34
95
 
35
- engine.handle(request, function (err, response) {
36
- // Do something with response.result, or handle response.error
96
+ const v2Engine = JsonRpcEngineV2.create({
97
+ middleware: [
98
+ // ...
99
+ ],
37
100
  });
38
101
 
39
- // There is also a Promise signature
40
- const response = await engine.handle(request);
102
+ legacyEngine.push(asLegacyMiddleware(v2Engine));
41
103
  ```
42
104
 
43
- Middleware have direct access to the request and response objects.
44
- They can let processing continue down the stack with `next()`, or complete the request with `end()`.
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
- ```js
47
- engine.push(function (req, res, next, end) {
48
- if (req.skipCache) return next();
49
- res.result = getResultFromCache(req);
50
- end();
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
- By passing a _return handler_ to the `next` function, you can get a peek at the result before it returns.
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
- ```js
57
- engine.push(function (req, res, next, end) {
58
- next(function (cb) {
59
- insertIntoCache(res, cb);
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
- If you specify a `notificationHandler` when constructing the engine, JSON-RPC notifications passed to `handle()` will be handed off directly to this function without touching the middleware stack:
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
- ```js
67
- const engine = new JsonRpcEngine({ notificationHandler });
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
- // A notification is defined as a JSON-RPC request without an `id` property.
70
- const notification = { jsonrpc: '2.0', method: 'hello' };
293
+ // Error: Middleware attempted to modify readonly property...
294
+ await engine.handle(anyRequest);
295
+ ```
71
296
 
72
- const response = await engine.handle(notification);
73
- console.log(typeof response); // 'undefined'
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
- Engines can be nested by converting them to middleware using `JsonRpcEngine.asMiddleware()`:
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
- ```js
79
- const engine = new JsonRpcEngine();
80
- const subengine = new JsonRpcEngine();
81
- engine.push(subengine.asMiddleware());
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
- ### `async` Middleware
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
- If you require your middleware function to be `async`, use `createAsyncMiddleware`:
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
- ```js
89
- const { createAsyncMiddleware } = require('@metamask/json-rpc-engine');
418
+ #### Constraining context keys and values
90
419
 
91
- let engine = new RpcEngine();
92
- engine.push(
93
- createAsyncMiddleware(async (req, res, next) => {
94
- res.result = 42;
95
- next();
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
- `async` middleware do not take an `end` callback.
101
- Instead, the request ends if the middleware returns without calling `next()`:
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
- ```js
104
- engine.push(
105
- createAsyncMiddleware(async (req, res, next) => {
106
- res.result = 42;
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 `next` callback of `async` middleware also don't take return handlers.
113
- Instead, you can `await next()`.
114
- When the execution of the middleware resumes, you can work with the response again.
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
- ```js
117
- engine.push(
118
- createAsyncMiddleware(async (req, res, next) => {
119
- res.result = 42;
120
- await next();
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
- You can freely mix callback-based and `async` middleware:
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
- ```js
130
- engine.push(function (req, res, next, end) {
131
- if (!isCached(req)) {
132
- return next((cb) => {
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
- engine.push(
141
- createAsyncMiddleware(async (req, res, next) => {
142
- res.result = 42;
143
- await next();
144
- addToMetrics(res);
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
- ### Teardown
550
+ The following middleware are incompatible due to mismatched request types:
150
551
 
151
- If your middleware has teardown to perform, you can assign a method `destroy()` to your middleware function(s),
152
- and calling `JsonRpcEngine.destroy()` will call this method on each middleware that has it.
153
- A destroyed engine can no longer be used.
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
- ```js
156
- const middleware = (req, res, next, end) => {
157
- /* do something */
158
- };
159
- middleware.destroy = () => {
160
- /* perform teardown */
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
- const engine = new JsonRpcEngine();
164
- engine.push(middleware);
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
- /* perform work */
626
+ const mainEngine = JsonRpcEngineV2.create({
627
+ middleware: [
628
+ loggingEngine.asMiddleware(),
629
+ ({ request }) => {
630
+ return 'success';
631
+ },
632
+ ],
633
+ });
167
634
 
168
- // This will call middleware.destroy() and destroy the engine itself.
169
- engine.destroy();
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
- // Calling any public method on the middleware other than `destroy()` itself
172
- // will throw an error.
173
- engine.handle(req);
641
+ // ATTN: This will throw "Nothing ended request"
642
+ const result2 = await loggingEngine.handle(request);
174
643
  ```
175
644
 
176
- ### Gotchas
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
- Handle errors via `end(err)`, _NOT_ `next(err)`.
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
- ```js
181
- /* INCORRECT */
182
- engine.push(function (req, res, next, end) {
183
- next(new Error());
661
+ const notificationEngine = JsonRpcEngineV2.create({
662
+ middleware: [
663
+ /* Notification-only middleware */
664
+ ],
184
665
  });
185
666
 
186
- /* CORRECT */
187
- engine.push(function (req, res, next, end) {
188
- end(new Error());
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
- However, `next()` will detect errors on the response object, and cause
193
- `end(res.error)` to be called.
679
+ ### `JsonRpcServer`
194
680
 
195
- ```js
196
- engine.push(function (req, res, next, end) {
197
- res.error = new Error();
198
- next(); /* This will cause end(res.error) to be called. */
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).