UNPKG

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