velocious 1.0.55 → 1.0.57

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
@@ -3,7 +3,7 @@
3
3
  "velocious": "bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.55",
6
+ "version": "1.0.57",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "test": "VELOCIOUS_TEST_DIR=../ cd spec/dummy && npx velocious test",
@@ -22,6 +22,7 @@
22
22
  "homepage": "https://github.com/kaspernj/velocious#readme",
23
23
  "description": "",
24
24
  "dependencies": {
25
+ "async-mutex": "^0.5.0",
25
26
  "awaitery": "^1.0.1",
26
27
  "bcryptjs": "^3.0.2",
27
28
  "better-localstorage": "^1.0.7",
@@ -57,13 +57,6 @@ export default class Dummy {
57
57
 
58
58
  this.application = new Application({
59
59
  configuration: dummyConfiguration,
60
- databases: {
61
- default: {
62
- host: "mysql",
63
- username: "user",
64
- password: ""
65
- }
66
- },
67
60
  httpServer: {
68
61
  maxWorkers: 1,
69
62
  port: 3006
@@ -1,3 +1,4 @@
1
+ import "../../../../src/utils/with-tracked-stack-async-hooks.js"
1
2
  import AsyncTrackedMultiConnection from "../../../../src/database/pool/async-tracked-multi-connection.js"
2
3
  import Configuration from "../../../../src/configuration.js"
3
4
  import dummyDirectory from "../../dummy-directory.js"
@@ -1,3 +1,4 @@
1
+ import "../../../../src/utils/with-tracked-stack-async-hooks.js"
1
2
  import AsyncTrackedMultiConnection from "../../../../src/database/pool/async-tracked-multi-connection.js"
2
3
  import Configuration from "../../../../src/configuration.js"
3
4
  import dummyDirectory from "../../dummy-directory.js"
@@ -1,3 +1,4 @@
1
+ import "../../../../src/utils/with-tracked-stack-async-hooks.js"
1
2
  import AsyncTrackedMultiConnection from "../../../../src/database/pool/async-tracked-multi-connection.js"
2
3
  import Configuration from "../../../../src/configuration.js"
3
4
  import dummyDirectory from "../../dummy-directory.js"
@@ -1,3 +1,4 @@
1
+ import "../../../../src/utils/with-tracked-stack-async-hooks.js"
1
2
  import AsyncTrackedMultiConnection from "../../../../src/database/pool/async-tracked-multi-connection.js"
2
3
  import Configuration from "../../../../src/configuration.js"
3
4
  import dummyDirectory from "../../dummy-directory.js"
@@ -1,3 +1,4 @@
1
+ import "../../../../src/utils/with-tracked-stack-async-hooks.js"
1
2
  import AsyncTrackedMultiConnection from "../../../../src/database/pool/async-tracked-multi-connection.js"
2
3
  import Configuration from "../../../../src/configuration.js"
3
4
  import dummyDirectory from "../../dummy-directory.js"
@@ -1,5 +1,8 @@
1
1
  import fetch from "node-fetch"
2
2
  import Dummy from "../dummy/index.js"
3
+ import Header from "../../src/http-client/header.js"
4
+ import HttpClient from "../../src/http-client/index.js"
5
+ import {wait, waitFor} from "awaitery"
3
6
 
4
7
  describe("HttpServer - get", {databaseCleaning: {transaction: false, truncate: true}}, () => {
5
8
  it("handles get requests", async () => {
@@ -27,4 +30,57 @@ describe("HttpServer - get", {databaseCleaning: {transaction: false, truncate: t
27
30
  }
28
31
  })
29
32
  })
33
+
34
+ it("supports HTTP 1.0 close connection", async () => {
35
+ await Dummy.run(async () => {
36
+ await wait(200)
37
+
38
+ const httpClient = new HttpClient({
39
+ debug: false,
40
+ headers: [
41
+ new Header("Connection", "Close")
42
+ ],
43
+ version: "1.0"
44
+ })
45
+
46
+ await httpClient.connect()
47
+
48
+ const {response} = await httpClient.get("/ping")
49
+
50
+ expect(response.json()).toEqual({message: "Pong"})
51
+ expect(response.getHeader("Connection")?.value).toEqual("Close")
52
+
53
+ await waitFor(() => {
54
+ if (httpClient.isConnected()) throw new Error("HTTP client is still connected")
55
+ })
56
+ })
57
+ })
58
+
59
+ it("supports HTTP 1.0 keep-alive", async () => {
60
+ await Dummy.run(async () => {
61
+ await wait(200)
62
+
63
+ const httpClient = new HttpClient({
64
+ debug: false,
65
+ headers: [
66
+ new Header("Connection", "Keep-Alive")
67
+ ],
68
+ version: "1.0"
69
+ })
70
+
71
+ await httpClient.connect()
72
+
73
+ for (let i = 0; i < 5; i++) {
74
+ const {response} = await httpClient.get("/ping")
75
+
76
+ expect(response.json()).toEqual({message: "Pong"})
77
+ expect(response.getHeader("Connection")?.value).toEqual("Keep-Alive")
78
+ await wait(100)
79
+ expect(httpClient.isConnected()).toBeTrue()
80
+ }
81
+
82
+ await wait(500)
83
+ expect(httpClient.isConnected()).toBeTrue()
84
+ })
85
+ })
30
86
  })
@@ -38,7 +38,6 @@ export default class VelociousApplication {
38
38
 
39
39
  async startHttpServer() {
40
40
  const {configuration, httpServerConfiguration} = digs(this, "configuration", "httpServerConfiguration")
41
-
42
41
  const port = httpServerConfiguration.port || 3006
43
42
 
44
43
  await this.logger.debug(`Starting server on port ${port}`)
@@ -6,13 +6,16 @@ import UUID from "pure-uuid"
6
6
  import TableData from "../table-data/index.js"
7
7
  import TableColumn from "../table-data/table-column.js"
8
8
  import TableForeignKey from "../table-data/table-foreign-key.js"
9
+ import {Mutex} from "async-mutex"
9
10
 
10
11
  export default class VelociousDatabaseDriversBase {
11
12
  constructor(config, configuration) {
12
13
  this._args = config
13
14
  this.configuration = configuration
15
+ this.mutex = new Mutex() // Can be used to lock this instance for exclusive use
14
16
  this.logger = new Logger(this)
15
17
  this._transactionsCount = 0
18
+ this._transactionsActionsMutex = new Mutex()
16
19
  }
17
20
 
18
21
  async addForeignKey(tableName, columnName, referencedTableName, referencedColumnName, args) {
@@ -206,18 +209,36 @@ export default class VelociousDatabaseDriversBase {
206
209
  }
207
210
 
208
211
  async startTransaction() {
212
+ await this._transactionsActionsMutex.runExclusive(async () => {
213
+ await this._startTransactionAction()
214
+ this._transactionsCount++
215
+ })
216
+ }
217
+
218
+ async _startTransactionAction() {
209
219
  await this.query("BEGIN TRANSACTION")
210
- this._transactionsCount++
211
220
  }
212
221
 
213
222
  async commitTransaction() {
223
+ await this._transactionsActionsMutex.runExclusive(async () => {
224
+ await this._commitTransactionAction()
225
+ this._transactionsCount--
226
+ })
227
+ }
228
+
229
+ async _commitTransactionAction() {
214
230
  await this.query("COMMIT")
215
- this._transactionsCount--
216
231
  }
217
232
 
218
233
  async rollbackTransaction() {
234
+ await this._transactionsActionsMutex.runExclusive(async () => {
235
+ await this._rollbackTransactionAction()
236
+ this._transactionsCount--
237
+ })
238
+ }
239
+
240
+ async _rollbackTransactionAction() {
219
241
  await this.query("ROLLBACK")
220
- this._transactionsCount--
221
242
  }
222
243
 
223
244
  generateSavePointName() {
@@ -225,6 +246,12 @@ export default class VelociousDatabaseDriversBase {
225
246
  }
226
247
 
227
248
  async startSavePoint(savePointName) {
249
+ await this._transactionsActionsMutex.runExclusive(async () => {
250
+ await this._startSavePointAction(savePointName)
251
+ })
252
+ }
253
+
254
+ async _startSavePointAction(savePointName) {
228
255
  await this.query(`SAVEPOINT ${savePointName}`)
229
256
  }
230
257
 
@@ -245,10 +272,22 @@ export default class VelociousDatabaseDriversBase {
245
272
  }
246
273
 
247
274
  async releaseSavePoint(savePointName) {
275
+ await this._transactionsActionsMutex.runExclusive(async () => {
276
+ await this._releaseSavePointAction(savePointName)
277
+ })
278
+ }
279
+
280
+ async _releaseSavePointAction(savePointName) {
248
281
  await this.query(`RELEASE SAVEPOINT ${savePointName}`)
249
282
  }
250
283
 
251
284
  async rollbackSavePoint(savePointName) {
285
+ await this._transactionsActionsMutex.runExclusive(async () => {
286
+ await this._rollbackSavePointAction(savePointName)
287
+ })
288
+ }
289
+
290
+ async _rollbackSavePointAction(savePointName) {
252
291
  await this.query(`ROLLBACK TO SAVEPOINT ${savePointName}`)
253
292
  }
254
293
 
@@ -203,42 +203,39 @@ export default class VelociousDatabaseDriversMssql extends Base{
203
203
  return this._options
204
204
  }
205
205
 
206
- async startTransaction() {
206
+ async _startTransactionAction() {
207
207
  if (!this.connection) throw new Error("No connection")
208
208
  if (this._currentTransaction) throw new Error("A transaction is already running")
209
209
 
210
210
  this._currentTransaction = new mssql.Transaction(this.connection)
211
211
 
212
212
  await this._currentTransaction.begin()
213
- this._transactionsCount++
214
213
  }
215
214
 
216
- async commitTransaction() {
215
+ async _commitTransactionAction() {
217
216
  if (!this._currentTransaction) throw new Error("A transaction isn't running")
218
217
 
219
218
  await this._currentTransaction.commit()
220
219
  this._currentTransaction = null
221
- this._transactionsCount--
222
220
  }
223
221
 
224
- async rollbackTransaction() {
222
+ async _rollbackTransactionAction() {
225
223
  if (!this._currentTransaction) throw new Error("A transaction isn't running")
226
224
 
227
225
  await this._currentTransaction.rollback()
228
226
 
229
227
  this._currentTransaction = null
230
- this._transactionsCount--
231
228
  }
232
229
 
233
- async startSavePoint(savePointName) {
230
+ async _startSavePointAction(savePointName) {
234
231
  await this.query(`SAVE TRANSACTION [${savePointName}]`)
235
232
  }
236
233
 
237
- async releaseSavePoint(savePointName) {
234
+ async _releaseSavePointAction(savePointName) {
238
235
  // Do nothing in MS-SQL.
239
236
  }
240
237
 
241
- async rollbackSavePoint(savePointName) {
238
+ async _rollbackSavePointAction(savePointName) {
242
239
  await this.query(`ROLLBACK TRANSACTION [${savePointName}]`)
243
240
  }
244
241
 
@@ -161,9 +161,8 @@ export default class VelociousDatabaseDriversMysql extends Base{
161
161
  return this._options
162
162
  }
163
163
 
164
- async startTransaction() {
164
+ async _startTransactionAction() {
165
165
  await this.query("START TRANSACTION")
166
- this._transactionsCount++
167
166
  }
168
167
 
169
168
  updateSql({conditions, data, tableName}) {
@@ -174,9 +174,8 @@ export default class VelociousDatabaseDriversPgsql extends Base{
174
174
  return this._options
175
175
  }
176
176
 
177
- async startTransaction() {
177
+ async _startTransactionAction() {
178
178
  await this.query("START TRANSACTION")
179
- this._transactionsCount++
180
179
  }
181
180
 
182
181
  updateSql({conditions, data, tableName}) {
@@ -27,7 +27,9 @@ export default class VelociousDatabaseDriversSqliteWeb extends Base {
27
27
  }
28
28
  }
29
29
 
30
- close = async () => await this.getConnection().close()
30
+ async close() {
31
+ await this.getConnection().close()
32
+ }
31
33
 
32
34
  getConnection() {
33
35
  if (this.args.getConnection) {
@@ -22,12 +22,12 @@ export default class VelociousDatabaseMigrator {
22
22
  const databaseConfiguration = this.configuration.getDatabaseIdentifier(dbIdentifier)
23
23
 
24
24
  if (!databaseConfiguration.migrations) {
25
- this.logger.log(`${dbIdentifier} isn't configured for migrations - skipping creating migrations table for it`)
25
+ this.logger.debug(`${dbIdentifier} isn't configured for migrations - skipping creating migrations table for it`)
26
26
  continue
27
27
  }
28
28
 
29
29
  if (await this.migrationsTableExist(db)) {
30
- this.logger.log(`${dbIdentifier} migrations table already exists - skipping`)
30
+ this.logger.debug(`${dbIdentifier} migrations table already exists - skipping`)
31
31
  continue
32
32
  }
33
33
 
@@ -0,0 +1,10 @@
1
+ export default class Header {
2
+ constructor(name, value) {
3
+ this.name = name
4
+ this.value = value
5
+ }
6
+
7
+ getName() { return this.name }
8
+ getValue() { return this.value }
9
+ toString() { return `${this.getName()}: ${this.getValue()}` }
10
+ }
@@ -0,0 +1,103 @@
1
+ import net from "net"
2
+ import Request from "./request.js"
3
+ import Response from "./response.js"
4
+ import {Logger} from "../logger.js"
5
+
6
+ export default class HttpClient {
7
+ constructor({debug = false, headers, version = "1.1"}) {
8
+ this.headers = headers || []
9
+ this.logger = new Logger(this, {debug})
10
+ this.version = version
11
+ }
12
+
13
+ connect() {
14
+ return new Promise((resolve, reject) => {
15
+ this.connectionReject = reject
16
+ this.connection = net.createConnection(3006, "127.0.0.1", () => {
17
+ this.connectionReject = null
18
+ resolve()
19
+ })
20
+
21
+ this.connection.on("data", this.onConnectionData)
22
+ this.connection.on("end", this.onConnectionEnd)
23
+ this.connection.on("error", this.onConnectionError)
24
+ })
25
+ }
26
+
27
+ get(path, {headers} = {}) {
28
+ if (!this.connection) throw new Error("Not connected yet")
29
+
30
+ return new Promise((resolve, reject) => {
31
+ this.currentRequestResolve = resolve
32
+ this.currentRequestReject = reject
33
+
34
+ const newHeaders = []
35
+
36
+ if (headers) {
37
+ for (const header of headers) {
38
+ newHeaders.push(header)
39
+ }
40
+ }
41
+
42
+ for (const header of this.headers) {
43
+ const existingNewHeader = newHeaders.find((newHeader) => {
44
+ return newHeader.key.toLowerCase().trim() === header.key.toLowerCase().trim()
45
+ })
46
+
47
+ if (!existingNewHeader) {
48
+ this.logger.debug(() => [`Pushing header from connection: ${header.toString()}`])
49
+ newHeaders.push(header)
50
+ }
51
+ }
52
+
53
+ this.currentResponse = new Response({method: "GET", onComplete: this.onResponseComplete})
54
+
55
+ this.currentRequest = new Request({headers: newHeaders, method: "GET", path, version: "1.0"})
56
+ this.currentRequest.stream((chunk) => {
57
+ this.logger.debug(() => [`Writing: ${chunk}`])
58
+
59
+ this.connection.write(chunk, "utf8", (error) => {
60
+ if (error) {
61
+ this.currentRequestReject(error)
62
+ }
63
+ })
64
+ })
65
+ })
66
+ }
67
+
68
+ onConnectionData = (data) => {
69
+ this.currentResponse.feed(data)
70
+ }
71
+
72
+ onConnectionEnd = () => {
73
+ this.connection = null
74
+ }
75
+
76
+ onConnectionError = (error) => {
77
+ if (this.connectionReject) {
78
+ this.connectionReject(error)
79
+ } else {
80
+ this.logger.error("HttpClient onConnectionError", error)
81
+ }
82
+ }
83
+
84
+ isConnected() {
85
+ if (this.connection) {
86
+ return true
87
+ }
88
+
89
+ return false
90
+ }
91
+
92
+ onResponseComplete = () => {
93
+ this.currentRequestResolve({
94
+ request: this.currentRequest,
95
+ response: this.currentResponse
96
+ })
97
+
98
+ this.currentRequestResolve = null
99
+ this.currentRequestReject = null
100
+ this.currentRequest = null
101
+ this.currentResponse = null
102
+ }
103
+ }
@@ -0,0 +1,57 @@
1
+ export default class Request {
2
+ constructor({body, method = "GET", headers = [], path, version = "1.1"}) {
3
+ this.body = body
4
+ this.headers = headers
5
+ this.method = method
6
+ this.path = path
7
+ this.version = version
8
+ }
9
+
10
+ asString() {
11
+ let requestString = ""
12
+
13
+ this.stream((chunk) => {
14
+ requestString += chunk
15
+ })
16
+
17
+ return requestString
18
+ }
19
+
20
+ getHeader(key) {
21
+ const compareName = key.toLowerCase().trim()
22
+
23
+ for (const header of this.headers) {
24
+ const headerCompareName = header.key.toLowerCase().trim()
25
+
26
+ if (compareName == headerCompareName) {
27
+ return header
28
+ }
29
+ }
30
+
31
+ throw new Error(`Header ${key} not found`)
32
+ }
33
+
34
+ prepare() {
35
+ if (this.body) {
36
+ this.addHeader("Content-Length", this.body.byteLength)
37
+ }
38
+ }
39
+
40
+ stream(callback) {
41
+ this.prepare()
42
+
43
+ const requestString = `${this.method} ${this.path} HTTP/${this.version}\r\n`
44
+
45
+ callback(requestString)
46
+
47
+ for (const header of this.headers) {
48
+ callback(`${header.toString()}\r\n`)
49
+ }
50
+
51
+ callback(`\r\n`)
52
+
53
+ if (this.body) {
54
+ callback(this.body)
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,112 @@
1
+ import Header from "./header.js"
2
+
3
+ export default class Response {
4
+ constructor({method = "GET", onComplete}) {
5
+ if (!method) throw new Error(`Invalid method given: ${method}`)
6
+
7
+ this.headers = []
8
+ this.method = method.toUpperCase().trim()
9
+ this.onComplete = onComplete
10
+ this.state = "status-line"
11
+
12
+ this.arrayBuffer = new ArrayBuffer()
13
+ this.response = new Uint8Array(this.arrayBuffer)
14
+ }
15
+
16
+ feed(data) {
17
+ this.response += data
18
+ this.tryToParse()
19
+ }
20
+
21
+ getHeader(name) {
22
+ const compareName = name.toLowerCase().trim()
23
+
24
+ for (const header of this.headers) {
25
+ const headerCompareName = header.getName().toLowerCase().trim()
26
+
27
+ if (compareName == headerCompareName) {
28
+ return header
29
+ }
30
+ }
31
+
32
+ throw new Error(`Header ${name} not found`)
33
+ }
34
+
35
+ json() {
36
+ const contentTypeHeader = this.getHeader("Content-Type")?.getValue()?.toLowerCase()?.trim()
37
+
38
+ if (!contentTypeHeader.startsWith("application/json")) {
39
+ throw new Error(`Content-Type is not JSON: ${contentTypeHeader}`)
40
+ }
41
+
42
+ const body = this.response.toString()
43
+ const json = JSON.parse(body)
44
+
45
+ return json
46
+ }
47
+
48
+ tryToParse() {
49
+ while (true) {
50
+ if (this.state == "body") {
51
+ const contentLengthValue = this.getHeader("Content-Length")?.value
52
+
53
+ if (contentLengthValue === undefined) {
54
+ throw new Error("No content length given")
55
+ }
56
+
57
+ const contentLengthNumber = parseInt(contentLengthValue)
58
+
59
+ if (this.response.byteLength >= contentLengthNumber) {
60
+ this.completeResponse()
61
+ break
62
+ }
63
+ } else {
64
+ const response = this.response.toString()
65
+ let lineEndIndex = response.indexOf("\r\n")
66
+ let lineEndLength = 2
67
+
68
+ if (lineEndIndex === -1) {
69
+ lineEndIndex = response.indexOf("\n")
70
+ lineEndLength = 1
71
+ }
72
+
73
+ if (lineEndIndex === -1) {
74
+ break // We need to get fed more to continue reading
75
+ } else {
76
+ const line = response.substring(0, lineEndIndex)
77
+
78
+ this.response = this.response.slice(lineEndIndex + lineEndLength)
79
+
80
+ if (this.state == "status-line") {
81
+ this.statusLine = line
82
+ this.state = "headers"
83
+ } else if (this.state == "headers") {
84
+ if (line == "") {
85
+ if (this.method == "GET" || this.method == "HEAD") {
86
+ this.completeResponse()
87
+ break
88
+ } else if (this.method == "POST") {
89
+ this.state = "body"
90
+ }
91
+ } else {
92
+ const headerMatch = line.match(/^(.+?):\s*(.+)$/)
93
+
94
+ if (!headerMatch) throw new Error(`Invalid header: ${line}`)
95
+
96
+ const header = new Header(headerMatch[1], headerMatch[2])
97
+
98
+ this.headers.push(header)
99
+ }
100
+ } else {
101
+ throw new Error(`Unexpected state: ${this.state}`)
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ completeResponse() {
109
+ this.state = "done"
110
+ this.onComplete()
111
+ }
112
+ }
@@ -38,7 +38,7 @@ export default class VeoliciousHttpServerClient {
38
38
 
39
39
  onWrite(data) {
40
40
  if (this.state == "initial") {
41
- this.currentRequest = new Request({configuration: this.configuration})
41
+ this.currentRequest = new Request({client: this, configuration: this.configuration})
42
42
  this.currentRequest.requestParser.events.on("done", this.executeCurrentRequest)
43
43
  this.currentRequest.feed(data)
44
44
  this.state = "requestStarted"
@@ -56,29 +56,49 @@ export default class VeoliciousHttpServerClient {
56
56
  sendDoneRequests() {
57
57
  while (true) {
58
58
  const requestRunner = this.requestRunners[0]
59
+ const request = requestRunner?.getRequest()
59
60
 
60
61
  if (requestRunner?.getState() == "done") {
62
+ const httpVersion = request.httpVersion()
63
+ const connectionHeader = request.header("connection")?.toLowerCase()?.trim()
64
+
61
65
  this.requestRunners.shift()
62
66
  this.sendResponse(requestRunner)
67
+ this.logger.debug(() => ["sendDoneRequests", {clientCount: this.clientCount, connectionHeader, httpVersion}])
68
+
69
+ if (httpVersion == "1.0" && connectionHeader != "keep-alive") {
70
+ this.logger.debug(() => [`Closing the connection because ${httpVersion} and connection header ${connectionHeader}`, {clientCount: this.clientCount}])
71
+ this.events.emit("close")
72
+ }
63
73
  } else {
64
74
  break
65
75
  }
66
76
  }
67
77
  }
68
78
 
69
- sendResponse = (requestRunner) => {
79
+ sendResponse(requestRunner) {
70
80
  const response = digg(requestRunner, "response")
81
+ const request = requestRunner.getRequest()
71
82
  const body = response.getBody()
72
83
  const date = new Date()
84
+ const connectionHeader = request.header("connection")?.toLowerCase()?.trim()
85
+ const httpVersion = request.httpVersion()
86
+
87
+ this.logger.debug("sendResponse", {clientCount: this.clientCount, connectionHeader, httpVersion})
88
+
89
+ if (httpVersion == "1.0" && connectionHeader == "keep-alive") {
90
+ response.addHeader("Connection", "Keep-Alive")
91
+ } else {
92
+ response.addHeader("Connection", "Close")
93
+ }
73
94
 
74
- response.addHeader("Connection", "keep-alive")
75
95
  response.addHeader("Content-Length", response.body.length)
76
96
  response.addHeader("Date", date.toUTCString())
77
97
  response.addHeader("Server", "Velocious")
78
98
 
79
99
  let headers = ""
80
100
 
81
- headers += `HTTP/1.1 ${response.getStatusCode()} ${response.getStatusMessage()}\r\n`
101
+ headers += `HTTP/${this.currentRequest.httpVersion()} ${response.getStatusCode()} ${response.getStatusMessage()}\r\n`
82
102
 
83
103
  for (const headerKey in response.headers) {
84
104
  for (const headerValue of response.headers[headerKey]) {
@@ -4,4 +4,9 @@ export default class Header {
4
4
  this.name = name
5
5
  this.value = value
6
6
  }
7
+
8
+ getName() { return this.name }
9
+ getFormattedName() { return this.formattedName }
10
+ getValue() { return this.value }
11
+ toString() { return `${this.getName()}: ${this.getValue()}` }
7
12
  }
@@ -10,7 +10,6 @@ export default class RequestBuffer {
10
10
  bodyLength = 0
11
11
  data = []
12
12
  events = new EventEmitter()
13
- headers = []
14
13
  headersByName = {}
15
14
  params = {}
16
15
  readingBody = false
@@ -18,7 +17,7 @@ export default class RequestBuffer {
18
17
 
19
18
  constructor({configuration}) {
20
19
  this.configuration = configuration
21
- this.logger = new Logger(this)
20
+ this.logger = new Logger(this, {debug: false})
22
21
  }
23
22
 
24
23
  feed(data) {
@@ -85,7 +84,25 @@ export default class RequestBuffer {
85
84
  }
86
85
  }
87
86
 
88
- getHeader = (name) => this.headersByName[name.toLowerCase().trim()]
87
+ getHeader(name) {
88
+ const result = this.headersByName[name.toLowerCase().trim()]
89
+
90
+ this.logger.debug(() => [`getHeader ${name}`, {result: result?.toString()}])
91
+
92
+ return result
93
+ }
94
+
95
+ getHeadersHash() {
96
+ const result = {}
97
+
98
+ for (const headerFormattedName in this.headersByName) {
99
+ const header = this.headersByName[headerFormattedName]
100
+
101
+ result[header.getName()] = header.getValue()
102
+ }
103
+
104
+ return result
105
+ }
89
106
 
90
107
  formDataPartDone() {
91
108
  const formDataPart = this.formDataPart
@@ -96,7 +113,9 @@ export default class RequestBuffer {
96
113
  this.events.emit("form-data-part", formDataPart)
97
114
  }
98
115
 
99
- isMultiPartyFormData = () => this.multiPartyFormData
116
+ isMultiPartyFormData() {
117
+ return this.multiPartyFormData
118
+ }
100
119
 
101
120
  newFormDataPart() {
102
121
  this.formDataPart = new FormDataPart()
@@ -140,14 +159,20 @@ export default class RequestBuffer {
140
159
  }
141
160
  }
142
161
 
162
+ addHeader(header) {
163
+ const formattedName = header.getFormattedName()
164
+
165
+ this.headersByName[formattedName] = header
166
+
167
+ if (formattedName == "content-length") this.contentLength = parseInt(header.getValue())
168
+ }
169
+
143
170
  parseHeader(line) {
144
171
  const header = this.readHeaderFromLine(line)
145
172
 
146
173
  if (header) {
147
- this.headersByName[header.formattedName] = header
148
-
149
- if (header.formattedName == "content-length") this.contentLength = parseInt(header.value)
150
-
174
+ this.logger.debug(() => [`Parsed header: ${header.toString()}`])
175
+ this.addHeader(header)
151
176
  this.events.emit("header", header)
152
177
  } else if (line == "\r\n") {
153
178
  if (this.httpMethod.toUpperCase() == "GET" || this.httpMethod.toUpperCase() == "OPTIONS") {
@@ -184,16 +209,17 @@ export default class RequestBuffer {
184
209
  }
185
210
 
186
211
  parseStatusLine(line) {
187
- const match = line.match(/^(GET|OPTIONS|POST) (.+?) HTTP\/1\.1\r\n/)
212
+ const match = line.match(/^(GET|OPTIONS|POST) (.+?) HTTP\/(.+)\r\n/)
188
213
 
189
214
  if (!match) {
190
215
  throw new Error(`Couldn't match status line from: ${line}`)
191
216
  }
192
217
 
193
218
  this.httpMethod = match[1]
219
+ this.httpVersion = match[3]
194
220
  this.path = match[2]
195
221
  this.setState("headers")
196
- this.logger.debug(() => ["Parsed status line", {httpMethod: this.httpMethod, path: this.path}])
222
+ this.logger.debug(() => ["Parsed status line", {httpMethod: this.httpMethod, httpVersion: this.httpVersion, path: this.path}])
197
223
  }
198
224
 
199
225
  postRequestDone() {
@@ -38,10 +38,11 @@ export default class VelociousHttpServerClientRequestParser {
38
38
  }
39
39
 
40
40
  feed = (data) => this.requestBuffer.feed(data)
41
- getHeader = (name) => this.requestBuffer.getHeader(name)?.value
42
- getHttpMethod = () => digg(this, "requestBuffer", "httpMethod")
41
+ getHeader(name) { return this.requestBuffer.getHeader(name)?.value }
42
+ getHttpMethod() { return digg(this, "requestBuffer", "httpMethod") }
43
+ getHttpVersion() { return digg(this, "requestBuffer", "httpVersion") }
43
44
 
44
- _getHostMatch = () => {
45
+ _getHostMatch() {
45
46
  const rawHost = this.requestBuffer.getHeader("origin")?.value
46
47
 
47
48
  if (!rawHost) return null
@@ -63,7 +64,7 @@ export default class VelociousHttpServerClientRequestParser {
63
64
  if (rawHostSplit && rawHostSplit[0]) return rawHostSplit[0]
64
65
  }
65
66
 
66
- getPath = () => digg(this, "requestBuffer", "path")
67
+ getPath() { return digg(this, "requestBuffer", "path") }
67
68
 
68
69
  getPort() {
69
70
  const rawHostSplit = this.requestBuffer.getHeader("host")?.value?.split(":")
@@ -78,7 +79,8 @@ export default class VelociousHttpServerClientRequestParser {
78
79
  }
79
80
  }
80
81
 
81
- getProtocol = () => this._getHostMatch()?.protocol
82
+ getProtocol() { return this._getHostMatch()?.protocol }
83
+ getRequestBuffer() { return this.requestBuffer }
82
84
 
83
85
  requestDone = () => {
84
86
  incorporate(this.params, this.requestBuffer.params)
@@ -18,7 +18,8 @@ export default class VelociousHttpServerClientRequestRunner {
18
18
  this.state = "running"
19
19
  }
20
20
 
21
- getState = () => this.state
21
+ getRequest() { return this.request }
22
+ getState() { return this.state }
22
23
 
23
24
  async run() {
24
25
  const {configuration, request, response} = this
@@ -1,16 +1,21 @@
1
1
  import {digg} from "diggerize"
2
2
  import RequestParser from "./request-parser.js"
3
+ import restArgsError from "../../utils/rest-args-error.js"
3
4
 
4
5
  export default class VelociousHttpServerClientRequest {
5
- constructor({configuration}) {
6
+ constructor({client, configuration, ...restArgs}) {
7
+ restArgsError(restArgs)
8
+
9
+ this.client = client
6
10
  this.configuration = configuration
7
11
  this.requestParser = new RequestParser({configuration})
8
12
  }
9
13
 
10
14
  baseURL() { return `${this.protocol()}://${this.hostWithPort()}` }
11
15
  feed(data) { return this.requestParser.feed(data) }
12
- header(headerName) { return this.requestParser.requestBuffer.getHeader(headerName)?.value }
16
+ header(headerName) { return this.getRequestBuffer().getHeader(headerName)?.getValue() }
13
17
  httpMethod() { return this.requestParser.getHttpMethod() }
18
+ httpVersion() { return this.requestParser.getHttpVersion() }
14
19
  host() { return this.requestParser.getHost() }
15
20
 
16
21
  hostWithPort() {
@@ -29,9 +34,12 @@ export default class VelociousHttpServerClientRequest {
29
34
  return hostWithPort
30
35
  }
31
36
 
32
- origin = () => this.header("origin")
33
- path = () => this.requestParser.getPath()
34
- params = () => digg(this, "requestParser", "params")
35
- port = () => this.requestParser.getPort()
36
- protocol = () => this.requestParser.getProtocol()
37
+ origin() { return this.header("origin") }
38
+ path() { return this.requestParser.getPath() }
39
+ params() { return digg(this, "requestParser", "params") }
40
+ port() { return this.requestParser.getPort() }
41
+ protocol() { return this.requestParser.getProtocol() }
42
+
43
+ getRequestBuffer() { return this.getRequestParser().getRequestBuffer() }
44
+ getRequestParser() { return this.requestParser }
37
45
  }
@@ -18,15 +18,8 @@ export default class ServerClient {
18
18
  listen = () => this.socket.on("data", this.onSocketData)
19
19
 
20
20
  close() {
21
- return new Promise((resolve, reject) => {
22
- try {
23
- this.socket.destroy()
24
- this.events.emit("close", this)
25
- resolve()
26
- } catch (error) {
27
- reject(error)
28
- }
29
- })
21
+ this.socket.destroy()
22
+ this.events.emit("close", this)
30
23
  }
31
24
 
32
25
  onSocketData = (chunk) => {
@@ -75,6 +75,10 @@ export default class VelociousHttpServerWorker {
75
75
  const {clientCount, output} = digs(data, "clientCount", "output")
76
76
 
77
77
  this.clients[clientCount].send(output)
78
+ } else if (command == "clientClose") {
79
+ const {clientCount} = digs(data, "clientCount")
80
+
81
+ this.clients[clientCount].close()
78
82
  } else {
79
83
  throw new Error(`Unknown command: ${command}`)
80
84
  }
@@ -9,7 +9,7 @@ export default class VelociousHttpServerWorkerHandlerWorkerThread {
9
9
  const {workerCount} = digs(workerData, "workerCount")
10
10
 
11
11
  this.clients = {}
12
- this.logger = new Logger(this)
12
+ this.logger = new Logger(this, {debug: false})
13
13
  this.parentPort = parentPort
14
14
  this.workerData = workerData
15
15
  this.workerCount = workerCount
@@ -56,6 +56,11 @@ export default class VelociousHttpServerWorkerHandlerWorkerThread {
56
56
  this.parentPort.postMessage({command: "clientOutput", clientCount, output})
57
57
  })
58
58
 
59
+ client.events.on("close", (output) => {
60
+ this.logger.log("Close received from client in worker - forwarding to worker parent")
61
+ this.parentPort.postMessage({command: "clientClose", clientCount, output})
62
+ })
63
+
59
64
  this.clients[clientCount] = client
60
65
  } else if (command == "clientWrite") {
61
66
  await this.logger.debug("Looking up client")
package/src/logger.js CHANGED
@@ -47,7 +47,9 @@ function messagesToMessage(...messages) {
47
47
  }
48
48
 
49
49
  class Logger {
50
- constructor(object) {
50
+ constructor(object, args) {
51
+ this._debug = args?.debug
52
+
51
53
  if (typeof object == "string") {
52
54
  this._subject = object
53
55
  } else {
@@ -69,7 +71,7 @@ class Logger {
69
71
  }
70
72
 
71
73
  async debug(...messages) {
72
- if (this.getConfiguration()?.debug) {
74
+ if (this._debug || this.getConfiguration()?.debug) {
73
75
  await this.log(...messages)
74
76
  }
75
77
  }