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 +2 -1
- package/spec/dummy/index.js +0 -7
- package/spec/dummy/src/config/configuration.example.js +1 -0
- package/spec/dummy/src/config/configuration.peakflow.mariadb.js +1 -0
- package/spec/dummy/src/config/configuration.peakflow.mssql.js +1 -0
- package/spec/dummy/src/config/configuration.peakflow.pgsql.js +1 -0
- package/spec/dummy/src/config/configuration.peakflow.sqlite.js +1 -0
- package/spec/http-server/get-spec.js +56 -0
- package/src/application.js +0 -1
- package/src/database/drivers/base.js +42 -3
- package/src/database/drivers/mssql/index.js +6 -9
- package/src/database/drivers/mysql/index.js +1 -2
- package/src/database/drivers/pgsql/index.js +1 -2
- package/src/database/drivers/sqlite/index.web.js +3 -1
- package/src/database/migrator.js +2 -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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"velocious": "bin/velocious.js"
|
|
4
4
|
},
|
|
5
5
|
"name": "velocious",
|
|
6
|
-
"version": "1.0.
|
|
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",
|
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"
|
|
@@ -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
|
})
|
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}`)
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
230
|
+
async _startSavePointAction(savePointName) {
|
|
234
231
|
await this.query(`SAVE TRANSACTION [${savePointName}]`)
|
|
235
232
|
}
|
|
236
233
|
|
|
237
|
-
async
|
|
234
|
+
async _releaseSavePointAction(savePointName) {
|
|
238
235
|
// Do nothing in MS-SQL.
|
|
239
236
|
}
|
|
240
237
|
|
|
241
|
-
async
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
@@ -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.
|
|
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.
|
|
30
|
+
this.logger.debug(`${dbIdentifier} migrations table already exists - skipping`)
|
|
31
31
|
continue
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -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
|
}
|