1 | 'use strict'
|
2 |
|
3 | const logError = require('./logError')
|
4 | const interpolate = require('./interpolate')
|
5 | const {
|
6 | $configurationInterface,
|
7 | $configurationRequests,
|
8 | $dispatcherEnd,
|
9 | $mappingMatch,
|
10 | $requestId,
|
11 | $responseEnded
|
12 | } = require('./symbols')
|
13 |
|
14 | function 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 |
|
26 | function emit (event, emitParameters, additionalParameters) {
|
27 | this.emit(event, { ...emitParameters, ...additionalParameters })
|
28 | }
|
29 |
|
30 | function emitError (reason) {
|
31 | try {
|
32 | emit.call(this.eventEmitter, 'error', this.emitParameters, { reason })
|
33 | } catch (e) {
|
34 | logError({ ...this.emitParameters, reason: e })
|
35 | }
|
36 | }
|
37 |
|
38 | function 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 |
|
53 | function 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 |
|
63 | this.response.end()
|
64 | redirected.call(this)
|
65 | } else {
|
66 | this.failed = true
|
67 | dispatch.call(this, statusCode)
|
68 | }
|
69 | }
|
70 |
|
71 | function 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 |
|
80 | function 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 |
|
108 | async 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 |
|
137 | module.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 | }
|