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 | $requestPromise,
|
12 | $requestRedirectCount,
|
13 | $responseEnded
|
14 | } = require('./symbols')
|
15 |
|
16 | function 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 |
|
28 | function emit (event, emitParameters, additionalParameters) {
|
29 | this.emit(event, { ...emitParameters, ...additionalParameters })
|
30 | }
|
31 |
|
32 | function emitError (reason) {
|
33 | try {
|
34 | emit.call(this.eventEmitter, 'error', this.emitParameters, { reason })
|
35 | } catch (e) {
|
36 | logError({ ...this.emitParameters, reason: e })
|
37 | }
|
38 | }
|
39 |
|
40 | function 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 |
|
54 | function 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 |
|
64 | this.response.end()
|
65 | redirected.call(this)
|
66 | } else {
|
67 | this.failed = true
|
68 | dispatch.call(this, statusCode)
|
69 | }
|
70 | }
|
71 |
|
72 | function 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 |
|
81 | function 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 |
|
106 | async 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 |
|
135 | module.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 | }
|