Remove outdated documentation files for companies and migration guide; implement server session management features including server assignment and session status updates; enhance database schema for servers and server sessions with new fields and validation; add auto-assign functionality for unassigned sessions.

This commit is contained in:
2025-10-06 15:59:55 +05:00
parent 9e4bc7b0f8
commit a49129f643
16 changed files with 2332 additions and 483 deletions
+44
View File
@@ -0,0 +1,44 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
/dist
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
**/*.trace
**/*.zip
**/*.tar.gz
**/*.tgz
**/*.log
package-lock.json
**/*.bun
+397
View File
@@ -0,0 +1,397 @@
# Session Server
Сессионный сервер для автоматической регистрации на основном API сервере.
## Описание
Session Server - это приложение, которое автоматически регистрируется на основном сервере (`stream.graff.tech`) и периодически обновляет свою информацию. Это позволяет основному серверу знать о доступных сессионных серверах для распределения нагрузки.
## Возможности
- 🚀 Автоматическая регистрация при запуске
- 🔄 Периодическое обновление информации о сервере (по умолчанию каждые 30 секунд после завершения предыдущего запроса)
- 🎮 Автоматическое определение и обновление свободной GPU памяти через nvidia-smi (по умолчанию каждую секунду после завершения предыдущего запроса)
- 🎯 Автоматическое управление игровыми сессиями
- Запуск приложений для сессий со статусом "starting"
- Остановка приложений для сессий со статусом "ending"
- Отслеживание PID запущенных процессов
- Автоматическое обновление статусов сессий на главном сервере
- 🌐 Автоматическое определение локального IP адреса
- 💻 Автоматическое определение hostname
- ⚙️ Гибкая конфигурация через переменные окружения
- 🔁 Автоматические повторные попытки при ошибках
- 📝 Подробное логирование
- ⚡ Защита от наложения запросов для всех операций (рекурсивный вызов через setTimeout)
## Установка
```bash
# Установка зависимостей
bun install
```
## Конфигурация
Создайте файл `.env` в корне проекта на основе `.env.example`:
```bash
# URL основного API сервера
API_URL=http://localhost:3000
# Тип сервера: stream или local
SERVER_TYPE=stream
# Расположение сервера (ОБЯЗАТЕЛЬНО для stream серверов)
# Возможные значения: ru1, uae1
SERVER_LOCATION=ru1
# Уровень сервера (только для stream серверов)
# Возможные значения: demo, prod
SERVER_TIER=demo
# ID филиала (ОБЯЗАТЕЛЬНО для local серверов)
BRANCH_ID=00000000-0000-0000-0000-000000000000
# Локальный IP адрес (опционально, определяется автоматически)
# LOCAL_IP=192.168.1.100
# Hostname сервера (опционально, определяется автоматически)
# HOSTNAME=my-session-server
# Интервал регистрации в миллисекундах (по умолчанию 30000 = 30 секунд)
REGISTER_INTERVAL_MS=30000
# Интервал обновления GPU памяти в миллисекундах (по умолчанию 1000 = 1 секунда)
GPU_UPDATE_INTERVAL_MS=1000
# Интервал проверки сессий в миллисекундах (по умолчанию 5000 = 5 секунд)
SESSION_CHECK_INTERVAL_MS=5000
```
### Переменные окружения
| Переменная | Описание | Обязательна | По умолчанию |
|------------|----------|-------------|--------------|
| `API_URL` | URL основного API сервера | Нет | `http://localhost:3000` |
| `SERVER_TYPE` | Тип сервера (`stream` или `local`) | Нет | `stream` |
| `SERVER_LOCATION` | Расположение (`ru1`, `uae1`) - **обязательно для `stream`** | Да (для stream) | - |
| `SERVER_TIER` | Уровень (`demo`, `prod`) - только для `stream` | Нет | `demo` (для stream) |
| `BRANCH_ID` | ID филиала - **обязательно для `local`** | Да (для local) | - |
| `LOCAL_IP` | Локальный IP адрес | Нет | Определяется автоматически |
| `HOSTNAME` | Hostname сервера | Нет | Определяется автоматически |
| `REGISTER_INTERVAL_MS` | Интервал регистрации в мс | Нет | `30000` |
| `GPU_UPDATE_INTERVAL_MS` | Интервал обновления GPU памяти в мс | Нет | `1000` |
| `SESSION_CHECK_INTERVAL_MS` | Интервал проверки сессий в мс | Нет | `5000` |
**Примечание:** Свободная память GPU (`gpuFreeMb`) автоматически определяется через `nvidia-smi` при каждой регистрации и обновляется каждую секунду (или согласно `GPU_UPDATE_INTERVAL_MS`) после завершения предыдущего запроса. Это предотвращает наложение запросов. Если `nvidia-smi` недоступен, сервер завершит работу с ошибкой.
## Управление сессиями
Session Server автоматически управляет игровыми сессиями на этом сервере:
### Как это работает
1. **Проверка сессий**: Каждую секунду (или согласно `SESSION_CHECK_INTERVAL_MS`) сервер запрашивает у основного API список сессий для этого сервера
2. **Запуск приложений**: Для сессий со статусом `starting`:
- ⏰ Проверяется время `startAt` - приложение запускается только если это время уже наступило
- **Timezone**: Все время хранится в UTC, сравнение корректно работает независимо от часового пояса сервера
- Запланированные сессии (с будущим `startAt`) логируются с информацией о времени до запуска
- 🎯 **Централизованное назначение сервера** (для stream-сессий):
- **Main server** автоматически назначает серверы каждые 5 секунд
- Выбирается сервер с максимальной свободной GPU памятью
- Учитываются требования приложения к GPU памяти (`gpuLimitMb`)
- Session-server просто проверяет, что сессия назначена ему
- Запускается соответствующее приложение
- Отслеживается PID процесса
- Статус сессии обновляется на `started` на главном сервере
3. **Остановка приложений**: Для сессий со статусом `ending`:
- Используется `taskkill /pid {PID} /T /F` для завершения всего дерева процессов
- `/T` - завершает указанный процесс и ВСЕ дочерние процессы
- `/F` - принудительное завершение
- Решает проблему с UE5 и другими приложениями, создающими дочерние процессы
- Статус сессии обновляется на `ended` на главном сервере
4. **Автоматическая очистка**: Процессы для неактивных сессий автоматически останавливаются
### API endpoints для управления сессиями
Session Server взаимодействует с основным сервером через следующие endpoints:
- `GET /servers/:id/sessions` - получить список сессий для сервера
- `PATCH /sessions/:id/status` - обновить статус сессии (публичный endpoint)
### Запуск приложений
Session Server автоматически запускает `.exe` приложения по стандартному пути:
```
C:\apps\{appName}\{appName}.exe
```
Где `{appName}` - это значение поля `name` из таблицы `apps`.
#### Структура директорий
Все приложения должны быть размещены в следующей структуре:
```
C:\apps\
├── minecraft\
│ └── minecraft.exe
├── fortnite\
│ └── fortnite.exe
└── cyberpunk\
└── cyberpunk.exe
```
#### Особенности запуска и остановки
- ✅ Автоматическая проверка существования exe файла
- ✅ Рабочая директория устанавливается в папку приложения (`C:\apps\{appName}\`)
- ✅ Окно консоли скрывается (`windowsHide: true`)
- ✅ PID процесса отслеживается и передается на main server
- ✅ Автоматическое обновление статуса при завершении процесса
-**Корректное завершение дочерних процессов** - использует `taskkill /T` для завершения всего дерева процессов
- ✅ Решает проблему с UE5 приложениями, которые создают множественные процессы
#### Логи запуска
```
[2025-10-06T10:00:00.000Z] 🚀 Запуск приложения "minecraft" для сессии abc-123
[2025-10-06T10:00:00.050Z] 📂 Путь к приложению: C:\apps\minecraft\minecraft.exe
[2025-10-06T10:00:01.000Z] ✅ Приложение запущено с PID 12345
[2025-10-06T10:00:01.100Z] ✅ Статус сессии abc-123 обновлен на "started"
```
#### Обработка ошибок
Если exe файл не найден:
```
❌ Файл приложения не найден: C:\apps\minecraft\minecraft.exe. Убедитесь, что приложение установлено.
```
### Примеры логов
Запланированная сессия:
```
[2025-10-06T10:00:00.000Z] ⏰ Сессия 123e4567-e89b-12d3-a456-426614174000 (minecraft) запланирована через 120 сек
```
Запуск сессии:
```
[2025-10-06T10:02:00.000Z] 🚀 Запуск приложения "minecraft" для сессии 123e4567-e89b-12d3-a456-426614174000
[2025-10-06T10:02:01.000Z] ✅ Приложение запущено с PID 12345
[2025-10-06T10:02:01.100Z] ✅ Статус сессии 123e4567-e89b-12d3-a456-426614174000 обновлен на "started"
```
Остановка сессии:
```
[2025-10-06T10:32:00.000Z] 🛑 Остановка приложения для сессии 123e4567-e89b-12d3-a456-426614174000 (PID: 12345)
[2025-10-06T10:32:00.500Z] ✅ Дерево процессов для PID 12345 успешно завершено
[2025-10-06T10:32:00.600Z] ✅ Приложение и все дочерние процессы остановлены для сессии 123e4567-e89b-12d3-a456-426614174000
[2025-10-06T10:32:00.700Z] ✅ Статус сессии 123e4567-e89b-12d3-a456-426614174000 обновлен на "ended"
```
## Отладка проблем (Troubleshooting)
Если приложение запускается и сразу завершается с ошибкой, см. подробное руководство по диагностике:
- **[TROUBLESHOOTING.md](./TROUBLESHOOTING.md)**
### Новые возможности логирования
После обновления вы увидите:
- 🔴 **STDERR логи** для диагностики ошибок приложений
- 📊 **Счётчик активных процессов** при запуске/завершении
- ⚠️ **Детальная информация** о кодах выхода и сигналах
Пример логов с ошибкой:
```
[10:51:56.498Z] 🚀 Запуск приложения "ShishimGorka" для сессии abc (активных процессов: 1)
[10:51:56.563Z] ✅ Приложение запущено с PID 84112 (всего активных: 2)
[10:51:57.500Z] 🔴 STDERR [abc]: Error: Port 8888 is already in use
[10:51:57.894Z] 🛑 Приложение для сессии abc завершилось с кодом 1
[10:51:57.894Z] ⚠️ Приложение завершилось с ошибкой! Код выхода: 1
```
## Работа с часовыми поясами (Timezone)
Session Server корректно работает с разными часовыми поясами:
### Как это работает
1. **База данных**: PostgreSQL хранит все timestamp с timezone в UTC
2. **API**: Основной сервер возвращает время в формате ISO 8601 с timezone (например, `2025-10-06T10:00:00.000Z`)
3. **Session Server**: JavaScript автоматически парсит ISO 8601 строки в UTC
4. **Сравнение**: Все сравнения времени происходят в UTC, независимо от локального часового пояса сервера
### Миграция базы данных
⚠️ **Важно**: Если вы обновляете существующую систему, необходимо выполнить миграцию для добавления timezone в поля `startAt` и `endAt`:
```bash
# Из директории server/
psql -d $DATABASE_URL -f timezone_migration.sql
```
См. `server/TIMEZONE_MIGRATION.md` для подробностей.
### Примеры
Если сервер в России создаёт сессию на 14:00 по московскому времени (UTC+3):
- В БД сохранится: `2025-10-06T11:00:00.000Z` (UTC)
- Сервер в ОАЭ (UTC+4) увидит: 15:00 по местному времени
- Session Server запустит приложение ровно в 11:00 UTC, независимо от локального времени
## Запуск
### Development режим
```bash
bun run dev
```
### Production режим
```bash
# Собрать проект
bun run build
# Запустить собранное приложение
bun run start
```
## Примеры использования
### Stream сервер в России (демо)
```env
API_URL=https://api.stream.graff.tech
SERVER_TYPE=stream
SERVER_LOCATION=ru1
SERVER_TIER=demo
REGISTER_INTERVAL_MS=30000
```
### Stream сервер в ОАЭ (продакшн)
```env
API_URL=https://api.stream.graff.tech
SERVER_TYPE=stream
SERVER_LOCATION=uae1
SERVER_TIER=prod
REGISTER_INTERVAL_MS=30000
```
### Local сервер для филиала
```env
API_URL=https://api.stream.graff.tech
SERVER_TYPE=local
BRANCH_ID=123e4567-e89b-12d3-a456-426614174000
REGISTER_INTERVAL_MS=60000
```
## Логирование
Сервер выводит подробную информацию о своей работе:
```text
============================================================
🚀 Запуск сессионного сервера
============================================================
Конфигурация:
API URL: http://localhost:3000
Hostname: DESKTOP-ABC123
Local IP: 192.168.1.100
Type: stream
GPU Free MB: 8192 (читается из nvidia-smi)
Location: ru1
Tier: demo
Register Interval: 30000ms
GPU Update Interval: 1000ms
============================================================
[2025-10-06T12:00:00.000Z] Регистрация сервера...
Данные: {
"localIp": "192.168.1.100",
"hostname": "DESKTOP-ABC123",
"type": "stream",
"gpuFreeMb": 8192,
"location": "ru1",
"tier": "demo"
}
[2025-10-06T12:00:00.123Z] ✅ Сервер успешно зарегистрирован
ID сервера: 123e4567-e89b-12d3-a456-426614174000
[2025-10-06T12:00:01.000Z] 🎮 GPU память обновлена: 8150 MB
[2025-10-06T12:00:02.000Z] 🎮 GPU память обновлена: 8120 MB
[2025-10-06T12:00:03.000Z] 🎮 GPU память обновлена: 8100 MB
```
## Обработка ошибок
Сервер автоматически обрабатывает ошибки:
- **Валидация конфигурации**:
- Stream-серверы: обязательно должен быть указан `SERVER_LOCATION`
- Local-серверы: обязательно должен быть указан `BRANCH_ID`
- При отсутствии обязательных полей сервер завершит работу с критической ошибкой
- **Сетевые ошибки**: Автоматические повторные попытки (до 3 раз)
- **Таймауты**: 10 секунд на запрос
- **Коды ошибок**: Повторная отправка при временных ошибках сервера (408, 429, 500, 502, 503, 504)
- **Ошибки GPU**: Если `nvidia-smi` недоступен или возвращает некорректные данные, сервер завершит работу с критической ошибкой
- **Критические ошибки**: Подробное логирование с завершением работы
## Архитектура
```text
┌──────────────────────────────────┐
│ Session Server │
│ │
│ Регистрация (рекурсивно): │
│ 1. Определение IP │
│ 2. Определение hostname │
│ 3. Запрос GPU (nvidia-smi) │
│ 4. POST /servers/register │
│ 5. setTimeout → повтор (30s) │
│ │
│ Обновление GPU (рекурсивно): │
│ 1. Запрос GPU (nvidia-smi) │
│ 2. PATCH /servers/:id/gpu │
│ 3. setTimeout → повтор (1s) │
└──────────┬───────────────────────┘
│ HTTP Requests
┌──────────────────────────────────┐
│ Main API Server │
│ │
│ 1. Проверка по hostname │
│ 2. Создание/обновление │
│ 3. Обновление GPU памяти │
│ 4. Возврат данных │
└──────────────────────────────────┘
```
## Разработка
### Структура проекта
```text
session-server/
├── src/
│ └── index.ts # Основной файл приложения
├── dist/ # Собранное приложение
├── package.json
├── tsconfig.json
├── bun.build.ts # Конфигурация сборки
└── README.md
```
### Требования
- Bun >= 1.0
- Node.js >= 18 (опционально)
- TypeScript >= 5.0
- **NVIDIA GPU с установленным `nvidia-smi`** (обязательно)
## Лицензия
MIT
+32
View File
@@ -0,0 +1,32 @@
import { $, Glob } from "bun";
import path from "path";
import { existsSync } from "fs";
// Cross-platform directory removal
if (process.platform === "win32") {
// await $`cmd /c "rmdir /s /q dist"`;
} else {
await $`rm -rf ./dist`;
}
// await Bun.build({
// entrypoints: ["./src/index.ts"],
// env: "inline",
// target: "bun",
// outdir: `./dist`,
// minify: true,
// });
// Build all files in src
for (const entrypoint of new Glob("./src/**/*.ts").scanSync()) {
const parts = entrypoint.split(path.sep);
const entrypointPath = path.join(...parts.slice(2, -1));
await Bun.build({
entrypoints: [entrypoint],
target: "bun",
outdir: path.join("dist", entrypointPath),
env: "inline",
minify: true,
});
}
+75
View File
@@ -0,0 +1,75 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "strem.graff.tech-session-server",
"dependencies": {
"got": "^14.4.9",
},
"devDependencies": {
"bun-types": "latest",
},
},
},
"packages": {
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
"@sindresorhus/is": ["@sindresorhus/is@7.1.0", "", {}, "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA=="],
"@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="],
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="],
"@types/node": ["@types/node@24.7.0", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw=="],
"@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="],
"bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
"cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="],
"cacheable-request": ["cacheable-request@12.0.1", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.4", "get-stream": "^9.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.4", "mimic-response": "^4.0.0", "normalize-url": "^8.0.1", "responselike": "^3.0.0" } }, "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
"form-data-encoder": ["form-data-encoder@4.1.0", "", {}, "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw=="],
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
"got": ["got@14.4.9", "", { "dependencies": { "@sindresorhus/is": "^7.0.1", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^12.0.1", "decompress-response": "^6.0.0", "form-data-encoder": "^4.0.2", "http2-wrapper": "^2.2.1", "lowercase-keys": "^3.0.0", "p-cancelable": "^4.0.1", "responselike": "^3.0.0", "type-fest": "^4.26.1" } }, "sha512-Dbu075Jwm3QwNCIoCenqkqY8l2gd7e/TanuhMbzZIEsb1mpAneImSusKhZ+XdqqC3S91SDV/1SdWpGXKAlm8tA=="],
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
"http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="],
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="],
"mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="],
"normalize-url": ["normalize-url@8.1.0", "", {}, "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w=="],
"p-cancelable": ["p-cancelable@4.0.1", "", {}, "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg=="],
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
"resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
"responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
"decompress-response/mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "strem.graff.tech-session-server",
"version": "1.0.50",
"module": "src/index.js",
"devDependencies": {
"bun-types": "latest"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "bun run --watch src/index.ts",
"build": "bun bun.build.ts",
"start": "bun ./dist/index.js"
},
"dependencies": {
"got": "^14.4.9"
}
}
+691
View File
@@ -0,0 +1,691 @@
import got, { RequestError } from "got";
import os from "os";
import { execSync, spawn, ChildProcess } from "child_process";
import { existsSync } from "fs";
// Конфигурация
const API_URL = process.env.API_URL || "http://localhost:3000";
const SERVER_TYPE = (process.env.SERVER_TYPE || "stream") as "stream" | "local";
const SERVER_LOCATION =
(process.env.SERVER_LOCATION as "ru1" | "uae1" | undefined) || undefined;
const SERVER_TIER =
(process.env.SERVER_TIER as "demo" | "prod" | undefined) || undefined;
const BRANCH_ID = process.env.BRANCH_ID || undefined;
const LOCAL_IP = getLocalIp();
const HOSTNAME = os.hostname();
const REGISTER_INTERVAL_MS = parseInt(
process.env.REGISTER_INTERVAL_MS || "30000",
10
); // 30 секунд по умолчанию
const GPU_UPDATE_INTERVAL_MS = parseInt(
process.env.GPU_UPDATE_INTERVAL_MS || "1000",
10
); // 1 секунда по умолчанию
const SESSION_CHECK_INTERVAL_MS = parseInt(
process.env.SESSION_CHECK_INTERVAL_MS || "1000",
10
); // 1 секунда по умолчанию
// ID зарегистрированного сервера (заполняется после регистрации)
let SERVER_ID: string | null = null;
// Карта активных процессов: sessionId -> процесс приложения
const activeProcesses = new Map<string, ChildProcess>();
// Карта сессий в процессе запуска/остановки для предотвращения дублирования
const processingSessions = new Set<string>();
// Карта последнего времени логирования запланированных сессий (для уменьшения спама в логах)
const lastScheduledLogTime = new Map<string, number>();
interface ServerRegistrationData {
localIp: string;
hostname: string;
type: "stream" | "local";
gpuFreeMb: number;
branchId?: string;
location?: "ru1" | "uae1";
tier?: "demo" | "prod";
}
interface ServerRegistrationResponse {
server: {
id: string;
localIp: string;
hostname: string;
type: "stream" | "local";
gpuFreeMb: number;
branchId?: string;
location?: "ru1" | "uae1";
tier?: "demo" | "prod";
createdAt: string;
updatedAt: string;
};
registered: boolean;
}
interface SessionData {
id: string;
serverId: string | null; // Nullable - для stream сессий назначается динамически
appId: string;
userId: string;
startAt: string;
endAt: string | null;
appPid: number | null;
cirrusPid: number | null;
mode: "stream" | "local";
status: "starting" | "started" | "ending" | "ended";
createdAt: string;
updatedAt: string;
app: {
id: string;
name: string;
title: string;
gpuLimitMb: number | null;
psVersion: number | null;
};
user: {
id: string;
email: string;
role: string;
};
}
/**
* Получить локальный IP адрес
*/
function getLocalIp(): string {
const interfaces = os.networkInterfaces();
for (const name of Object.keys(interfaces)) {
const netInterface = interfaces[name];
if (!netInterface) continue;
for (const iface of netInterface) {
// Пропустить внутренние и non-IPv4 адреса
if (iface.family === "IPv4" && !iface.internal) {
return iface.address;
}
}
}
return "127.0.0.1";
}
/**
* Получить свободную память GPU через nvidia-smi
* Возвращает количество свободной памяти в МБ
* Выбрасывает ошибку, если не удалось получить данные
*/
function getGpuFreeMb(): number {
try {
// Выполняем nvidia-smi с форматом вывода только свободной памяти
// --query-gpu=memory.free - запрашиваем свободную память
// --format=csv,noheader,nounits - CSV формат без заголовков и единиц измерения
const output = execSync(
"nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits",
{
encoding: "utf-8",
timeout: 5000, // 5 секунд таймаут
}
);
// Парсим вывод (может быть несколько GPU, берём первый)
const lines = output.trim().split("\n");
if (lines.length > 0 && lines[0]) {
const freeMb = parseInt(lines[0].trim(), 10);
if (!isNaN(freeMb) && freeMb >= 0) {
return freeMb;
}
}
throw new Error("Не удалось распарсить вывод nvidia-smi");
} catch (error) {
console.error(
`[${new Date().toISOString()}] ❌ Критическая ошибка при получении данных GPU:`,
error instanceof Error ? error.message : error
);
throw new Error(
`Невозможно получить данные о GPU через nvidia-smi: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
/**
* Регистрация сервера на главном сервере
*/
async function registerServer(isRecursive: boolean = false): Promise<void> {
// Валидация для stream-серверов
if (SERVER_TYPE === "stream" && !SERVER_LOCATION) {
console.error(
`[${new Date().toISOString()}] ❌ Ошибка: для stream-серверов обязательно должен быть указан SERVER_LOCATION`
);
process.exit(1);
}
// Валидация для local-серверов
if (SERVER_TYPE === "local" && !BRANCH_ID) {
console.error(
`[${new Date().toISOString()}] ❌ Ошибка: для local-серверов обязательно должен быть указан BRANCH_ID`
);
process.exit(1);
}
// Установить tier по умолчанию для stream-серверов
const tier = SERVER_TYPE === "stream" && !SERVER_TIER ? "demo" : SERVER_TIER;
// Получаем актуальное значение свободной GPU памяти
const gpuFreeMb = getGpuFreeMb();
const registrationData: ServerRegistrationData = {
localIp: LOCAL_IP,
hostname: HOSTNAME,
type: SERVER_TYPE,
gpuFreeMb: gpuFreeMb,
branchId: BRANCH_ID,
location: SERVER_LOCATION,
tier: tier,
};
console.log(`[${new Date().toISOString()}] Регистрация сервера...`);
console.log("Данные:", JSON.stringify(registrationData, null, 2));
try {
const response = await got
.post(`${API_URL}/servers/register`, {
json: registrationData,
timeout: {
request: 10000, // 10 секунд таймаут
},
retry: {
limit: 3,
methods: ["POST"],
statusCodes: [408, 413, 429, 500, 502, 503, 504],
},
})
.json<ServerRegistrationResponse>();
// Сохраняем ID сервера для дальнейших обновлений
SERVER_ID = response.server.id;
if (response.registered) {
console.log(
`[${new Date().toISOString()}] ✅ Сервер успешно зарегистрирован`
);
console.log("ID сервера:", response.server.id);
} else {
console.log(
`[${new Date().toISOString()}] 🔄 Информация о сервере обновлена`
);
}
// При первом запуске запускаем обновление GPU и проверку сессий после успешной регистрации
if (!isRecursive && SERVER_ID) {
updateGpuMemory();
checkSessions();
}
} catch (error: unknown) {
console.error(
`[${new Date().toISOString()}] ❌ Ошибка регистрации сервера:`,
error
);
if (error instanceof RequestError) {
console.error("Детали ошибки:", {
code: error.code,
message: error.message,
});
} else if (error instanceof Error) {
console.error("Ошибка:", error.message);
}
}
// Планируем следующую регистрацию после завершения текущей
setTimeout(() => registerServer(true), REGISTER_INTERVAL_MS);
}
/**
* Обновить информацию о свободной памяти GPU на сервере
*/
async function updateGpuMemory(): Promise<void> {
try {
// Получаем актуальное значение свободной GPU памяти
const gpuFreeMb = getGpuFreeMb();
await got.patch(`${API_URL}/servers/${SERVER_ID}/gpu`, {
json: { gpuFreeMb },
timeout: {
request: 5000, // 5 секунд таймаут
},
retry: {
limit: 2,
methods: ["PATCH"],
statusCodes: [408, 429, 500, 502, 503, 504],
},
});
console.log(
`[${new Date().toISOString()}] 🎮 GPU память обновлена: ${gpuFreeMb} MB`
);
} catch (error: unknown) {
console.error(
`[${new Date().toISOString()}] ❌ Ошибка обновления GPU памяти:`,
error instanceof RequestError ? error.message : error
);
}
// Планируем следующее обновление после завершения текущего
setTimeout(() => updateGpuMemory(), GPU_UPDATE_INTERVAL_MS);
}
/**
* Получить сессии для этого сервера
*/
async function fetchSessions(): Promise<SessionData[]> {
if (!SERVER_ID) {
return [];
}
try {
const response = await got
.get(`${API_URL}/servers/${SERVER_ID}/sessions`, {
timeout: {
request: 5000,
},
})
.json<{ sessions: SessionData[] }>();
return response.sessions;
} catch (error: unknown) {
console.error(
`[${new Date().toISOString()}] ❌ Ошибка получения сессий:`,
error instanceof RequestError ? error.message : error
);
return [];
}
}
/**
* Обновить статус сессии на главном сервере
*/
async function updateSessionStatus(
sessionId: string,
status: "starting" | "started" | "ending" | "ended",
appPid?: number,
cirrusPid?: number
): Promise<void> {
try {
await got.patch(`${API_URL}/sessions/${sessionId}/status`, {
json: {
status,
appPid,
cirrusPid,
},
timeout: {
request: 5000,
},
});
console.log(
`[${new Date().toISOString()}] ✅ Статус сессии ${sessionId} обновлен на "${status}"`
);
} catch (error: unknown) {
console.error(
`[${new Date().toISOString()}] ❌ Ошибка обновления статуса сессии:`,
error instanceof RequestError ? error.message : error
);
}
}
/**
* Запустить приложение для сессии
*/
async function startApplication(session: SessionData): Promise<void> {
const { id: sessionId, app, serverId } = session;
// Проверить, не обрабатывается ли уже эта сессия
if (processingSessions.has(sessionId)) {
return;
}
// Проверить, не запущено ли уже приложение для этой сессии
if (activeProcesses.has(sessionId)) {
console.log(
`[${new Date().toISOString()}] ⚠️ Приложение для сессии ${sessionId} уже запущено`
);
return;
}
// Проверить, что сервер назначен
// Main server автоматически назначает серверы для готовых к запуску сессий
if (!serverId) {
console.log(
`[${new Date().toISOString()}] ⏳ Сессия ${sessionId} ожидает назначения сервера main сервером`
);
return;
}
// Проверить, что сессия назначена именно этому серверу
if (serverId !== SERVER_ID) {
// Это нормально - сессия назначена другому серверу
return;
}
processingSessions.add(sessionId);
try {
console.log(
`[${new Date().toISOString()}] 🚀 Запуск приложения "${app.name}" для сессии ${sessionId} (активных процессов: ${activeProcesses.size})`
);
// Формируем путь к exe файлу приложения
// Путь: C:\apps\{appName}\{appName}.exe
const appPath = `C:\\apps\\${app.name}\\${app.name}.exe`;
console.log(
`[${new Date().toISOString()}] 📂 Путь к приложению: ${appPath}`
);
// Проверяем существование exe файла
if (!existsSync(appPath)) {
throw new Error(
`Файл приложения не найден: ${appPath}. Убедитесь, что приложение установлено.`
);
}
// Запускаем exe приложение
// Используем 'pipe' для stderr чтобы видеть ошибки, но 'ignore' для stdin/stdout
const appProcess = spawn(appPath, [], {
detached: false,
stdio: ["ignore", "ignore", "pipe"], // stdin: ignore, stdout: ignore, stderr: pipe
windowsHide: true, // Скрывать окно консоли на Windows
cwd: `C:\\apps\\${app.name}`, // Устанавливаем рабочую директорию приложения
});
const appPid = appProcess.pid;
if (!appPid) {
throw new Error("Не удалось получить PID процесса");
}
// Сохранить процесс в карте активных процессов
activeProcesses.set(sessionId, appProcess);
// Логирование stderr для диагностики
if (appProcess.stderr) {
appProcess.stderr.on("data", (data) => {
console.error(
`[${new Date().toISOString()}] 🔴 STDERR [${sessionId}]: ${data.toString().trim()}`
);
});
}
// Обработка завершения процесса
appProcess.on("exit", async (code, signal) => {
console.log(
`[${new Date().toISOString()}] 🛑 Приложение для сессии ${sessionId} завершилось с кодом ${code}${signal ? ` (сигнал: ${signal})` : ""}`
);
if (code !== 0 && code !== null) {
console.error(
`[${new Date().toISOString()}] ⚠️ Приложение завершилось с ошибкой! Код выхода: ${code}`
);
}
activeProcesses.delete(sessionId);
// Обновить статус на "ended"
await updateSessionStatus(sessionId, "ended");
});
appProcess.on("error", async (error) => {
console.error(
`[${new Date().toISOString()}] ❌ Ошибка процесса для сессии ${sessionId}:`,
error
);
activeProcesses.delete(sessionId);
// Обновить статус на "ended" в случае ошибки
await updateSessionStatus(sessionId, "ended");
});
// Обновить статус на "started" с PID
await updateSessionStatus(sessionId, "started", appPid);
console.log(
`[${new Date().toISOString()}] ✅ Приложение запущено с PID ${appPid} (всего активных: ${activeProcesses.size})`
);
} catch (error) {
console.error(
`[${new Date().toISOString()}] ❌ Ошибка запуска приложения:`,
error instanceof Error ? error.message : error
);
// Обновить статус на "ended" в случае ошибки
await updateSessionStatus(sessionId, "ended");
} finally {
processingSessions.delete(sessionId);
}
}
/**
* Убить процесс и всё его дерево дочерних процессов (для Windows)
*/
function killProcessTree(pid: number): void {
try {
// На Windows используем taskkill с флагом /T для убийства дерева процессов
// /F - принудительное завершение
// /T - завершить указанный процесс и все дочерние процессы
execSync(`taskkill /pid ${pid} /T /F`, {
stdio: "ignore",
timeout: 10000,
});
console.log(
`[${new Date().toISOString()}] ✅ Дерево процессов для PID ${pid} успешно завершено`
);
} catch (error) {
console.error(
`[${new Date().toISOString()}] ⚠️ Ошибка при завершении дерева процессов PID ${pid}:`,
error instanceof Error ? error.message : error
);
}
}
/**
* Остановить приложение для сессии
*/
async function stopApplication(session: SessionData): Promise<void> {
const { id: sessionId, appPid } = session;
// Проверить, не обрабатывается ли уже эта сессия
if (processingSessions.has(sessionId)) {
return;
}
const appProcess = activeProcesses.get(sessionId);
if (!appProcess && !appPid) {
console.log(
`[${new Date().toISOString()}] ⚠️ Приложение для сессии ${sessionId} не найдено в активных процессах`
);
// Всё равно обновляем статус на "ended"
await updateSessionStatus(sessionId, "ended");
return;
}
processingSessions.add(sessionId);
try {
console.log(
`[${new Date().toISOString()}] 🛑 Остановка приложения для сессии ${sessionId} (PID: ${appPid || appProcess?.pid || "неизвестен"})`
);
// Используем PID из базы данных если он есть, иначе из процесса
const pidToKill = appPid || appProcess?.pid;
if (pidToKill) {
// Убиваем весь процесс и все его дочерние процессы
killProcessTree(pidToKill);
} else if (appProcess) {
// Если по какой-то причине нет PID, пробуем стандартный способ
appProcess.kill("SIGTERM");
await new Promise((resolve) => setTimeout(resolve, 2000));
if (!appProcess.killed) {
appProcess.kill("SIGKILL");
}
}
activeProcesses.delete(sessionId);
// Обновить статус на "ended"
await updateSessionStatus(sessionId, "ended");
console.log(
`[${new Date().toISOString()}] ✅ Приложение и все дочерние процессы остановлены для сессии ${sessionId}`
);
} catch (error) {
console.error(
`[${new Date().toISOString()}] ❌ Ошибка остановки приложения:`,
error instanceof Error ? error.message : error
);
} finally {
processingSessions.delete(sessionId);
}
}
/**
* Проверить и обработать сессии
*/
async function checkSessions(): Promise<void> {
if (!SERVER_ID) {
return;
}
try {
const sessions = await fetchSessions();
// Получаем текущее время в UTC
const now = new Date();
// Обработать сессии со статусом "starting"
// Запускать только если время startAt уже наступило
// Примечание: PostgreSQL возвращает timestamp with timezone как ISO 8601 строку,
// которая автоматически парсится в UTC при создании Date объекта
const allStartingSessions = sessions.filter((s) => s.status === "starting");
const startingSessions = allStartingSessions.filter((s) => {
const startAt = new Date(s.startAt);
return startAt <= now;
});
// Логировать запланированные сессии, которые ещё не пришло время запускать
// Логируем не чаще раза в 10 секунд для каждой сессии, чтобы не спамить логами
const scheduledSessions = allStartingSessions.filter((s) => {
const startAt = new Date(s.startAt);
return startAt > now;
});
if (scheduledSessions.length > 0) {
for (const session of scheduledSessions) {
const lastLogTime = lastScheduledLogTime.get(session.id) || 0;
const timeSinceLastLog = now.getTime() - lastLogTime;
// Логируем только если прошло больше 10 секунд с последнего лога
if (timeSinceLastLog > 10000) {
const startAt = new Date(session.startAt);
const timeUntilStart = Math.round((startAt.getTime() - now.getTime()) / 1000);
console.log(
`[${new Date().toISOString()}] ⏰ Сессия ${session.id} (${session.app.name}) запланирована через ${timeUntilStart} сек`
);
lastScheduledLogTime.set(session.id, now.getTime());
}
}
}
// Очистить карту логирования для сессий, которые больше не запланированы
const scheduledSessionIds = new Set(scheduledSessions.map((s) => s.id));
for (const sessionId of lastScheduledLogTime.keys()) {
if (!scheduledSessionIds.has(sessionId)) {
lastScheduledLogTime.delete(sessionId);
}
}
for (const session of startingSessions) {
await startApplication(session);
}
// Обработать сессии со статусом "ending"
const endingSessions = sessions.filter((s) => s.status === "ending");
for (const session of endingSessions) {
await stopApplication(session);
}
// Проверить, что все активные процессы соответствуют активным сессиям
// Сессия считается активной, если:
// 1. Статус "started"
// 2. Статус "starting" И время startAt уже наступило
const activeSessions = sessions.filter((s) => {
if (s.status === "started") return true;
if (s.status === "starting") {
const startAt = new Date(s.startAt);
return startAt <= now;
}
return false;
});
const activeSessionIds = new Set(activeSessions.map((s) => s.id));
// Остановить процессы для сессий, которые больше не активны
for (const [sessionId, process] of activeProcesses.entries()) {
if (!activeSessionIds.has(sessionId)) {
console.log(
`[${new Date().toISOString()}] ⚠️ Найден процесс для неактивной сессии ${sessionId}, остановка`
);
process.kill("SIGTERM");
activeProcesses.delete(sessionId);
}
}
} catch (error: unknown) {
console.error(
`[${new Date().toISOString()}] ❌ Ошибка проверки сессий:`,
error instanceof Error ? error.message : error
);
}
// Планируем следующую проверку
setTimeout(() => checkSessions(), SESSION_CHECK_INTERVAL_MS);
}
/**
* Основная функция
*/
async function main() {
console.log("=".repeat(60));
console.log("🚀 Запуск сессионного сервера");
console.log("=".repeat(60));
console.log("Конфигурация:");
console.log(` API URL: ${API_URL}`);
console.log(` Hostname: ${HOSTNAME}`);
console.log(` Local IP: ${LOCAL_IP}`);
console.log(` Type: ${SERVER_TYPE}`);
console.log(` GPU Free MB: ${getGpuFreeMb()} (читается из nvidia-smi)`);
if (SERVER_LOCATION) console.log(` Location: ${SERVER_LOCATION}`);
if (SERVER_TIER) console.log(` Tier: ${SERVER_TIER}`);
if (BRANCH_ID) console.log(` Branch ID: ${BRANCH_ID}`);
console.log(` Register Interval: ${REGISTER_INTERVAL_MS}ms`);
console.log(` GPU Update Interval: ${GPU_UPDATE_INTERVAL_MS}ms`);
console.log(` Session Check Interval: ${SESSION_CHECK_INTERVAL_MS}ms`);
console.log("=".repeat(60));
// Запуск рекурсивной регистрации
// Использует setTimeout вместо setInterval, чтобы избежать наложения запросов
// После первой успешной регистрации запускается обновление GPU
await registerServer();
}
// Запуск
main().catch((error: unknown) => {
console.error(
"Критическая ошибка:",
error instanceof Error ? error.message : error
);
process.exit(1);
});
+103
View File
@@ -0,0 +1,103 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "ES2022", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */
// "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. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
"types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "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. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}