Skip to content

Instantly share code, notes, and snippets.

@rhythnic
Created May 30, 2019 18:45
Show Gist options
  • Save rhythnic/a740e22cd4a4d6e927c4ca2db52a1a02 to your computer and use it in GitHub Desktop.
Save rhythnic/a740e22cd4a4d6e927c4ca2db52a1a02 to your computer and use it in GitHub Desktop.
Remote Control Server - Send and listen for HTTP requests via HTTP.
const express = require('express')
const R = require('ramda')
const uuid = require('uuid')
const request = require('request')
// *******************************************************
// Models
// *******************************************************
// RequestSummary
// {
// id: String
// method: String
// path: String
// params: Object
// query: Object
// headers: Object
// body: Object,
// timestamp: String
// }
// QueryResponse
// {
// id: String
// query: ExpressRequestQueryObject
// timestamp: Number
// response: ExpressResponseObject
// }
// *******************************************************
// Config
// *******************************************************
const defaultConfig = {
PORT: 8080,
QUERY_TIMEOUT: 5000,
NAME: 'RC Server',
QUERY_RESPONSE_INTERVAL: 2000,
ACCESS_PATH: '/requests'
}
const config = R.merge(defaultConfig, R.pick(R.keys(defaultConfig), process.env))
// *******************************************************
// Helpers
// *******************************************************
// Parse nested objects
const parseObjectValues = R.curry((keys, obj) => R.reduce(
(acc, x) => R.assoc(x, JSON.parse(acc[x]), acc),
obj,
keys.filter(R.has(R.__, obj))
))
const mapProp = prop => R.map(R.prop(prop))
const excludeByProp = R.curry((prop, xs, ys) => {
const _mapProp = mapProp(prop)
return R.filter(
R.compose(R.not, R.contains(R.__, _mapProp(xs)), R.prop(prop)),
ys
)
})
// For each QueryResponse, attempt to find a match in the requestSummaries list
// If found, call QueryResponse.resolve with the match
// If not found and expired, call QueryResponse.reject with a NotFound error
// Remove all found or expired QueryResponses from state
function resolveFoundOrExpiredQueries (state, queryTimeout) {
return queryResponses => {
const isExpired = R.compose(R.gt(Date.now()), R.add(queryTimeout), R.prop('timestamp'))
if (!queryResponses) queryResponses = state.queryResponses
const responses = queryResponses.map(x => {
const found = state.requestSummaries.filter(partialObjectEqual(x.query))
return found.length ? { ...x, found } : { ...x, isExpired: isExpired(x) }
})
const foundOrExpired = filterInFoundOrExpired(responses)
state.queryResponses = excludeByProp('id', foundOrExpired, state.queryResponses)
foundOrExpired.forEach(x => {
x.response.json(x.found || [])
})
}
}
// return true if the equivalent of the first object
// is contained in the second object
const partialObjectEqual = R.curry(function partialObjectEqual (a, b) {
return R.keys(a).every(x => typeof a[x] === 'object'
? partialObjectEqual(a[x], b[x])
: R.equals(a[x], b[x])
)
})
const filterInFoundOrExpired = R.filter(R.either(R.prop('found'), R.prop('isExpired')))
const pickRequestSummaryProps = R.pick(
['method', 'path', 'params', 'query', 'headers', 'body']
)
const parseQueryObjects = parseObjectValues(['params', 'query', 'headers', 'body'])
// *******************************************************
// Express middleware/handlers
// *******************************************************
// Function for querying requests received by the server
// These requests are excluded from the request queue
// If not found, the request is kept open
const listRequests = R.curry(function listRequests (state, respondToQueries, req, res) {
const query = parseQueryObjects(req.query)
const queryResponse = { id: uuid.v4(), query, timestamp: Date.now(), response: res }
state.queryResponses.push(queryResponse)
respondToQueries([queryResponse])
})
// Push a RequestSummary onto the queue for each request
// Respond with 204 to all requests
const queueRequest = R.curry(function queueRequest (state, req, res) {
state.requestSummaries.push({ id: uuid.v4(), ...pickRequestSummaryProps(req) })
res.status(204).end()
})
// Use req.body as the options for the request library
// Pipe the response back to the client
function proxyRequest (req, res) {
return request(req.body).pipe(res)
}
// *******************************************************
// Express App
// *******************************************************
function createRcServer () {
const state = {
requestSummaries: [],
queryResponses: []
}
const app = express()
app.use(express.json())
const respondToQueries = resolveFoundOrExpiredQueries(state, config.QUERY_TIMEOUT)
app.get(config.ACCESS_PATH, listRequests(state, respondToQueries))
app.post(config.ACCESS_PATH, proxyRequest)
app.use(queueRequest(state))
app.listen(config.PORT, err => {
if (err) throw err
console.info(`${config.NAME} listening on port ${config.PORT}!`)
setInterval(respondToQueries, config.QUERY_RESPONSE_INTERVAL)
})
}
createRcServer()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment