@srfnstack/spliffy 1.2.9 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srfnstack/spliffy",
3
- "version": "1.2.9",
3
+ "version": "1.4.0",
4
4
  "author": "snowbldr",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/SRFNStack/spliffy",
@@ -22,7 +22,8 @@
22
22
  "scripts": {
23
23
  "test": "npm run lint:fix && jest",
24
24
  "lint": "standard --env jest src ./*js && standard --env jest --env browser --global Prism docs example",
25
- "lint:fix": "standard --env jest --fix src ./*js && standard --env jest --env browser --global Prism --fix docs example"
25
+ "lint:fix": "standard --env jest --fix src ./*js && standard --env jest --env browser --global Prism --fix docs example",
26
+ "bench": "node bench/compare.mjs"
26
27
  },
27
28
  "keywords": [
28
29
  "node",
@@ -39,6 +40,7 @@
39
40
  "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.56.0"
40
41
  },
41
42
  "devDependencies": {
43
+ "autocannon": "^8.0.0",
42
44
  "helmet": "^4.6.0",
43
45
  "jest": "^27.3.1",
44
46
  "node-fetch": "^2.6.7",
package/src/decorator.mjs CHANGED
@@ -1,213 +1,214 @@
1
1
  import cookie from 'cookie'
2
- import http from 'http'
3
2
  import { parseQuery } from './url.mjs'
4
- import log from './log.mjs'
5
3
  import { v4 as uuid } from 'uuid'
6
4
  import stream from 'stream'
7
- import httpStatusCodes, { defaultStatusMessages } from './httpStatusCodes.mjs'
5
+ import { defaultStatusMessages } from './httpStatusCodes.mjs'
8
6
 
9
7
  const { Writable } = stream
10
8
 
11
- const addressArrayBufferToString = addrBuf => String.fromCharCode.apply(null, new Int8Array(addrBuf))
12
- const excludedMessageProps = {
13
- setTimeout: true,
14
- _read: true,
15
- destroy: true,
16
- _addHeaderLines: true,
17
- _addHeaderLine: true,
18
- _dump: true,
19
- __proto__: true
20
- }
21
-
22
- const normalizeHeader = header => header.toLowerCase()
9
+ export class SpliffyRequest {
10
+ constructor (uwsReq, paramToIndex, res, config) {
11
+ this.init(uwsReq, paramToIndex, res, config)
12
+ }
23
13
 
24
- const reqProtoProps = () => Object.keys(http.IncomingMessage.prototype).filter(p => !excludedMessageProps[p])
14
+ init (uwsReq, paramToIndex, res, config, method, urlPath) {
15
+ this.res = res
16
+ this._uwsReq = uwsReq
17
+ this._config = config
18
+ this._paramToIndex = paramToIndex
19
+ this.path = urlPath || (uwsReq ? uwsReq.getUrl() : '')
20
+ this.method = method || (uwsReq ? uwsReq.getMethod().toUpperCase() : '')
21
+ this._url = null
22
+ this._headers = null
23
+ this._spliffyUrl = null
24
+ this._cookies = null
25
+ }
25
26
 
26
- export const setCookie = (res) => function () {
27
- return res.setHeader('Set-Cookie', [...(res.getHeader('Set-Cookie') || []), cookie.serialize(...arguments)])
28
- }
27
+ forceCache () {
28
+ if (this._headers === null) this._cacheHeaders()
29
+ if (this._spliffyUrl === null) this._cacheSpliffyUrl()
30
+ }
29
31
 
30
- export function decorateRequest (uwsReq, pathParameters, res, {
31
- decodeQueryParameters,
32
- decodePathParameters,
33
- parseCookie,
34
- extendIncomingMessage
35
- } = {}) {
36
- // uwsReq can't be used in async functions because it gets de-allocated when the handler function returns
37
- const req = {}
38
- if (extendIncomingMessage) {
39
- // frameworks like passport like to modify the message prototype
40
- // Setting the prototype of req is not desirable because the entire api of IncomingMessage is not supported
41
- for (const p of reqProtoProps()) {
42
- if (!req[p]) req[p] = http.IncomingMessage.prototype[p]
32
+ _cacheHeaders () {
33
+ if (this._headers === null) {
34
+ this._headers = {}
35
+ this._uwsReq.forEach((header, value) => { this._headers[header] = value })
43
36
  }
44
37
  }
45
- const query = uwsReq.getQuery()
46
- req.path = uwsReq.getUrl()
47
- req.url = `${req.path}${query ? '?' + query : ''}`
48
- const paramToIndex = pathParameters.reduce((acc, cur, i) => {
49
- acc[cur] = i
50
- return acc
51
- }, {})
52
- req.spliffyUrl = {
53
- path: req.path,
54
- query: (query && parseQuery(query, decodeQueryParameters)) || {},
55
- param: name => uwsReq.getParameter(paramToIndex[name])
56
- }
57
- req.query = req.spliffyUrl.query
58
- req.headers = {}
59
- uwsReq.forEach((header, value) => { req.headers[header] = value })
60
- req.method = uwsReq.getMethod().toUpperCase()
61
- req.remoteAddress = addressArrayBufferToString(res.getRemoteAddressAsText())
62
- req.proxiedRemoteAddress = addressArrayBufferToString(res.getProxiedRemoteAddressAsText())
63
- req.get = header => req.headers[header]
64
- if (parseCookie) {
65
- req.cookies = (req.headers.cookie && cookie.parse(req.headers.cookie)) || {}
66
- }
67
- return req
68
- }
69
-
70
- function toArrayBuffer (buffer) {
71
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
72
- }
73
38
 
74
- export function decorateResponse (res, req, finalizeResponse, errorTransformer, endError, { acceptsDefault }) {
75
- res.onAborted(() => {
76
- res.ended = true
77
- res.writableEnded = true
78
- res.finalized = true
79
- log.error(`Request to ${req.url} was aborted`)
80
- })
81
- res.acceptsDefault = acceptsDefault
82
- res.headers = {}
83
- res.headersSent = false
84
- res.setHeader = (header, value) => {
85
- res.headers[normalizeHeader(header)] = value
86
- }
87
- res.removeHeader = header => {
88
- delete res.headers[normalizeHeader(header)]
89
- }
90
- res.flushHeaders = () => {
91
- if (res.headersSent) return
92
- if (!res.statusCode) res.statusCode = httpStatusCodes.OK
93
- if (!res.statusMessage) res.statusMessage = defaultStatusMessages[res.statusCode]
94
- res.headersSent = true
95
- res.writeStatus(`${res.statusCode} ${res.statusMessage}`)
96
- if (typeof res.onFlushHeaders === 'function') {
97
- res.onFlushHeaders(res)
98
- }
99
- for (const header of Object.keys(res.headers)) {
100
- if (Array.isArray(res.headers[header])) {
101
- for (const multiple of res.headers[header]) {
102
- res.writeHeader(header, multiple.toString())
103
- }
104
- } else {
105
- res.writeHeader(header, res.headers[header].toString())
39
+ _cacheSpliffyUrl () {
40
+ if (this._spliffyUrl === null) {
41
+ const q = this._uwsReq.getQuery()
42
+ const p2i = this._paramToIndex
43
+ const params = {}
44
+ for (const name in p2i) {
45
+ params[name] = this._uwsReq.getParameter(p2i[name])
46
+ }
47
+ this._spliffyUrl = {
48
+ path: this.path,
49
+ query: (q && parseQuery(q, this._config.decodeQueryParameters)) || {},
50
+ param: name => params[name]
106
51
  }
107
52
  }
108
53
  }
109
- res.writeHead = (status, headers) => {
110
- res.statusCode = status
111
- res.assignHeaders(headers)
54
+
55
+ get url () {
56
+ if (this._url === null) {
57
+ const q = this._uwsReq.getQuery()
58
+ this._url = q ? `${this.path}?${q}` : this.path
59
+ }
60
+ return this._url
112
61
  }
113
- res.assignHeaders = headers => {
114
- for (const header of Object.keys(headers)) {
115
- res.headers[normalizeHeader(header)] = headers[header]
62
+
63
+ get headers () {
64
+ if (this._headers === null) this._cacheHeaders()
65
+ return this._headers
66
+ }
67
+
68
+ get spliffyUrl () {
69
+ if (this._spliffyUrl === null) this._cacheSpliffyUrl()
70
+ return this._spliffyUrl
71
+ }
72
+
73
+ get query () { return this.spliffyUrl.query }
74
+ get cookies () {
75
+ if (this._cookies === null) {
76
+ this._cookies = (this.headers.cookie && cookie.parse(this.headers.cookie)) || {}
116
77
  }
78
+ return this._cookies
117
79
  }
118
- res.getHeader = header => {
119
- return res.headers[normalizeHeader(header)]
80
+
81
+ get remoteAddress () {
82
+ const val = Buffer.from(this.res.getRemoteAddressAsText()).toString()
83
+ Object.defineProperty(this, 'remoteAddress', { value: val, enumerable: true })
84
+ return val
120
85
  }
121
- res.status = (code) => {
122
- res.statusCode = code
123
- return this
86
+
87
+ get proxiedRemoteAddress () {
88
+ const val = Buffer.from(this.res.getProxiedRemoteAddressAsText()).toString()
89
+ Object.defineProperty(this, 'proxiedRemoteAddress', { value: val, enumerable: true })
90
+ return val
124
91
  }
125
92
 
126
- res.uwsWrite = res.write
127
- res.write = (chunk, encoding, cb) => {
128
- res.cork(() => {
129
- try {
130
- res.streaming = true
131
- res.flushHeaders()
132
- let data
133
- if (chunk instanceof Buffer) {
134
- data = toArrayBuffer(chunk)
135
- } else if (typeof chunk === 'string') {
136
- data = toArrayBuffer(Buffer.from(chunk, encoding || 'utf8'))
137
- } else {
138
- data = toArrayBuffer(Buffer.from(JSON.stringify(chunk), encoding || 'utf8'))
139
- }
140
- const result = res.uwsWrite(data)
141
- if (typeof cb === 'function') {
142
- cb()
143
- }
144
- return result
145
- } catch (e) {
146
- if (typeof cb === 'function') {
147
- cb(e)
93
+ get (header) { return this.headers[header.toLowerCase()] }
94
+ }
95
+
96
+ export function decorateRequest (uwsReq, paramToIndex, res, config) {
97
+ return new SpliffyRequest(uwsReq, paramToIndex, res, config)
98
+ }
99
+
100
+ const resProto = {
101
+ setHeader (h, v) { this.headers[h.toLowerCase()] = v },
102
+ removeHeader (h) { delete this.headers[h.toLowerCase()] },
103
+ assignHeaders (headers) {
104
+ for (const h in headers) { this.headers[h.toLowerCase()] = headers[h] }
105
+ },
106
+ getHeader (h) { return this.headers[h.toLowerCase()] },
107
+ status (c) { this.statusCode = c; return this },
108
+ writeHead (s, h) { this.statusCode = s; if (h) this.assignHeaders(h) },
109
+ redirect (c, l) {
110
+ if (typeof c === 'string') { l = c; c = 301 }
111
+ return this._finalizeResponse(this._sReq, this, { statusCode: c, headers: { location: l } })
112
+ },
113
+ send (b) { return this._finalizeResponse(this._sReq, this, b) },
114
+ json (b) { return this._finalizeResponse(this._sReq, this, b) },
115
+ setCookie (n, v, o) {
116
+ const s = cookie.serialize(n, v, o)
117
+ const e = this.headers['set-cookie']
118
+ if (e) {
119
+ if (Array.isArray(e)) e.push(s)
120
+ else this.headers['set-cookie'] = [e, s]
121
+ } else this.headers['set-cookie'] = s
122
+ },
123
+ cookie (n, v, o) { return this.setCookie(n, v, o) },
124
+ flushHeaders (noCork) {
125
+ if (this.headersSent) return
126
+ this.headersSent = true
127
+ const flush = () => {
128
+ if (this.statusCode && this.statusCode !== 200) {
129
+ this.writeStatus(this.statusCode + ' ' + (defaultStatusMessages[this.statusCode] || ''))
130
+ }
131
+ const headers = this.headers
132
+ for (const h in headers) {
133
+ const v = headers[h]
134
+ if (Array.isArray(v)) {
135
+ for (let i = 0, l = v.length; i < l; i++) this.writeHeader(h, String(v[i]))
148
136
  } else {
149
- throw e
137
+ this.writeHeader(h, String(v))
150
138
  }
151
139
  }
140
+ }
141
+ if (noCork) flush()
142
+ else this.cork(flush)
143
+ },
144
+ ensureOnAborted () {
145
+ if (this._onAbortedRegistered) return
146
+ this._onAbortedRegistered = true
147
+ this.onAborted(() => { this.ended = true; this.writableEnded = true; this.finalized = true })
148
+ },
149
+ write (chunk, encoding, cb) {
150
+ this.ensureOnAborted()
151
+ this.streaming = true
152
+ return this.cork(() => {
153
+ if (!this.headersSent) this.flushHeaders(true)
154
+ const result = this._uwsWrite((chunk instanceof Buffer || chunk instanceof Uint8Array || typeof chunk === 'string') ? chunk : JSON.stringify(chunk))
155
+ if (cb) cb()
156
+ return result
152
157
  })
153
- }
154
- let outStream
155
- res.getWritable = () => {
156
- if (!outStream) {
157
- res.streaming = true
158
- outStream = new Writable({
159
- write: res.write
160
- })
161
- .on('finish', res.end)
162
- .on('end', res.end)
163
- .on('error', e => {
164
- try {
165
- outStream.destroy()
166
- } finally {
167
- endError(res, e, uuid(), errorTransformer)
168
- }
169
- })
158
+ },
159
+ end (body) {
160
+ if (this.ended) return
161
+ this.ended = true
162
+ this.writableEnded = true
163
+ this.cork(() => {
164
+ if (!this.headersSent) this.flushHeaders(true)
165
+ this._uwsEnd(body || '')
166
+ })
167
+ if (this.onEnd) this.onEnd()
168
+ },
169
+ getWritable () {
170
+ this.ensureOnAborted()
171
+ if (!this.outStream) {
172
+ this.streaming = true
173
+ this.outStream = new Writable({ write: (c, e, callback) => { this.write(c, e, callback) } })
174
+ .on('finish', () => this.end())
175
+ .on('error', e => { try { this.outStream.destroy() } finally { this._endError(this, e, uuid(), this._errorTransformer) } })
170
176
  }
171
- return outStream
177
+ return this.outStream
172
178
  }
179
+ }
180
+
181
+ export function decorateResponse (res, sReq, finalizeResponse, errorTransformer, endError, config) {
182
+ res._sReq = sReq
183
+ res._finalizeResponse = finalizeResponse
184
+ res._errorTransformer = errorTransformer
185
+ res._endError = endError
186
+ res._onAbortedRegistered = false
187
+ res.ensureOnAborted = resProto.ensureOnAborted
188
+ res.acceptsDefault = config.acceptsDefault
189
+ res.headers = {}
190
+ res.headersSent = false
191
+ res.setHeader = resProto.setHeader
192
+ res.removeHeader = resProto.removeHeader
193
+ res.assignHeaders = resProto.assignHeaders
194
+ res.getHeader = resProto.getHeader
195
+ res.status = resProto.status
196
+ res.writeHead = resProto.writeHead
197
+ res.flushHeaders = resProto.flushHeaders
198
+ res.redirect = resProto.redirect
199
+ res.send = resProto.send
200
+ res.json = resProto.json
201
+ res.setCookie = resProto.setCookie
202
+ res.cookie = resProto.cookie
203
+
204
+ res._uwsWrite = res.write
205
+ res.write = resProto.write
206
+
207
+ res.getWritable = resProto.getWritable
173
208
 
174
- const uwsEnd = res.end
209
+ res._uwsEnd = res.end
175
210
  res.ended = false
176
- res.end = body => {
177
- if (res.ended) {
178
- return
179
- }
180
- // provide writableEnded like node does, with slightly different behavior
181
- if (!res.writableEnded) {
182
- res.cork(() => {
183
- res.flushHeaders()
184
- uwsEnd.call(res, body)
185
- res.writableEnded = true
186
- res.ended = true
187
- })
188
- }
189
- if (typeof res.onEnd === 'function') {
190
- res.onEnd()
191
- }
192
- }
211
+ res.end = resProto.end
193
212
 
194
- res.redirect = function (code, location) {
195
- if (arguments.length === 1) {
196
- location = code
197
- code = httpStatusCodes.MOVED_PERMANENTLY
198
- }
199
- return finalizeResponse(req, res, {
200
- statusCode: code,
201
- headers: {
202
- location: location
203
- }
204
- })
205
- }
206
- res.send = (body) => {
207
- finalizeResponse(req, res, body)
208
- }
209
- res.json = res.send
210
- res.setCookie = setCookie(res)
211
- res.cookie = res.setCookie
212
213
  return res
213
214
  }
package/src/handler.mjs CHANGED
@@ -1,40 +1,29 @@
1
1
  import log from './log.mjs'
2
2
  import { deserializeBody, serializeBody } from './content.mjs'
3
- import { invokeMiddleware } from './middleware.mjs'
4
- import { decorateResponse, decorateRequest } from './decorator.mjs'
3
+ import { invokeMiddleware, preProcessMiddleware } from './middleware.mjs'
4
+ import { decorateResponse, SpliffyRequest } from './decorator.mjs'
5
5
  import { v4 as uuid } from 'uuid'
6
6
  import stream from 'stream'
7
7
  const { Readable } = stream
8
8
 
9
- /**
10
- * Execute the handler
11
- * @param url The url being requested
12
- * @param res The uws response object
13
- * @param req The uws request object
14
- * @param bodyPromise The request body promise
15
- * @param handler The handler function for the route
16
- * @param middleware The middleware that applies to this request
17
- * @param errorTransformer An errorTransformer to convert error objects into response data
18
- */
19
- const executeHandler = async (url, res, req, bodyPromise, handler, middleware, errorTransformer) => {
20
- try {
21
- bodyPromise = bodyPromise.then(bodyContent => {
22
- if (bodyContent instanceof Readable) return bodyContent
23
- if (res.writableEnded) return
24
- return deserializeBody(bodyContent, req.headers['content-type'], res.acceptsDefault)
25
- })
26
- } catch (e) {
27
- log.error('Failed to parse request.', e)
28
- end(res, 400, handler.statusCodeOverride)
29
- return
30
- }
9
+ const NULL_PROMISE = Promise.resolve(null)
31
10
 
11
+ const executeHandler = async (url, res, req, bodyPromise, handler, middleware, errorTransformer) => {
32
12
  try {
33
- const handled = await handler({ url, bodyPromise, headers: req.headers, req, res })
34
- finalizeResponse(req, res, handled, handler.statusCodeOverride)
13
+ const bodyContent = await (bodyPromise || NULL_PROMISE)
14
+ if (!res.writableEnded) {
15
+ const deserializedBody = (bodyContent instanceof Readable || !bodyContent)
16
+ ? bodyContent
17
+ : deserializeBody(bodyContent, req.headers['content-type'], res.acceptsDefault)
18
+
19
+ const handled = await handler({ url, bodyPromise: Promise.resolve(deserializedBody), headers: req.headers, req, res })
20
+ finalizeResponse(req, res, handled, handler.statusCodeOverride)
21
+ }
35
22
  } catch (e) {
36
23
  const refId = uuid()
37
- await executeMiddleware(middleware, req, res, errorTransformer, refId, e)
24
+ if (middleware) {
25
+ try { await executeMiddleware(middleware, req, res, errorTransformer, refId, e) } catch (me) { log.error(me) }
26
+ }
38
27
  endError(res, e, refId, errorTransformer)
39
28
  }
40
29
  }
@@ -48,199 +37,202 @@ const endError = (res, e, refId, errorTransformer) => {
48
37
  }
49
38
  res.headers['x-ref-id'] = refId
50
39
  const status = e.statusCode || 500
51
- if (status === 500) {
52
- log.error(e)
53
- }
40
+ if (status === 500) log.error(e)
54
41
  end(res, status, null, e.body || '')
55
42
  }
56
43
 
57
44
  const end = (res, defaultStatusCode, statusCodeOverride, body) => {
58
- // status set directly on res wins
59
45
  res.statusCode = statusCodeOverride || res.statusCode || defaultStatusCode
46
+ if (currentDate) res.setHeader('Date', currentDate)
60
47
  if (body instanceof Readable || res.streaming) {
61
48
  res.streaming = true
62
- if (body instanceof Readable) {
63
- pipeResponse(res, body)
64
- }
65
- // handler is responsible for ending the response if they are streaming
49
+ if (body instanceof Readable) pipeResponse(res, body)
66
50
  } else {
67
- res.end(doSerializeBody(body, res) || '')
68
- }
69
- }
70
-
71
- const ipv6CompressRegex = /\b:?(?:0+:?){2,}/g
72
-
73
- const compressIpv6 = ip => ip && ip.includes(':') ? ip.replaceAll(ipv6CompressRegex, '::') : ip
74
-
75
- const writeAccess = function (req, res) {
76
- const start = new Date().getTime()
77
- return () => {
78
- log.access(compressIpv6(req.remoteAddress), compressIpv6(res.proxiedRemoteAddress) || '', res.statusCode, req.method, req.url, new Date().getTime() - start + 'ms')
51
+ if (typeof body === 'string' || !body || body instanceof Buffer) {
52
+ res.end(body || '')
53
+ } else {
54
+ res.end(doSerializeBody(body, res) || '')
55
+ }
79
56
  }
80
57
  }
81
58
 
82
59
  const finalizeResponse = (req, res, handled, statusCodeOverride) => {
83
- if (!res.finalized) {
84
- if (!handled) {
85
- // if no error was thrown, assume everything is fine. Otherwise each handler must return truthy which is un-necessary for methods that don't need to return anything
86
- end(res, 200, statusCodeOverride)
87
- } else {
88
- // if the returned object has known fields, treat it as a response object instead of the body
89
- if (handled.body || handled.statusMessage || handled.statusCode || handled.headers) {
90
- if (handled.headers) {
91
- res.assignHeaders(handled.headers)
92
- }
93
- res.statusMessage = handled.statusMessage || res.statusMessage
94
- end(res, handled.statusCode || 200, statusCodeOverride, handled.body)
95
- } else {
96
- end(res, 200, statusCodeOverride, handled)
97
- }
98
- }
99
- res.finalized = true
60
+ if (res.finalized) return
61
+ res.finalized = true
62
+
63
+ if (typeof handled === 'string' || !handled || handled instanceof Buffer) {
64
+ end(res, 200, statusCodeOverride, handled)
65
+ } else if (handled.body || handled.statusMessage || handled.statusCode || handled.headers) {
66
+ if (handled.headers) res.assignHeaders(handled.headers)
67
+ if (handled.statusMessage) res.statusMessage = handled.statusMessage
68
+ end(res, handled.statusCode || 200, statusCodeOverride, handled.body)
69
+ } else {
70
+ end(res, 200, statusCodeOverride, handled)
100
71
  }
101
72
  }
102
73
 
103
74
  const pipeResponse = (res, readStream, errorTransformer) => {
104
- readStream.on('data', res.write)
105
- .on('end', res.end)
75
+ readStream.on('data', chunk => res.write(chunk))
76
+ .on('end', () => res.end())
106
77
  .on('error', e => {
107
- try {
108
- readStream.destroy()
109
- } finally {
110
- endError(res, e, uuid(), errorTransformer)
111
- }
78
+ try { readStream.destroy() } finally { endError(res, e, uuid(), errorTransformer) }
112
79
  })
113
80
  }
114
81
 
115
82
  const doSerializeBody = (body, res) => {
116
- if (!body || typeof body === 'string' || body instanceof Readable) {
117
- return body
118
- }
119
- const contentType = res.getHeader('content-type')
83
+ const contentType = res.headers['content-type']
120
84
  const serialized = serializeBody(body, contentType, res.acceptsDefault)
121
-
122
85
  if (serialized?.contentType && !contentType) {
123
86
  res.headers['content-type'] = serialized.contentType
124
87
  }
125
88
  return serialized?.data || ''
126
89
  }
127
90
 
128
- async function executeMiddleware (middleware, req, res, errorTransformer, refId, e) {
129
- if (!middleware) return
130
-
131
- let applicableMiddleware = middleware[req.method]
132
- if (middleware.ALL) {
133
- if (applicableMiddleware) applicableMiddleware = middleware.ALL.concat(applicableMiddleware)
134
- else applicableMiddleware = middleware.ALL
135
- }
136
-
137
- if (!applicableMiddleware || applicableMiddleware.length === 0) {
138
- return
139
- }
91
+ async function executeMiddleware (processedMiddleware, req, res, errorTransformer, refId, e) {
92
+ if (!processedMiddleware) return
93
+ const methodMiddleware = processedMiddleware[req.method]
94
+ const allMiddleware = processedMiddleware.ALL
140
95
  if (e) {
141
- await invokeMiddleware(applicableMiddleware.filter(mw => mw.length === 4), req, res, e)
96
+ if (allMiddleware?.error) await invokeMiddleware(allMiddleware.error, req, res, e)
97
+ if (methodMiddleware?.error) await invokeMiddleware(methodMiddleware.error, req, res, e)
142
98
  } else {
143
- await invokeMiddleware(applicableMiddleware.filter(mw => mw.length === 3), req, res)
99
+ if (allMiddleware?.normal) await invokeMiddleware(allMiddleware.normal, req, res)
100
+ if (methodMiddleware?.normal) await invokeMiddleware(methodMiddleware.normal, req, res)
144
101
  }
145
102
  }
146
103
 
147
- const handleRequest = async (req, res, handler, middleware, errorTransformer) => {
104
+ export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE', 'WEBSOCKET']
105
+
106
+ let currentDate = ''
107
+ let dateInterval = null
108
+
109
+ const handleRequest = async (sReq, res, handler, processedMiddleware, config) => {
148
110
  try {
149
- let reqBody
150
- if (!handler.streamRequestBody) {
151
- let buffer
152
- reqBody = new Promise(
153
- resolve =>
154
- res.onData(async (data, isLast) => {
155
- if (isLast) {
156
- buffer = data.byteLength > 0 ? Buffer.concat([buffer, Buffer.from(data)].filter(b => b)) : buffer
157
- resolve(buffer)
158
- }
159
- buffer = Buffer.concat([buffer, Buffer.from(data)].filter(b => b))
160
- })
161
- )
162
- } else {
163
- const readable = new Readable({
164
- read: () => {
165
- }
166
- })
167
- res.onData(async (data, isLast) => {
168
- if (data.byteLength === 0 && !isLast) return
169
- // data must be copied so it isn't lost
170
- readable.push(Buffer.concat([Buffer.from(data)]))
171
- if (isLast) {
172
- readable.push(null)
173
- }
174
- })
175
- reqBody = Promise.resolve(readable)
111
+ let reqBodyPromise = NULL_PROMISE
112
+ if (sReq.method !== 'GET' && sReq.method !== 'HEAD') {
113
+ if (handler.streamRequestBody) {
114
+ const readable = new Readable({ read () {} })
115
+ reqBodyPromise = Promise.resolve(readable)
116
+ res.onData((data, isLast) => {
117
+ if (data.byteLength > 0 || isLast) {
118
+ readable.push(Buffer.concat([Buffer.from(data)]))
119
+ if (isLast) readable.push(null)
120
+ }
121
+ })
122
+ } else {
123
+ reqBodyPromise = new Promise(resolve => res.onData((data, isLast) => {
124
+ const chunk = Buffer.concat([Buffer.from(data)])
125
+ res._buffer = res._buffer ? Buffer.concat([res._buffer, chunk]) : chunk
126
+ if (isLast) resolve(res._buffer)
127
+ }))
128
+ }
176
129
  }
177
- await executeMiddleware(middleware, req, res, errorTransformer)
178
- if (!res.writableEnded && !res.ended) {
179
- await executeHandler(req.spliffyUrl, res, req, reqBody, handler, middleware, errorTransformer)
130
+
131
+ if (processedMiddleware) {
132
+ await executeMiddleware(processedMiddleware, sReq, res, config.errorTransformer)
133
+ if (!res.writableEnded && !res.ended) {
134
+ await executeHandler(sReq.spliffyUrl, res, sReq, reqBodyPromise, handler, processedMiddleware, config.errorTransformer)
135
+ }
136
+ } else {
137
+ await executeHandler(sReq.spliffyUrl, res, sReq, reqBodyPromise, handler, null, config.errorTransformer)
180
138
  }
181
139
  } catch (e) {
182
140
  const refId = uuid()
183
- await executeMiddleware(middleware, req, res, errorTransformer, refId, e)
184
- if (!res.writableEnded) { endError(res, e, refId, errorTransformer) }
141
+ if (processedMiddleware) {
142
+ await executeMiddleware(processedMiddleware, sReq, res, config.errorTransformer, refId, e)
143
+ }
144
+ if (!res.writableEnded) { endError(res, e, refId, config.errorTransformer) }
185
145
  }
186
146
  }
187
147
 
188
- export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE', 'WEBSOCKET']
148
+ export const createHandler = (handler, middleware, paramToIndex, config, urlPath) => {
149
+ const processedMiddleware = preProcessMiddleware(middleware)
150
+ if (config.writeDateHeader && !dateInterval) {
151
+ currentDate = new Date().toUTCString()
152
+ dateInterval = setInterval(() => { currentDate = new Date().toUTCString() }, 1000)
153
+ }
189
154
 
190
- let currentDate = new Date().toISOString()
191
- setInterval(() => { currentDate = new Date().toISOString() }, 1000)
155
+ const isWildcardPath = urlPath?.indexOf('*') > -1
192
156
 
193
- export const createHandler = (handler, middleware, pathParameters, config) => function (res, req) {
194
- try {
195
- res.cork(() => {
196
- req = decorateRequest(req, pathParameters, res, config)
197
- res = decorateResponse(res, req, finalizeResponse, config.errorTransformer, endError, config)
157
+ // Pre-allocated request object for sync path
158
+ const syncSReq = new SpliffyRequest(null, paramToIndex, null, config)
159
+ const syncContext = {
160
+ get url () { return syncSReq.spliffyUrl },
161
+ bodyPromise: NULL_PROMISE,
162
+ get headers () { return syncSReq.headers },
163
+ req: syncSReq,
164
+ res: null
165
+ }
198
166
 
199
- if (config.logAccess) {
200
- res.onEnd = writeAccess(req, res)
167
+ return function (res, req) {
168
+ try {
169
+ const rawMethod = req.getMethod()
170
+ if (!processedMiddleware && !handler.streamRequestBody && (rawMethod === 'get' || rawMethod === 'head')) {
171
+ // HYPER FAST PATH
172
+ const method = rawMethod === 'get' ? 'GET' : 'HEAD'
173
+ syncSReq.init(req, paramToIndex, res, config, method, isWildcardPath ? null : urlPath)
174
+ syncContext.res = res
175
+ decorateResponse(res, syncSReq, finalizeResponse, config.errorTransformer, endError, config)
176
+ const handled = handler(syncContext)
177
+ if (handled instanceof Promise) {
178
+ syncSReq.forceCache()
179
+ res.ensureOnAborted()
180
+ handled.then(h => finalizeResponse(syncSReq, res, h, handler.statusCodeOverride))
181
+ .catch(e => endError(res, e, uuid(), config.errorTransformer))
182
+ } else if (typeof handled === 'string' || handled instanceof Buffer) {
183
+ end(res, 200, handler.statusCodeOverride, handled)
184
+ } else {
185
+ finalizeResponse(syncSReq, res, handled, handler.statusCodeOverride)
186
+ }
187
+ } else {
188
+ const sReq = new SpliffyRequest(req, paramToIndex, res, config)
189
+ sReq.forceCache()
190
+ decorateResponse(res, sReq, finalizeResponse, config.errorTransformer, endError, config)
191
+ res.ensureOnAborted()
192
+
193
+ handleRequest(sReq, res, handler, processedMiddleware, config)
194
+ .catch(e => {
195
+ log.error('Failed handling request', e)
196
+ if (!res.writableEnded) {
197
+ res.statusCode = 500
198
+ res.end()
199
+ }
200
+ })
201
201
  }
202
-
203
- if (config.writeDateHeader) {
204
- res.headers.date = currentDate
202
+ } catch (e) {
203
+ log.error('Failed handling request', e)
204
+ if (!res.writableEnded) {
205
+ res.statusCode = 500
206
+ res.end()
205
207
  }
206
-
207
- handleRequest(req, res, handler, middleware, config.errorTransformer)
208
- .catch(e => {
209
- log.error('Failed handling request', e)
210
- res.statusCode = 500
211
- res.end()
212
- })
213
- })
214
- } catch (e) {
215
- log.error('Failed handling request', e)
216
- res.statusCode = 500
217
- res.end()
208
+ }
218
209
  }
219
210
  }
220
211
 
221
212
  export const createNotFoundHandler = config => {
222
213
  const handler = config.defaultRouteHandler || config.notFoundRouteHandler
223
- const params = handler?.pathParameters || []
214
+ const paramToIndex = handler?.paramToIndex || {}
224
215
  return (res, req) => {
225
216
  try {
226
- req = decorateRequest(req, params, res, config)
227
- res = decorateResponse(res, req, finalizeResponse, config.errorTransformer, endError, config)
228
- if (config.logAccess) {
229
- res.onEnd = writeAccess(req, res)
230
- }
217
+ const sReq = new SpliffyRequest(req, paramToIndex, res, config)
218
+ sReq.forceCache()
219
+ decorateResponse(res, sReq, finalizeResponse, config.errorTransformer, endError, config)
220
+ res.ensureOnAborted()
231
221
  if (handler && typeof handler === 'object') {
232
- if (handler.handlers && typeof handler.handlers[req.method] === 'function') {
222
+ const processedMiddleware = preProcessMiddleware(handler.middleware)
223
+ if (handler.handlers && typeof handler.handlers[sReq.method] === 'function') {
224
+ const h = handler.handlers[sReq.method]
233
225
  if ('statusCodeOverride' in handler) {
234
- handler.handlers[req.method].statusCodeOverride = handler.statusCodeOverride
226
+ h.statusCodeOverride = handler.statusCodeOverride
235
227
  }
236
- handleRequest(req, res,
237
- handler.handlers[req.method],
238
- handler.middleware,
239
- config.errorTransformer
240
- ).catch((e) => {
241
- log.error('Unexpected exception during request handling', e)
242
- res.statusCode = 500
243
- })
228
+ handleRequest(sReq, res, h, processedMiddleware, config)
229
+ .catch(e => {
230
+ log.error('Unexpected exception during request handling', e)
231
+ if (!res.writableEnded) {
232
+ res.statusCode = 500
233
+ res.end()
234
+ }
235
+ })
244
236
  } else {
245
237
  res.statusCode = 404
246
238
  res.end()
@@ -61,26 +61,39 @@ export const validateMiddlewareArray = (arr) => {
61
61
  }
62
62
  }
63
63
 
64
+ export function preProcessMiddleware (middleware) {
65
+ if (!middleware) return null
66
+ const processed = {}
67
+ let hasAny = false
68
+ for (const method in middleware) {
69
+ const list = middleware[method]
70
+ if (list && list.length > 0) {
71
+ processed[method] = {
72
+ normal: list.filter(mw => mw.length <= 3),
73
+ error: list.filter(mw => mw.length === 4)
74
+ }
75
+ hasAny = true
76
+ }
77
+ }
78
+ return hasAny ? processed : null
79
+ }
80
+
64
81
  export async function invokeMiddleware (middleware, req, res, reqErr) {
65
- await new Promise((resolve, reject) => {
66
- let current = -1
82
+ if (!middleware || middleware.length === 0) return
83
+ return new Promise((resolve, reject) => {
84
+ let current = 0
67
85
  const next = (err) => {
68
- if (err) reject(err)
69
- if (res.writableEnded) {
70
- resolve()
71
- return
86
+ if (err) return reject(err)
87
+ if (res.writableEnded || current === middleware.length) {
88
+ return resolve()
72
89
  }
73
- current++
74
- if (current === middleware.length) {
75
- resolve()
76
- } else {
77
- try {
78
- if (reqErr) middleware[current](reqErr, req, res, next)
79
- else middleware[current](req, res, next)
80
- } catch (e) {
81
- log.error('Middleware threw exception', e)
82
- reject(e)
83
- }
90
+ const mw = middleware[current++]
91
+ try {
92
+ if (reqErr) mw(reqErr, req, res, next)
93
+ else mw(req, res, next)
94
+ } catch (e) {
95
+ log.error('Middleware threw exception', e)
96
+ reject(e)
84
97
  }
85
98
  }
86
99
 
package/src/routes.mjs CHANGED
@@ -113,6 +113,10 @@ const buildJSHandlerRoute = async (name, filePath, urlPath, inheritedMiddleware,
113
113
  }
114
114
  const route = {
115
115
  pathParameters,
116
+ paramToIndex: pathParameters.reduce((acc, cur, i) => {
117
+ acc[cur] = i
118
+ return acc
119
+ }, {}),
116
120
  urlPath: `${urlPath}/${getPathPart(name)}`,
117
121
  filePath,
118
122
  handlers: {}
@@ -163,6 +167,10 @@ const buildStaticRoutes = (name, filePath, urlPath, inheritedMiddleware, pathPar
163
167
  const contentType = getContentTypeByExtension(name, config.staticContentTypes)
164
168
  const route = {
165
169
  pathParameters,
170
+ paramToIndex: pathParameters.reduce((acc, cur, i) => {
171
+ acc[cur] = i
172
+ return acc
173
+ }, {}),
166
174
  urlPath: `${urlPath}/${getPathPart(name)}`,
167
175
  filePath,
168
176
  handlers: createStaticHandler(filePath, contentType, config.cacheStatic, config.staticCacheControl),
package/src/server.mjs CHANGED
@@ -102,7 +102,7 @@ export async function startServer (config) {
102
102
  if (method === 'WEBSOCKET') {
103
103
  theHandler = route.handlers[method]
104
104
  } else {
105
- theHandler = createHandler(route.handlers[method], route.middleware, route.pathParameters, config)
105
+ theHandler = createHandler(route.handlers[method], route.middleware, route.paramToIndex, config, route.urlPath)
106
106
  }
107
107
  app[appMethods[method]](route.urlPath, theHandler)
108
108
  if (hadSlash && config.serveRoutesWithSlash) {