velocious 1.0.54 → 1.0.56

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.54",
6
+ "version": "1.0.56",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "test": "VELOCIOUS_TEST_DIR=../ cd spec/dummy && npx velocious test",
@@ -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"
@@ -17,7 +18,8 @@ export default new Configuration({
17
18
  host: "mariadb",
18
19
  username: "username",
19
20
  password: "password",
20
- database: "velocious_development"
21
+ database: "velocious_development",
22
+ migrations: true
21
23
  }
22
24
  },
23
25
  production: {
@@ -28,7 +30,8 @@ export default new Configuration({
28
30
  host: "mariadb",
29
31
  username: "username",
30
32
  password: "password",
31
- database: "velocious_production"
33
+ database: "velocious_production",
34
+ migrations: true
32
35
  }
33
36
  },
34
37
  test: {
@@ -39,7 +42,8 @@ export default new Configuration({
39
42
  host: "mariadb",
40
43
  username: "username",
41
44
  password: "password",
42
- database: "velocious_test"
45
+ database: "velocious_test",
46
+ migrations: true
43
47
  }
44
48
  }
45
49
  },
@@ -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"
@@ -19,7 +20,8 @@ export default new Configuration({
19
20
  username: "peakflow",
20
21
  password: "password",
21
22
  database: "velocious_test",
22
- useDatabase: "velocious_test"
23
+ useDatabase: "velocious_test",
24
+ migrations: true
23
25
  },
24
26
  mssql: {
25
27
  driver: MssqlDriver,
@@ -27,6 +29,7 @@ export default new Configuration({
27
29
  type: "mssql",
28
30
  database: "velocious_test",
29
31
  useDatabase: "default",
32
+ migrations: true,
30
33
  sqlConfig: {
31
34
  user: "sa",
32
35
  password: "Super-Secret-Password",
@@ -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"
@@ -16,6 +17,7 @@ export default new Configuration({
16
17
  type: "mssql",
17
18
  database: "velocious_test",
18
19
  useDatabase: "default",
20
+ migrations: true,
19
21
  sqlConfig: {
20
22
  user: "sa",
21
23
  password: "Super-Secret-Password",
@@ -33,6 +35,7 @@ export default new Configuration({
33
35
  type: "mssql",
34
36
  database: "velocious_test",
35
37
  useDatabase: "default",
38
+ migrations: true,
36
39
  sqlConfig: {
37
40
  user: "sa",
38
41
  password: "Super-Secret-Password",
@@ -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"
@@ -19,7 +20,8 @@ export default new Configuration({
19
20
  username: "peakflow",
20
21
  password: "password",
21
22
  database: "velocious_test",
22
- useDatabase: "velocious_test"
23
+ useDatabase: "velocious_test",
24
+ migrations: true
23
25
  },
24
26
  mssql: {
25
27
  driver: MssqlDriver,
@@ -27,6 +29,7 @@ export default new Configuration({
27
29
  type: "mssql",
28
30
  database: "velocious_test",
29
31
  useDatabase: "default",
32
+ migrations: true,
30
33
  sqlConfig: {
31
34
  user: "sa",
32
35
  password: "Super-Secret-Password",
@@ -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"
@@ -16,7 +17,8 @@ export default new Configuration({
16
17
  driver: SqliteDriver,
17
18
  poolType: SingleMultiUsePool,
18
19
  type: "sqlite",
19
- name: "test-db"
20
+ name: "test-db",
21
+ migrations: true
20
22
  },
21
23
  mssql: {
22
24
  driver: MssqlDriver,
@@ -24,6 +26,7 @@ export default new Configuration({
24
26
  type: "mssql",
25
27
  database: "velocious_test",
26
28
  useDatabase: "default",
29
+ migrations: true,
27
30
  sqlConfig: {
28
31
  user: "sa",
29
32
  password: "Super-Secret-Password",
@@ -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}`)
@@ -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) {
@@ -17,8 +17,19 @@ export default class VelociousDatabaseMigrator {
17
17
  async createMigrationsTable() {
18
18
  const dbs = await this.configuration.getCurrentConnections()
19
19
 
20
- for (const db of Object.values(dbs)) {
21
- if (await this.migrationsTableExist(db)) continue
20
+ for (const dbIdentifier in dbs) {
21
+ const db = dbs[dbIdentifier]
22
+ const databaseConfiguration = this.configuration.getDatabaseIdentifier(dbIdentifier)
23
+
24
+ if (!databaseConfiguration.migrations) {
25
+ this.logger.debug(`${dbIdentifier} isn't configured for migrations - skipping creating migrations table for it`)
26
+ continue
27
+ }
28
+
29
+ if (await this.migrationsTableExist(db)) {
30
+ this.logger.debug(`${dbIdentifier} migrations table already exists - skipping`)
31
+ continue
32
+ }
22
33
 
23
34
  const schemaMigrationsTable = new TableData("schema_migrations", {ifNotExists: true})
24
35
 
@@ -107,6 +118,18 @@ export default class VelociousDatabaseMigrator {
107
118
 
108
119
  for (const dbIdentifier in dbs) {
109
120
  const db = dbs[dbIdentifier]
121
+ const databaseConfiguration = this.configuration.getDatabaseIdentifier(dbIdentifier)
122
+
123
+ if (!databaseConfiguration.migrations) {
124
+ this.logger.debug(`${dbIdentifier} isn't configured for migrations - skipping loading migrations versions for it`)
125
+ continue
126
+ }
127
+
128
+ if (!await this.migrationsTableExist(db)) {
129
+ this.logger.log(`Migration table does not exist for ${dbIdentifier} - skipping loading migrations versions for it`)
130
+ continue
131
+ }
132
+
110
133
  const rows = await db.select("schema_migrations")
111
134
 
112
135
  this.migrationsVersions[dbIdentifier] = {}
@@ -204,6 +227,13 @@ export default class VelociousDatabaseMigrator {
204
227
  const migrationDatabaseIdentifiers = migrationClass.getDatabaseIdentifiers() || ["default"]
205
228
 
206
229
  for (const dbIdentifier in dbs) {
230
+ const databaseConfiguration = this.configuration.getDatabaseIdentifier(dbIdentifier)
231
+
232
+ if (!databaseConfiguration.migrations) {
233
+ this.logger.debug(`${dbIdentifier} isn't configured for migrations - skipping migration ${digg(migration, "date")}`)
234
+ continue
235
+ }
236
+
207
237
  if (!migrationDatabaseIdentifiers.includes(dbIdentifier)) {
208
238
  this.logger.debug(`${dbIdentifier} shouldn't run migration ${migration.file}`, {migrationDatabaseIdentifiers})
209
239
  continue
@@ -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
  }