From bca95e23dc201baff7c0e0edb43b4dc66ac70d2b Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 11 Jun 2026 20:29:00 -0500 Subject: [PATCH 1/7] log matches --- .env.example | 1 + .gitignore | 6 + package-lock.json | 531 +++++++++++++++++++++++++++++-- package.json | 3 + src/MatchLog/MatchLog.module.ts | 8 + src/MatchLog/MatchLog.service.ts | 113 +++++++ src/events/events.gateway.ts | 15 +- src/events/events.module.ts | 2 + src/events/utils.ts | 19 ++ src/main.ts | 1 + 10 files changed, 674 insertions(+), 25 deletions(-) create mode 100644 .env.example create mode 100644 src/MatchLog/MatchLog.module.ts create mode 100644 src/MatchLog/MatchLog.service.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a0cd5f1 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +SQLITE3_DB_PATH=C:\database.db diff --git a/.gitignore b/.gitignore index a410be6..6dd9a07 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,12 @@ /node_modules /build +# Persistent data +/data + +# Environment variables +.env + # Logs logs *.log diff --git a/package-lock.json b/package-lock.json index ec6a3ab..efce0dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "@nestjs/platform-express": "^10.3.7", "@nestjs/platform-ws": "^10.3.7", "@nestjs/websockets": "^10.3.7", + "better-sqlite3": "^12.10.0", + "dotenv": "^17.4.2", "lodash": "^4.17.21", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", @@ -25,6 +27,7 @@ "@nestjs/cli": "^9.0.0", "@nestjs/schematics": "^9.0.0", "@nestjs/testing": "^10.3.7", + "@types/better-sqlite3": "^7.6.13", "@types/express": "^4.17.13", "@types/jest": "28.1.8", "@types/lodash": "^4.17.13", @@ -1873,6 +1876,16 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -2922,7 +2935,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -2948,6 +2960,20 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/better-sqlite3": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", + "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2957,11 +2983,19 @@ "node": ">=8" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -2972,7 +3006,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -3092,7 +3125,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -3260,6 +3292,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -3592,12 +3630,36 @@ "node": ">=0.10.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3651,6 +3713,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3712,6 +3783,18 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3753,7 +3836,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -4271,6 +4353,15 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", @@ -4459,6 +4550,12 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -4609,6 +4706,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -4711,6 +4814,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5376,7 +5485,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -5468,6 +5576,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/inquirer": { "version": "8.2.5", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", @@ -6752,6 +6866,18 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -6814,6 +6940,12 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6843,6 +6975,12 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6878,6 +7016,18 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -7340,6 +7490,33 @@ "node": ">=4" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7446,7 +7623,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -7535,6 +7711,30 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -7971,7 +8171,6 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -7986,7 +8185,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -7997,8 +8195,7 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/send": { "version": "0.18.0", @@ -8126,6 +8323,51 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -8538,6 +8780,48 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -8928,6 +9212,18 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -10820,6 +11116,15 @@ "@babel/types": "^7.3.0" } }, + "@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -11641,8 +11946,7 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "base64id": { "version": "2.0.0", @@ -11651,17 +11955,33 @@ "optional": true, "peer": true }, + "better-sqlite3": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", + "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", + "requires": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "requires": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -11672,7 +11992,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -11767,7 +12086,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -11866,6 +12184,11 @@ "readdirp": "~3.6.0" } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -12111,12 +12434,25 @@ } } }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -12154,6 +12490,11 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -12200,6 +12541,11 @@ "esutils": "^2.0.2" } }, + "dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==" + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -12232,7 +12578,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -12602,6 +12947,11 @@ "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", "dev": true }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, "expect": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", @@ -12770,6 +13120,11 @@ "flat-cache": "^3.0.4" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -12887,6 +13242,11 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -12961,6 +13321,11 @@ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -13442,8 +13807,7 @@ "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "ignore": { "version": "5.2.4", @@ -13497,6 +13861,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "inquirer": { "version": "8.2.5", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", @@ -14466,6 +14835,11 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, "min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -14510,6 +14884,11 @@ "minimist": "^1.2.6" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -14536,6 +14915,11 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -14565,6 +14949,14 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "requires": { + "semver": "^7.3.5" + } + }, "node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -14894,6 +15286,25 @@ "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "dev": true }, + "prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -14969,7 +15380,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -15026,6 +15436,24 @@ "unpipe": "1.0.0" } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + } + } + }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -15343,7 +15771,6 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, "requires": { "lru-cache": "^6.0.0" }, @@ -15352,7 +15779,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -15360,8 +15786,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, @@ -15474,6 +15899,21 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -15791,6 +16231,41 @@ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true }, + "tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -16051,6 +16526,14 @@ } } }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index d32fd2b..b779599 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "@nestjs/platform-express": "^10.3.7", "@nestjs/platform-ws": "^10.3.7", "@nestjs/websockets": "^10.3.7", + "better-sqlite3": "^12.10.0", + "dotenv": "^17.4.2", "lodash": "^4.17.21", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", @@ -43,6 +45,7 @@ "@nestjs/cli": "^9.0.0", "@nestjs/schematics": "^9.0.0", "@nestjs/testing": "^10.3.7", + "@types/better-sqlite3": "^7.6.13", "@types/express": "^4.17.13", "@types/jest": "28.1.8", "@types/lodash": "^4.17.13", diff --git a/src/MatchLog/MatchLog.module.ts b/src/MatchLog/MatchLog.module.ts new file mode 100644 index 0000000..7d1abb3 --- /dev/null +++ b/src/MatchLog/MatchLog.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { MatchLogService } from './MatchLog.service'; + +@Module({ + providers: [MatchLogService], + exports: [MatchLogService], +}) +export class MatchLogModule {} diff --git a/src/MatchLog/MatchLog.service.ts b/src/MatchLog/MatchLog.service.ts new file mode 100644 index 0000000..7e84235 --- /dev/null +++ b/src/MatchLog/MatchLog.service.ts @@ -0,0 +1,113 @@ +import { Injectable, OnApplicationShutdown } from '@nestjs/common'; +import * as path from 'path'; +import * as fs from 'fs'; +import Database = require('better-sqlite3'); +import { v4 as uuidv4 } from 'uuid'; +import { Lobby, Player } from '../types/models.types'; + +@Injectable() +export class MatchLogService implements OnApplicationShutdown { + private readonly db: Database.Database; + + constructor() { + const dbPath = + process.env.SQLITE3_DB_PATH || + path.join(__dirname, '../../data/matches.db'); + const dataDir = path.dirname(dbPath); + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } + + this.db = new Database(dbPath); + this.db.exec(` + CREATE TABLE IF NOT EXISTS scores ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + matchId TEXT NOT NULL, + dateAdded INTEGER NOT NULL, + lobbyCode TEXT NOT NULL, + playerId TEXT NOT NULL, + profileName TEXT NOT NULL, + score INTEGER, + exScore INTEGER, + fantasticPlus INTEGER, + fantastics INTEGER, + excellents INTEGER, + greats INTEGER, + decents INTEGER, + wayOffs INTEGER, + misses INTEGER, + holdsHeld INTEGER, + rollsHeld INTEGER, + minesHit INTEGER, + songTitle TEXT, + songArtist TEXT, + songPath TEXT, + totalSteps INTEGER, + totalHolds INTEGER, + totalRolls INTEGER, + totalMines INTEGER + ) + `); + } + + /** + * Records each player's score for a completed match, once all players in + * the lobby have reached the evaluation/results screen. Every row from the + * same match shares the same matchId and dateAdded values. + */ + logMatch(lobby: Lobby): void { + const players = Object.values(lobby.machines).flatMap((machine) => + [machine.player1, machine.player2].filter( + (player): player is Player => player !== undefined, + ), + ); + + const matchId = uuidv4(); + const dateAdded = Date.now(); + const insertQuery = this.db.prepare( + `INSERT INTO scores + (matchId, dateAdded, lobbyCode, playerId, profileName, score, exScore, + fantasticPlus, fantastics, excellents, greats, decents, wayOffs, + misses, totalSteps, minesHit, totalMines, holdsHeld, totalHolds, + rollsHeld, totalRolls, songTitle, songArtist, songPath) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ); + + const insertManyScores = this.db.transaction(() => { + for (const player of players) { + const judgments = player.judgments; + insertQuery.run( + matchId, + dateAdded, + lobby.code, + player.playerId, + player.profileName, + player.score ?? null, + player.exScore ?? null, + judgments?.fantasticPlus ?? null, + judgments?.fantastics ?? null, + judgments?.excellents ?? null, + judgments?.greats ?? null, + judgments?.decents ?? null, + judgments?.wayOffs ?? null, + judgments?.misses ?? null, + judgments?.totalSteps ?? null, + judgments?.minesHit ?? null, + judgments?.totalMines ?? null, + judgments?.holdsHeld ?? null, + judgments?.totalHolds ?? null, + judgments?.rollsHeld ?? null, + judgments?.totalRolls ?? null, + lobby.songInfo?.title ?? null, + lobby.songInfo?.artist ?? null, + lobby.songInfo?.songPath ?? null, + ); + } + }); + insertManyScores(); + } + + onApplicationShutdown(): void { + this.db.close(); + } +} diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 4c3a917..ecfda90 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -20,6 +20,7 @@ import { getPlayerCountForLobby, RETAINED_PLAYER_KEYS, inSongSelect, + isInScreenEvaluationStage, responseStatusFailure, } from './utils'; import { @@ -38,6 +39,7 @@ import { import { merge, pick } from 'lodash'; import { ClientService } from '../clients/client.service'; +import { MatchLogService } from '../MatchLog/MatchLog.service'; @WebSocketGateway({ cors: { @@ -63,7 +65,10 @@ export class EventsGateway private cleanupIntervalId: NodeJS.Timeout | null = null; - constructor(private readonly clients: ClientService) {} + constructor( + private readonly clients: ClientService, + private readonly matchLog: MatchLogService, + ) {} afterInit() { this.handlers = { @@ -366,8 +371,16 @@ export class EventsGateway // Merge the incoming machine data with the respective lobby's machine const playersInSongSelectBefore = inSongSelect(lobby); + const isInScreenEvaluationStageBefore = isInScreenEvaluationStage(lobby); merge(lobby.machines[socketId], machine); const playersInSongSelectAfter = inSongSelect(lobby); + const isInScreenEvaluationStageAfter = isInScreenEvaluationStage(lobby); + + // If all players have just reached the evaluation/results screen, + // log the completed match. + if (!isInScreenEvaluationStageBefore && isInScreenEvaluationStageAfter) { + this.matchLog.logMatch(lobby); + } // If all players have transitioned back to song select, // Ensure the scores and currently-selected song get reset diff --git a/src/events/events.module.ts b/src/events/events.module.ts index 74bd8d1..683f25a 100644 --- a/src/events/events.module.ts +++ b/src/events/events.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; import { EventsGateway } from './events.gateway'; import { ClientService } from '../clients/client.service'; +import { MatchLogModule } from '../MatchLog/MatchLog.module'; @Module({ + imports: [MatchLogModule], providers: [EventsGateway, ClientService], }) export class EventsModule {} diff --git a/src/events/utils.ts b/src/events/utils.ts index 8f5c5ff..d6efbe3 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -157,3 +157,22 @@ export function inSongSelect(lobby: Lobby): boolean { }); return selecting; } + +/** + * Determines if every player across every machine in the lobby has reached + * the evaluation/results screen. + */ +export function isInScreenEvaluationStage(lobby: Lobby): boolean { + let isInScreenEvaluationStage = true; + Object.values(lobby.machines).forEach(({ player1, player2 }) => { + if (player1 && player1.screenName !== 'ScreenEvaluationStage') { + isInScreenEvaluationStage = false; + return; + } + if (player2 && player2.screenName !== 'ScreenEvaluationStage') { + isInScreenEvaluationStage = false; + return; + } + }); + return isInScreenEvaluationStage; +} diff --git a/src/main.ts b/src/main.ts index bff6961..32a7a35 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,4 @@ +import 'dotenv/config'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { LOBBYMAN } from './types/models.types'; From 7bc299268a321afaf2c583f21e110fa7f0e40571 Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 11 Jun 2026 21:06:25 -0500 Subject: [PATCH 2/7] GET /match/list --- src/MatchLog/MatchLog.service.ts | 98 ++++++++++++++++++++++++-------- src/MatchLog/MatchLog.types.ts | 46 +++++++++++++++ src/app.controller.spec.ts | 2 + src/app.controller.ts | 12 +++- src/app.module.ts | 3 +- 5 files changed, 134 insertions(+), 27 deletions(-) create mode 100644 src/MatchLog/MatchLog.types.ts diff --git a/src/MatchLog/MatchLog.service.ts b/src/MatchLog/MatchLog.service.ts index 7e84235..e151015 100644 --- a/src/MatchLog/MatchLog.service.ts +++ b/src/MatchLog/MatchLog.service.ts @@ -3,7 +3,9 @@ import * as path from 'path'; import * as fs from 'fs'; import Database = require('better-sqlite3'); import { v4 as uuidv4 } from 'uuid'; +import { omit } from 'lodash'; import { Lobby, Player } from '../types/models.types'; +import { Match, NewScoreRow, ScoreRow } from './MatchLog.types'; @Injectable() export class MatchLogService implements OnApplicationShutdown { @@ -70,43 +72,89 @@ export class MatchLogService implements OnApplicationShutdown { fantasticPlus, fantastics, excellents, greats, decents, wayOffs, misses, totalSteps, minesHit, totalMines, holdsHeld, totalHolds, rollsHeld, totalRolls, songTitle, songArtist, songPath) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + VALUES + (@matchId, @dateAdded, @lobbyCode, @playerId, @profileName, @score, @exScore, + @fantasticPlus, @fantastics, @excellents, @greats, @decents, @wayOffs, + @misses, @totalSteps, @minesHit, @totalMines, @holdsHeld, @totalHolds, + @rollsHeld, @totalRolls, @songTitle, @songArtist, @songPath)`, ); const insertManyScores = this.db.transaction(() => { for (const player of players) { const judgments = player.judgments; - insertQuery.run( + const row: NewScoreRow = { matchId, dateAdded, - lobby.code, - player.playerId, - player.profileName, - player.score ?? null, - player.exScore ?? null, - judgments?.fantasticPlus ?? null, - judgments?.fantastics ?? null, - judgments?.excellents ?? null, - judgments?.greats ?? null, - judgments?.decents ?? null, - judgments?.wayOffs ?? null, - judgments?.misses ?? null, - judgments?.totalSteps ?? null, - judgments?.minesHit ?? null, - judgments?.totalMines ?? null, - judgments?.holdsHeld ?? null, - judgments?.totalHolds ?? null, - judgments?.rollsHeld ?? null, - judgments?.totalRolls ?? null, - lobby.songInfo?.title ?? null, - lobby.songInfo?.artist ?? null, - lobby.songInfo?.songPath ?? null, - ); + lobbyCode: lobby.code, + playerId: player.playerId, + profileName: player.profileName, + score: player.score ?? null, + exScore: player.exScore ?? null, + fantasticPlus: judgments?.fantasticPlus ?? null, + fantastics: judgments?.fantastics ?? null, + excellents: judgments?.excellents ?? null, + greats: judgments?.greats ?? null, + decents: judgments?.decents ?? null, + wayOffs: judgments?.wayOffs ?? null, + misses: judgments?.misses ?? null, + totalSteps: judgments?.totalSteps ?? null, + minesHit: judgments?.minesHit ?? null, + totalMines: judgments?.totalMines ?? null, + holdsHeld: judgments?.holdsHeld ?? null, + totalHolds: judgments?.totalHolds ?? null, + rollsHeld: judgments?.rollsHeld ?? null, + totalRolls: judgments?.totalRolls ?? null, + songTitle: lobby.songInfo?.title ?? null, + songArtist: lobby.songInfo?.artist ?? null, + songPath: lobby.songInfo?.songPath ?? null, + }; + insertQuery.run(row); } }); insertManyScores(); } + /** + * Returns all logged matches, sorted reverse chronologically (most recent + * first). + */ + getMatches(): Match[] { + const rows = this.db + .prepare<[], ScoreRow>( + 'SELECT * FROM scores ORDER BY dateAdded DESC, id ASC', + ) + .all(); + + const matchesById = new Map(); + for (const row of rows) { + let match = matchesById.get(row.matchId); + if (!match) { + match = { + matchId: row.matchId, + dateAdded: row.dateAdded, + lobbyCode: row.lobbyCode, + isSameSong: true, + scores: [], + }; + matchesById.set(row.matchId, match); + } + + match.scores.push(omit(row, ['matchId', 'dateAdded', 'lobbyCode'])); + } + + for (const match of matchesById.values()) { + const [first, ...rest] = match.scores; + match.isSameSong = rest.every( + (score) => + score.songTitle === first.songTitle && + score.songArtist === first.songArtist && + score.songPath === first.songPath, + ); + } + + return Array.from(matchesById.values()); + } + onApplicationShutdown(): void { this.db.close(); } diff --git a/src/MatchLog/MatchLog.types.ts b/src/MatchLog/MatchLog.types.ts new file mode 100644 index 0000000..296956f --- /dev/null +++ b/src/MatchLog/MatchLog.types.ts @@ -0,0 +1,46 @@ +import { PlayerId } from '../types/models.types'; + +/** A single row in the `scores` table. */ +export interface ScoreRow { + id: number; + matchId: string; + dateAdded: number; + lobbyCode: string; + playerId: PlayerId; + profileName: string; + score: number | null; + exScore: number | null; + fantasticPlus: number | null; + fantastics: number | null; + excellents: number | null; + greats: number | null; + decents: number | null; + wayOffs: number | null; + misses: number | null; + totalSteps: number | null; + minesHit: number | null; + totalMines: number | null; + holdsHeld: number | null; + totalHolds: number | null; + rollsHeld: number | null; + totalRolls: number | null; + songTitle: string | null; + songArtist: string | null; + songPath: string | null; +} + +/** A single player's score within a match. */ +export type PlayerScore = Omit; + +/** A row to be inserted into the `scores` table (the `id` is auto-assigned). */ +export type NewScoreRow = Omit; + +/** A completed match, made up of one score per player. */ +export interface Match { + matchId: string; + dateAdded: number; + lobbyCode: string; + /** True if every player's songTitle, songArtist, and songPath match. */ + isSameSong: boolean; + scores: PlayerScore[]; +} diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts index d22f389..d9f43ea 100644 --- a/src/app.controller.spec.ts +++ b/src/app.controller.spec.ts @@ -1,12 +1,14 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { MatchLogModule } from './MatchLog/MatchLog.module'; describe('AppController', () => { let appController: AppController; beforeEach(async () => { const app: TestingModule = await Test.createTestingModule({ + imports: [MatchLogModule], controllers: [AppController], providers: [AppService], }).compile(); diff --git a/src/app.controller.ts b/src/app.controller.ts index cce879e..64e83e8 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,12 +1,22 @@ import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; +import { MatchLogService } from './MatchLog/MatchLog.service'; +import { Match } from './MatchLog/MatchLog.types'; @Controller() export class AppController { - constructor(private readonly appService: AppService) {} + constructor( + private readonly appService: AppService, + private readonly matchLogService: MatchLogService, + ) {} @Get() getHello(): string { return this.appService.getHello(); } + + @Get('match/list') + getMatchList(): Match[] { + return this.matchLogService.getMatches(); + } } diff --git a/src/app.module.ts b/src/app.module.ts index f8eac6f..6d12e67 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,9 +3,10 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { EventsModule } from './events/events.module'; import { ClientModule } from './clients/client.module'; +import { MatchLogModule } from './MatchLog/MatchLog.module'; @Module({ - imports: [EventsModule, ClientModule], + imports: [EventsModule, ClientModule, MatchLogModule], controllers: [AppController], providers: [AppService], }) From 6c2e69c6cef0dd545e40c0fcdf70b68779269341 Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 11 Jun 2026 21:39:32 -0500 Subject: [PATCH 3/7] enable cors --- src/events/events.gateway.ts | 6 +----- src/main.ts | 1 + 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index ecfda90..9e61056 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -41,11 +41,7 @@ import { merge, pick } from 'lodash'; import { ClientService } from '../clients/client.service'; import { MatchLogService } from '../MatchLog/MatchLog.service'; -@WebSocketGateway({ - cors: { - origin: '*', - }, -}) +@WebSocketGateway() export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect, OnApplicationShutdown { diff --git a/src/main.ts b/src/main.ts index 32a7a35..9fefca6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,7 @@ import { WsAdapter } from '@nestjs/platform-ws'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.enableCors(); app.useWebSocketAdapter(new WsAdapter(app)); LOBBYMAN.lobbies = {}; From 3696afad89f89f59db2db7e6cb0e8bbc74158741 Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 11 Jun 2026 23:42:46 -0500 Subject: [PATCH 4/7] extend lobby timeout to 2 hours --- src/events/events.gateway.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 9e61056..b48c78a 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -57,7 +57,7 @@ export class EventsGateway /** Cleanup interval in milliseconds */ private readonly CLEANUP_INTERVAL = 30000; // 30 seconds /** Lobby inactivity timeout in milliseconds */ - private readonly LOBBY_INACTIVITY_TIMEOUT = 30 * 60 * 1000; // 30 minutes + private readonly LOBBY_INACTIVITY_TIMEOUT = 120 * 60 * 1000; // 120 minutes private cleanupIntervalId: NodeJS.Timeout | null = null; From 6c957de07262473facc949c3226861543d6e0403 Mon Sep 17 00:00:00 2001 From: Vincent Date: Thu, 11 Jun 2026 23:43:00 -0500 Subject: [PATCH 5/7] update schema to detect incorrect songs --- src/MatchLog/MatchLog.service.ts | 146 ++++++++++++++++++------------- src/MatchLog/MatchLog.types.ts | 39 +++++---- 2 files changed, 107 insertions(+), 78 deletions(-) diff --git a/src/MatchLog/MatchLog.service.ts b/src/MatchLog/MatchLog.service.ts index e151015..f9a2e3b 100644 --- a/src/MatchLog/MatchLog.service.ts +++ b/src/MatchLog/MatchLog.service.ts @@ -5,7 +5,14 @@ import Database = require('better-sqlite3'); import { v4 as uuidv4 } from 'uuid'; import { omit } from 'lodash'; import { Lobby, Player } from '../types/models.types'; -import { Match, NewScoreRow, ScoreRow } from './MatchLog.types'; +import { + Match, + MatchRow, + NewMatchRow, + NewScoreRow, + PlayerScore, + ScoreRow, +} from './MatchLog.types'; @Injectable() export class MatchLogService implements OnApplicationShutdown { @@ -21,12 +28,24 @@ export class MatchLogService implements OnApplicationShutdown { } this.db = new Database(dbPath); + this.db.exec(` + CREATE TABLE IF NOT EXISTS matches ( + id TEXT PRIMARY KEY, + dateAdded INTEGER NOT NULL, + lobbyCode TEXT NOT NULL, + songTitle TEXT, + songArtist TEXT, + songPath TEXT, + totalSteps INTEGER, + totalHolds INTEGER, + totalRolls INTEGER, + totalMines INTEGER + ) + `); this.db.exec(` CREATE TABLE IF NOT EXISTS scores ( id INTEGER PRIMARY KEY AUTOINCREMENT, matchId TEXT NOT NULL, - dateAdded INTEGER NOT NULL, - lobbyCode TEXT NOT NULL, playerId TEXT NOT NULL, profileName TEXT NOT NULL, score INTEGER, @@ -38,24 +57,19 @@ export class MatchLogService implements OnApplicationShutdown { decents INTEGER, wayOffs INTEGER, misses INTEGER, + minesHit INTEGER, holdsHeld INTEGER, rollsHeld INTEGER, - minesHit INTEGER, - songTitle TEXT, - songArtist TEXT, - songPath TEXT, - totalSteps INTEGER, - totalHolds INTEGER, - totalRolls INTEGER, - totalMines INTEGER + isValid INTEGER NOT NULL ) `); } /** * Records each player's score for a completed match, once all players in - * the lobby have reached the evaluation/results screen. Every row from the - * same match shares the same matchId and dateAdded values. + * the lobby have reached the evaluation/results screen. Song info and + * chart totals are recorded once per match; each player gets their own + * row in the `scores` table. */ logMatch(lobby: Lobby): void { const players = Object.values(lobby.machines).flatMap((machine) => @@ -66,26 +80,57 @@ export class MatchLogService implements OnApplicationShutdown { const matchId = uuidv4(); const dateAdded = Date.now(); - const insertQuery = this.db.prepare( + const chartTotals = players.find((player) => player.judgments)?.judgments; + + const matchRow: NewMatchRow = { + id: matchId, + dateAdded, + lobbyCode: lobby.code, + songTitle: lobby.songInfo?.title ?? null, + songArtist: lobby.songInfo?.artist ?? null, + songPath: lobby.songInfo?.songPath ?? null, + totalSteps: chartTotals?.totalSteps ?? null, + totalHolds: chartTotals?.totalHolds ?? null, + totalRolls: chartTotals?.totalRolls ?? null, + totalMines: chartTotals?.totalMines ?? null, + }; + + this.db + .prepare( + `INSERT INTO matches + (id, dateAdded, lobbyCode, songTitle, songArtist, songPath, + totalSteps, totalHolds, totalRolls, totalMines) + VALUES + (@id, @dateAdded, @lobbyCode, @songTitle, @songArtist, @songPath, + @totalSteps, @totalHolds, @totalRolls, @totalMines)`, + ) + .run(matchRow); + + const insertScore = this.db.prepare( `INSERT INTO scores - (matchId, dateAdded, lobbyCode, playerId, profileName, score, exScore, - fantasticPlus, fantastics, excellents, greats, decents, wayOffs, - misses, totalSteps, minesHit, totalMines, holdsHeld, totalHolds, - rollsHeld, totalRolls, songTitle, songArtist, songPath) + (matchId, playerId, profileName, score, exScore, fantasticPlus, + fantastics, excellents, greats, decents, wayOffs, misses, minesHit, + holdsHeld, rollsHeld, isValid) VALUES - (@matchId, @dateAdded, @lobbyCode, @playerId, @profileName, @score, @exScore, - @fantasticPlus, @fantastics, @excellents, @greats, @decents, @wayOffs, - @misses, @totalSteps, @minesHit, @totalMines, @holdsHeld, @totalHolds, - @rollsHeld, @totalRolls, @songTitle, @songArtist, @songPath)`, + (@matchId, @playerId, @profileName, @score, @exScore, @fantasticPlus, + @fantastics, @excellents, @greats, @decents, @wayOffs, @misses, @minesHit, + @holdsHeld, @rollsHeld, @isValid)`, ); const insertManyScores = this.db.transaction(() => { for (const player of players) { const judgments = player.judgments; + const judgmentSum = + (judgments?.fantasticPlus ?? 0) + + (judgments?.fantastics ?? 0) + + (judgments?.excellents ?? 0) + + (judgments?.greats ?? 0) + + (judgments?.decents ?? 0) + + (judgments?.wayOffs ?? 0) + + (judgments?.misses ?? 0); + const row: NewScoreRow = { matchId, - dateAdded, - lobbyCode: lobby.code, playerId: player.playerId, profileName: player.profileName, score: player.score ?? null, @@ -97,18 +142,12 @@ export class MatchLogService implements OnApplicationShutdown { decents: judgments?.decents ?? null, wayOffs: judgments?.wayOffs ?? null, misses: judgments?.misses ?? null, - totalSteps: judgments?.totalSteps ?? null, minesHit: judgments?.minesHit ?? null, - totalMines: judgments?.totalMines ?? null, holdsHeld: judgments?.holdsHeld ?? null, - totalHolds: judgments?.totalHolds ?? null, rollsHeld: judgments?.rollsHeld ?? null, - totalRolls: judgments?.totalRolls ?? null, - songTitle: lobby.songInfo?.title ?? null, - songArtist: lobby.songInfo?.artist ?? null, - songPath: lobby.songInfo?.songPath ?? null, + isValid: judgmentSum === matchRow.totalSteps ? 1 : 0, }; - insertQuery.run(row); + insertScore.run(row); } }); insertManyScores(); @@ -119,40 +158,25 @@ export class MatchLogService implements OnApplicationShutdown { * first). */ getMatches(): Match[] { - const rows = this.db - .prepare<[], ScoreRow>( - 'SELECT * FROM scores ORDER BY dateAdded DESC, id ASC', - ) + const matchRows = this.db + .prepare<[], MatchRow>('SELECT * FROM matches ORDER BY dateAdded DESC') .all(); - const matchesById = new Map(); - for (const row of rows) { - let match = matchesById.get(row.matchId); - if (!match) { - match = { - matchId: row.matchId, - dateAdded: row.dateAdded, - lobbyCode: row.lobbyCode, - isSameSong: true, - scores: [], - }; - matchesById.set(row.matchId, match); - } - - match.scores.push(omit(row, ['matchId', 'dateAdded', 'lobbyCode'])); - } + const scoreRows = this.db + .prepare<[], ScoreRow>('SELECT * FROM scores') + .all(); - for (const match of matchesById.values()) { - const [first, ...rest] = match.scores; - match.isSameSong = rest.every( - (score) => - score.songTitle === first.songTitle && - score.songArtist === first.songArtist && - score.songPath === first.songPath, - ); + const scoresByMatchId = new Map(); + for (const row of scoreRows) { + const scores = scoresByMatchId.get(row.matchId) ?? []; + scores.push(omit(row, ['matchId'])); + scoresByMatchId.set(row.matchId, scores); } - return Array.from(matchesById.values()); + return matchRows.map((match) => ({ + ...match, + scores: scoresByMatchId.get(match.id) ?? [], + })); } onApplicationShutdown(): void { diff --git a/src/MatchLog/MatchLog.types.ts b/src/MatchLog/MatchLog.types.ts index 296956f..a62d9cc 100644 --- a/src/MatchLog/MatchLog.types.ts +++ b/src/MatchLog/MatchLog.types.ts @@ -1,11 +1,23 @@ import { PlayerId } from '../types/models.types'; +/** A single row in the `matches` table. */ +export interface MatchRow { + id: string; + dateAdded: number; + lobbyCode: string; + songTitle: string | null; + songArtist: string | null; + songPath: string | null; + totalSteps: number | null; + totalHolds: number | null; + totalRolls: number | null; + totalMines: number | null; +} + /** A single row in the `scores` table. */ export interface ScoreRow { id: number; matchId: string; - dateAdded: number; - lobbyCode: string; playerId: PlayerId; profileName: string; score: number | null; @@ -17,30 +29,23 @@ export interface ScoreRow { decents: number | null; wayOffs: number | null; misses: number | null; - totalSteps: number | null; minesHit: number | null; - totalMines: number | null; holdsHeld: number | null; - totalHolds: number | null; rollsHeld: number | null; - totalRolls: number | null; - songTitle: string | null; - songArtist: string | null; - songPath: string | null; + /** 1 if the sum of judgments matches the match's totalSteps, 0 otherwise. */ + isValid: number; } -/** A single player's score within a match. */ -export type PlayerScore = Omit; +/** A row to be inserted into the `matches` table. */ +export type NewMatchRow = MatchRow; /** A row to be inserted into the `scores` table (the `id` is auto-assigned). */ export type NewScoreRow = Omit; +/** A single player's score within a match. */ +export type PlayerScore = Omit; + /** A completed match, made up of one score per player. */ -export interface Match { - matchId: string; - dateAdded: number; - lobbyCode: string; - /** True if every player's songTitle, songArtist, and songPath match. */ - isSameSong: boolean; +export interface Match extends MatchRow { scores: PlayerScore[]; } From 064f621c47d50ce4aeaabd2bb8f54fabf608400d Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 12 Jun 2026 01:05:47 -0500 Subject: [PATCH 6/7] use websockets to push match completions --- src/MatchLog/MatchLog.service.ts | 14 +++++++++++--- src/events/events.gateway.ts | 3 ++- src/events/events.types.ts | 9 +++++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/MatchLog/MatchLog.service.ts b/src/MatchLog/MatchLog.service.ts index f9a2e3b..1a87b7d 100644 --- a/src/MatchLog/MatchLog.service.ts +++ b/src/MatchLog/MatchLog.service.ts @@ -71,7 +71,7 @@ export class MatchLogService implements OnApplicationShutdown { * chart totals are recorded once per match; each player gets their own * row in the `scores` table. */ - logMatch(lobby: Lobby): void { + logMatch(lobby: Lobby): Match { const players = Object.values(lobby.machines).flatMap((machine) => [machine.player1, machine.player2].filter( (player): player is Player => player !== undefined, @@ -118,6 +118,7 @@ export class MatchLogService implements OnApplicationShutdown { ); const insertManyScores = this.db.transaction(() => { + const scores: PlayerScore[] = []; for (const player of players) { const judgments = player.judgments; const judgmentSum = @@ -147,10 +148,17 @@ export class MatchLogService implements OnApplicationShutdown { rollsHeld: judgments?.rollsHeld ?? null, isValid: judgmentSum === matchRow.totalSteps ? 1 : 0, }; - insertScore.run(row); + const { lastInsertRowid } = insertScore.run(row); + scores.push({ ...omit(row, ['matchId']), id: Number(lastInsertRowid) }); } + return scores; }); - insertManyScores(); + const scores = insertManyScores(); + + return { + ...matchRow, + scores, + }; } /** diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index b48c78a..0281139 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -375,7 +375,8 @@ export class EventsGateway // If all players have just reached the evaluation/results screen, // log the completed match. if (!isInScreenEvaluationStageBefore && isInScreenEvaluationStageAfter) { - this.matchLog.logMatch(lobby); + const match = this.matchLog.logMatch(lobby); + this.clients.sendAll({ event: 'matchLogged', data: match }); } // If all players have transitioned back to song select, diff --git a/src/events/events.types.ts b/src/events/events.types.ts index 912561d..a710eec 100644 --- a/src/events/events.types.ts +++ b/src/events/events.types.ts @@ -7,6 +7,7 @@ import { SongInfo, Spectator, } from '../types/models.types'; +import { Match } from '../MatchLog/MatchLog.types'; export type EventType = | 'createLobby' @@ -26,7 +27,8 @@ export type EventType = | 'lobbyState' | 'selectSong' | 'responseStatus' - | 'startSong'; + | 'startSong' + | 'matchLogged'; export type EventData = | CreateLobbyData @@ -42,7 +44,8 @@ export type EventData = | ReadyUpResultPayload | LobbyStatePayload | SelectSongPayload - | StartSongPayload; + | StartSongPayload + | MatchLoggedPayload; export interface EventMessage { event: EventType; @@ -125,3 +128,5 @@ export interface LobbyStatePayload { export interface StartSongPayload { start: boolean; } + +export type MatchLoggedPayload = Match; From dfa68c508ab6cc580f73d439d6752b702035d77a Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 12 Jun 2026 02:39:05 -0500 Subject: [PATCH 7/7] expose lobby list, allow subscribing to lobby changes --- src/app.controller.ts | 6 ++++ src/events/events.gateway.spec.ts | 20 +++++++++-- src/events/events.gateway.ts | 55 ++++++++++++++++++++++++------- src/events/events.types.ts | 22 +++++++++++-- src/events/utils.ts | 11 ++++--- 5 files changed, 92 insertions(+), 22 deletions(-) diff --git a/src/app.controller.ts b/src/app.controller.ts index 64e83e8..47ee568 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; import { MatchLogService } from './MatchLog/MatchLog.service'; import { Match } from './MatchLog/MatchLog.types'; +import { Lobby, LOBBYMAN } from './types/models.types'; @Controller() export class AppController { @@ -19,4 +20,9 @@ export class AppController { getMatchList(): Match[] { return this.matchLogService.getMatches(); } + + @Get('lobby/list') + getLobbyList(): Lobby[] { + return Object.values(LOBBYMAN.lobbies); + } } diff --git a/src/events/events.gateway.spec.ts b/src/events/events.gateway.spec.ts index e9dc989..45b116d 100644 --- a/src/events/events.gateway.spec.ts +++ b/src/events/events.gateway.spec.ts @@ -319,14 +319,28 @@ describe('EventsGateway', () => { }); }); +// Broadcast-only events used to keep the lobby list in sync. These can +// arrive interleaved with request/response messages, so `send` ignores them. +const LOBBY_LIST_BROADCAST_EVENTS = new Set([ + 'lobbyAdded', + 'lobbyUpdated', + 'lobbyRemoved', +]); + function send( client: WebSocket, message: EventMessage, ): Promise> { return new Promise((resolve) => { - client.on('message', (response: EventMessage) => { - resolve(JSON.parse(response.toString())); - }); + const handler = (response: EventMessage) => { + const parsed = JSON.parse(response.toString()); + if (LOBBY_LIST_BROADCAST_EVENTS.has(parsed.event)) { + return; + } + client.off('message', handler); + resolve(parsed); + }; + client.on('message', handler); client.send(JSON.stringify(message)); }); } diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 0281139..31ae129 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -83,17 +83,31 @@ export class EventsGateway } /** - * Updates a lobby's lastUpdate timestamp to track activity. - * This is used for inactivity-based cleanup of zombie lobbies. + * Updates a lobby's lastUpdate timestamp to track activity, and notifies + * all clients so they can update their lobby list in realtime. * @param code The lobby code to update */ private updateLobbyActivity(code: LobbyCode): void { const lobby = LOBBYMAN.lobbies[code]; if (lobby) { lobby.lastUpdate = Date.now(); + this.clients.sendAll({ event: 'lobbyUpdated', data: lobby }); } } + /** Notifies all clients that a new lobby was created. */ + private broadcastLobbyAdded(code: LobbyCode): void { + const lobby = LOBBYMAN.lobbies[code]; + if (lobby) { + this.clients.sendAll({ event: 'lobbyAdded', data: lobby }); + } + } + + /** Notifies all clients that a lobby was removed. */ + private broadcastLobbyRemoved(code: LobbyCode): void { + this.clients.sendAll({ event: 'lobbyRemoved', data: { code } }); + } + /** * Starts the periodic cleanup interval to remove stale lobbies. * Lobbies that haven't been updated for LOBBY_INACTIVITY_TIMEOUT are deleted. @@ -156,6 +170,7 @@ export class EventsGateway // Delete the lobby and its room delete ROOMMAN.rooms[code]; delete LOBBYMAN.lobbies[code]; + this.broadcastLobbyRemoved(code); } } } @@ -221,7 +236,11 @@ export class EventsGateway } if (socketId in LOBBYMAN.spectatorConnections) { - disconnectSpectator(socketId); + const oldCode = disconnectSpectator(socketId); + if (oldCode) { + this.updateLobbyActivity(oldCode); + this.broadcastLobbyState(oldCode); + } } } @@ -248,7 +267,11 @@ export class EventsGateway { machine, password }: CreateLobbyData, ): Promise { if (socketId in LOBBYMAN.spectatorConnections) { - disconnectSpectator(socketId); + const oldCode = disconnectSpectator(socketId); + if (oldCode) { + this.updateLobbyActivity(oldCode); + this.broadcastLobbyState(oldCode); + } } if (socketId in LOBBYMAN.machineConnections) { @@ -277,6 +300,7 @@ export class EventsGateway ROOMMAN.join(socketId, code); LOBBYMAN.machineConnections[socketId] = code; + this.broadcastLobbyAdded(code); this.updateLobbyActivity(code); this.broadcastLobbyState(code); @@ -310,7 +334,11 @@ export class EventsGateway } if (socketId in LOBBYMAN.spectatorConnections) { - disconnectSpectator(socketId); + const oldCode = disconnectSpectator(socketId); + if (oldCode) { + this.updateLobbyActivity(oldCode); + this.broadcastLobbyState(oldCode); + } } if (socketId in LOBBYMAN.machineConnections) { @@ -483,7 +511,11 @@ export class EventsGateway if (socketId in LOBBYMAN.spectatorConnections) { // A spectator can only spectate one lobby at a time. - disconnectSpectator(socketId); + const oldCode = disconnectSpectator(socketId); + if (oldCode && oldCode !== code.toUpperCase()) { + this.updateLobbyActivity(oldCode); + this.broadcastLobbyState(oldCode); + } } const lobby = LOBBYMAN.lobbies[code.toUpperCase()]; lobby.spectators[socketId] = { @@ -525,19 +557,18 @@ export class EventsGateway private getLobbyState( code: LobbyCode, ): EventMessage | null { - // Send back the machine state with the socket ids omitted - const players: Player[] = []; + const players: Array = []; const lobby = LOBBYMAN.lobbies[code]; if (lobby === undefined) { return null; } Object.values(lobby.machines).forEach((machine) => { - const { player1, player2 } = machine; + const { player1, player2, socketId } = machine; if (player1) { - players.push(player1); + players.push({ ...player1, socketId }); } if (player2) { - players.push(player2); + players.push({ ...player2, socketId }); } }); const { songInfo } = lobby; @@ -605,12 +636,14 @@ export class EventsGateway } delete ROOMMAN.rooms[code]; delete LOBBYMAN.lobbies[code]; + this.broadcastLobbyRemoved(code); } else { // When a client disconnects, notify other clients const stateMessage = this.getLobbyState(code); if (stateMessage) { this.clients.sendLobby(stateMessage, code); } + this.updateLobbyActivity(code); } return true; } diff --git a/src/events/events.types.ts b/src/events/events.types.ts index a710eec..de8eca0 100644 --- a/src/events/events.types.ts +++ b/src/events/events.types.ts @@ -1,9 +1,11 @@ import { + Lobby, LobbyCode, LobbyInfo, Machine, Player, PlayerId, + SocketId, SongInfo, Spectator, } from '../types/models.types'; @@ -28,7 +30,10 @@ export type EventType = | 'selectSong' | 'responseStatus' | 'startSong' - | 'matchLogged'; + | 'matchLogged' + | 'lobbyAdded' + | 'lobbyUpdated' + | 'lobbyRemoved'; export type EventData = | CreateLobbyData @@ -45,7 +50,10 @@ export type EventData = | LobbyStatePayload | SelectSongPayload | StartSongPayload - | MatchLoggedPayload; + | MatchLoggedPayload + | LobbyAddedPayload + | LobbyUpdatedPayload + | LobbyRemovedPayload; export interface EventMessage { event: EventType; @@ -119,7 +127,7 @@ export interface ClientDisconnectedPayload { } export interface LobbyStatePayload { - players: Array; + players: Array; spectators: Array; code: LobbyCode; songInfo?: SongInfo; @@ -130,3 +138,11 @@ export interface StartSongPayload { } export type MatchLoggedPayload = Match; + +export type LobbyAddedPayload = Lobby; + +export type LobbyUpdatedPayload = Lobby; + +export interface LobbyRemovedPayload { + code: LobbyCode; +} diff --git a/src/events/utils.ts b/src/events/utils.ts index d6efbe3..a43074c 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -1,6 +1,7 @@ import { LOBBYMAN, Lobby, + LobbyCode, Player, ROOMMAN, SocketId, @@ -75,20 +76,20 @@ export function getPlayerCountForLobby(lobby: Lobby): number { * @param socketId, The socket ID of the spectator to disconnect. * @returns, True if the spectator left the lobby, false otherwise. */ -export function disconnectSpectator(socketId: SocketId): boolean { +export function disconnectSpectator(socketId: SocketId): LobbyCode | undefined { const code = LOBBYMAN.spectatorConnections[socketId]; if (code === undefined) { - return false; + return undefined; } const lobby = LOBBYMAN.lobbies[code]; if (lobby === undefined) { - return false; + return undefined; } const spectator = lobby.spectators[socketId]; if (spectator === undefined) { - return false; + return undefined; } if (spectator.socketId) { @@ -97,7 +98,7 @@ export function disconnectSpectator(socketId: SocketId): boolean { } delete lobby.spectators[socketId]; delete LOBBYMAN.spectatorConnections[socketId]; - return true; + return code; } /** Gets the lobby for specific connection.