From 72740232578802cecbae5fcf13dc491b92417cce Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sat, 25 Sep 2021 18:16:43 +0200 Subject: [PATCH] Track ATG status on a per connector basis. MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- docker/Dockerfile | 3 +- docker/docker-compose.yml | 1 + manifest-cf-template.yml | 2 +- package-lock.json | 106 +++++++++++------- package.json | 6 +- .../AutomaticTransactionGenerator.ts | 49 +++++--- src/charging-station/ChargingStation.ts | 2 +- .../ocpp/1.6/OCPP16RequestService.ts | 4 +- src/utils/Utils.ts | 2 +- 9 files changed, 111 insertions(+), 64 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index c1329905..c8a684fa 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -13,11 +13,12 @@ RUN npm run build FROM node:lts-alpine +ARG STACK_TRACE_LIMIT ARG MAX_OLD_SPACE_SIZE WORKDIR /usr/app -ENV NODE_OPTIONS=--max-old-space-size=${MAX_OLD_SPACE_SIZE} +ENV NODE_OPTIONS="--stack-trace-limit=${STACK_TRACE_LIMIT} --max-old-space-size=${MAX_OLD_SPACE_SIZE}" COPY --from=builder /usr/builder/node_modules ./node_modules COPY --from=builder /usr/builder/dist ./dist diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index ebc8d8b9..55c0526e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -8,6 +8,7 @@ services: context: .. dockerfile: docker/Dockerfile args: + STACK_TRACE_LIMIT: 1024 MAX_OLD_SPACE_SIZE: 1024 networks: - ev_network diff --git a/manifest-cf-template.yml b/manifest-cf-template.yml index b4bcbc0b..42c8b299 100644 --- a/manifest-cf-template.yml +++ b/manifest-cf-template.yml @@ -11,4 +11,4 @@ applications: command: node -r source-map-support/register dist/start.js env: # OPTIMIZE_MEMORY: true - NODE_OPTIONS: --max-old-space-size=512 + NODE_OPTIONS: --stack-trace-limit=1024 --max-old-space-size=512 diff --git a/package-lock.json b/package-lock.json index bdf8bca9..8097dc2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5038,9 +5038,9 @@ } }, "@types/node": { - "version": "14.17.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.18.tgz", - "integrity": "sha512-haYyibw4pbteEhkSg0xdDLAI3679L75EJ799ymVrPxOA922bPx3ML59SoDsQ//rHlvqpu+e36kcbR3XRQtFblA==" + "version": "14.17.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.19.tgz", + "integrity": "sha512-jjYI6NkyfXykucU6ELEoT64QyKOdvaA6enOqKtP4xUsGY0X0ZUZz29fUmrTRo+7v7c6TgDu82q3GHHaCEkqZwA==" }, "@types/object-path": { "version": "0.11.1", @@ -9959,16 +9959,16 @@ } }, "expect": { - "version": "27.2.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.2.1.tgz", - "integrity": "sha512-ekOA2mBtT2phxcoPVHCXIzbJxCvRXhx2fr7m28IgGdZxUOh8UvxvoRz1FcPlfgZMpE92biHB6woIcAKXqR28hA==", + "version": "27.2.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.2.2.tgz", + "integrity": "sha512-sjHBeEk47/eshN9oLbvPJZMgHQihOXXQzSMPCJ4MqKShbU9HOVFSNHEEU4dp4ujzxFSiNvPFzB2AMOFmkizhvA==", "dev": true, "requires": { "@jest/types": "^27.1.1", "ansi-styles": "^5.0.0", "jest-get-type": "^27.0.6", - "jest-matcher-utils": "^27.2.0", - "jest-message-util": "^27.2.0", + "jest-matcher-utils": "^27.2.2", + "jest-message-util": "^27.2.2", "jest-regex-util": "^27.0.6" }, "dependencies": { @@ -12302,15 +12302,15 @@ } }, "jest-diff": { - "version": "27.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.2.0.tgz", - "integrity": "sha512-QSO9WC6btFYWtRJ3Hac0sRrkspf7B01mGrrQEiCW6TobtViJ9RWL0EmOs/WnBsZDsI/Y2IoSHZA2x6offu0sYw==", + "version": "27.2.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.2.2.tgz", + "integrity": "sha512-o3LaDbQDSaMJif4yztJAULI4xVatxbBasbKLbEw3K8CiRdDdbxMrLArS9EKDHQFYh6Tgfrm1PC2mIYR1xhu0hQ==", "dev": true, "requires": { "chalk": "^4.0.0", "diff-sequences": "^27.0.6", "jest-get-type": "^27.0.6", - "pretty-format": "^27.2.0" + "pretty-format": "^27.2.2" } }, "jest-get-type": { @@ -12320,21 +12320,21 @@ "dev": true }, "jest-matcher-utils": { - "version": "27.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.2.0.tgz", - "integrity": "sha512-F+LG3iTwJ0gPjxBX6HCyrARFXq6jjiqhwBQeskkJQgSLeF1j6ui1RTV08SR7O51XTUhtc8zqpDj8iCG4RGmdKw==", + "version": "27.2.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.2.2.tgz", + "integrity": "sha512-xN3wT4p2i9DGB6zmL3XxYp5lJmq9Q6ff8XKlMtVVBS2SAshmgsPBALJFQ8dWRd2G/xf5q/N0SD0Mipt8QBA26A==", "dev": true, "requires": { "chalk": "^4.0.0", - "jest-diff": "^27.2.0", + "jest-diff": "^27.2.2", "jest-get-type": "^27.0.6", - "pretty-format": "^27.2.0" + "pretty-format": "^27.2.2" } }, "jest-message-util": { - "version": "27.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.2.0.tgz", - "integrity": "sha512-y+sfT/94CiP8rKXgwCOzO1mUazIEdEhrLjuiu+RKmCP+8O/TJTSne9dqQRbFIHBtlR2+q7cddJlWGir8UATu5w==", + "version": "27.2.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.2.2.tgz", + "integrity": "sha512-/iS5/m2FSF7Nn6APFoxFymJpyhB/gPf0CJa7uFSkbYaWvrADUfQ9NTsuyjpszKErOS2/huFs44ysWhlQTKvL8Q==", "dev": true, "requires": { "@babel/code-frame": "^7.12.13", @@ -12343,7 +12343,7 @@ "chalk": "^4.0.0", "graceful-fs": "^4.2.4", "micromatch": "^4.0.4", - "pretty-format": "^27.2.0", + "pretty-format": "^27.2.2", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -13326,16 +13326,16 @@ "dev": true }, "mocha": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.1.tgz", - "integrity": "sha512-0wE74YMgOkCgBUj8VyIDwmLUjTsS13WV1Pg7l0SHea2qzZzlq7MDnfbPsHKcELBRk3+izEVkRofjmClpycudCA==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.2.tgz", + "integrity": "sha512-ta3LtJ+63RIBP03VBjMGtSqbe6cWXRejF9SyM9Zyli1CKZJZ+vfCTj3oW24V7wAphMJdpOFLoMI3hjJ1LWbs0w==", "dev": true, "requires": { "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", "chokidar": "3.5.2", - "debug": "4.3.1", + "debug": "4.3.2", "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", @@ -13346,12 +13346,11 @@ "log-symbols": "4.1.0", "minimatch": "3.0.4", "ms": "2.1.3", - "nanoid": "3.1.23", + "nanoid": "3.1.25", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", "which": "2.0.2", - "wide-align": "1.1.3", "workerpool": "6.1.5", "yargs": "16.2.0", "yargs-parser": "20.2.4", @@ -13359,9 +13358,9 @@ }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "ansi-styles": { @@ -13405,6 +13404,23 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -13477,6 +13493,12 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "nanoid": { + "version": "3.1.25", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", + "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", + "dev": true + }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -13511,23 +13533,23 @@ } }, "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } }, "strip-json-comments": { @@ -15776,13 +15798,13 @@ "dev": true }, "pretty-format": { - "version": "27.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.2.0.tgz", - "integrity": "sha512-KyJdmgBkMscLqo8A7K77omgLx5PWPiXJswtTtFV7XgVZv2+qPk6UivpXXO+5k6ZEbWIbLoKdx1pZ6ldINzbwTA==", + "version": "27.2.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.2.2.tgz", + "integrity": "sha512-+DdLh+rtaElc2SQOE/YPH8k2g3Rf2OXWEpy06p8Szs3hdVSYD87QOOlYRHWAeb/59XTmeVmRKvDD0svHqf6ycA==", "dev": true, "requires": { "@jest/types": "^27.1.1", - "ansi-regex": "^5.0.0", + "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" }, diff --git a/package.json b/package.json index cbdb973b..ddb241db 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "@rollup/plugin-json": "^4.1.0", "@types/mocha": "^9.0.0", "@types/mochawesome": "^6.2.1", - "@types/node": "^14.17.18", + "@types/node": "^14.17.19", "@types/proper-lockfile": "^4.1.2", "@types/tar": "^4.0.5", "@types/uuid": "^8.3.1", @@ -108,8 +108,8 @@ "eslint-plugin-import": "^2.24.2", "eslint-plugin-jsdoc": "^36.1.0", "eslint-plugin-node": "^11.1.0", - "expect": "^27.2.1", - "mocha": "^9.1.1", + "expect": "^27.2.2", + "mocha": "^9.1.2", "mochawesome": "^6.2.2", "npm-check": "^5.9.2", "nyc": "^15.1.0", diff --git a/src/charging-station/AutomaticTransactionGenerator.ts b/src/charging-station/AutomaticTransactionGenerator.ts index 3646b891..5a5ca993 100644 --- a/src/charging-station/AutomaticTransactionGenerator.ts +++ b/src/charging-station/AutomaticTransactionGenerator.ts @@ -10,13 +10,16 @@ import logger from '../utils/Logger'; export default class AutomaticTransactionGenerator { public started: boolean; + private chargingStation: ChargingStation; + private connectorsStartStatus: Record; private startDate!: Date; private lastRunDate!: Date; private stopDate!: Date; - private chargingStation: ChargingStation; constructor(chargingStation: ChargingStation) { this.chargingStation = chargingStation; + this.connectorsStartStatus = {} as Record; + this.stopConnectors(); this.started = false; } @@ -31,15 +34,8 @@ export default class AutomaticTransactionGenerator { this.stopDate = new Date(this.startDate.getTime() + (this.chargingStation.stationInfo?.AutomaticTransactionGenerator?.stopAfterHours ?? Constants.CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS) * 3600 * 1000 - previousRunDuration); + this.startConnectors(); this.started = true; - for (const connector in this.chargingStation.connectors) { - if (Utils.convertToInt(connector) > 0) { - // Avoid hogging the event loop with a busy loop - setImmediate(() => { - this.startOnConnector(Utils.convertToInt(connector)).catch(() => { /* This is intentional */ }); - }); - } - } logger.info(this.logPrefix() + ' started and will run for ' + Utils.formatDurationMilliSeconds(this.stopDate.getTime() - this.startDate.getTime())); } @@ -48,15 +44,38 @@ export default class AutomaticTransactionGenerator { logger.error(`${this.logPrefix()} trying to stop while not started`); return; } + this.stopConnectors(); this.started = false; logger.info(`${this.logPrefix()} over and lasted for ${Utils.formatDurationMilliSeconds(this.lastRunDate.getTime() - this.startDate.getTime())}. Stopping all transactions`); } - private async startOnConnector(connectorId: number): Promise { + private startConnectors(): void { + for (const connector in this.chargingStation.connectors) { + const connectorId = Utils.convertToInt(connector); + if (connectorId > 0) { + // Avoid hogging the event loop with a busy loop + setImmediate(() => { + this.startConnector(connectorId).catch(() => { /* This is intentional */ }); + }); + } + } + } + + private stopConnectors(): void { + for (const connector in this.chargingStation.connectors) { + const connectorId = Utils.convertToInt(connector); + if (connectorId > 0) { + this.stopConnector(connectorId); + } + } + } + + private async startConnector(connectorId: number): Promise { logger.info(this.logPrefix(connectorId) + ' started on connector'); let skippedTransactions = 0; let skippedTransactionsTotal = 0; - while (this.started) { + this.connectorsStartStatus[connectorId] = true; + while (this.connectorsStartStatus[connectorId]) { if ((new Date()) > this.stopDate) { this.stop(); break; @@ -80,7 +99,7 @@ export default class AutomaticTransactionGenerator { await Utils.sleep(Constants.CHARGING_STATION_ATG_INITIALIZATION_TIME); } while (!this.chargingStation?.ocppRequestService); } - const wait = Utils.getRandomInt(this.chargingStation.stationInfo.AutomaticTransactionGenerator.maxDelayBetweenTwoTransactions, + const wait = Utils.getRandomInteger(this.chargingStation.stationInfo.AutomaticTransactionGenerator.maxDelayBetweenTwoTransactions, this.chargingStation.stationInfo.AutomaticTransactionGenerator.minDelayBetweenTwoTransactions) * 1000; logger.info(this.logPrefix(connectorId) + ' waiting for ' + Utils.formatDurationMilliSeconds(wait)); await Utils.sleep(wait); @@ -94,7 +113,7 @@ export default class AutomaticTransactionGenerator { await Utils.sleep(Constants.CHARGING_STATION_ATG_WAIT_TIME); } else { // Wait until end of transaction - const waitTrxEnd = Utils.getRandomInt(this.chargingStation.stationInfo.AutomaticTransactionGenerator.maxDuration, + const waitTrxEnd = Utils.getRandomInteger(this.chargingStation.stationInfo.AutomaticTransactionGenerator.maxDuration, this.chargingStation.stationInfo.AutomaticTransactionGenerator.minDuration) * 1000; logger.info(this.logPrefix(connectorId) + ' transaction ' + this.chargingStation.getConnector(connectorId).transactionId.toString() + ' will stop in ' + Utils.formatDurationMilliSeconds(waitTrxEnd)); await Utils.sleep(waitTrxEnd); @@ -113,6 +132,10 @@ export default class AutomaticTransactionGenerator { logger.info(this.logPrefix(connectorId) + ' stopped on connector'); } + private stopConnector(connectorId: number): void { + this.connectorsStartStatus[connectorId] = false; + } + private async startTransaction(connectorId: number): Promise { const measureId = 'StartTransaction with ATG'; const beginId = PerformanceStatistics.beginMeasure(measureId); diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 14710b65..5b5ac56e 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -525,7 +525,7 @@ export default class ChargingStation { // Generate all connectors if ((this.stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) { for (let index = 1; index <= maxConnectors; index++) { - const randConnectorId = this.stationInfo.randomConnectors ? Utils.getRandomInt(Utils.convertToInt(lastConnector), 1) : index; + const randConnectorId = this.stationInfo.randomConnectors ? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1) : index; this.connectors[index] = Utils.cloneObject(this.stationInfo.Connectors[randConnectorId]); this.connectors[index].availability = AvailabilityType.OPERATIVE; if (Utils.isUndefined(this.connectors[lastConnector]?.chargingProfiles)) { diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts index 1a0190e5..a8fb4890 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -132,7 +132,7 @@ export default class OCPP16RequestService extends OCPPRequestService { if (socSampledValueTemplate) { const socSampledValueTemplateValue = socSampledValueTemplate.value ? Utils.getRandomFloatFluctuatedRounded(parseInt(socSampledValueTemplate.value), socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT) - : Utils.getRandomInt(100); + : Utils.getRandomInteger(100); meterValue.sampledValue.push(OCPP16ServiceUtils.buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue)); const sampledValuesIndex = meterValue.sampledValue.length - 1; if (Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > 100 || debug) { @@ -313,7 +313,7 @@ export default class OCPP16RequestService extends OCPPRequestService { const energyMeasurandValue = energySampledValueTemplate.value // Cumulate the fluctuated value around the static one ? Utils.getRandomFloatFluctuatedRounded(parseInt(energySampledValueTemplate.value), energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT) - : Utils.getRandomInt(this.chargingStation.stationInfo.maxPower / (this.chargingStation.stationInfo.powerDivider * 3600000) * interval); + : Utils.getRandomInteger(this.chargingStation.stationInfo.maxPower / (this.chargingStation.stationInfo.powerDivider * 3600000) * interval); // Persist previous value on connector if (connector && !Utils.isNullOrUndefined(connector.energyActiveImportRegisterValue) && connector.energyActiveImportRegisterValue >= 0 && !Utils.isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) && connector.transactionEnergyActiveImportRegisterValue >= 0) { diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 92373cf0..b04814a7 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -108,7 +108,7 @@ export default class Utils { return sign * (randomPositiveFloat * (max - min) + min); } - public static getRandomInt(max: number, min = 0): number { + public static getRandomInteger(max: number, min = 0): number { if (max < 0) { throw new RangeError('Invalid interval'); } -- 2.34.1