UNPKG

4.66 kBJavaScriptView Raw
1'use strict'
2
3const logError = require('./logError')
4const interpolate = require('./interpolate')
5const {
6 $configurationInterface,
7 $configurationRequests,
8 $dispatcherEnd,
9 $mappingMatch,
10 $requestId,
11 $requestPromise,
12 $requestRedirectCount,
13 $responseEnded
14} = require('./symbols')
15
16function hookEnd (response) {
17 if (!response.end[$dispatcherEnd]) {
18 const end = response.end
19 response.end = function () {
20 this[$responseEnded] = true
21 return end.apply(this, arguments)
22 }
23 response.end[$dispatcherEnd] = true
24 }
25 return response
26}
27
28function emit (event, emitParameters, additionalParameters) {
29 this.emit(event, { ...emitParameters, ...additionalParameters })
30}
31
32function emitError (reason) {
33 try {
34 emit.call(this.eventEmitter, 'error', this.emitParameters, { reason })
35 } catch (e) {
36 logError({ ...this.emitParameters, reason: e }) // Unhandled error
37 }
38}
39
40function redirected () {
41 const end = new Date()
42 try {
43 emit.call(this.eventEmitter, 'redirected', this.emitParameters, {
44 end,
45 timeSpent: end - this.emitParameters.start,
46 statusCode: this.response.statusCode
47 })
48 } catch (reason) {
49 emitError.call(this, reason)
50 }
51 this.resolve()
52}
53
54function error (reason) {
55 let statusCode
56 if (typeof reason === 'number') {
57 statusCode = reason
58 } else {
59 statusCode = 500
60 }
61 emitError.call(this, reason)
62 if (this.failed) {
63 // Error during error: finalize the response (whatever it means)
64 this.response.end()
65 redirected.call(this)
66 } else {
67 this.failed = true
68 dispatch.call(this, statusCode)
69 }
70}
71
72function redispatch (url) {
73 const redirectCount = ++this.request[$requestRedirectCount]
74 if (redirectCount > this.configuration['max-redirect']) {
75 error.call(this, 508)
76 } else {
77 dispatch.call(this, url)
78 }
79}
80
81function redirecting ({ mapping, match, handler, type, redirect, url, index = 0 }) {
82 try {
83 emit.call(this.eventEmitter, 'redirecting', this.emitParameters, { type, redirect })
84 return handler.redirect({
85 configuration: this.configuration[$configurationInterface],
86 mapping,
87 match,
88 redirect,
89 request: this.request,
90 response: hookEnd(this.response)
91 })
92 .then(result => {
93 if (undefined !== result) {
94 redispatch.call(this, result)
95 } else if (this.response[$responseEnded]) {
96 redirected.call(this)
97 } else {
98 dispatch.call(this, url, index + 1)
99 }
100 }, error.bind(this))
101 } catch (e) {
102 error.call(this, e)
103 }
104}
105
106async function dispatch (url, index = 0) {
107 if (typeof url === 'number') {
108 return redirecting.call(this, {
109 type: 'status',
110 handler: this.configuration.handlers.status,
111 redirect: url
112 })
113 }
114 const length = this.configuration.mappings.length
115 while (index < length) {
116 const mapping = this.configuration.mappings[index]
117 let match
118 try {
119 match = await mapping[$mappingMatch](this.request, url)
120 } catch (reason) {
121 return error.call(this, reason)
122 }
123 if (match) {
124 if (['string', 'number'].includes(typeof match)) {
125 return redispatch.call(this, match)
126 }
127 const { handler, redirect, type } = this.configuration.handler(mapping)
128 return redirecting.call(this, { mapping, match, handler, type, redirect: interpolate(match, redirect), url, index })
129 }
130 ++index
131 }
132 error.call(this, 501)
133}
134
135module.exports = function (configuration, request, response) {
136 const configurationRequests = configuration[$configurationRequests]
137 const emitParameters = { id: ++configurationRequests.lastId, method: request.method, url: request.url, start: new Date() }
138 let promiseResolver
139 const requestPromise = new Promise(resolve => { promiseResolver = resolve })
140 const context = { eventEmitter: this, emitParameters, configuration, request, response, resolve: promiseResolver }
141 request[$requestId] = emitParameters.id
142 request[$requestPromise] = requestPromise
143 request[$requestRedirectCount] = 0
144 request.on('aborted', emit.bind(this, 'aborted', emitParameters))
145 request.on('close', emit.bind(this, 'closed', emitParameters))
146 try {
147 emit.call(this, 'incoming', emitParameters)
148 } catch (reason) {
149 error.call(context, reason)
150 return requestPromise
151 }
152 return configurationRequests.hold
153 .then(() => {
154 configurationRequests.promises.push(requestPromise)
155 dispatch.call(context, request.url)
156 return requestPromise
157 })
158 .then(() => {
159 configurationRequests.promises = configurationRequests.promises
160 .filter(promise => promise !== requestPromise)
161 })
162}