Compare commits
No commits in common. "88311881375bcd025072315878199fc5d3cccd85" and "c06854595875c4016f1ca24755449a4347dcaf32" have entirely different histories.
8831188137
...
c068545958
55
README.md
55
README.md
|
@ -2,57 +2,4 @@
|
||||||
|
|
||||||
This is a new MPP server currently in development for [MPP.dev](https://www.multiplayerpiano.dev). The original server is old and it needs a new one.
|
This is a new MPP server currently in development for [MPP.dev](https://www.multiplayerpiano.dev). The original server is old and it needs a new one.
|
||||||
|
|
||||||
This server uses Bun.
|
This new server will use uWebSockets.js, like [MPPClone](https://mppclone.com).
|
||||||
|
|
||||||
The commit history includes BopItFreak's server because this server is (debatably) a heavy reimplementation of my fork of it.
|
|
||||||
|
|
||||||
## How to run
|
|
||||||
|
|
||||||
0. Install bun
|
|
||||||
|
|
||||||
```
|
|
||||||
$ curl -fsSL https://bun.sh/install | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Configure
|
|
||||||
|
|
||||||
- Copy environment variables
|
|
||||||
|
|
||||||
```
|
|
||||||
$ cp .env.template .env
|
|
||||||
```
|
|
||||||
|
|
||||||
Edit `.env` to your needs.
|
|
||||||
|
|
||||||
- Edit the files in the `config` folder to match your needs
|
|
||||||
|
|
||||||
2. Install packages
|
|
||||||
|
|
||||||
```
|
|
||||||
$ bun i
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Setup database
|
|
||||||
|
|
||||||
```
|
|
||||||
$ bunx prisma generate
|
|
||||||
$ bunx prisma db push
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Build
|
|
||||||
|
|
||||||
```
|
|
||||||
$ bun run build
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Run
|
|
||||||
|
|
||||||
```
|
|
||||||
$ bun start
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
```
|
|
||||||
$ bun dev
|
|
||||||
```
|
|
||||||
|
|
|
@ -6,20 +6,9 @@ lobbySettings:
|
||||||
lobby: true
|
lobby: true
|
||||||
chat: true
|
chat: true
|
||||||
crownsolo: false
|
crownsolo: false
|
||||||
visible: true
|
|
||||||
color: "#eeeeee"
|
|
||||||
color2: "#888888"
|
|
||||||
|
|
||||||
defaultSettings:
|
|
||||||
chat: true
|
|
||||||
crownsolo: false
|
|
||||||
color: "#480505"
|
|
||||||
color2: "#000000"
|
|
||||||
visible: true
|
|
||||||
|
|
||||||
lobbyRegexes:
|
lobbyRegexes:
|
||||||
- "^lobby[1-9]?[1-9]?$"
|
- "^lobby\\d\\d$"
|
||||||
- "^test/.+$"
|
- "^test/.*$"
|
||||||
|
|
||||||
lobbyBackdoor: "lolwutsecretlobbybackdoor"
|
lobbyBackdoor: "lolwutsecretlobbybackdoor"
|
||||||
fullChannel: "test/awkward"
|
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
"description": "Hri7566's MPP Server",
|
"description": "Hri7566's MPP Server",
|
||||||
"main": "out/index.js",
|
"main": "out/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun .",
|
"start": "node .",
|
||||||
"build": "bun build ./src/index.ts --outdir=out",
|
"build": "npx tsc",
|
||||||
"dev": "bun run src/index.ts"
|
"dev": "pnpm build && pnpm start"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Hri7566",
|
"author": "Hri7566",
|
||||||
|
@ -14,14 +14,13 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "5.2.0",
|
"@prisma/client": "5.2.0",
|
||||||
"@t3-oss/env-core": "^0.6.1",
|
"@t3-oss/env-core": "^0.6.1",
|
||||||
"bun": "^1.0.0",
|
|
||||||
"bun-types": "^1.0.1",
|
|
||||||
"date-holidays": "^3.21.5",
|
"date-holidays": "^3.21.5",
|
||||||
"dotenv": "^8.6.0",
|
"dotenv": "^8.6.0",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"fancy-text-converter": "^1.0.9",
|
"fancy-text-converter": "^1.0.9",
|
||||||
"keccak": "^2.1.0",
|
"keccak": "^2.1.0",
|
||||||
"mppclone-client": "^1.1.3",
|
"mppclone-client": "^1.1.3",
|
||||||
|
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.31.0",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
"yaml": "^2.3.2",
|
"yaml": "^2.3.2",
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.22.2"
|
||||||
|
|
|
@ -0,0 +1,292 @@
|
||||||
|
lockfileVersion: '6.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
'@prisma/client':
|
||||||
|
specifier: 5.2.0
|
||||||
|
version: 5.2.0(prisma@5.2.0)
|
||||||
|
'@t3-oss/env-core':
|
||||||
|
specifier: ^0.6.1
|
||||||
|
version: 0.6.1(typescript@5.2.2)(zod@3.22.2)
|
||||||
|
date-holidays:
|
||||||
|
specifier: ^3.21.5
|
||||||
|
version: 3.21.5
|
||||||
|
dotenv:
|
||||||
|
specifier: ^8.6.0
|
||||||
|
version: 8.6.0
|
||||||
|
events:
|
||||||
|
specifier: ^3.3.0
|
||||||
|
version: 3.3.0
|
||||||
|
fancy-text-converter:
|
||||||
|
specifier: ^1.0.9
|
||||||
|
version: 1.0.9
|
||||||
|
keccak:
|
||||||
|
specifier: ^2.1.0
|
||||||
|
version: 2.1.0
|
||||||
|
mppclone-client:
|
||||||
|
specifier: ^1.1.3
|
||||||
|
version: 1.1.3
|
||||||
|
uWebSockets.js:
|
||||||
|
specifier: github:uNetworking/uWebSockets.js#v20.31.0
|
||||||
|
version: github.com/uNetworking/uWebSockets.js/809b99d2d7d12e2cbf89b7135041e9b41ff84084
|
||||||
|
unique-names-generator:
|
||||||
|
specifier: ^4.7.1
|
||||||
|
version: 4.7.1
|
||||||
|
yaml:
|
||||||
|
specifier: ^2.3.2
|
||||||
|
version: 2.3.2
|
||||||
|
zod:
|
||||||
|
specifier: ^3.22.2
|
||||||
|
version: 3.22.2
|
||||||
|
|
||||||
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^20.5.9
|
||||||
|
version: 20.5.9
|
||||||
|
prisma:
|
||||||
|
specifier: ^5.2.0
|
||||||
|
version: 5.2.0
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.2.2
|
||||||
|
version: 5.2.2
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
/@prisma/client@5.2.0(prisma@5.2.0):
|
||||||
|
resolution: {integrity: sha512-AiTjJwR4J5Rh6Z/9ZKrBBLel3/5DzUNntMohOy7yObVnVoTNVFi2kvpLZlFuKO50d7yDspOtW6XBpiAd0BVXbQ==}
|
||||||
|
engines: {node: '>=16.13'}
|
||||||
|
requiresBuild: true
|
||||||
|
peerDependencies:
|
||||||
|
prisma: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
prisma:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@prisma/engines-version': 5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f
|
||||||
|
prisma: 5.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@prisma/engines-version@5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f:
|
||||||
|
resolution: {integrity: sha512-jsnKT5JIDIE01lAeCj2ghY9IwxkedhKNvxQeoyLs6dr4ZXynetD0vTy7u6wMJt8vVPv8I5DPy/I4CFaoXAgbtg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@prisma/engines@5.2.0:
|
||||||
|
resolution: {integrity: sha512-dT7FOLUCdZmq+AunLqB1Iz+ZH/IIS1Fz2THmKZQ6aFONrQD/BQ5ecJ7g2wGS2OgyUFf4OaLam6/bxmgdOBDqig==}
|
||||||
|
requiresBuild: true
|
||||||
|
|
||||||
|
/@t3-oss/env-core@0.6.1(typescript@5.2.2)(zod@3.22.2):
|
||||||
|
resolution: {integrity: sha512-KQD7qEDJtkWIWWmTVjNvk0wnHpkvAQ6CRbUxbWMFNG/fiosBQDQvtRpBNu6USxBscJCoC4z6y7P9MN52/mLOzw==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.7.2'
|
||||||
|
zod: ^3.0.0
|
||||||
|
dependencies:
|
||||||
|
typescript: 5.2.2
|
||||||
|
zod: 3.22.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@types/node@20.5.9:
|
||||||
|
resolution: {integrity: sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/argparse@2.0.1:
|
||||||
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/astronomia@4.1.1:
|
||||||
|
resolution: {integrity: sha512-TcJD9lUC5eAo0/Ji7rnQauX/yQbi0yZWM+JsNr77W3OA5fsrgvuFgubLMFwfw4VlZ29cu9dG/yfJbfvuTSftjg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/bindings@1.5.0:
|
||||||
|
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||||
|
dependencies:
|
||||||
|
file-uri-to-path: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/caldate@2.0.5:
|
||||||
|
resolution: {integrity: sha512-JndhrUuDuE975KUhFqJaVR1OQkCHZqpOrJur/CFXEIEhWhBMjxO85cRSK8q4FW+B+yyPq6GYua2u4KvNzTcq0w==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
dependencies:
|
||||||
|
moment-timezone: 0.5.43
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/date-bengali-revised@2.0.2:
|
||||||
|
resolution: {integrity: sha512-q9iDru4+TSA9k4zfm0CFHJj6nBsxP7AYgWC/qodK/i7oOIlj5K2z5IcQDtESfs/Qwqt/xJYaP86tkazd/vRptg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/date-chinese@2.1.4:
|
||||||
|
resolution: {integrity: sha512-WY+6+Qw92ZGWFvGtStmNQHEYpNa87b8IAQ5T8VKt4wqrn24lBXyyBnWI5jAIyy7h/KVwJZ06bD8l/b7yss82Ww==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
dependencies:
|
||||||
|
astronomia: 4.1.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/date-easter@1.0.2:
|
||||||
|
resolution: {integrity: sha512-mpC1izx7lUSLYl4B88V2W57eNB4xS2ic+ahxK2AYUsaBTsCeHzT6K5ymUKzL9YPFf/GlygFqpiD4/NO1hxDsLw==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/date-holidays-parser@3.4.4:
|
||||||
|
resolution: {integrity: sha512-R5aO4oT8H51ZKdvApqHrqYEiNBrqT6tRj2PFXNcZfqMI4nxY7KKKly0ZsmquR5gY+x9ldKR8SAMdozzIInaoXg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
dependencies:
|
||||||
|
astronomia: 4.1.1
|
||||||
|
caldate: 2.0.5
|
||||||
|
date-bengali-revised: 2.0.2
|
||||||
|
date-chinese: 2.1.4
|
||||||
|
date-easter: 1.0.2
|
||||||
|
deepmerge: 4.3.1
|
||||||
|
jalaali-js: 1.2.6
|
||||||
|
moment-timezone: 0.5.43
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/date-holidays@3.21.5:
|
||||||
|
resolution: {integrity: sha512-5X/UK7FunfIiM/q7CwepNfzh1XkkukdZNfTPyKlD5kx01MQzJ9DqKyTcFNzlQJ+HgpAxqUqSs3+F8mwV9bzo/w==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
date-holidays-parser: 3.4.4
|
||||||
|
js-yaml: 4.1.0
|
||||||
|
lodash.omit: 4.5.0
|
||||||
|
lodash.pick: 4.4.0
|
||||||
|
prepin: 1.0.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/deepmerge@4.3.1:
|
||||||
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/dotenv@8.6.0:
|
||||||
|
resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/events@3.3.0:
|
||||||
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
|
engines: {node: '>=0.8.x'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/fancy-text-converter@1.0.9:
|
||||||
|
resolution: {integrity: sha512-tFUAWpEfZOYhdsjILVu7c0PL9Ud9pTQmonm/2mdvFC7WcEHIYi9NYS5irJYFdBJDIRSqi64XV+IhHPc/ngxtyw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/file-uri-to-path@1.0.0:
|
||||||
|
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/inherits@2.0.4:
|
||||||
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/jalaali-js@1.2.6:
|
||||||
|
resolution: {integrity: sha512-io974va+Qyu+UfuVX3UIAgJlxLhAMx9Y8VMfh+IG00Js7hXQo1qNQuwSiSa0xxco0SVgx5HWNkaiCcV+aZ8WPw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/js-yaml@4.1.0:
|
||||||
|
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
argparse: 2.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/keccak@2.1.0:
|
||||||
|
resolution: {integrity: sha512-m1wbJRTo+gWbctZWay9i26v5fFnYkOn7D5PCxJ3fZUGUEb49dE1Pm4BREUYCt/aoO6di7jeoGmhvqN9Nzylm3Q==}
|
||||||
|
engines: {node: '>=5.12.0'}
|
||||||
|
requiresBuild: true
|
||||||
|
dependencies:
|
||||||
|
bindings: 1.5.0
|
||||||
|
inherits: 2.0.4
|
||||||
|
nan: 2.17.0
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.omit@4.5.0:
|
||||||
|
resolution: {integrity: sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.pick@4.4.0:
|
||||||
|
resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/moment-timezone@0.5.43:
|
||||||
|
resolution: {integrity: sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==}
|
||||||
|
dependencies:
|
||||||
|
moment: 2.29.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/moment@2.29.4:
|
||||||
|
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/mppclone-client@1.1.3:
|
||||||
|
resolution: {integrity: sha512-5DSkQmZOj823/BPwi6CQa4UWkoAX7itfNxf6L26NJS/qj9AljuKoqnIZxhtSKdak75qZd5Jgx+zD1aXflRNxHg==}
|
||||||
|
dependencies:
|
||||||
|
ws: 8.14.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/nan@2.17.0:
|
||||||
|
resolution: {integrity: sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/prepin@1.0.3:
|
||||||
|
resolution: {integrity: sha512-0XL2hreherEEvUy0fiaGEfN/ioXFV+JpImqIzQjxk6iBg4jQ2ARKqvC4+BmRD8w/pnpD+lbxvh0Ub+z7yBEjvA==}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/prisma@5.2.0:
|
||||||
|
resolution: {integrity: sha512-FfFlpjVCkZwrqxDnP4smlNYSH1so+CbfjgdpioFzGGqlQAEm6VHAYSzV7jJgC3ebtY9dNOhDMS2+4/1DDSM7bQ==}
|
||||||
|
engines: {node: '>=16.13'}
|
||||||
|
hasBin: true
|
||||||
|
requiresBuild: true
|
||||||
|
dependencies:
|
||||||
|
'@prisma/engines': 5.2.0
|
||||||
|
|
||||||
|
/safe-buffer@5.2.1:
|
||||||
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/typescript@5.2.2:
|
||||||
|
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
/unique-names-generator@4.7.1:
|
||||||
|
resolution: {integrity: sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/ws@8.14.0:
|
||||||
|
resolution: {integrity: sha512-WR0RJE9Ehsio6U4TuM+LmunEsjQ5ncHlw4sn9ihD6RoJKZrVyH9FWV3dmnwu8B2aNib1OvG2X6adUCyFpQyWcg==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: '>=5.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/yaml@2.3.2:
|
||||||
|
resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==}
|
||||||
|
engines: {node: '>= 14'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/zod@3.22.2:
|
||||||
|
resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
github.com/uNetworking/uWebSockets.js/809b99d2d7d12e2cbf89b7135041e9b41ff84084:
|
||||||
|
resolution: {tarball: https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/809b99d2d7d12e2cbf89b7135041e9b41ff84084}
|
||||||
|
name: uWebSockets.js
|
||||||
|
version: 20.31.0
|
||||||
|
dev: false
|
|
@ -1,322 +1,11 @@
|
||||||
import EventEmitter from "events";
|
// TODO Load channel config file
|
||||||
import { Logger } from "../util/Logger";
|
|
||||||
import { loadConfig } from "../util/config";
|
|
||||||
import {
|
|
||||||
ChannelSettingValue,
|
|
||||||
ChannelSettings,
|
|
||||||
ClientEvents,
|
|
||||||
Participant,
|
|
||||||
ServerEvents
|
|
||||||
} from "../util/types";
|
|
||||||
import { Socket } from "../ws/Socket";
|
|
||||||
import { validateChannelSettings } from "./settings";
|
|
||||||
import { socketsBySocketID } from "../ws/server";
|
|
||||||
|
|
||||||
interface ChannelConfig {
|
export class Channel {
|
||||||
forceLoad: string[];
|
constructor(private _id: string) {}
|
||||||
lobbySettings: Partial<ChannelSettings>;
|
|
||||||
defaultSettings: Partial<ChannelSettings>;
|
|
||||||
lobbyRegexes: string[];
|
|
||||||
lobbyBackdoor: string;
|
|
||||||
fullChannel: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = loadConfig<ChannelConfig>("config/channels.yml", {
|
getID() {
|
||||||
forceLoad: ["lobby", "test/awkward"],
|
|
||||||
lobbySettings: {
|
|
||||||
lobby: true,
|
|
||||||
chat: true,
|
|
||||||
crownsolo: false,
|
|
||||||
visible: true
|
|
||||||
},
|
|
||||||
defaultSettings: {
|
|
||||||
chat: true,
|
|
||||||
crownsolo: false,
|
|
||||||
color: "#480505",
|
|
||||||
color2: "#000000",
|
|
||||||
visible: true
|
|
||||||
},
|
|
||||||
// TODO Test this regex
|
|
||||||
lobbyRegexes: ["^lobby[1-9]?[1-9]?$", "^test/.+$"],
|
|
||||||
lobbyBackdoor: "lolwutsecretlobbybackdoor",
|
|
||||||
fullChannel: "test/awkward"
|
|
||||||
});
|
|
||||||
|
|
||||||
export const channelList = new Array<Channel>();
|
|
||||||
|
|
||||||
export class Channel extends EventEmitter {
|
|
||||||
private settings: Partial<ChannelSettings> = config.defaultSettings;
|
|
||||||
private ppl = new Array<Participant>();
|
|
||||||
|
|
||||||
public logger: Logger;
|
|
||||||
public chatHistory = new Array<ClientEvents["a"]>();
|
|
||||||
|
|
||||||
// TODO Add the crown
|
|
||||||
|
|
||||||
constructor(private _id: string, set?: Partial<ChannelSettings>) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.logger = new Logger("Channel - " + _id);
|
|
||||||
|
|
||||||
// Validate settings in set
|
|
||||||
// Set the verified settings
|
|
||||||
|
|
||||||
if (set && !this.isLobby()) {
|
|
||||||
const validatedSet = validateChannelSettings(set);
|
|
||||||
|
|
||||||
for (const key in Object.keys(validatedSet)) {
|
|
||||||
if (!(validatedSet as any)[key]) continue;
|
|
||||||
|
|
||||||
(this.settings as any)[key] = (set as any)[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isLobby()) {
|
|
||||||
this.settings = config.lobbySettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.bindEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getID() {
|
|
||||||
return this._id;
|
return this._id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isLobby() {
|
isLobby() {}
|
||||||
for (const reg of config.lobbyRegexes) {
|
|
||||||
let exp = new RegExp(reg, "g");
|
|
||||||
|
|
||||||
if (this.getID().match(exp)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public changeSettings(
|
|
||||||
set: Partial<ChannelSettings>,
|
|
||||||
admin: boolean = false
|
|
||||||
) {
|
|
||||||
if (!admin) {
|
|
||||||
if (set.lobby) set.lobby = undefined;
|
|
||||||
if (set.owner_id) set.owner_id = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isLobby() && !admin) return;
|
|
||||||
|
|
||||||
// Verify settings
|
|
||||||
const validSettings = validateChannelSettings(set);
|
|
||||||
|
|
||||||
for (const key of Object.keys(validSettings)) {
|
|
||||||
// Setting is valid?
|
|
||||||
if ((validSettings as Record<string, boolean>)[key]) {
|
|
||||||
// Change setting
|
|
||||||
(this.settings as Record<string, ChannelSettingValue>)[key] = (
|
|
||||||
set as Record<string, ChannelSettingValue>
|
|
||||||
)[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public join(socket: Socket) {
|
|
||||||
const part = socket.getParticipant() as Participant;
|
|
||||||
|
|
||||||
// Unknown side-effects, but for type safety...
|
|
||||||
// if (!part) return;
|
|
||||||
|
|
||||||
let hasChangedChannel = false;
|
|
||||||
let oldChannelID = socket.currentChannelID;
|
|
||||||
|
|
||||||
// this.logger.debug("Has user?", this.hasUser(part._id));
|
|
||||||
|
|
||||||
// Is user in this channel?
|
|
||||||
if (this.hasUser(part._id)) {
|
|
||||||
// Alreay in channel, don't add to list, but tell them they're here
|
|
||||||
hasChangedChannel = true;
|
|
||||||
this.ppl.push(part);
|
|
||||||
} else {
|
|
||||||
// Are we full?
|
|
||||||
if (!this.isFull()) {
|
|
||||||
// Add to channel
|
|
||||||
hasChangedChannel = true;
|
|
||||||
this.ppl.push(part);
|
|
||||||
} else {
|
|
||||||
// Put us in full channel
|
|
||||||
return socket.setChannel(config.fullChannel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasChangedChannel) {
|
|
||||||
if (socket.currentChannelID) {
|
|
||||||
const ch = channelList.find(
|
|
||||||
ch => ch._id == socket.currentChannelID
|
|
||||||
);
|
|
||||||
if (ch) {
|
|
||||||
ch?.leave(socket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.currentChannelID = this.getID();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send our data back
|
|
||||||
socket.sendArray([
|
|
||||||
{
|
|
||||||
m: "ch",
|
|
||||||
ch: this.getInfo(),
|
|
||||||
p: part.id,
|
|
||||||
ppl: this.getParticipantList()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
m: "c",
|
|
||||||
c: this.chatHistory.slice(-50)
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const cursorPos = socket.getCursorPos();
|
|
||||||
|
|
||||||
// Broadcast participant update
|
|
||||||
this.sendArray([
|
|
||||||
{
|
|
||||||
m: "p",
|
|
||||||
_id: part._id,
|
|
||||||
name: part.name,
|
|
||||||
color: part.color,
|
|
||||||
id: part.id,
|
|
||||||
x: cursorPos.x,
|
|
||||||
y: cursorPos.y
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public leave(socket: Socket) {
|
|
||||||
// this.logger.debug("Leave called");
|
|
||||||
const part = socket.getParticipant() as Participant;
|
|
||||||
|
|
||||||
let dupeCount = 0;
|
|
||||||
for (const s of socketsBySocketID.values()) {
|
|
||||||
if (s.getParticipantID() == part.id) {
|
|
||||||
if (s.currentChannelID == this.getID()) {
|
|
||||||
dupeCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this.logger.debug("Dupes:", dupeCount);
|
|
||||||
|
|
||||||
if (dupeCount == 1) {
|
|
||||||
const p = this.ppl.find(p => p.id == socket.getParticipantID());
|
|
||||||
|
|
||||||
if (p) {
|
|
||||||
this.ppl.splice(this.ppl.indexOf(p), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast bye
|
|
||||||
this.sendArray([
|
|
||||||
{
|
|
||||||
m: "bye",
|
|
||||||
p: part.id
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.emit("update");
|
|
||||||
}
|
|
||||||
|
|
||||||
public isFull() {
|
|
||||||
// TODO Use limit setting
|
|
||||||
|
|
||||||
// if (this.isLobby() && this.ppl.length >= 20) {
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getInfo() {
|
|
||||||
return {
|
|
||||||
_id: this.getID(),
|
|
||||||
id: this.getID(),
|
|
||||||
count: this.ppl.length,
|
|
||||||
settings: this.settings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getParticipantList() {
|
|
||||||
return this.ppl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasUser(_id: string) {
|
|
||||||
const foundPart = this.ppl.find(p => p._id == _id);
|
|
||||||
return !!foundPart;
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasParticipant(id: string) {
|
|
||||||
const foundPart = this.ppl.find(p => p.id == id);
|
|
||||||
return !!foundPart;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendArray<EventID extends keyof ClientEvents>(
|
|
||||||
arr: ClientEvents[EventID][]
|
|
||||||
) {
|
|
||||||
let sentSocketIDs = new Array<string>();
|
|
||||||
|
|
||||||
for (const p of this.ppl) {
|
|
||||||
socketLoop: for (const socket of socketsBySocketID.values()) {
|
|
||||||
if (socket.isDestroyed()) continue socketLoop;
|
|
||||||
if (socket.getParticipantID() != p.id) continue socketLoop;
|
|
||||||
if (sentSocketIDs.includes(socket.socketID))
|
|
||||||
continue socketLoop;
|
|
||||||
socket.sendArray(arr);
|
|
||||||
sentSocketIDs.push(socket.socketID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private alreadyBound = false;
|
|
||||||
|
|
||||||
private bindEventListeners() {
|
|
||||||
if (this.alreadyBound) return;
|
|
||||||
this.alreadyBound = true;
|
|
||||||
|
|
||||||
this.on("update", () => {
|
|
||||||
for (const socket of socketsBySocketID.values()) {
|
|
||||||
for (const p of this.ppl) {
|
|
||||||
if (socket.getParticipantID() == p.id) {
|
|
||||||
socket.sendChannelUpdate(
|
|
||||||
this.getInfo(),
|
|
||||||
this.getParticipantList()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.on("message", (msg: ServerEvents["a"], socket: Socket) => {
|
|
||||||
if (!msg.message) return;
|
|
||||||
|
|
||||||
let outgoing: ClientEvents["a"] = {
|
|
||||||
m: "a",
|
|
||||||
a: msg.message,
|
|
||||||
t: Date.now(),
|
|
||||||
p: socket.getParticipant() as Participant
|
|
||||||
};
|
|
||||||
|
|
||||||
this.sendArray([outgoing]);
|
|
||||||
this.chatHistory.push(outgoing);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forceloader
|
|
||||||
let hasFullChannel = false;
|
|
||||||
|
|
||||||
for (const id of config.forceLoad) {
|
|
||||||
channelList.push(new Channel(id));
|
|
||||||
if (id == config.fullChannel) hasFullChannel = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasFullChannel) {
|
|
||||||
channelList.push(new Channel(config.fullChannel));
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import env from "./util/env";
|
import env from "./util/env";
|
||||||
// import { app } from "./ws/server";
|
import { app } from "./ws/server";
|
||||||
import "./ws/server";
|
|
||||||
import { Logger } from "./util/Logger";
|
import { Logger } from "./util/Logger";
|
||||||
|
|
||||||
const logger = new Logger("Main");
|
const logger = new Logger("Main");
|
||||||
|
|
||||||
|
// No IPv6 (yet)
|
||||||
|
app.listen("0.0.0.0", env.PORT, () => {
|
||||||
|
logger.info("Listening on :" + env.PORT);
|
||||||
|
});
|
||||||
|
|
|
@ -6,9 +6,10 @@ export function loadConfig<T>(filepath: string, def: T) {
|
||||||
const data = readFileSync(filepath).toString();
|
const data = readFileSync(filepath).toString();
|
||||||
const parsed = YAML.parse(data);
|
const parsed = YAML.parse(data);
|
||||||
|
|
||||||
return parsed as T;
|
return parsed as T || def;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Unable to load config:", err);
|
console.error("Unable to load config:", err);
|
||||||
|
} finally {
|
||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,6 @@ export function createUserID(ip: string) {
|
||||||
.substring(0, 24);
|
.substring(0, 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSocketID() {
|
|
||||||
return crypto.randomUUID();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createColor(ip: string) {
|
export function createColor(ip: string) {
|
||||||
return (
|
return (
|
||||||
"#" +
|
"#" +
|
||||||
|
|
|
@ -32,14 +32,13 @@ declare type ChannelSettings = {
|
||||||
crownsolo: boolean;
|
crownsolo: boolean;
|
||||||
chat: boolean;
|
chat: boolean;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
limit: number;
|
||||||
} & Partial<{
|
} & Partial<{
|
||||||
color2: string;
|
color2: string;
|
||||||
lobby: boolean;
|
lobby: boolean;
|
||||||
owner_id: string;
|
owner_id: string;
|
||||||
"lyrical notes": boolean;
|
"lyrical notes": boolean;
|
||||||
"no cussing": boolean;
|
"no cussing": boolean;
|
||||||
|
|
||||||
limit: number;
|
|
||||||
noindex: boolean;
|
noindex: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
@ -101,10 +100,9 @@ declare interface ChannelInfo {
|
||||||
id: string;
|
id: string;
|
||||||
_id: string;
|
_id: string;
|
||||||
crown?: Crown;
|
crown?: Crown;
|
||||||
settings: Partial<ChannelSettings>;
|
settings: ChannelSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events copied from Hri7566/mppclone-client typedefs
|
|
||||||
declare interface ServerEvents {
|
declare interface ServerEvents {
|
||||||
a: {
|
a: {
|
||||||
m: "a";
|
m: "a";
|
||||||
|
@ -248,7 +246,6 @@ declare interface ClientEvents {
|
||||||
m: "ch";
|
m: "ch";
|
||||||
p: string;
|
p: string;
|
||||||
ch: ChannelInfo;
|
ch: ChannelInfo;
|
||||||
ppl: Participant[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
custom: {
|
custom: {
|
||||||
|
@ -274,8 +271,8 @@ declare interface ClientEvents {
|
||||||
|
|
||||||
m: {
|
m: {
|
||||||
m: "m";
|
m: "m";
|
||||||
x: string;
|
x: number;
|
||||||
y: string;
|
y: number;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -287,7 +284,6 @@ declare interface ClientEvents {
|
||||||
};
|
};
|
||||||
|
|
||||||
notification: {
|
notification: {
|
||||||
m: "notification";
|
|
||||||
duration?: number;
|
duration?: number;
|
||||||
class?: string;
|
class?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -306,8 +302,8 @@ declare interface ClientEvents {
|
||||||
|
|
||||||
p: {
|
p: {
|
||||||
m: "p";
|
m: "p";
|
||||||
x: number | string;
|
x: number;
|
||||||
y: number | string;
|
y: number;
|
||||||
} & Participant;
|
} & Participant;
|
||||||
|
|
||||||
t: {
|
t: {
|
||||||
|
@ -315,11 +311,6 @@ declare interface ClientEvents {
|
||||||
t: number;
|
t: number;
|
||||||
e: number;
|
e: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
bye: {
|
|
||||||
m: "bye";
|
|
||||||
p: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare type ServerEventListener<EventID extends keyof ServerEvents> = {
|
declare type ServerEventListener<EventID extends keyof ServerEvents> = {
|
||||||
|
|
168
src/ws/Socket.ts
168
src/ws/Socket.ts
|
@ -1,22 +1,13 @@
|
||||||
|
import { WebSocket } from "uWebSockets.js";
|
||||||
import { createColor, createID, createUserID } from "../util/id";
|
import { createColor, createID, createUserID } from "../util/id";
|
||||||
import { decoder, encoder } from "../util/helpers";
|
import { decoder, encoder } from "../util/helpers";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import {
|
import { ChannelSettings, ClientEvents, UserFlags } from "../util/types";
|
||||||
ChannelInfo,
|
|
||||||
ChannelSettings,
|
|
||||||
ClientEvents,
|
|
||||||
Participant,
|
|
||||||
UserFlags
|
|
||||||
} from "../util/types";
|
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import { createUser, readUser } from "../data/user";
|
import { createUser, readUser } from "../data/user";
|
||||||
import { eventGroups } from "./events";
|
import { eventGroups } from "./events";
|
||||||
import { loadConfig } from "../util/config";
|
import { loadConfig } from "../util/config";
|
||||||
import { Gateway } from "./Gateway";
|
import { Gateway } from "./Gateway";
|
||||||
import { Channel, channelList } from "../channel/Channel";
|
|
||||||
import { ServerWebSocket } from "bun";
|
|
||||||
import { findSocketByUserID, socketsBySocketID } from "./server";
|
|
||||||
import { Logger } from "../util/Logger";
|
|
||||||
|
|
||||||
interface UsersConfig {
|
interface UsersConfig {
|
||||||
defaultName: string;
|
defaultName: string;
|
||||||
|
@ -30,8 +21,6 @@ const usersConfig = loadConfig<UsersConfig>("config/users.yml", {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const logger = new Logger("Sockets");
|
|
||||||
|
|
||||||
export class Socket extends EventEmitter {
|
export class Socket extends EventEmitter {
|
||||||
private id: string;
|
private id: string;
|
||||||
private _id: string;
|
private _id: string;
|
||||||
|
@ -42,48 +31,25 @@ export class Socket extends EventEmitter {
|
||||||
|
|
||||||
public desiredChannel: {
|
public desiredChannel: {
|
||||||
_id: string | undefined;
|
_id: string | undefined;
|
||||||
set: Partial<ChannelSettings> | undefined;
|
set: Partial<ChannelSettings>;
|
||||||
} = {
|
} = {
|
||||||
_id: undefined,
|
_id: undefined,
|
||||||
set: {}
|
set: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
public currentChannelID: string | undefined;
|
constructor(private ws: WebSocket<unknown>) {
|
||||||
private cursorPos = {
|
|
||||||
x: "-10.00",
|
|
||||||
y: "-10.00"
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(private ws: ServerWebSocket<unknown>, public socketID: string) {
|
|
||||||
super();
|
super();
|
||||||
this.ip = ws.remoteAddress; // Participant ID
|
this.ip = decoder.decode(this.ws.getRemoteAddressAsText());
|
||||||
|
|
||||||
|
// Participant ID
|
||||||
|
this.id = createID();
|
||||||
|
|
||||||
// User ID
|
// User ID
|
||||||
this._id = createUserID(this.getIP());
|
this._id = createUserID(this.getIP());
|
||||||
|
// *cough* lapis
|
||||||
// Check if we're already connected
|
|
||||||
// We need to skip ourselves, so we loop here instead of using a helper
|
|
||||||
let foundSocket;
|
|
||||||
|
|
||||||
for (const socket of socketsBySocketID.values()) {
|
|
||||||
if (socket.socketID == this.socketID) continue;
|
|
||||||
|
|
||||||
if (socket.getUserID() == this.getUserID()) {
|
|
||||||
foundSocket = socket;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// logger.debug("Found socket?", foundSocket);
|
|
||||||
|
|
||||||
if (!foundSocket) {
|
|
||||||
// Use new session ID
|
|
||||||
this.id = createID();
|
|
||||||
} else {
|
|
||||||
// Use original session ID
|
|
||||||
this.id = foundSocket.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadUser();
|
this.loadUser();
|
||||||
|
|
||||||
this.bindEventListeners();
|
this.bindEventListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,40 +61,9 @@ export class Socket extends EventEmitter {
|
||||||
return this._id;
|
return this._id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getParticipantID() {
|
public setChannel(_id: string, set: Partial<ChannelSettings>) {
|
||||||
return this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setChannel(_id: string, set?: Partial<ChannelSettings>) {
|
|
||||||
if (this.isDestroyed()) return;
|
|
||||||
|
|
||||||
this.desiredChannel._id = _id;
|
this.desiredChannel._id = _id;
|
||||||
this.desiredChannel.set = set;
|
this.desiredChannel.set = set;
|
||||||
|
|
||||||
let channel;
|
|
||||||
for (const ch of channelList) {
|
|
||||||
if (ch.getID() == _id) {
|
|
||||||
channel = ch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("Found channel:", channel);
|
|
||||||
|
|
||||||
// Does channel exist?
|
|
||||||
if (channel) {
|
|
||||||
// Exists, join normally
|
|
||||||
channel.join(this);
|
|
||||||
} else {
|
|
||||||
// Doesn't exist, create
|
|
||||||
channel = new Channel(
|
|
||||||
this.desiredChannel._id,
|
|
||||||
this.desiredChannel.set
|
|
||||||
);
|
|
||||||
|
|
||||||
channel.join(this);
|
|
||||||
|
|
||||||
// TODO Give the crown upon joining
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bindEventListeners() {
|
private bindEventListeners() {
|
||||||
|
@ -145,8 +80,7 @@ export class Socket extends EventEmitter {
|
||||||
public sendArray<EventID extends keyof ClientEvents>(
|
public sendArray<EventID extends keyof ClientEvents>(
|
||||||
arr: ClientEvents[EventID][]
|
arr: ClientEvents[EventID][]
|
||||||
) {
|
) {
|
||||||
if (this.isDestroyed()) return;
|
this.ws.send(encoder.encode(JSON.stringify(arr)));
|
||||||
this.ws.send(JSON.stringify(arr));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadUser() {
|
private async loadUser() {
|
||||||
|
@ -208,81 +142,7 @@ export class Socket extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private destroyed = false;
|
public getParticipantID() {
|
||||||
|
return this.id;
|
||||||
public destroy() {
|
|
||||||
// Socket was closed or should be closed, clear data
|
|
||||||
// logger.debug("Destroying UID:", this._id);
|
|
||||||
|
|
||||||
const foundCh = channelList.find(
|
|
||||||
ch => ch.getID() === this.currentChannelID
|
|
||||||
);
|
|
||||||
|
|
||||||
// logger.debug("(Destroying) Found channel:", foundCh);
|
|
||||||
|
|
||||||
if (foundCh) {
|
|
||||||
foundCh.leave(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate closure
|
|
||||||
try {
|
|
||||||
this.ws.close();
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn("Problem closing socket:", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.destroyed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isDestroyed() {
|
|
||||||
return this.destroyed == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCursorPos() {
|
|
||||||
return this.cursorPos;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setCursorPos(x: number | string, y: number | string) {
|
|
||||||
if (typeof x == "number") {
|
|
||||||
x = x.toFixed(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof y == "number") {
|
|
||||||
y = y.toFixed(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cursorPos.x = x;
|
|
||||||
this.cursorPos.y = y;
|
|
||||||
|
|
||||||
// Send through channel
|
|
||||||
const ch = this.getCurrentChannel();
|
|
||||||
if (!ch) return;
|
|
||||||
|
|
||||||
const part = this.getParticipant();
|
|
||||||
if (!part) return;
|
|
||||||
|
|
||||||
ch.sendArray([
|
|
||||||
{
|
|
||||||
m: "m",
|
|
||||||
id: part.id,
|
|
||||||
x: this.cursorPos.x,
|
|
||||||
y: this.cursorPos.y
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCurrentChannel() {
|
|
||||||
return channelList.find(ch => ch.getID() == this.currentChannelID);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendChannelUpdate(ch: ChannelInfo, ppl: Participant[]) {
|
|
||||||
this.sendArray([
|
|
||||||
{
|
|
||||||
m: "ch",
|
|
||||||
ch,
|
|
||||||
p: this.getParticipantID(),
|
|
||||||
ppl
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
// Bun hoists import, but not require?
|
import "./events/user";
|
||||||
require("./events/user");
|
import "./events/admin";
|
||||||
require("./events/admin");
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { ServerEventListener } from "../../../../util/types";
|
|
||||||
|
|
||||||
export const a: ServerEventListener<"a"> = {
|
|
||||||
id: "a",
|
|
||||||
callback: (msg, socket) => {
|
|
||||||
// Send chat message
|
|
||||||
const ch = socket.getCurrentChannel();
|
|
||||||
if (!ch) return;
|
|
||||||
|
|
||||||
ch.emit("message", msg, socket);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { ServerEventListener } from "../../../../util/types";
|
|
||||||
|
|
||||||
export const ch: ServerEventListener<"ch"> = {
|
|
||||||
id: "ch",
|
|
||||||
callback: (msg, socket) => {
|
|
||||||
// Switch channel
|
|
||||||
if (!msg._id) return;
|
|
||||||
socket.setChannel(msg._id, msg.set);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { ServerEventListener } from "../../../../util/types";
|
import { ServerEventListener } from "../../../../util/types";
|
||||||
|
import { eventGroups } from "../../../events";
|
||||||
|
|
||||||
export const devices: ServerEventListener<"devices"> = {
|
export const devices: ServerEventListener<"devices"> = {
|
||||||
id: "devices",
|
id: "devices",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { ServerEventListener } from "../../../../util/types";
|
import { ServerEventListener } from "../../../../util/types";
|
||||||
|
import { eventGroups } from "../../../events";
|
||||||
|
|
||||||
export const hi: ServerEventListener<"hi"> = {
|
export const hi: ServerEventListener<"hi"> = {
|
||||||
id: "hi",
|
id: "hi",
|
||||||
|
@ -11,7 +12,7 @@ export const hi: ServerEventListener<"hi"> = {
|
||||||
_id: socket.getUserID(),
|
_id: socket.getUserID(),
|
||||||
name: "Anonymous",
|
name: "Anonymous",
|
||||||
color: "#777",
|
color: "#777",
|
||||||
id: ""
|
id: socket.getParticipantID()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,11 +22,7 @@ export const hi: ServerEventListener<"hi"> = {
|
||||||
accountInfo: undefined,
|
accountInfo: undefined,
|
||||||
permissions: undefined,
|
permissions: undefined,
|
||||||
t: Date.now(),
|
t: Date.now(),
|
||||||
u: {
|
u: part
|
||||||
_id: part._id,
|
|
||||||
color: part.color,
|
|
||||||
name: part.name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { ServerEventListener } from "../../../../util/types";
|
|
||||||
|
|
||||||
export const m: ServerEventListener<"m"> = {
|
|
||||||
id: "m",
|
|
||||||
callback: (msg, socket) => {
|
|
||||||
if (!msg.x || !msg.y) return;
|
|
||||||
socket.setCursorPos(msg.x, msg.y);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -4,14 +4,8 @@ export const EVENTGROUP_USER = new EventGroup("user");
|
||||||
|
|
||||||
import { hi } from "./handlers/hi";
|
import { hi } from "./handlers/hi";
|
||||||
import { devices } from "./handlers/devices";
|
import { devices } from "./handlers/devices";
|
||||||
import { ch } from "./handlers/ch";
|
|
||||||
import { m } from "./handlers/m";
|
|
||||||
import { a } from "./handlers/a";
|
|
||||||
|
|
||||||
EVENTGROUP_USER.add(hi);
|
EVENTGROUP_USER.add(hi);
|
||||||
EVENTGROUP_USER.add(devices);
|
EVENTGROUP_USER.add(devices);
|
||||||
EVENTGROUP_USER.add(ch);
|
|
||||||
EVENTGROUP_USER.add(m);
|
|
||||||
EVENTGROUP_USER.add(a);
|
|
||||||
|
|
||||||
eventGroups.push(EVENTGROUP_USER);
|
eventGroups.push(EVENTGROUP_USER);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { WebSocket } from "uWebSockets.js";
|
||||||
import { Logger } from "../util/Logger";
|
import { Logger } from "../util/Logger";
|
||||||
import { Socket } from "./Socket";
|
import { Socket } from "./Socket";
|
||||||
import { hasOwn } from "../util/helpers";
|
import { hasOwn } from "../util/helpers";
|
||||||
|
@ -18,7 +19,6 @@ export function handleMessage(socket: Socket, text: string) {
|
||||||
} else {
|
} else {
|
||||||
for (const msg of transmission) {
|
for (const msg of transmission) {
|
||||||
if (!hasOwn(msg, "m")) continue;
|
if (!hasOwn(msg, "m")) continue;
|
||||||
if (socket.isDestroyed()) continue;
|
|
||||||
socket.emit(msg.m, msg, socket);
|
socket.emit(msg.m, msg, socket);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
161
src/ws/server.ts
161
src/ws/server.ts
|
@ -1,96 +1,97 @@
|
||||||
|
import {
|
||||||
|
App,
|
||||||
|
DEDICATED_COMPRESSOR_8KB,
|
||||||
|
HttpRequest,
|
||||||
|
HttpResponse,
|
||||||
|
WebSocket
|
||||||
|
} from "uWebSockets.js";
|
||||||
import { Logger } from "../util/Logger";
|
import { Logger } from "../util/Logger";
|
||||||
import { createSocketID, createUserID } from "../util/id";
|
import { createUserID } from "../util/id";
|
||||||
import fs from "fs";
|
import { readFileSync, lstatSync } from "fs";
|
||||||
import path from "path";
|
import { join } from "path/posix";
|
||||||
import { handleMessage } from "./message";
|
import { handleMessage } from "./message";
|
||||||
import { decoder } from "../util/helpers";
|
import { decoder } from "../util/helpers";
|
||||||
import { Socket } from "./Socket";
|
import { Socket } from "./Socket";
|
||||||
import { serve, file } from "bun";
|
|
||||||
import env from "../util/env";
|
|
||||||
|
|
||||||
const logger = new Logger("WebSocket Server");
|
const logger = new Logger("WebSocket Server");
|
||||||
|
|
||||||
export const socketsBySocketID = new Map<string, Socket>();
|
export const app = App()
|
||||||
|
.get("/*", async (res, req) => {
|
||||||
|
const url = req.getUrl();
|
||||||
|
const ip = decoder.decode(res.getRemoteAddressAsText());
|
||||||
|
// logger.debug(`${req.getMethod()} ${url} ${ip}`);
|
||||||
|
// res.writeStatus(`200 OK`).end("HI!");
|
||||||
|
const file = join("./public/", url);
|
||||||
|
|
||||||
export function findSocketByPartID(id: string) {
|
// TODO Cleaner file serving
|
||||||
for (const socket of socketsBySocketID.values()) {
|
try {
|
||||||
if (socket.getParticipantID() == id) return socket;
|
const stats = lstatSync(file);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findSocketByUserID(_id: string) {
|
let data;
|
||||||
for (const socket of socketsBySocketID.values()) {
|
if (!stats.isDirectory()) {
|
||||||
// logger.debug("User ID:", socket.getUserID());
|
data = readFileSync(file);
|
||||||
if (socket.getUserID() == _id) return socket;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findSocketByIP(ip: string) {
|
// logger.debug(filename);
|
||||||
for (const socket of socketsBySocketID.values()) {
|
|
||||||
if (socket.getIP() == ip) {
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const app = Bun.serve({
|
if (!data) {
|
||||||
port: env.PORT,
|
const index = readFileSync("./public/index.html");
|
||||||
hostname: "0.0.0.0",
|
|
||||||
fetch: (req, server) => {
|
|
||||||
if (server.upgrade(req)) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
const url = new URL(req.url).pathname;
|
|
||||||
// const ip = decoder.decode(res.getRemoteAddressAsText());
|
|
||||||
// logger.debug(`${req.getMethod()} ${url} ${ip}`);
|
|
||||||
// res.writeStatus(`200 OK`).end("HI!");
|
|
||||||
const file = path.join("./public/", url);
|
|
||||||
|
|
||||||
try {
|
if (!index) {
|
||||||
if (fs.lstatSync(file).isFile()) {
|
return void res
|
||||||
const data = Bun.file(file);
|
.writeStatus(`404 Not Found`)
|
||||||
|
.end("uh oh :(");
|
||||||
if (data) {
|
|
||||||
return new Response(data);
|
|
||||||
} else {
|
|
||||||
return new Response(Bun.file("./public/index.html"));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return new Response(Bun.file("./public/index.html"));
|
return void res.writeStatus(`200 OK`).end(index);
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
return new Response(Bun.file("./public/index.html"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
websocket: {
|
|
||||||
open: ws => {
|
|
||||||
const socket = new Socket(ws, createSocketID());
|
|
||||||
(ws as unknown as any).socket = socket;
|
|
||||||
// logger.debug("Connection at " + socket.getIP());
|
|
||||||
|
|
||||||
socketsBySocketID.set(socket.socketID, socket);
|
|
||||||
},
|
|
||||||
|
|
||||||
message: (ws, message) => {
|
|
||||||
const msg = message.toString();
|
|
||||||
handleMessage((ws as unknown as any).socket, msg);
|
|
||||||
},
|
|
||||||
|
|
||||||
close: (ws, code, message) => {
|
|
||||||
// logger.debug("Close called");
|
|
||||||
const socket = (ws as unknown as any).socket as Socket;
|
|
||||||
socket.destroy();
|
|
||||||
|
|
||||||
for (const sockID of socketsBySocketID.keys()) {
|
|
||||||
const sock = socketsBySocketID.get(sockID);
|
|
||||||
|
|
||||||
if (sock == socket) {
|
|
||||||
socketsBySocketID.delete(sockID);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info("Listening on port", env.PORT);
|
res.writeStatus(`200 OK`).end(data);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn("Unable to serve file at", file);
|
||||||
|
logger.error(err);
|
||||||
|
const index = readFileSync("./public/index.html");
|
||||||
|
|
||||||
|
if (!index) {
|
||||||
|
return void res.writeStatus(`404 Not Found`).end("uh oh :(");
|
||||||
|
} else {
|
||||||
|
return void res.writeStatus(`200 OK`).end(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ws("/*", {
|
||||||
|
idleTimeout: 180,
|
||||||
|
maxBackpressure: 1024,
|
||||||
|
maxPayloadLength: 8192,
|
||||||
|
compression: DEDICATED_COMPRESSOR_8KB,
|
||||||
|
|
||||||
|
open: ((ws: WebSocket<unknown> & { socket: Socket }) => {
|
||||||
|
ws.socket = new Socket(ws);
|
||||||
|
// logger.debug("Connection at " + ws.socket.getIP());
|
||||||
|
}) as (ws: WebSocket<unknown>) => void,
|
||||||
|
|
||||||
|
message: ((
|
||||||
|
ws: WebSocket<unknown> & { socket: Socket },
|
||||||
|
message,
|
||||||
|
isBinary
|
||||||
|
) => {
|
||||||
|
const msg = decoder.decode(message);
|
||||||
|
handleMessage(ws.socket, msg);
|
||||||
|
}) as (
|
||||||
|
ws: WebSocket<unknown>,
|
||||||
|
message: ArrayBuffer,
|
||||||
|
isBinary: boolean
|
||||||
|
) => void,
|
||||||
|
|
||||||
|
close: ((
|
||||||
|
ws: WebSocket<unknown> & { socket: Socket },
|
||||||
|
code: number,
|
||||||
|
message: ArrayBuffer
|
||||||
|
) => {
|
||||||
|
// TODO handle close event
|
||||||
|
}) as (
|
||||||
|
ws: WebSocket<unknown>,
|
||||||
|
code: number,
|
||||||
|
message: ArrayBuffer
|
||||||
|
) => void
|
||||||
|
});
|
||||||
|
|
|
@ -11,10 +11,8 @@
|
||||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
/* Language and Environment */
|
/* Language and Environment */
|
||||||
// "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||||
"target": "ESNext",
|
|
||||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
"lib": ["ESNext"],
|
|
||||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
@ -25,25 +23,20 @@
|
||||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
"moduleDetection": "force",
|
|
||||||
|
|
||||||
/* Modules */
|
/* Modules */
|
||||||
// "module": "commonjs" /* Specify what module code is generated. */,
|
"module": "commonjs" /* Specify what module code is generated. */,
|
||||||
"module": "ESNext",
|
|
||||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||||
"rootDir": "./src/",
|
"rootDir": "./src/",
|
||||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
"moduleResolution": "Bundler",
|
|
||||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
"types": ["bun-types"],
|
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||||
|
@ -67,7 +60,6 @@
|
||||||
"outDir": "./out/",
|
"outDir": "./out/",
|
||||||
// "removeComments": true, /* Disable emitting comments. */
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
"noEmit": true,
|
|
||||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
|
Loading…
Reference in New Issue