reshuffle
Version:
Reshuffle is a fast, unopinionated, minimalist integration framework
132 lines (106 loc) • 3.9 kB
text/typescript
import fetch, { RequestInfo, RequestInit } from 'node-fetch'
import { format as _formatURL, URL } from 'url'
import { Request, Response, NextFunction } from 'express'
import Reshuffle from '../Reshuffle'
import { BaseHttpConnector, EventConfiguration } from 'reshuffle-base-connector'
class TimeoutError extends Error {}
export interface HttpConnectorConfigOptions {
authKey?: string
authScript?: string
}
export interface HttpConnectorEventOptions {
method: string
path: string
}
const sanitizePath = (path: string) => {
const pathNoQueryParam = path.split('?')[0]
return pathNoQueryParam.startsWith('/') ? pathNoQueryParam : `/${pathNoQueryParam}`
}
export default class HttpConnector extends BaseHttpConnector<
HttpConnectorConfigOptions,
HttpConnectorEventOptions
> {
on(options: HttpConnectorEventOptions, eventId?: string): EventConfiguration {
const optionsSanitized = { method: options.method, path: sanitizePath(options.path) }
if (!eventId) {
eventId = `HTTP/${optionsSanitized.method}${optionsSanitized.path}/${this.id}`
}
const event = new EventConfiguration(eventId, this, optionsSanitized)
this.eventConfigurations[event.id] = event
this.app?.registerHTTPDelegate(event.options.path, this)
return event
}
onStart(app: Reshuffle) {
Object.values(this.eventConfigurations).forEach((eventConfiguration) =>
app.registerHTTPDelegate(eventConfiguration.options.path, this),
)
}
async handle(req: Request, res: Response, next: NextFunction) {
const { method, params } = req
const requestPath = params[0]
let handled = false
const eventConfiguration = Object.values(this.eventConfigurations).find(
({ options }) => options.path === requestPath && options.method === method,
)
if (eventConfiguration) {
this.app?.getLogger().info('Handling event')
handled = this.app
? await this.app.handleEvent(eventConfiguration.id, {
...eventConfiguration,
context: { req, res },
})
: false
}
next()
return handled
}
onStop() {
Object.values(this.eventConfigurations).forEach((eventConfiguration) =>
this.app?.unregisterHTTPDelegate(eventConfiguration.options.path),
)
}
fetch(url: RequestInfo, options?: RequestInit) {
return fetch(url, options)
}
async fetchWithRetries(url: string, options: RequestInit = {}, retry: Record<string, any> = {}) {
const interval = retry.interval !== undefined ? retry.interval : 2000
if (typeof interval !== 'number' || interval < 50 || 5000 < interval) {
throw new Error(`Http: Invalid retry interval: ${interval}`)
}
const repeat = retry.repeat !== undefined ? retry.repeat : 5
if (typeof repeat !== 'number' || repeat < 1 || 10 < repeat) {
throw new Error(`Http: Invalid retry repeat: ${repeat}`)
}
const backoff = retry.backoff !== undefined ? retry.backoff : 2
if (typeof backoff !== 'number' || backoff < 1 || 3 < backoff) {
throw new Error(`Http: Invalid retry backoff: ${backoff}`)
}
let ms = interval
for (let i = 0; i < repeat; i++) {
try {
return await this.fetchWithTimeout(url, options, ms)
} catch (e) {
if (!(e instanceof TimeoutError)) {
throw e
}
}
ms *= backoff
}
throw new TimeoutError('Retries timed out')
}
fetchWithTimeout(url: string, options: RequestInit, ms: number) {
if (typeof ms !== 'number' || ms < 1) {
throw new Error(`Http: Invalid timeout: ${ms}`)
}
return new Promise((resolve, reject) => {
setTimeout(() => reject(new TimeoutError('Timed out')), ms)
this.fetch(url, options).then(resolve, reject)
})
}
formatURL(components: Record<string, any>) {
return _formatURL(components)
}
parseURL(url: string) {
return new URL(url)
}
}