UNPKG

4.96 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 $requestInternal,
12 $responseEnded
13} = require('./symbols')
14
15function hookEnd (response) {
16 if (!response.end[$dispatcherEnd]) {
17 const end = response.end
18 response.end = function () {
19 this[$responseEnded] = true
20 return end.apply(this, arguments)
21 }
22 response.end[$dispatcherEnd] = true
23 }
24 return response
25}
26
27function emit (event, emitParameters, additionalParameters) {
28 this.emit(event, { ...emitParameters, ...additionalParameters })
29}
30
31function emitError (reason) {
32 try {
33 emit.call(this.eventEmitter, 'error', this.emitParameters, { reason })
34 } catch (e) {
35 logError({ ...this.emitParameters, reason: e }) // Unhandled error
36 }
37}
38
39function redirected () {
40 const end = new Date()
41 Object.assign(this.emitParameters, {
42 end,
43 timeSpent: end - this.emitParameters.start,
44 statusCode: this.response.statusCode
45 })
46 try {
47 emit.call(this.eventEmitter, 'redirected', this.emitParameters)
48 } catch (reason) {
49 emitError.call(this, reason)
50 }
51 this.redirected()
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.redirectCount
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 if (mapping['exclude-from-holding-list']) {
85 this.setAsNonHolding()
86 }
87 return handler.redirect({
88 configuration: this.configuration[$configurationInterface],
89 mapping,
90 match,
91 redirect,
92 request: this.request,
93 response: hookEnd(this.response)
94 })
95 .then(result => {
96 if (undefined !== result) {
97 redispatch.call(this, result)
98 } else if (this.response[$responseEnded]) {
99 redirected.call(this)
100 } else {
101 dispatch.call(this, url, index + 1)
102 }
103 }, error.bind(this))
104 } catch (e) {
105 error.call(this, e)
106 }
107}
108
109async function dispatch (url, index = 0) {
110 if (typeof url === 'number') {
111 return redirecting.call(this, {
112 type: 'status',
113 handler: this.configuration.handlers.status,
114 redirect: url
115 })
116 }
117 const length = this.configuration.mappings.length
118 while (index < length) {
119 const mapping = this.configuration.mappings[index]
120 let match
121 try {
122 match = await mapping[$mappingMatch](this.request, url)
123 } catch (reason) {
124 return error.call(this, reason)
125 }
126 if (match) {
127 if (['string', 'number'].includes(typeof match)) {
128 return redispatch.call(this, match)
129 }
130 const { handler, redirect, type } = this.configuration.handler(mapping)
131 return redirecting.call(this, { mapping, match, handler, type, redirect: interpolate(match, redirect), url, index })
132 }
133 ++index
134 }
135 error.call(this, 501)
136}
137
138module.exports = function (configuration, request, response) {
139 const configurationRequests = configuration[$configurationRequests]
140 const { contexts } = configurationRequests
141 const emitParameters = {
142 id: ++configurationRequests.lastId,
143 internal: !!request[$requestInternal],
144 method: request.method,
145 url: request.url,
146 start: new Date()
147 }
148 let dispatched
149 const dispatching = new Promise(resolve => { dispatched = resolve })
150 let release
151 const holding = new Promise(resolve => { release = resolve })
152 const context = {
153 configuration,
154 eventEmitter: this,
155 emitParameters,
156 holding,
157 redirectCount: 0,
158 redirected () {
159 this.setAsNonHolding()
160 dispatched()
161 },
162 request,
163 response,
164 setAsNonHolding () {
165 this.released = true
166 release()
167 }
168 }
169 request[$requestId] = emitParameters.id
170 request.on('aborted', emit.bind(this, 'aborted', emitParameters))
171 request.on('close', emit.bind(this, 'closed', emitParameters))
172 try {
173 emit.call(this, 'incoming', emitParameters)
174 } catch (reason) {
175 error.call(context, reason)
176 return dispatching
177 }
178 return configurationRequests.holding
179 .then(() => {
180 contexts.push(context)
181 dispatch.call(context, request.url)
182 return dispatching
183 })
184 .then(() => {
185 const index = contexts.findIndex(candidate => candidate === context)
186 contexts.splice(index, 1)
187 })
188}