json-rpc-engine
Advanced tools
Comparing version 5.1.8 to 5.2.0
@@ -1,8 +0,22 @@ | ||
# [5.0.0] | ||
upgraded to babelify@10 | ||
# Changelog | ||
<a name="3.0.0"></a> | ||
# [3.0.0](https://github.com/kumavis/json-rpc-engine/compare/v2.2.3...v3.0.0) (2017-04-04) | ||
All notable changes to this project will be documented in this file. | ||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
## [Unreleased] | ||
## [5.2.0] - 2020-07-24 | ||
### Added | ||
- Promise signatures for `engine.handle` ([#55](https://github.com/MetaMask/json-rpc-engine/pull/55)) | ||
- So, in addition to `engine.handle(request, callback)`, you can do e.g. `await engine.handle(request)`. | ||
### Changed | ||
- Remove `async` and `promise-to-callback` dependencies | ||
- These dependencies were used internally for asynchronous control flow. | ||
They have been replaced with Promises and native `async`/`await`. | ||
This has made middleware execution faster, and may affect consumers that rely on middleware timing, advertently or not. |
{ | ||
"name": "json-rpc-engine", | ||
"version": "5.1.8", | ||
"version": "5.2.0", | ||
"description": "a tool for processing JSON RPC", | ||
@@ -9,35 +9,33 @@ "license": "ISC", | ||
"types": "src/index.d.ts", | ||
"files": [ | ||
"src" | ||
], | ||
"scripts": { | ||
"lint": "aegir lint", | ||
"build": "aegir build && cp ./src/index.d.ts ./dist", | ||
"test": "npm run test:node", | ||
"test:node": "aegir test --target node", | ||
"test:browser": "aegir test --target browser", | ||
"release": "aegir release", | ||
"release-minor": "aegir release --type minor", | ||
"release-major": "aegir release --type major", | ||
"coverage": "aegir coverage", | ||
"coverage-publish": "aegir coverage --provider coveralls" | ||
"lint": "eslint . --ext js,json", | ||
"lint:fix": "eslint . --ext js,json --fix", | ||
"test": "mocha ./test", | ||
"coverage": "nyc --check-coverage yarn test" | ||
}, | ||
"dependencies": { | ||
"async": "^2.0.1", | ||
"eth-json-rpc-errors": "^2.0.1", | ||
"promise-to-callback": "^1.0.0", | ||
"eth-rpc-errors": "^2.1.1", | ||
"safe-event-emitter": "^1.0.1" | ||
}, | ||
"devDependencies": { | ||
"@babel/core": "^7.5.5", | ||
"@babel/preset-env": "^7.5.5", | ||
"aegir": "^20.2.0", | ||
"babelify": "^10.0.0", | ||
"browserify": "^16.2.3" | ||
"@metamask/eslint-config": "^2.1.0", | ||
"eslint": "^6.8.0", | ||
"eslint-plugin-import": "^2.20.1", | ||
"eslint-plugin-json": "^2.1.0", | ||
"eslint-plugin-mocha": "^6.3.0", | ||
"mocha": "^7.1.1", | ||
"nyc": "^15.0.0", | ||
"sinon": "^9.0.2" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/kumavis/json-rpc-engine.git" | ||
"url": "git+https://github.com/MetaMask/json-rpc-engine.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/kumavis/json-rpc-engine/issues" | ||
"url": "https://github.com/MetaMask/json-rpc-engine/issues" | ||
}, | ||
"homepage": "https://github.com/kumavis/json-rpc-engine#readme", | ||
"homepage": "https://github.com/MetaMask/json-rpc-engine#readme", | ||
"directories": { | ||
@@ -44,0 +42,0 @@ "test": "test" |
@@ -1,6 +0,6 @@ | ||
# RpcEngine | ||
# json-rpc-engine | ||
a tool for processing JSON RPC | ||
A tool for processing JSON-RPC requests and responses. | ||
### usage | ||
## Usage | ||
@@ -13,3 +13,3 @@ ```js | ||
Build a stack of json rpc processors by pushing in RpcEngine middleware. | ||
Build a stack of JSON-RPC processors by pushing middleware to the engine. | ||
@@ -23,3 +23,3 @@ ```js | ||
JSON RPC are handled asynchronously, stepping down the stack until complete. | ||
Requests are handled asynchronously, stepping down the stack until complete. | ||
@@ -29,9 +29,12 @@ ```js | ||
engine.handle(request, function(err, res){ | ||
// do something with res.result | ||
engine.handle(request, function(err, response){ | ||
// do something with response.result | ||
}) | ||
// there is also a Promise signature | ||
const response = await engine.handle(request) | ||
``` | ||
RpcEngine middleware has direct access to the request and response objects. | ||
It can let processing continue down the stack with `next()` or complete the request with `end()`. | ||
Middleware have direct access to the request and response objects. | ||
They can let processing continue down the stack with `next()`, or complete the request with `end()`. | ||
@@ -46,3 +49,3 @@ ```js | ||
By passing a 'return handler' to the `next` function, you can get a peek at the result before it returns. | ||
By passing a _return handler_ to the `next` function, you can get a peek at the result before it returns. | ||
@@ -57,6 +60,6 @@ ```js | ||
RpcEngines can be nested by converting them to middleware `asMiddleware(engine)` | ||
RpcEngines can be nested by converting them to middleware using `asMiddleware(engine)`: | ||
```js | ||
const asMiddleware = require('json-rpc-engine/lib/asMiddleware') | ||
const asMiddleware = require('json-rpc-engine/src/asMiddleware') | ||
@@ -68,4 +71,61 @@ let engine = new RpcEngine() | ||
### gotchas | ||
### `async` Middleware | ||
If you require your middleware function to be `async`, use `createAsyncMiddleware`: | ||
```js | ||
const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') | ||
let engine = new RpcEngine() | ||
engine.push(createAsyncMiddleware(async (req, res, next) => { | ||
res.result = 42 | ||
next() | ||
})) | ||
``` | ||
`async` middleware do not take an `end` callback. | ||
Instead, the request ends if the middleware returns without calling `next()`: | ||
```js | ||
engine.push(createAsyncMiddleware(async (req, res, next) => { | ||
res.result = 42 | ||
/* The request will end when this returns */ | ||
})) | ||
``` | ||
The `next` callback of `async` middleware also don't take return handlers. | ||
Instead, you can `await next()`. | ||
When the execution of the middleware resumes, you can work with the response again. | ||
```js | ||
engine.push(createAsyncMiddleware(async (req, res, next) => { | ||
res.result = 42 | ||
await next() | ||
/* Your return handler logic goes here */ | ||
addToMetrics(res) | ||
})) | ||
``` | ||
You can freely mix callback-based and `async` middleware: | ||
```js | ||
engine.push(function(req, res, next, end){ | ||
if (!isCached(req)) { | ||
return next((cb) => { | ||
insertIntoCache(res, cb) | ||
}) | ||
} | ||
res.result = getResultFromCache(req) | ||
end() | ||
}) | ||
engine.push(createAsyncMiddleware(async (req, res, next) => { | ||
res.result = 42 | ||
await next() | ||
addToMetrics(res) | ||
})) | ||
``` | ||
### Gotchas | ||
Handle errors via `end(err)`, *NOT* `next(err)`. | ||
@@ -85,3 +145,10 @@ | ||
That said, `next()` will detect errors on the RPC response, and cause | ||
However, `next()` will detect errors on the response object, and cause | ||
`end(res.error)` to be called. | ||
```js | ||
engine.push(function(req, res, next, end){ | ||
res.error = new Error() | ||
next() /* This will cause end(res.error) to be called. */ | ||
}) | ||
``` |
'use strict' | ||
module.exports = asMiddleware | ||
module.exports = function asMiddleware (engine) { | ||
return function engineAsMiddleware (req, res, next, end) { | ||
engine._runAllMiddleware(req, res) | ||
.then(async ({ isComplete, returnHandlers }) => { | ||
function asMiddleware (engine) { | ||
return function engineAsMiddleware (req, res, next, end) { | ||
engine._runMiddlewareDown(req, res, function (err, { isComplete, returnHandlers }) { | ||
if (err) return end(err) | ||
if (isComplete) { | ||
engine._runReturnHandlersUp(returnHandlers, end) | ||
} else { | ||
next((cb) => { | ||
engine._runReturnHandlersUp(returnHandlers, cb) | ||
if (isComplete) { | ||
await engine._runReturnHandlers(returnHandlers) | ||
return end() | ||
} | ||
return next(async (handlerCallback) => { | ||
try { | ||
await engine._runReturnHandlers(returnHandlers) | ||
} catch (err) { | ||
return handlerCallback(err) | ||
} | ||
return handlerCallback() | ||
}) | ||
} | ||
}) | ||
}) | ||
.catch((error) => { | ||
end(error) | ||
}) | ||
} | ||
} |
@@ -1,40 +0,60 @@ | ||
const promiseToCallback = require('promise-to-callback') | ||
/** | ||
* JsonRpcEngine only accepts callback-based middleware directly. | ||
* createAsyncMiddleware exists to enable consumers to pass in async middleware | ||
* functions. | ||
* | ||
* Async middleware have no "end" function. Instead, they "end" if they return | ||
* without calling "next". Rather than passing in explicit return handlers, | ||
* async middleware can simply await "next", and perform operations on the | ||
* response object when execution resumes. | ||
* | ||
* To accomplish this, createAsyncMiddleware passes the async middleware a | ||
* wrapped "next" function. That function calls the internal JsonRpcEngine | ||
* "next" function with a return handler that resolves a promise when called. | ||
* | ||
* The return handler will always be called. Its resolution of the promise | ||
* enables the control flow described above. | ||
*/ | ||
module.exports = createAsyncMiddleware | ||
module.exports = function createAsyncMiddleware (asyncMiddleware) { | ||
return (req, res, next, end) => { | ||
function createAsyncMiddleware(asyncMiddleware) { | ||
return (req, res, next, end) => { | ||
let nextDonePromise = null | ||
const finishedPromise = asyncMiddleware(req, res, getNextPromise) | ||
promiseToCallback(finishedPromise)((err) => { | ||
// async middleware ended | ||
if (nextDonePromise) { | ||
// next handler was called - complete nextHandler | ||
promiseToCallback(nextDonePromise)((nextErr, nextHandlerSignalDone) => { | ||
// nextErr is only present if something went really wrong | ||
// if an error is thrown after `await next()` it appears as `err` and not `nextErr` | ||
if (nextErr) { | ||
console.error(nextErr) | ||
return end(nextErr) | ||
} | ||
nextHandlerSignalDone(err) | ||
}) | ||
} else { | ||
// next handler was not called - complete middleware | ||
end(err) | ||
} | ||
// nextPromise is the key to the implementation | ||
// it is resolved by the return handler passed to the | ||
// "next" function | ||
let resolveNextPromise | ||
const nextPromise = new Promise((resolve) => { | ||
resolveNextPromise = resolve | ||
}) | ||
async function getNextPromise() { | ||
nextDonePromise = getNextDoneCallback() | ||
await nextDonePromise | ||
return undefined | ||
let returnHandlerCallback, nextWasCalled | ||
const asyncNext = async () => { | ||
nextWasCalled = true | ||
next((callback) => { // eslint-disable-line callback-return | ||
returnHandlerCallback = callback | ||
resolveNextPromise() | ||
}) | ||
await nextPromise | ||
} | ||
function getNextDoneCallback() { | ||
return new Promise((resolve) => { | ||
next((cb) => resolve(cb)) | ||
asyncMiddleware(req, res, asyncNext) | ||
.then(async () => { | ||
if (nextWasCalled) { | ||
await nextPromise // we must wait until the return handler is called | ||
returnHandlerCallback(null) | ||
} else { | ||
end(null) | ||
} | ||
}) | ||
} | ||
.catch((error) => { | ||
if (returnHandlerCallback) { | ||
returnHandlerCallback(error) | ||
} else { | ||
end(error) | ||
} | ||
}) | ||
} | ||
} |
@@ -1,4 +0,2 @@ | ||
module.exports = createScaffoldMiddleware | ||
function createScaffoldMiddleware (handlers) { | ||
module.exports = function createScaffoldMiddleware (handlers) { | ||
return (req, res, next, end) => { | ||
@@ -16,4 +14,4 @@ const handler = handlers[req.method] | ||
res.result = handler | ||
end() | ||
return end() | ||
} | ||
} |
@@ -6,7 +6,5 @@ // uint32 (two's complement) max | ||
module.exports = getUniqueId | ||
function getUniqueId () { | ||
module.exports = function getUniqueId () { | ||
idCounter = (idCounter + 1) % MAX | ||
return idCounter | ||
} | ||
} |
const getUniqueId = require('./getUniqueId') | ||
module.exports = createIdRemapMiddleware | ||
function createIdRemapMiddleware() { | ||
return (req, res, next, end) => { | ||
module.exports = function createIdRemapMiddleware () { | ||
return (req, res, next, _end) => { | ||
const originalId = req.id | ||
@@ -8,0 +6,0 @@ const newId = getUniqueId() |
import { IEthereumRpcError } from 'eth-json-rpc-errors/@types' | ||
import { IEthereumRpcError } from 'eth-rpc-errors/@types' | ||
@@ -4,0 +4,0 @@ /** A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0". */ |
243
src/index.js
'use strict' | ||
const async = require('async') | ||
const SafeEventEmitter = require('safe-event-emitter') | ||
const { | ||
serializeError, EthereumRpcError, ERROR_CODES | ||
} = require('eth-json-rpc-errors') | ||
serializeError, | ||
EthereumRpcError, | ||
ERROR_CODES, | ||
} = require('eth-rpc-errors') | ||
class RpcEngine extends SafeEventEmitter { | ||
module.exports = class RpcEngine extends SafeEventEmitter { | ||
constructor () { | ||
@@ -23,8 +25,17 @@ super() | ||
handle (req, cb) { | ||
// batch request support | ||
if (Array.isArray(req)) { | ||
this._handleBatch(req, cb) | ||
} else { | ||
this._handle(req, cb) | ||
if (cb) { | ||
this._handleBatch(req) | ||
.then((res) => cb(null, res)) | ||
.catch((err) => cb(err)) // fatal error | ||
return undefined | ||
} | ||
return this._handleBatch(req) | ||
} | ||
if (!cb) { | ||
return this._promiseHandle(req) | ||
} | ||
return this._handle(req, cb) | ||
} | ||
@@ -36,27 +47,17 @@ | ||
async _handleBatch (reqs, cb) { | ||
async _handleBatch (reqs) { | ||
// The order here is important | ||
try { | ||
const batchRes = await Promise.all( // 2. Wait for all requests to finish | ||
// 1. Begin executing each request in the order received | ||
reqs.map(this._promiseHandle.bind(this)) | ||
) | ||
cb(null, batchRes) // 3a. Return batch response | ||
} catch (err) { | ||
cb(err) // 3b. Some kind of fatal error; all requests are lost | ||
} | ||
// 3. Return batch response, or reject on some kind of fatal error | ||
return await Promise.all( // 2. Wait for all requests to finish | ||
// 1. Begin executing each request in the order received | ||
reqs.map(this._promiseHandle.bind(this)), | ||
) | ||
} | ||
_promiseHandle (req) { | ||
return new Promise((resolve, reject) => { | ||
this._handle(req, (err, res) => { | ||
if (!res) { // defensive programming | ||
reject(err || new EthereumRpcError( | ||
ERROR_CODES.rpc.internal, | ||
'JsonRpcEngine: Request handler returned neither error nor response.' | ||
)) | ||
} else { | ||
resolve(res) | ||
} | ||
return new Promise((resolve) => { | ||
this._handle(req, (_err, res) => { | ||
// there will always be a response, and it will always have any error | ||
// that is caught and propagated | ||
resolve(res) | ||
}) | ||
@@ -66,118 +67,114 @@ }) | ||
_handle (_req, cb) { | ||
// shallow clone request object | ||
const req = Object.assign({}, _req) | ||
// create response obj | ||
_handle (callerReq, cb) { | ||
const req = Object.assign({}, callerReq) | ||
const res = { | ||
id: req.id, | ||
jsonrpc: req.jsonrpc | ||
jsonrpc: req.jsonrpc, | ||
} | ||
// process all middleware | ||
this._runMiddleware(req, res, (err) => { | ||
// take a clear any responseError | ||
const responseError = res._originalError | ||
delete res._originalError | ||
if (responseError) { | ||
// ensure no result is present on an errored response | ||
delete res.result | ||
// return originalError and response | ||
return cb(responseError, res) | ||
} | ||
// return response | ||
cb(err, res) | ||
}) | ||
let processingError | ||
this._processRequest(req, res) | ||
.catch((error) => { | ||
// either from return handlers or something unexpected | ||
processingError = error | ||
}) | ||
.finally(() => { | ||
// preserve unserialized error, if any, for use in callback | ||
const responseError = res._originalError | ||
delete res._originalError | ||
const error = responseError || processingError || null | ||
if (error) { | ||
// ensure no result is present on an errored response | ||
delete res.result | ||
if (!res.error) { | ||
res.error = serializeError(error) | ||
} | ||
} | ||
cb(error, res) | ||
}) | ||
} | ||
_runMiddleware (req, res, onDone) { | ||
// flow | ||
async.waterfall([ | ||
(cb) => this._runMiddlewareDown(req, res, cb), | ||
checkForCompletion, | ||
(returnHandlers, cb) => this._runReturnHandlersUp(returnHandlers, cb), | ||
], onDone) | ||
async _processRequest (req, res) { | ||
const { isComplete, returnHandlers } = await this._runAllMiddleware(req, res) | ||
this._checkForCompletion(req, res, isComplete) | ||
await this._runReturnHandlers(returnHandlers) | ||
} | ||
function checkForCompletion({ isComplete, returnHandlers }, cb) { | ||
// fail if not completed | ||
if (!('result' in res) && !('error' in res)) { | ||
const requestBody = JSON.stringify(req, null, 2) | ||
const message = 'JsonRpcEngine: Response has no error or result for request:\n' + requestBody | ||
return cb(new EthereumRpcError( | ||
ERROR_CODES.rpc.internal, message, req | ||
)) | ||
} | ||
if (!isComplete) { | ||
const requestBody = JSON.stringify(req, null, 2) | ||
const message = 'JsonRpcEngine: Nothing ended request:\n' + requestBody | ||
return cb(new EthereumRpcError( | ||
ERROR_CODES.rpc.internal, message, req | ||
)) | ||
} | ||
// continue | ||
return cb(null, returnHandlers) | ||
async _runReturnHandlers (handlers) { | ||
for (const handler of handlers) { | ||
await new Promise((resolve, reject) => { | ||
handler((err) => (err ? reject(err) : resolve())) | ||
}) | ||
} | ||
} | ||
_checkForCompletion (req, res, isComplete) { | ||
if (!('result' in res) && !('error' in res)) { | ||
const requestBody = JSON.stringify(req, null, 2) | ||
const message = `JsonRpcEngine: Response has no error or result for request:\n${requestBody}` | ||
throw new EthereumRpcError(ERROR_CODES.rpc.internal, message, req) | ||
} | ||
if (!isComplete) { | ||
const requestBody = JSON.stringify(req, null, 2) | ||
const message = `JsonRpcEngine: Nothing ended request:\n${requestBody}` | ||
throw new EthereumRpcError(ERROR_CODES.rpc.internal, message, req) | ||
} | ||
} | ||
// walks down stack of middleware | ||
_runMiddlewareDown (req, res, onDone) { | ||
// for climbing back up the stack | ||
let allReturnHandlers = [] | ||
// flag for stack return | ||
async _runAllMiddleware (req, res) { | ||
const returnHandlers = [] | ||
// flag for early return | ||
let isComplete = false | ||
// down stack of middleware, call and collect optional allReturnHandlers | ||
async.mapSeries(this._middleware, eachMiddleware, completeRequest) | ||
// go down stack of middleware, call and collect optional returnHandlers | ||
for (const middleware of this._middleware) { | ||
isComplete = await RpcEngine._runMiddleware( | ||
req, res, middleware, returnHandlers, | ||
) | ||
if (isComplete) { | ||
break | ||
} | ||
} | ||
return { isComplete, returnHandlers: returnHandlers.reverse() } | ||
} | ||
// runs an individual middleware | ||
function eachMiddleware (middleware, cb) { | ||
// skip middleware if completed | ||
if (isComplete) return cb() | ||
// run individual middleware | ||
middleware(req, res, next, end) | ||
// runs an individual middleware | ||
static _runMiddleware (req, res, middleware, returnHandlers) { | ||
return new Promise((resolve) => { | ||
function next (returnHandler) { | ||
const end = (err) => { | ||
const error = err || (res && res.error) | ||
if (error) { | ||
res.error = serializeError(error) | ||
res._originalError = error | ||
} | ||
resolve(true) // true indicates the request should end | ||
} | ||
const next = (returnHandler) => { | ||
if (res.error) { | ||
end(res.error) | ||
} else { | ||
// add return handler | ||
allReturnHandlers.push(returnHandler) | ||
cb() | ||
if (returnHandler) { | ||
returnHandlers.push(returnHandler) | ||
} | ||
resolve(false) // false indicates the request should not end | ||
} | ||
} | ||
function end (err) { | ||
// if errored, set the error but dont pass to callback | ||
const _err = err || (res && res.error) | ||
// const _err = err | ||
if (_err) { | ||
res.error = serializeError(_err) | ||
res._originalError = _err | ||
} | ||
// mark as completed | ||
isComplete = true | ||
cb() | ||
try { | ||
middleware(req, res, next, end) | ||
} catch (error) { | ||
end(error) | ||
} | ||
} | ||
// returns, indicating whether or not it ended | ||
function completeRequest (err) { | ||
// this is an internal catastrophic error, not an error from middleware | ||
if (err) { | ||
// prepare error message | ||
res.error = serializeError(err) | ||
// remove result if present | ||
delete res.result | ||
// return error-first and res with err | ||
return onDone(err, res) | ||
} | ||
const returnHandlers = allReturnHandlers.filter(Boolean).reverse() | ||
onDone(null, { isComplete, returnHandlers }) | ||
} | ||
}) | ||
} | ||
// climbs the stack calling return handlers | ||
_runReturnHandlersUp (returnHandlers, cb) { | ||
async.eachSeries(returnHandlers, (handler, next) => handler(next), cb) | ||
} | ||
} | ||
module.exports = RpcEngine |
@@ -1,10 +0,8 @@ | ||
const JsonRpcEngine = require('./index') | ||
const asMiddleware = require('./asMiddleware') | ||
const JsonRpcEngine = require('.') | ||
module.exports = mergeMiddleware | ||
function mergeMiddleware(middlewareStack) { | ||
module.exports = function mergeMiddleware (middlewareStack) { | ||
const engine = new JsonRpcEngine() | ||
middlewareStack.forEach(middleware => engine.push(middleware)) | ||
middlewareStack.forEach((middleware) => engine.push(middleware)) | ||
return asMiddleware(engine) | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2
148
16204
8
12
319
1
+ Addedeth-rpc-errors@^2.1.1
+ Addedeth-rpc-errors@2.1.1(transitive)
- Removedasync@^2.0.1
- Removedeth-json-rpc-errors@^2.0.1
- Removedpromise-to-callback@^1.0.0
- Removedasync@2.6.4(transitive)
- Removedeth-json-rpc-errors@2.0.2(transitive)
- Removedis-fn@1.0.0(transitive)
- Removedlodash@4.17.21(transitive)
- Removedpromise-to-callback@1.0.0(transitive)
- Removedset-immediate-shim@1.0.1(transitive)