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 +1 -1
- package/spec/dummy/index.js +0 -7
- package/spec/dummy/src/config/configuration.example.js +7 -3
- package/spec/dummy/src/config/configuration.peakflow.mariadb.js +4 -1
- package/spec/dummy/src/config/configuration.peakflow.mssql.js +3 -0
- package/spec/dummy/src/config/configuration.peakflow.pgsql.js +4 -1
- package/spec/dummy/src/config/configuration.peakflow.sqlite.js +4 -1
- package/spec/http-server/get-spec.js +56 -0
- package/src/application.js +0 -1
- package/src/database/drivers/sqlite/index.web.js +3 -1
- package/src/database/migrator.js +32 -2
- package/src/http-client/header.js +10 -0
- package/src/http-client/index.js +103 -0
- package/src/http-client/request.js +57 -0
- package/src/http-client/response.js +112 -0
- package/src/http-server/client/index.js +24 -4
- package/src/http-server/client/request-buffer/header.js +5 -0
- package/src/http-server/client/request-buffer/index.js +36 -10
- package/src/http-server/client/request-parser.js +7 -5
- package/src/http-server/client/request-runner.js +2 -1
- package/src/http-server/client/request.js +15 -7
- package/src/http-server/server-client.js +2 -9
- package/src/http-server/worker-handler/index.js +4 -0
- package/src/http-server/worker-handler/worker-thread.js +6 -1
- package/src/logger.js +4 -2
package/package.json
CHANGED
package/spec/dummy/index.js
CHANGED
|
@@ -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
|
})
|
package/src/application.js
CHANGED
|
@@ -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
|
-
|
|
30
|
+
async close() {
|
|
31
|
+
await this.getConnection().close()
|
|
32
|
+
}
|
|
31
33
|
|
|
32
34
|
getConnection() {
|
|
33
35
|
if (this.args.getConnection) {
|
package/src/database/migrator.js
CHANGED
|
@@ -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
|
|
21
|
-
|
|
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,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
|
|
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
|
|
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]) {
|
|
@@ -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
|
|
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
|
|
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.
|
|
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\/
|
|
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
|
|
42
|
-
getHttpMethod
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
33
|
-
path
|
|
34
|
-
params
|
|
35
|
-
port
|
|
36
|
-
protocol
|
|
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
|
-
|
|
22
|
-
|
|
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
|
}
|