Skip to content

chore: release preparations #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,11 @@ Standard Options:

--httpTimeout

Maximum time in ms to wait for an HTTP HEAD/GET request, default 0
which results in using the OS default
Maximum time in ms to wait for an HTTP HEAD/GET request, default 60000

--socketTimeout

Maximum time in ms to wait for an Socket connection establishment, default 60000.

-i, --interval

Expand All @@ -123,7 +126,7 @@ Standard Options:

--tcpTimeout

Maximum time in ms for tcp connect, default 300ms
Maximum time in ms for tcp connect, default 60000

-v, --verbose

Expand Down
78 changes: 55 additions & 23 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
/// <reference types="node" />
import { AgentOptions as HTTPAgentOptions } from "node:http";
import { AgentOptions as HTTPSAgentOptions } from "node:https";
import { ProxyAgent } from 'undici';

type WaitOnCallback = (err?: Error, result: boolean) => unknown;

declare function WaitOn(
options: WaitOnOptions,
cb?: WaitOnCallback
): Promise<void> | void;
cb: WaitOnCallback
): Promise<boolean>;

type WaitOnCallback = (err?: Error) => unknown;
declare function WaitOn(options: WaitOnOptions): Promise<boolean>;

type WaitOnProxyConfig = {
host?: string;
protocol?: string;
auth?: WaitOnOptions["auth"];
};
type WaitOnProxyConfig = ProxyAgent.Options;

/**
* @description Invoked when an unsuccessful response is received from resource
*/
type WaitOnEventHandler = (
resource: WaitOnResourcesType,
response: string
) => void;
/**
* @description invoked when an invalid resource is encountered
* @note It won't be invoked if the 'throwOnInvalidResource' option is on
*/
type WaitOnInvalidResourceEventHandler = (
resource: WaitOnResourcesType
) => void;
/**
* @description Invoked when the resource becomes available and stable
*/
type WaitOnDoneEventHandler = (resource: WaitOnResourcesType) => void;
/**
* @description Invoked when an unexpected error or a timed out waiting for the resource
* occurs
*/
type WaitOnErrorHandler = (resource: WaitOnResourcesType, error: Error) => void;

type WaitOnResourcesType =
| `file:${string}`
Expand All @@ -28,26 +49,37 @@ type WaitOnValidateStatusCallback = (status: number) => boolean;

type WaitOnOptions = {
resources: WaitOnResourcesType[];
throwOnInvalidResource?: boolean;
delay?: number;
interval?: number;
log?: boolean;
timeout?: number;
reverse?: boolean;
simultaneous?: number;
timeout?: number;
tcpTimeout?: number;
verbose?: boolean;
http?: {
bodyTimeout?: number;
headersTimeout?: number;
maxRedirects?: number;
followRedirect?: boolean;
headers?: Record<string, string | number>;
validateStatus?: WaitOnValidateStatusCallback
};
socket?: {
timeout?: number;
};
tcp?: {
timeout?: number;
};
window?: number;
passphrase?: string;
proxy?: boolean | WaitOnProxyConfig;
auth?: {
user: string;
pass: string;
proxy?: WaitOnProxyConfig;
events?: {
onInvalidResource?: WaitOnInvalidResourceEventHandler;
onResourceTimeout?: WaitOnErrorHandler;
onResourceError?: WaitOnErrorHandler;
onResourceResponse?: WaitOnEventHandler;
onResourceDone?: WaitOnDoneEventHandler;
};
headers?: Record<string, string | number>;
validateStatus?: WaitOnValidateStatusCallback;
strictSSL?: boolean;
} & HTTPAgentOptions &
HTTPSAgentOptions;
};

export default WaitOn;
export {
Expand Down
47 changes: 12 additions & 35 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ const { join, isAbsolute } = require('node:path')
const { AsyncPool } = require('@metcoder95/tiny-pool')

const {
validateHooks,
validateOptions,
validateHooks,
parseAjvError,
parseAjvErrors
} = require('./lib/validate')
Expand All @@ -17,41 +17,12 @@ const { createTCPResource } = require('./lib/tcp')
const { createSocketResource } = require('./lib/socket')
const { createFileResource } = require('./lib/file')

/**
Waits for resources to become available before calling callback

Polls file, http(s), tcp ports, sockets for availability.

Resource types are distinquished by their prefix with default being `file:`
- file:/path/to/file - waits for file to be available and size to stabilize
- http://foo.com:8000/bar verifies HTTP HEAD request returns 2XX
- https://my.bar.com/cat verifies HTTPS HEAD request returns 2XX
- http-get: - HTTP GET returns 2XX response. ex: http://m.com:90/foo
- https-get: - HTTPS GET returns 2XX response. ex: https://my/bar
- tcp:my.server.com:3000 verifies a service is listening on port
- socket:/path/sock verifies a service is listening on (UDS) socket
For http over socket, use http://unix:SOCK_PATH:URL_PATH
like http://unix:/path/to/sock:/foo/bar or
http-get://unix:/path/to/sock:/foo/bar

@param opts object configuring waitOn
@param opts.resources array of string resources to wait for. prefix determines the type of resource with the default type of `file:`
@param opts.delay integer - optional initial delay in ms, default 0
@param opts.httpTimeout integer - optional http HEAD/GET timeout to wait for request, default 0
@param opts.interval integer - optional poll resource interval in ms, default 250ms
@param opts.log boolean - optional flag to turn on logging to stdout
@param opts.reverse boolean - optional flag which reverses the mode, succeeds when resources are not available
@param opts.simultaneous integer - optional limit of concurrent connections to a resource, default Infinity
@param opts.tcpTimeout - Maximum time in ms for tcp connect, default 300ms
@param opts.timeout integer - optional timeout in ms, default Infinity. Aborts with error.
@param opts.verbose boolean - optional flag to turn on debug log
@param opts.window integer - optional stabilization time in ms, default 750ms. Waits this amount of time for file sizes to stabilize or other resource availability to remain unchanged. If less than interval then will be reset to interval
@param cb optional callback function with signature cb(err) - if err is provided then, resource checks did not succeed
if not specified, wait-on will return a promise that will be rejected if resource checks did not succeed or resolved otherwise
*/
// Main function
function WaitOn (opts, cb) {
if (cb != null && cb.constructor.name === 'Function') {
waitOnImpl(opts).then(cb, cb)
waitOnImpl(opts).then(result => {
cb(null, result)
}, cb)
} else {
return waitOnImpl(opts)
}
Expand Down Expand Up @@ -82,6 +53,7 @@ async function waitOnImpl (opts) {

const {
resources: incomingResources,
throwOnInvalidResource,
timeout,
simultaneous,
events
Expand All @@ -99,6 +71,10 @@ async function waitOnImpl (opts) {
}

if (invalidResources.length > 0 && events?.onInvalidResource != null) {
if (throwOnInvalidResource) {
throw new Error(`Invalid resources: ${invalidResources.join(', ')}`)
}

for (const resource of invalidResources) {
events.onInvalidResource(resource)
}
Expand Down Expand Up @@ -223,7 +199,8 @@ function handleResponse ({ resource, pool, signal, waitOnOptions, state }) {

return timerPromise
.then(() => pool.run(resource.exec.bind(null, signal)))
.then(onResponse, onError).catch(onError)
.then(onResponse, onError)
.catch(onError)
}

function onError (err) {
Expand Down
2 changes: 1 addition & 1 deletion lib/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function createFileResource (_, resource) {
name: href
}

async function exec (signal) {
async function exec () {
const operation = {
successfull: false,
reason: 'unknown'
Expand Down
106 changes: 86 additions & 20 deletions lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,24 @@ const { Agent, ProxyAgent, request } = require('undici')
const HTTP_GET_RE = /^https?-get:/
const HTTP_UNIX_RE = /^http:\/\/unix:([^:]+):([^:]+)$/

function getHTTPAgent (config, resource) {
function getHTTPAgent (config, href) {
const {
followRedirect,
maxRedirections,
timeout,
http: { bodyTimeout, headersTimeout } = {},
proxy,
strictSSL: rejectUnauthorized
http: {
bodyTimeout,
headersTimeout,
followRedirect,
maxRedirections,
rejectUnauthorized
} = {},
proxy
} = config
const isProxied = proxy != null
const url = resource.replace('-get:', ':')
// http://unix:/sock:/url
const matchHttpUnixSocket = HTTP_UNIX_RE.exec(url)
const matchHttpUnixSocket = HTTP_UNIX_RE.exec(href)
const socketPath = matchHttpUnixSocket != null ? matchHttpUnixSocket[1] : null

/** @type {import('undici').Agent.Options} */
const httpOptions = {
maxRedirections: followRedirect != null ? maxRedirections : 0,
bodyTimeout,
Expand All @@ -38,39 +41,102 @@ function getHTTPAgent (config, resource) {
}

function createHTTPResource (config, resource) {
const source = new URL(resource)
const dispatcher = getHTTPAgent(config, resource)
const method = HTTP_GET_RE.test(resource) ? 'get' : 'head'
const url = resource.replace('-get:', ':')
/** @type { import('..').WaitOnOptions } */
const { http: httpConfig } = config
const method = HTTP_GET_RE.test(resource) ? 'GET' : 'HEAD'
const href = source.href.replace('-get:', ':')
const isStatusValid = httpConfig?.validateStatus
// TODO: this will last as long as happy-eyeballs is not implemented
// within node core
/** @type {{ options: import('undici').Dispatcher.RequestOptions, url: URL }} */
const primary = {
options: null,
url: null
}
/** @type {{ options?: import('undici').Dispatcher.RequestOptions, url?: URL }} */
const secondary = {
options: null,
url: null
}

if (href.includes('localhost')) {
primary.url = new URL(href.replace('localhost', '127.0.0.1'))

secondary.url = new URL(href.replace('localhost', '[::1]'))
secondary.options = {
path: secondary.url.pathname,
origin: secondary.url.origin,
query: secondary.url.search,
method,
dispatcher,
signal: null,
headers: httpConfig?.headers
}
} else {
primary.url = new URL(href)
}

primary.options = {
path: primary.url.pathname,
origin: primary.url.origin,
query: primary.url.search,
method,
dispatcher,
signal: null,
headers: httpConfig?.headers
}

return {
exec,
name: resource
}

async function exec (signal) {
async function exec (
signal,
handler = primary,
handlerSecondary = secondary,
isSecondary = false
) {
const start = Date.now()
const operation = {
successfull: false,
reason: 'unknown'
}

handler.options.signal = signal

if (handlerSecondary.options != null) {
handlerSecondary.options.signal = signal
}

try {
// TODO: implement small happy eyeballs algorithm for IPv4/IPv6 on localhost
// TODO: implement window tolerance feature
const { statusCode, body } = await request(url, {
method,
dispatcher,
signal
})
const options = isSecondary ? handlerSecondary.options : handler.options
const { statusCode, body } = await request(options)
const duration = Date.now() - start

// We allow data to flow without worrying about it
body.resume()

// TODO: add support allow range of status codes
operation.successfull = statusCode >= 200 && statusCode < 500
operation.successfull =
isStatusValid != null
? isStatusValid(statusCode)
: statusCode >= 200 && statusCode < 500

if (
!operation.successfull &&
!isSecondary &&
handlerSecondary.url != null
) {
return await exec(signal, handler, handlerSecondary, true)
}
operation.reason = `HTTP(s) request for ${method}-${resource} replied with code ${statusCode} - duration ${duration}ms`
} catch (e) {
if (!isSecondary && handlerSecondary.url != null) {
return await exec(signal, handler, handlerSecondary, true)
}

operation.reason = `HTTP(s) request for ${method}-${resource} errored: ${
e.message
} - duration ${Date.now() - start}`
Expand Down
Loading