Skip to content

Commit

Permalink
feat: new hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
ronag committed Nov 25, 2024
1 parent 5a47b01 commit f37d5f4
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 121 deletions.
5 changes: 5 additions & 0 deletions lib/core/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,11 @@ function assertRequestHandler (handler, method, upgrade) {
throw new InvalidArgumentError('handler must be an object')
}

if (typeof handler.onRequestStart === 'function') {
// TODO (fix): More checks...
return
}

if (typeof handler.onConnect !== 'function') {
throw new InvalidArgumentError('invalid onConnect method')
}
Expand Down
3 changes: 2 additions & 1 deletion lib/dispatcher/dispatcher-base.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const Dispatcher = require('./dispatcher')
const UnwrapHandler = require('../handler/unwrap-handler')
const {
ClientDestroyedError,
ClientClosedError,
Expand Down Expand Up @@ -142,7 +143,7 @@ class DispatcherBase extends Dispatcher {
throw new ClientClosedError()
}

return this[kDispatch](opts, handler)
return this[kDispatch](opts, UnwrapHandler.unwrap(handler))
} catch (err) {
if (typeof handler.onError !== 'function') {
throw new InvalidArgumentError('invalid onError method')
Expand Down
4 changes: 4 additions & 0 deletions lib/dispatcher/dispatcher.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
'use strict'
const EventEmitter = require('node:events')
const WrapHandler = require('../handler/wrap-handler')

const wrapInterceptor = (dispatch) => (opts, handler) => dispatch(opts, WrapHandler.wrap(handler))

class Dispatcher extends EventEmitter {
dispatch () {
Expand Down Expand Up @@ -28,6 +31,7 @@ class Dispatcher extends EventEmitter {
throw new TypeError(`invalid interceptor, expected function received ${typeof interceptor}`)
}

dispatch = wrapInterceptor(dispatch)
dispatch = interceptor(dispatch)

if (dispatch == null || typeof dispatch !== 'function' || dispatch.length !== 2) {
Expand Down
2 changes: 0 additions & 2 deletions lib/handler/redirect-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ class RedirectHandler {
throw new InvalidArgumentError('maxRedirections must be a positive number')
}

util.assertRequestHandler(handler, opts.method, opts.upgrade)

this.dispatch = dispatch
this.location = null
this.abort = null
Expand Down
98 changes: 98 additions & 0 deletions lib/handler/unwrap-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use strict'

const { parseHeaders } = require('../core/util')
const { InvalidArgumentError } = require('../core/errors')

const kResume = Symbol('resume')

class UnwrapController {
#paused = false
#reason = null
#aborted = false
#abort

[kResume] = null

constructor (abort) {
this.#abort = abort
}

pause () {
this.#paused = true
}

resume () {
if (this.#paused) {
this.#paused = false
this[kResume]?.()
}
}

abort (reason) {
if (!this.#aborted) {
this.#aborted = true
this.#reason = reason
this.#abort(reason)
}
}

get aborted () {
return this.#aborted
}

get reason () {
return this.#reason
}

get paused () {
return this.#paused
}
}

module.exports = class UnwrapHandler {
#handler
#controller

constructor (handler) {
this.#handler = handler
}

static unwrap (handler) {
// TODO (fix): More checks...
return handler.onConnect ? handler : new UnwrapHandler(handler)
}

onConnect (abort, context) {
this.#controller = new UnwrapController(abort)
this.#handler.onRequestStart?.(this.#controller, context)
}

onUpgrade (statusCode, rawHeaders, socket) {
this.#handler.onRequestUpgrade?.(statusCode, parseHeaders(rawHeaders), socket)
}

onHeaders (statusCode, rawHeaders, resume, statusMessage) {
this.#controller[kResume] = resume
this.#handler.onResponseStart?.(this.#controller, statusCode, statusMessage)
this.#handler.onResponseHeaders?.(this.#controller, parseHeaders(rawHeaders))
return !this.#controller.paused
}

onData (data) {
this.#handler.onResponseData?.(this.#controller, data)
return !this.#controller.paused
}

onComplete (rawTrailers) {
this.#handler.onResponseTrailer?.(this.#controller, parseHeaders(rawTrailers))
this.#handler.onResponseEnd?.(this.#controller)
}

onError (err) {
if (!this.#handler.onError) {
throw new InvalidArgumentError('invalid onError method')
}

this.#handler.onResponseError?.(this.#controller, err)
}
}
126 changes: 126 additions & 0 deletions lib/handler/wrap-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
'use strict'

const { InvalidArgumentError } = require('../core/errors')

module.exports = class WrapHandler {
#handler
#statusCode = 0
#statusMessage = ''
#trailers = {}

constructor (handler) {
this.#handler = handler
}

static wrap (handler) {
// TODO (fix): More checks...
return handler.onRequestStart ? handler : new WrapHandler(handler)
}

// Unwrap Interface

onConnect (abort, context) {
return this.#handler.onConnect?.(abort, context)
}

onHeaders (statusCode, rawHeaders, resume, statusMessage) {
return this.#handler.onHeaders?.(statusCode, rawHeaders, resume, statusMessage)
}

onUpgrade (statusCode, rawHeaders, socket) {
return this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
}

onData (data) {
return this.#handler.onData?.(data)
}

onComplete (trailers) {
return this.#handler.onComplete?.(trailers)
}

onError (err) {
if (!this.#handler.onError) {
throw new InvalidArgumentError('invalid onError method')
}

return this.#handler.onError?.(err)
}

// Wrap Interface

onRequestStart (controller, context) {
this.#handler.onConnect?.((reason) => controller.abort(reason), context)
this.#statusCode = 0
this.#statusMessage = ''
this.#trailers = {}
}

onRequestError (controller, error) {
if (!this.#handler.onError) {
throw new InvalidArgumentError('invalid onError method')
}

this.#handler.onError?.(error)
}

onRequestUpgrade (statusCode, headers, socket) {
const rawHeaders = []
for (const [key, val] of Object.entries(headers)) {
// TODO (fix): What if val is Array
rawHeaders.push(Buffer.from(key), Buffer.from(val))
}

this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
}

onResponseStart (controller, statusCode, statusMessage) {
this.#statusCode = statusCode
this.#statusMessage = statusMessage
}

onResponseHeaders (controller, headers) {
const rawHeaders = []
for (const [key, val] of Object.entries(headers)) {
// TODO (fix): What if val is Array
rawHeaders.push(Buffer.from(key), Buffer.from(val))
}

if (this.#handler.onHeaders?.(
this.#statusCode,
rawHeaders,
() => controller.resume(),
this.#statusMessage
) === false) {
controller.pause()
}
}

onResponseData (controller, data) {
if (this.#handler.onData?.(data) === false) {
controller.pause()
}
}

onResponseTrailer (controller, trailers) {
this.#trailers = trailers
}

onResponseEnd (controller) {
const rawTrailers = []
for (const [key, val] of Object.entries(this.#trailers)) {
// TODO (fix): What if val is Array
rawTrailers.push(Buffer.from(key), Buffer.from(val))
}

this.#handler.onComplete?.(rawTrailers)
}

onResponseError (controller, error) {
if (!this.#handler.onError) {
throw new InvalidArgumentError('invalid onError method')
}

this.#handler.onError?.(error)
}
}
83 changes: 0 additions & 83 deletions test/mock-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,89 +142,6 @@ describe('MockAgent - dispatch', () => {
onError: () => {}
}))
})

test('should throw if handler is not valid on redirect', (t) => {
t = tspl(t, { plan: 7 })

const baseUrl = 'http://localhost:9999'

const mockAgent = new MockAgent()
after(() => mockAgent.close())

t.throws(() => mockAgent.dispatch({
origin: baseUrl,
path: '/foo',
method: 'GET'
}, {
onError: 'INVALID'
}), new InvalidArgumentError('invalid onError method'))

t.throws(() => mockAgent.dispatch({
origin: baseUrl,
path: '/foo',
method: 'GET'
}, {
onError: (err) => { throw err },
onConnect: 'INVALID'
}), new InvalidArgumentError('invalid onConnect method'))

t.throws(() => mockAgent.dispatch({
origin: baseUrl,
path: '/foo',
method: 'GET'
}, {
onError: (err) => { throw err },
onConnect: () => {},
onBodySent: 'INVALID'
}), new InvalidArgumentError('invalid onBodySent method'))

t.throws(() => mockAgent.dispatch({
origin: baseUrl,
path: '/foo',
method: 'CONNECT'
}, {
onError: (err) => { throw err },
onConnect: () => {},
onBodySent: () => {},
onUpgrade: 'INVALID'
}), new InvalidArgumentError('invalid onUpgrade method'))

t.throws(() => mockAgent.dispatch({
origin: baseUrl,
path: '/foo',
method: 'GET'
}, {
onError: (err) => { throw err },
onConnect: () => {},
onBodySent: () => {},
onHeaders: 'INVALID'
}), new InvalidArgumentError('invalid onHeaders method'))

t.throws(() => mockAgent.dispatch({
origin: baseUrl,
path: '/foo',
method: 'GET'
}, {
onError: (err) => { throw err },
onConnect: () => {},
onBodySent: () => {},
onHeaders: () => {},
onData: 'INVALID'
}), new InvalidArgumentError('invalid onData method'))

t.throws(() => mockAgent.dispatch({
origin: baseUrl,
path: '/foo',
method: 'GET'
}, {
onError: (err) => { throw err },
onConnect: () => {},
onBodySent: () => {},
onHeaders: () => {},
onData: () => {},
onComplete: 'INVALID'
}), new InvalidArgumentError('invalid onComplete method'))
})
})

test('MockAgent - .close should clean up registered pools', async (t) => {
Expand Down
Loading

0 comments on commit f37d5f4

Please sign in to comment.