Skip to content

Commit 9ab150a

Browse files
committed
feat: new hooks
1 parent 5a47b01 commit 9ab150a

File tree

8 files changed

+295
-146
lines changed

8 files changed

+295
-146
lines changed

lib/core/util.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,11 @@ function assertRequestHandler (handler, method, upgrade) {
511511
throw new InvalidArgumentError('handler must be an object')
512512
}
513513

514+
if (typeof handler.onRequestStart === 'function') {
515+
// TODO (fix): More checks...
516+
return
517+
}
518+
514519
if (typeof handler.onConnect !== 'function') {
515520
throw new InvalidArgumentError('invalid onConnect method')
516521
}

lib/dispatcher/dispatcher-base.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict'
22

33
const Dispatcher = require('./dispatcher')
4+
const UnwrapHandler = require('../handler/unwrap-handler')
45
const {
56
ClientDestroyedError,
67
ClientClosedError,
@@ -142,7 +143,7 @@ class DispatcherBase extends Dispatcher {
142143
throw new ClientClosedError()
143144
}
144145

145-
return this[kDispatch](opts, handler)
146+
return this[kDispatch](opts, UnwrapHandler.unwrap(handler))
146147
} catch (err) {
147148
if (typeof handler.onError !== 'function') {
148149
throw new InvalidArgumentError('invalid onError method')

lib/dispatcher/dispatcher.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
'use strict'
22
const EventEmitter = require('node:events')
3+
const WrapHandler = require('../handler/wrap-handler')
4+
5+
const wrapInterceptor = (dispatch) => (opts, handler) => dispatch(opts, WrapHandler.wrap(handler))
36

47
class Dispatcher extends EventEmitter {
58
dispatch () {
@@ -28,6 +31,7 @@ class Dispatcher extends EventEmitter {
2831
throw new TypeError(`invalid interceptor, expected function received ${typeof interceptor}`)
2932
}
3033

34+
dispatch = wrapInterceptor(dispatch)
3135
dispatch = interceptor(dispatch)
3236

3337
if (dispatch == null || typeof dispatch !== 'function' || dispatch.length !== 2) {

lib/handler/redirect-handler.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ class RedirectHandler {
4040
throw new InvalidArgumentError('maxRedirections must be a positive number')
4141
}
4242

43-
util.assertRequestHandler(handler, opts.method, opts.upgrade)
44-
4543
this.dispatch = dispatch
4644
this.location = null
4745
this.abort = null

lib/handler/unwrap-handler.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
'use strict'
2+
3+
const { parseHeaders } = require('../core/util')
4+
const { InvalidArgumentError } = require('../core/errors')
5+
6+
const kResume = Symbol('resume')
7+
8+
class UnwrapController {
9+
#paused = false
10+
#reason = null
11+
#aborted = false
12+
#abort
13+
14+
[kResume] = null
15+
16+
constructor (abort) {
17+
this.#abort = abort
18+
}
19+
20+
pause () {
21+
this.#paused = true
22+
}
23+
24+
resume () {
25+
if (this.#paused) {
26+
this.#paused = false
27+
this[kResume]?.()
28+
}
29+
}
30+
31+
abort (reason) {
32+
if (!this.#aborted) {
33+
this.#aborted = true
34+
this.#reason = reason
35+
this.#abort(reason)
36+
}
37+
}
38+
39+
get aborted () {
40+
return this.#aborted
41+
}
42+
43+
get reason () {
44+
return this.#reason
45+
}
46+
47+
get paused () {
48+
return this.#paused
49+
}
50+
}
51+
52+
module.exports = class UnwrapHandler {
53+
#handler
54+
#controller
55+
56+
constructor (handler) {
57+
this.#handler = handler
58+
}
59+
60+
static unwrap (handler) {
61+
// TODO (fix): More checks...
62+
return handler.onConnect ? handler : new UnwrapHandler(handler)
63+
}
64+
65+
onConnect (abort, context) {
66+
this.#controller = new UnwrapController(abort)
67+
this.#handler.onRequestStart?.(this.#controller, context)
68+
}
69+
70+
onUpgrade (statusCode, rawHeaders, socket) {
71+
this.#handler.onRequestUpgrade?.(statusCode, parseHeaders(rawHeaders), socket)
72+
}
73+
74+
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
75+
this.#controller[kResume] = resume
76+
this.#handler.onResponseStart?.(this.#controller, statusCode, statusMessage)
77+
this.#handler.onResponseHeaders?.(this.#controller, parseHeaders(rawHeaders))
78+
return !this.#controller.paused
79+
}
80+
81+
onData (data) {
82+
this.#handler.onResponseData?.(this.#controller, data)
83+
return !this.#controller.paused
84+
}
85+
86+
onComplete (rawTrailers) {
87+
this.#handler.onResponseTrailer?.(this.#controller, parseHeaders(rawTrailers))
88+
this.#handler.onResponseEnd?.(this.#controller)
89+
}
90+
91+
onError (err) {
92+
if (!this.#handler.onError) {
93+
throw new InvalidArgumentError('invalid onError method')
94+
}
95+
96+
this.#handler.onResponseError?.(this.#controller, err)
97+
}
98+
}

lib/handler/wrap-handler.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
'use strict'
2+
3+
const { InvalidArgumentError } = require('../core/errors')
4+
5+
module.exports = class WrapHandler {
6+
#handler
7+
#statusCode = 0
8+
#statusMessage = ''
9+
#trailers = {}
10+
11+
constructor (handler) {
12+
this.#handler = handler
13+
}
14+
15+
static wrap (handler) {
16+
// TODO (fix): More checks...
17+
return handler.onRequestStart ? handler : new WrapHandler(handler)
18+
}
19+
20+
// Unwrap Interface
21+
22+
onConnect (abort, context) {
23+
return this.#handler.onConnect?.(abort, context)
24+
}
25+
26+
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
27+
return this.#handler.onHeaders?.(statusCode, rawHeaders, resume, statusMessage)
28+
}
29+
30+
onUpgrade (statusCode, rawHeaders, socket) {
31+
return this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
32+
}
33+
34+
onData (data) {
35+
return this.#handler.onData?.(data)
36+
}
37+
38+
onComplete (trailers) {
39+
return this.#handler.onComplete?.(trailers)
40+
}
41+
42+
onError (err) {
43+
if (!this.#handler.onError) {
44+
throw new InvalidArgumentError('invalid onError method')
45+
}
46+
47+
return this.#handler.onError?.(err)
48+
}
49+
50+
// Wrap Interface
51+
52+
onRequestStart (controller, context) {
53+
this.#handler.onConnect?.((reason) => controller.abort(reason), context)
54+
this.#statusCode = 0
55+
this.#statusMessage = ''
56+
this.#trailers = {}
57+
}
58+
59+
onRequestError (controller, error) {
60+
if (!this.#handler.onError) {
61+
throw new InvalidArgumentError('invalid onError method')
62+
}
63+
64+
this.#handler.onError?.(error)
65+
}
66+
67+
onRequestUpgrade (statusCode, headers, socket) {
68+
const rawHeaders = []
69+
for (const [key, val] of Object.entries(headers)) {
70+
// TODO (fix): What if val is Array
71+
rawHeaders.push(Buffer.from(key), Buffer.from(val))
72+
}
73+
74+
this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
75+
}
76+
77+
onResponseStart (controller, statusCode, statusMessage) {
78+
this.#statusCode = statusCode
79+
this.#statusMessage = statusMessage
80+
}
81+
82+
onResponseHeaders (controller, headers) {
83+
const rawHeaders = []
84+
for (const [key, val] of Object.entries(headers)) {
85+
// TODO (fix): What if val is Array
86+
rawHeaders.push(Buffer.from(key), Buffer.from(val))
87+
}
88+
89+
if (this.#handler.onHeaders?.(
90+
this.#statusCode,
91+
rawHeaders,
92+
() => controller.resume(),
93+
this.#statusMessage
94+
) === false) {
95+
controller.pause()
96+
}
97+
}
98+
99+
onResponseData (controller, data) {
100+
if (this.#handler.onData?.(data) === false) {
101+
controller.pause()
102+
}
103+
}
104+
105+
onResponseTrailer (controller, trailers) {
106+
this.#trailers = trailers
107+
}
108+
109+
onResponseEnd (controller) {
110+
const rawTrailers = []
111+
for (const [key, val] of Object.entries(this.#trailers)) {
112+
// TODO (fix): What if val is Array
113+
rawTrailers.push(Buffer.from(key), Buffer.from(val))
114+
}
115+
116+
this.#handler.onComplete?.(rawTrailers)
117+
}
118+
119+
onResponseError (controller, error) {
120+
if (!this.#handler.onError) {
121+
throw new InvalidArgumentError('invalid onError method')
122+
}
123+
124+
this.#handler.onError?.(error)
125+
}
126+
}

test/mock-agent.js

Lines changed: 0 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -142,89 +142,6 @@ describe('MockAgent - dispatch', () => {
142142
onError: () => {}
143143
}))
144144
})
145-
146-
test('should throw if handler is not valid on redirect', (t) => {
147-
t = tspl(t, { plan: 7 })
148-
149-
const baseUrl = 'http://localhost:9999'
150-
151-
const mockAgent = new MockAgent()
152-
after(() => mockAgent.close())
153-
154-
t.throws(() => mockAgent.dispatch({
155-
origin: baseUrl,
156-
path: '/foo',
157-
method: 'GET'
158-
}, {
159-
onError: 'INVALID'
160-
}), new InvalidArgumentError('invalid onError method'))
161-
162-
t.throws(() => mockAgent.dispatch({
163-
origin: baseUrl,
164-
path: '/foo',
165-
method: 'GET'
166-
}, {
167-
onError: (err) => { throw err },
168-
onConnect: 'INVALID'
169-
}), new InvalidArgumentError('invalid onConnect method'))
170-
171-
t.throws(() => mockAgent.dispatch({
172-
origin: baseUrl,
173-
path: '/foo',
174-
method: 'GET'
175-
}, {
176-
onError: (err) => { throw err },
177-
onConnect: () => {},
178-
onBodySent: 'INVALID'
179-
}), new InvalidArgumentError('invalid onBodySent method'))
180-
181-
t.throws(() => mockAgent.dispatch({
182-
origin: baseUrl,
183-
path: '/foo',
184-
method: 'CONNECT'
185-
}, {
186-
onError: (err) => { throw err },
187-
onConnect: () => {},
188-
onBodySent: () => {},
189-
onUpgrade: 'INVALID'
190-
}), new InvalidArgumentError('invalid onUpgrade method'))
191-
192-
t.throws(() => mockAgent.dispatch({
193-
origin: baseUrl,
194-
path: '/foo',
195-
method: 'GET'
196-
}, {
197-
onError: (err) => { throw err },
198-
onConnect: () => {},
199-
onBodySent: () => {},
200-
onHeaders: 'INVALID'
201-
}), new InvalidArgumentError('invalid onHeaders method'))
202-
203-
t.throws(() => mockAgent.dispatch({
204-
origin: baseUrl,
205-
path: '/foo',
206-
method: 'GET'
207-
}, {
208-
onError: (err) => { throw err },
209-
onConnect: () => {},
210-
onBodySent: () => {},
211-
onHeaders: () => {},
212-
onData: 'INVALID'
213-
}), new InvalidArgumentError('invalid onData method'))
214-
215-
t.throws(() => mockAgent.dispatch({
216-
origin: baseUrl,
217-
path: '/foo',
218-
method: 'GET'
219-
}, {
220-
onError: (err) => { throw err },
221-
onConnect: () => {},
222-
onBodySent: () => {},
223-
onHeaders: () => {},
224-
onData: () => {},
225-
onComplete: 'INVALID'
226-
}), new InvalidArgumentError('invalid onComplete method'))
227-
})
228145
})
229146

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

0 commit comments

Comments
 (0)