Add browser support with Socket.io transport, authentication, performance-optimized IPC, and CI/CD (#925)
This commit is contained in:
46
.devcontainer/devcontainer.json
Normal file
46
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "MQTT Explorer Development",
|
||||||
|
"dockerComposeFile": "docker-compose.yml",
|
||||||
|
"service": "app",
|
||||||
|
"workspaceFolder": "/workspace",
|
||||||
|
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"ms-vscode.vscode-typescript-next",
|
||||||
|
"ms-azuretools.vscode-docker",
|
||||||
|
"eamodio.gitlens"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"forwardPorts": [3000, 8080, 1883],
|
||||||
|
"portsAttributes": {
|
||||||
|
"3000": {
|
||||||
|
"label": "MQTT Explorer Server",
|
||||||
|
"onAutoForward": "notify"
|
||||||
|
},
|
||||||
|
"8080": {
|
||||||
|
"label": "Webpack Dev Server",
|
||||||
|
"onAutoForward": "notify"
|
||||||
|
},
|
||||||
|
"1883": {
|
||||||
|
"label": "MQTT Broker",
|
||||||
|
"onAutoForward": "ignore"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"postCreateCommand": "yarn install",
|
||||||
|
|
||||||
|
"remoteUser": "node"
|
||||||
|
}
|
||||||
21
.devcontainer/docker-compose.yml
Normal file
21
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: mcr.microsoft.com/devcontainers/javascript-node:20
|
||||||
|
volumes:
|
||||||
|
- ../..:/workspace:cached
|
||||||
|
command: sleep infinity
|
||||||
|
network_mode: service:mosquitto
|
||||||
|
environment:
|
||||||
|
- MQTT_EXPLORER_USERNAME=dev
|
||||||
|
- MQTT_EXPLORER_PASSWORD=dev123
|
||||||
|
|
||||||
|
mosquitto:
|
||||||
|
image: eclipse-mosquitto:2
|
||||||
|
ports:
|
||||||
|
- "1883:1883"
|
||||||
|
- "3000:3000"
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
|
||||||
4
.devcontainer/mosquitto.conf
Normal file
4
.devcontainer/mosquitto.conf
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Mosquitto configuration for development
|
||||||
|
listener 1883
|
||||||
|
allow_anonymous true
|
||||||
|
persistence false
|
||||||
6
.github/copilot-instructions.md
vendored
6
.github/copilot-instructions.md
vendored
@@ -25,6 +25,10 @@ yarn install
|
|||||||
# Build the project
|
# Build the project
|
||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
|
# Set password for browser testing
|
||||||
|
export MQTT_EXPLORER_USERNAME=admin
|
||||||
|
export MQTT_EXPLORER_PASSWORD=secretpassword
|
||||||
|
|
||||||
# Start the application
|
# Start the application
|
||||||
yarn start
|
yarn start
|
||||||
|
|
||||||
@@ -322,5 +326,5 @@ yarn package-with-docker
|
|||||||
- The app uses Electron (see `package.json` for version)
|
- The app uses Electron (see `package.json` for version)
|
||||||
- MQTT communication is handled via [mqttjs](https://github.com/mqttjs/MQTT.js)
|
- MQTT communication is handled via [mqttjs](https://github.com/mqttjs/MQTT.js)
|
||||||
- All code changes should pass linting (`yarn lint`)
|
- All code changes should pass linting (`yarn lint`)
|
||||||
- Node.js version requirement: >= 18
|
- Node.js version requirement: >= 20
|
||||||
- The project uses workspace-like structure with separate package.json files for app and backend
|
- The project uses workspace-like structure with separate package.json files for app and backend
|
||||||
|
|||||||
2
.github/workflows/copilot-setup.yml
vendored
2
.github/workflows/copilot-setup.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '20'
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
|
|||||||
50
.github/workflows/tests.yml
vendored
50
.github/workflows/tests.yml
vendored
@@ -74,3 +74,53 @@ jobs:
|
|||||||
run: echo '${{ steps.upload.outputs.file-url }}'
|
run: echo '${{ steps.upload.outputs.file-url }}'
|
||||||
id: artifact-upload-step
|
id: artifact-upload-step
|
||||||
- run: echo '<picture><img src="${{ steps.upload.outputs.file-url }}"></picture>' >> $GITHUB_STEP_SUMMARY
|
- run: echo '<picture><img src="${{ steps.upload.outputs.file-url }}"></picture>' >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
test-browser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
mosquitto:
|
||||||
|
image: eclipse-mosquitto:2
|
||||||
|
ports:
|
||||||
|
- 1883:1883
|
||||||
|
options: >-
|
||||||
|
--health-cmd "mosquitto_sub -t '$SYS/#' -C 1"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
- name: Build Browser Mode
|
||||||
|
run: yarn build:server
|
||||||
|
- name: Test App
|
||||||
|
run: yarn test:app
|
||||||
|
- name: Test Backend
|
||||||
|
run: yarn test:backend
|
||||||
|
- name: Start Server in Background
|
||||||
|
run: |
|
||||||
|
yarn start:server &
|
||||||
|
echo $! > server.pid
|
||||||
|
env:
|
||||||
|
MQTT_EXPLORER_USERNAME: test
|
||||||
|
MQTT_EXPLORER_PASSWORD: test123
|
||||||
|
PORT: 3000
|
||||||
|
- name: Wait for Server
|
||||||
|
run: |
|
||||||
|
timeout 30 bash -c 'until curl -f http://localhost:3000; do sleep 1; done'
|
||||||
|
- name: Browser Smoke Test
|
||||||
|
run: |
|
||||||
|
# Test server is running
|
||||||
|
curl -f http://localhost:3000 || exit 1
|
||||||
|
echo "Browser mode server is running successfully"
|
||||||
|
- name: Stop Server
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ -f server.pid ]; then
|
||||||
|
kill $(cat server.pid) || true
|
||||||
|
rm server.pid
|
||||||
|
fi
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,5 +15,5 @@ mqtt-explorer-mcp-screenshot.png
|
|||||||
screenshot-mcp-*.png
|
screenshot-mcp-*.png
|
||||||
test-mcp-introspection.js
|
test-mcp-introspection.js
|
||||||
|
|
||||||
# UI test artifacts
|
/data
|
||||||
test-screenshot-*.png
|
test-screenshot-*.png
|
||||||
|
|||||||
193
BROWSER_MODE.md
Normal file
193
BROWSER_MODE.md
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
# Browser Mode Documentation
|
||||||
|
|
||||||
|
MQTT Explorer now supports running as a web application served by a Node.js server, in addition to the existing Electron desktop app.
|
||||||
|
|
||||||
|
## Running in Browser Mode
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
1. Build the application for browser mode:
|
||||||
|
```bash
|
||||||
|
yarn build:server
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start the server:
|
||||||
|
```bash
|
||||||
|
yarn start:server
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Open your browser and navigate to `http://localhost:3000`
|
||||||
|
|
||||||
|
4. You'll be prompted to log in with credentials that were generated on server startup.
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
To run in development mode with hot reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev:server
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts both the webpack dev server and the backend server.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
You can set custom authentication credentials using environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MQTT_EXPLORER_USERNAME=admin
|
||||||
|
export MQTT_EXPLORER_PASSWORD=secretpassword
|
||||||
|
yarn start:server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generated Credentials
|
||||||
|
|
||||||
|
If no environment variables are set, the server will generate credentials on first startup and save them to `data/credentials.json`. The generated credentials will be printed to the console:
|
||||||
|
|
||||||
|
```
|
||||||
|
============================================================
|
||||||
|
Generated new credentials:
|
||||||
|
Username: user-abc123
|
||||||
|
Password: 123e4567-e89b-12d3-a456-426614174000
|
||||||
|
============================================================
|
||||||
|
Please save these credentials. They will be persisted to:
|
||||||
|
/path/to/data/credentials.json
|
||||||
|
============================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Certificate Upload
|
||||||
|
|
||||||
|
In browser mode, certificate files are uploaded directly through the browser using the HTML5 File API. The certificates are:
|
||||||
|
- Read client-side as base64
|
||||||
|
- Stored in the connection configuration
|
||||||
|
- Used when establishing MQTT connections
|
||||||
|
|
||||||
|
### Data Storage
|
||||||
|
|
||||||
|
In browser mode, all data is stored on the server:
|
||||||
|
- Credentials: `data/credentials.json`
|
||||||
|
- Uploaded certificates: `data/certificates/`
|
||||||
|
- File uploads: `data/uploads/`
|
||||||
|
|
||||||
|
### Port Configuration
|
||||||
|
|
||||||
|
The default port is 3000. You can change it using the `PORT` environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PORT=8080 yarn start:server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Client-Server Communication
|
||||||
|
|
||||||
|
- **Electron Mode**: Uses Electron IPC for communication between renderer and main process
|
||||||
|
- **Browser Mode**: Uses Socket.io WebSockets for real-time communication between browser and server
|
||||||
|
|
||||||
|
The application automatically detects the environment and uses the appropriate transport layer.
|
||||||
|
|
||||||
|
### Event Bus Abstraction
|
||||||
|
|
||||||
|
Both Electron IPC and Socket.io implement the same `EventBusInterface`, allowing the application code to work seamlessly in both modes without modification.
|
||||||
|
|
||||||
|
## Differences from Electron Mode
|
||||||
|
|
||||||
|
### Browser Mode Limitations
|
||||||
|
|
||||||
|
1. **File System Access**: Limited to server-side operations
|
||||||
|
2. **Native Dialogs**: File selection uses browser file input instead of native dialogs
|
||||||
|
3. **Auto-Updates**: Not available in browser mode
|
||||||
|
4. **Tray Icon**: Not available in browser mode
|
||||||
|
|
||||||
|
### Browser Mode Advantages
|
||||||
|
|
||||||
|
1. **No Installation**: Access from any browser
|
||||||
|
2. **Cross-Platform**: Works on any device with a modern browser
|
||||||
|
3. **Remote Access**: Can be deployed on a server for remote access
|
||||||
|
4. **Multi-User**: Can support authentication for multiple users
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **HTTPS**: For production, always use HTTPS to encrypt credentials and MQTT data
|
||||||
|
2. **Authentication**: Keep credentials secure and rotate them regularly
|
||||||
|
3. **Network**: Ensure the server is on a trusted network or behind a firewall
|
||||||
|
4. **Environment Variables**: Use environment variables for production credentials, not the generated ones
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
For production deployment:
|
||||||
|
|
||||||
|
1. Build the application:
|
||||||
|
```bash
|
||||||
|
yarn build:server
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Set environment variables:
|
||||||
|
```bash
|
||||||
|
export MQTT_EXPLORER_USERNAME=your_username
|
||||||
|
export MQTT_EXPLORER_PASSWORD=your_secure_password
|
||||||
|
export PORT=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Start the server:
|
||||||
|
```bash
|
||||||
|
yarn start:server
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Use a reverse proxy (nginx, Apache) to add HTTPS and additional security features
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
Enable detailed Socket.IO connection and lifecycle debugging:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEBUG=mqtt-explorer:socketio* yarn start:server
|
||||||
|
```
|
||||||
|
|
||||||
|
Available debug namespaces:
|
||||||
|
- `mqtt-explorer:socketio` - General Socket.IO events and metrics
|
||||||
|
- `mqtt-explorer:socketio:connect` - Client connection events
|
||||||
|
- `mqtt-explorer:socketio:disconnect` - Client disconnection and cleanup
|
||||||
|
- `mqtt-explorer:socketio:subscriptions` - Subscription lifecycle tracking
|
||||||
|
- `mqtt-explorer:socketio:connections` - MQTT connection ownership
|
||||||
|
|
||||||
|
This will log:
|
||||||
|
- Client connect/disconnect events
|
||||||
|
- Subscription counts per socket
|
||||||
|
- MQTT connection ownership tracking
|
||||||
|
- Memory leak detection metrics (subscriptions, handlers, connections)
|
||||||
|
|
||||||
|
Example output:
|
||||||
|
```
|
||||||
|
mqtt-explorer:socketio:connect Client connected: abc123de
|
||||||
|
mqtt-explorer:socketio [connect] clients=1 subscriptions=8 mqttConns=0 | socket[abc123de]: subs=8 conns=0
|
||||||
|
mqtt-explorer:socketio:connections Connection my-mqtt owned by socket abc123de (total: 1)
|
||||||
|
mqtt-explorer:socketio:disconnect Client disconnected: abc123de
|
||||||
|
mqtt-explorer:socketio:subscriptions Removed 8 subscriptions for socket abc123de
|
||||||
|
mqtt-explorer:socketio [disconnect] clients=0 subscriptions=0 mqttConns=0 | socket[abc123de]: subs=0 conns=0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication Fails
|
||||||
|
|
||||||
|
1. Check the console output for the generated credentials
|
||||||
|
2. Clear browser session storage: `sessionStorage.clear()` in browser console
|
||||||
|
3. Restart the server to regenerate credentials
|
||||||
|
|
||||||
|
### Connection Issues
|
||||||
|
|
||||||
|
1. Check that the server is running: `http://localhost:3000`
|
||||||
|
2. Check browser console for Socket.io connection errors
|
||||||
|
3. Verify firewall rules allow the port
|
||||||
|
|
||||||
|
### Certificate Upload Issues
|
||||||
|
|
||||||
|
In browser mode, certificates are handled differently:
|
||||||
|
- Use the file upload button to select certificate files
|
||||||
|
- Files are read and encoded client-side
|
||||||
|
- Large certificate files (>16KB) will be rejected
|
||||||
149
CI_CD.md
Normal file
149
CI_CD.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# CI/CD Pipeline Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
MQTT Explorer uses GitHub Actions for continuous integration and testing. The pipeline tests both Electron (desktop) and browser modes.
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
### Test Workflow (`.github/workflows/tests.yml`)
|
||||||
|
|
||||||
|
This workflow runs on pull requests to `master`, `beta`, and `release` branches.
|
||||||
|
|
||||||
|
#### Jobs
|
||||||
|
|
||||||
|
##### 1. `test` - Electron Mode Tests
|
||||||
|
|
||||||
|
Tests the traditional Electron desktop application:
|
||||||
|
|
||||||
|
- **Environment**: Custom Docker container (`ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest`)
|
||||||
|
- **Steps**:
|
||||||
|
1. Install dependencies with frozen lockfile
|
||||||
|
2. Build the Electron application
|
||||||
|
3. Run unit tests (app + backend)
|
||||||
|
4. Run UI tests with video recording
|
||||||
|
5. Upload test video to S3
|
||||||
|
6. Display test results in GitHub summary
|
||||||
|
|
||||||
|
**Artifacts**: UI test video (GIF format) uploaded to S3
|
||||||
|
|
||||||
|
##### 2. `test-browser` - Browser Mode Tests
|
||||||
|
|
||||||
|
Tests the new browser/server mode:
|
||||||
|
|
||||||
|
- **Environment**: Ubuntu latest with Node.js 20
|
||||||
|
- **Services**:
|
||||||
|
- **Mosquitto MQTT Broker**: Eclipse Mosquitto v2 on port 1883
|
||||||
|
- Health checks enabled
|
||||||
|
- Anonymous connections allowed
|
||||||
|
- **Steps**:
|
||||||
|
1. Setup Node.js 20
|
||||||
|
2. Install dependencies
|
||||||
|
3. Build browser mode (`yarn build:server`)
|
||||||
|
4. Run unit tests (app + backend)
|
||||||
|
5. Start server in background with test credentials
|
||||||
|
6. Wait for server to be ready
|
||||||
|
7. Run browser smoke tests
|
||||||
|
8. Clean up server process
|
||||||
|
|
||||||
|
**Environment Variables**:
|
||||||
|
- `MQTT_EXPLORER_USERNAME=test`
|
||||||
|
- `MQTT_EXPLORER_PASSWORD=test123`
|
||||||
|
- `PORT=3000`
|
||||||
|
|
||||||
|
## Test Commands
|
||||||
|
|
||||||
|
The following npm scripts are used in CI/CD:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Unit tests
|
||||||
|
yarn test # Run all tests (app + backend)
|
||||||
|
yarn test:app # Frontend tests only
|
||||||
|
yarn test:backend # Backend tests only
|
||||||
|
|
||||||
|
# Build
|
||||||
|
yarn build # Build Electron mode
|
||||||
|
yarn build:server # Build browser mode
|
||||||
|
|
||||||
|
# UI Tests (Electron only)
|
||||||
|
yarn ui-test # Run UI tests with video recording
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding New Tests
|
||||||
|
|
||||||
|
### For Electron Mode
|
||||||
|
|
||||||
|
Add tests to the `test` job. UI tests should be added to the test suite that `yarn ui-test` runs.
|
||||||
|
|
||||||
|
### For Browser Mode
|
||||||
|
|
||||||
|
Browser-specific tests should:
|
||||||
|
1. Use the pre-configured Mosquitto service
|
||||||
|
2. Connect to `mqtt://mosquitto:1883`
|
||||||
|
3. Test server endpoints at `http://localhost:3000`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```yaml
|
||||||
|
- name: Browser Integration Test
|
||||||
|
run: |
|
||||||
|
# Test MQTT connection through server
|
||||||
|
curl -X POST http://localhost:3000/api/test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local Testing
|
||||||
|
|
||||||
|
### Electron Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
yarn test
|
||||||
|
yarn ui-test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Browser Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start Mosquitto in Docker
|
||||||
|
docker run -d -p 1883:1883 eclipse-mosquitto:2
|
||||||
|
|
||||||
|
# Build and test
|
||||||
|
yarn build:server
|
||||||
|
yarn test
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
MQTT_EXPLORER_USERNAME=test MQTT_EXPLORER_PASSWORD=test123 yarn start:server
|
||||||
|
|
||||||
|
# Run manual tests
|
||||||
|
curl http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## GitHub Codespaces / Devcontainer
|
||||||
|
|
||||||
|
The repository includes a devcontainer configuration that automatically sets up:
|
||||||
|
- Node.js 20
|
||||||
|
- MQTT broker (Mosquitto)
|
||||||
|
- All development dependencies
|
||||||
|
- Port forwarding for development
|
||||||
|
|
||||||
|
See [.devcontainer/README.md](.devcontainer/README.md) for details.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Browser Tests Failing
|
||||||
|
|
||||||
|
1. **Server won't start**: Check if port 3000 is already in use
|
||||||
|
2. **MQTT connection fails**: Ensure Mosquitto service is healthy
|
||||||
|
3. **Timeout errors**: Increase timeout in "Wait for Server" step
|
||||||
|
|
||||||
|
### Electron Tests Failing
|
||||||
|
|
||||||
|
1. **UI tests timeout**: Check if the Docker container has display access
|
||||||
|
2. **Build fails**: Verify all dependencies are in yarn.lock
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
- [ ] Add E2E browser tests with Playwright
|
||||||
|
- [ ] Test WebSocket connections in browser mode
|
||||||
|
- [ ] Add performance benchmarks
|
||||||
|
- [ ] Test with different MQTT broker versions
|
||||||
|
- [ ] Add security scanning for browser mode
|
||||||
39
Readme.md
39
Readme.md
@@ -18,8 +18,22 @@ Downloads can be found at the link above.
|
|||||||
This page is dedicated to its development.
|
This page is dedicated to its development.
|
||||||
Pull-Requests and error reports are welcome.
|
Pull-Requests and error reports are welcome.
|
||||||
|
|
||||||
|
## Quick Start with GitHub Codespaces
|
||||||
|
|
||||||
|
The fastest way to start developing is with GitHub Codespaces:
|
||||||
|
|
||||||
|
1. Click the green "Code" button above
|
||||||
|
2. Select "Codespaces" tab
|
||||||
|
3. Click "Create codespace on [branch]"
|
||||||
|
4. Wait for the environment to set up (includes Node.js and MQTT broker)
|
||||||
|
5. Run `yarn dev:server` to start development
|
||||||
|
|
||||||
|
The devcontainer includes a pre-configured MQTT broker and all development tools. See [.devcontainer/README.md](.devcontainer/README.md) for details.
|
||||||
|
|
||||||
## Run from sources
|
## Run from sources
|
||||||
|
|
||||||
|
### Desktop Application (Electron)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g yarn
|
npm install -g yarn
|
||||||
yarn
|
yarn
|
||||||
@@ -27,8 +41,23 @@ yarn build
|
|||||||
yarn start
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Browser Mode (Web Application)
|
||||||
|
|
||||||
|
MQTT Explorer can also run as a web application served by a Node.js server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g yarn
|
||||||
|
yarn
|
||||||
|
yarn build:server
|
||||||
|
yarn start:server
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open your browser to `http://localhost:3000`. For more details, see [BROWSER_MODE.md](BROWSER_MODE.md).
|
||||||
|
|
||||||
## Develop
|
## Develop
|
||||||
|
|
||||||
|
### Desktop Application
|
||||||
|
|
||||||
Launch Application
|
Launch Application
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -37,6 +66,16 @@ yarn
|
|||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Browser Mode
|
||||||
|
|
||||||
|
Launch in development mode with hot reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g yarn
|
||||||
|
yarn
|
||||||
|
yarn dev:server
|
||||||
|
```
|
||||||
|
|
||||||
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management, `src` contains all the electron bindings. [mqttjs](https://github.com/mqttjs/MQTT.js) is used to facilitate communication to MQTT brokers.
|
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management, `src` contains all the electron bindings. [mqttjs](https://github.com/mqttjs/MQTT.js) is used to facilitate communication to MQTT brokers.
|
||||||
|
|
||||||
## Automated Tests
|
## Automated Tests
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"mochatest": "mocha --require ts-node/register --require source-map-support/register --recursive src/*/**/*.spec.ts"
|
"mochatest": "mocha --require ts-node/register --require source-map-support/register --recursive src/*/**/*.spec.ts"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "CC-BY-ND-4.0",
|
"license": "CC-BY-ND-4.0",
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"d3-shape": "^1.3.5",
|
"d3-shape": "^1.3.5",
|
||||||
"diff": "^4.0.1",
|
"diff": "^4.0.1",
|
||||||
"dot-prop": "^5.0.0",
|
"dot-prop": "^5.0.0",
|
||||||
|
"events": "^3.3.0",
|
||||||
"get-value": "^3.0.1",
|
"get-value": "^3.0.1",
|
||||||
"immutable": "^4.0.0-rc.12",
|
"immutable": "^4.0.0-rc.12",
|
||||||
"in-viewport": "^3.6.0",
|
"in-viewport": "^3.6.0",
|
||||||
@@ -37,7 +38,9 @@
|
|||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"moving-average": "^1.0.0",
|
"moving-average": "^1.0.0",
|
||||||
"number-abbreviate": "^2.0.0",
|
"number-abbreviate": "^2.0.0",
|
||||||
|
"os-browserify": "^0.3.0",
|
||||||
"parse-duration": "^0.1.1",
|
"parse-duration": "^0.1.1",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
"prismjs": "^1.15.0",
|
"prismjs": "^1.15.0",
|
||||||
"react": "^16.11",
|
"react": "^16.11",
|
||||||
"react-ace": "^8",
|
"react-ace": "^8",
|
||||||
@@ -51,7 +54,8 @@
|
|||||||
"redux-batched-actions": "0.5",
|
"redux-batched-actions": "0.5",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"sha1": "^1.1.1",
|
"sha1": "^1.1.1",
|
||||||
"socket.io-client": "^2.2.0",
|
"socket.io-client": "^4.8.1",
|
||||||
|
"url": "^0.11.4",
|
||||||
"uuid": "7"
|
"uuid": "7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -66,7 +70,7 @@
|
|||||||
"@types/react-redux": "^7.0.9",
|
"@types/react-redux": "^7.0.9",
|
||||||
"@types/react-resize-detector": "^4.0.1",
|
"@types/react-resize-detector": "^4.0.1",
|
||||||
"@types/sha1": "^1.1.1",
|
"@types/sha1": "^1.1.1",
|
||||||
"@types/socket.io-client": "^1.4.32",
|
"@types/socket.io-client": "^3.0.0",
|
||||||
"@types/uuid": "^7.0.2",
|
"@types/uuid": "^7.0.2",
|
||||||
"@types/vis": "^4.21.9",
|
"@types/vis": "^4.21.9",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
|
|||||||
65
app/src/components/BrowserAuthWrapper.tsx
Normal file
65
app/src/components/BrowserAuthWrapper.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { LoginDialog } from './LoginDialog'
|
||||||
|
|
||||||
|
interface BrowserAuthWrapperProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBrowserMode =
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
(typeof process === 'undefined' || process.env?.BROWSER_MODE === 'true')
|
||||||
|
|
||||||
|
export function BrowserAuthWrapper(props: BrowserAuthWrapperProps) {
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = React.useState(false)
|
||||||
|
const [loginError, setLoginError] = React.useState<string | undefined>()
|
||||||
|
const [showLogin, setShowLogin] = React.useState(false)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isBrowserMode) {
|
||||||
|
// Not in browser mode, skip authentication
|
||||||
|
setIsAuthenticated(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already authenticated
|
||||||
|
const username = sessionStorage.getItem('mqtt-explorer-username')
|
||||||
|
const password = sessionStorage.getItem('mqtt-explorer-password')
|
||||||
|
|
||||||
|
if (username && password) {
|
||||||
|
// Try to use stored credentials
|
||||||
|
setIsAuthenticated(true)
|
||||||
|
} else {
|
||||||
|
// Show login dialog
|
||||||
|
setShowLogin(true)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleLogin = async (username: string, password: string) => {
|
||||||
|
try {
|
||||||
|
// Store credentials in session storage
|
||||||
|
sessionStorage.setItem('mqtt-explorer-username', username)
|
||||||
|
sessionStorage.setItem('mqtt-explorer-password', password)
|
||||||
|
|
||||||
|
// The socket will use these credentials on next connection
|
||||||
|
setIsAuthenticated(true)
|
||||||
|
setShowLogin(false)
|
||||||
|
setLoginError(undefined)
|
||||||
|
|
||||||
|
// Reload to reinitialize socket with new auth
|
||||||
|
window.location.reload()
|
||||||
|
} catch (error) {
|
||||||
|
setLoginError('Login failed. Please check your credentials.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isBrowserMode) {
|
||||||
|
// Not in browser mode, render children directly
|
||||||
|
return <>{props.children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <LoginDialog open={showLogin} onLogin={handleLogin} error={loginError} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{props.children}</>
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import ClearAdornment from '../helper/ClearAdornment'
|
||||||
|
import Lock from '@material-ui/icons/Lock'
|
||||||
|
import { bindActionCreators } from 'redux'
|
||||||
|
import { Button, Theme, Tooltip, Typography } from '@material-ui/core'
|
||||||
|
import { CertificateParameters, ConnectionOptions } from '../../model/ConnectionOptions'
|
||||||
|
import { CertificateTypes } from '../../actions/ConnectionManager'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { connectionManagerActions } from '../../actions'
|
||||||
|
import { withStyles } from '@material-ui/styles'
|
||||||
|
import { rendererRpc } from '../../../../events'
|
||||||
|
import { RpcEvents } from '../../../../events/EventsV2'
|
||||||
|
|
||||||
|
function BrowserCertificateFileSelection(props: {
|
||||||
|
certificateType: CertificateTypes
|
||||||
|
title: string
|
||||||
|
certificate?: CertificateParameters
|
||||||
|
classes: any
|
||||||
|
actions: {
|
||||||
|
connectionManager: typeof connectionManagerActions
|
||||||
|
}
|
||||||
|
connection: ConnectionOptions
|
||||||
|
}) {
|
||||||
|
const fileInputRef = React.useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const clearCertificate = React.useCallback(() => {
|
||||||
|
props.actions.connectionManager.updateConnection(props.connection.id, {
|
||||||
|
[props.certificateType]: undefined,
|
||||||
|
})
|
||||||
|
}, [props.connection, props.certificateType])
|
||||||
|
|
||||||
|
const handleFileSelect = React.useCallback(
|
||||||
|
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0]
|
||||||
|
if (!file) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read file content
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = async e => {
|
||||||
|
const content = e.target?.result
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
// Convert to base64
|
||||||
|
const base64Data = content.split(',')[1] || content
|
||||||
|
|
||||||
|
// Upload via IPC instead of HTTP POST
|
||||||
|
const result = await rendererRpc.call(RpcEvents.uploadCertificate, {
|
||||||
|
filename: file.name,
|
||||||
|
data: base64Data,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create certificate parameters
|
||||||
|
const certificate: CertificateParameters = {
|
||||||
|
name: result.name,
|
||||||
|
data: result.data,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update connection
|
||||||
|
props.actions.connectionManager.updateConnection(props.connection.id, {
|
||||||
|
[props.certificateType]: certificate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading certificate:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset input
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[props.connection.id, props.certificateType, props.actions.connectionManager]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleButtonClick = () => {
|
||||||
|
fileInputRef.current?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".pem,.crt,.cer,.key"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
/>
|
||||||
|
<Tooltip title="Select certificate" placement="top">
|
||||||
|
<Button variant="contained" className={props.classes.button} onClick={handleButtonClick}>
|
||||||
|
<Lock /> {props.title}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<ClearCertificate classes={props.classes} certificate={props.certificate} action={clearCertificate} />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClearCertificate(props: { classes: any; certificate?: CertificateParameters; action: () => void }) {
|
||||||
|
if (!props.certificate) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={props.certificate.name}>
|
||||||
|
<Typography className={props.classes.certificateName}>
|
||||||
|
<ClearAdornment action={props.action} value={props.certificate.name} />
|
||||||
|
{props.certificate.name}
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: any) => {
|
||||||
|
return {
|
||||||
|
actions: {
|
||||||
|
connectionManager: bindActionCreators(connectionManagerActions, dispatch),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = (theme: Theme) => ({
|
||||||
|
certificateName: {
|
||||||
|
width: '100%',
|
||||||
|
height: 'calc(1em + 4px)',
|
||||||
|
overflow: 'hidden' as 'hidden',
|
||||||
|
whiteSpace: 'nowrap' as 'nowrap',
|
||||||
|
textOverflow: 'ellipsis' as 'ellipsis',
|
||||||
|
color: theme.palette.text.hint,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginTop: theme.spacing(3),
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(BrowserCertificateFileSelection))
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import CertificateFileSelection from './CertificateFileSelection'
|
import CertificateFileSelection from './CertificateFileSelection'
|
||||||
|
import BrowserCertificateFileSelection from './BrowserCertificateFileSelection'
|
||||||
import Undo from '@material-ui/icons/Undo'
|
import Undo from '@material-ui/icons/Undo'
|
||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
import { Button, Grid } from '@material-ui/core'
|
import { Button, Grid } from '@material-ui/core'
|
||||||
@@ -8,6 +9,12 @@ import { connectionManagerActions } from '../../actions'
|
|||||||
import { ConnectionOptions } from '../../model/ConnectionOptions'
|
import { ConnectionOptions } from '../../model/ConnectionOptions'
|
||||||
import { Theme, withStyles } from '@material-ui/core/styles'
|
import { Theme, withStyles } from '@material-ui/core/styles'
|
||||||
|
|
||||||
|
// Check if we're in browser mode
|
||||||
|
const isBrowserMode =
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
(typeof process === 'undefined' || process.env?.BROWSER_MODE === 'true')
|
||||||
|
const CertSelector = isBrowserMode ? BrowserCertificateFileSelection : CertificateFileSelection
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
connection: ConnectionOptions
|
connection: ConnectionOptions
|
||||||
classes: any
|
classes: any
|
||||||
@@ -45,7 +52,7 @@ class Certificates extends React.PureComponent<Props, State> {
|
|||||||
<form noValidate={true} autoComplete="off">
|
<form noValidate={true} autoComplete="off">
|
||||||
<Grid container={true} spacing={3}>
|
<Grid container={true} spacing={3}>
|
||||||
<Grid item={true} xs={12} className={classes.gridPadding}>
|
<Grid item={true} xs={12} className={classes.gridPadding}>
|
||||||
<CertificateFileSelection
|
<CertSelector
|
||||||
connection={this.props.connection}
|
connection={this.props.connection}
|
||||||
certificate={this.props.connection.selfSignedCertificate}
|
certificate={this.props.connection.selfSignedCertificate}
|
||||||
title="Server Certificate (CA)"
|
title="Server Certificate (CA)"
|
||||||
@@ -53,7 +60,7 @@ class Certificates extends React.PureComponent<Props, State> {
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item={true} xs={12} className={classes.gridPadding}>
|
<Grid item={true} xs={12} className={classes.gridPadding}>
|
||||||
<CertificateFileSelection
|
<CertSelector
|
||||||
connection={this.props.connection}
|
connection={this.props.connection}
|
||||||
certificate={this.props.connection.clientCertificate}
|
certificate={this.props.connection.clientCertificate}
|
||||||
title="Client Certificate"
|
title="Client Certificate"
|
||||||
@@ -61,7 +68,7 @@ class Certificates extends React.PureComponent<Props, State> {
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item={true} xs={12} className={classes.gridPadding}>
|
<Grid item={true} xs={12} className={classes.gridPadding}>
|
||||||
<CertificateFileSelection
|
<CertSelector
|
||||||
connection={this.props.connection}
|
connection={this.props.connection}
|
||||||
certificate={this.props.connection.clientKey}
|
certificate={this.props.connection.clientKey}
|
||||||
title="Client Key"
|
title="Client Key"
|
||||||
|
|||||||
57
app/src/components/LoginDialog.tsx
Normal file
57
app/src/components/LoginDialog.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { Dialog, DialogTitle, DialogContent, DialogActions, TextField, Button, Typography } from '@material-ui/core'
|
||||||
|
|
||||||
|
interface LoginDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onLogin: (username: string, password: string) => void
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginDialog(props: LoginDialogProps) {
|
||||||
|
const [username, setUsername] = React.useState('')
|
||||||
|
const [password, setPassword] = React.useState('')
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
props.onLogin(username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={props.open} disableEscapeKeyDown disableBackdropClick>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<DialogTitle>Login to MQTT Explorer</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{props.error && (
|
||||||
|
<Typography color="error" style={{ marginBottom: 16 }}>
|
||||||
|
{props.error}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
label="Username"
|
||||||
|
type="text"
|
||||||
|
fullWidth
|
||||||
|
value={username}
|
||||||
|
onChange={e => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
fullWidth
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button type="submit" color="primary" variant="contained">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import compareVersions from 'compare-versions'
|
import compareVersions from 'compare-versions'
|
||||||
import electron from 'electron'
|
import electron from 'electron'
|
||||||
import os from 'os'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
@@ -182,9 +181,10 @@ class UpdateNotifier extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
private assetForCurrentPlatform(asset: GithubAsset) {
|
private assetForCurrentPlatform(asset: GithubAsset) {
|
||||||
let regex: RegExp
|
let regex: RegExp
|
||||||
if (os.platform() === 'darwin') {
|
const platform = this.getPlatform()
|
||||||
|
if (platform === 'darwin') {
|
||||||
regex = /\.dmg$/
|
regex = /\.dmg$/
|
||||||
} else if (os.platform() === 'win32') {
|
} else if (platform === 'win32') {
|
||||||
regex = /\.exe$/
|
regex = /\.exe$/
|
||||||
} else {
|
} else {
|
||||||
regex = /\.AppImage$/
|
regex = /\.AppImage$/
|
||||||
@@ -193,6 +193,14 @@ class UpdateNotifier extends React.PureComponent<Props, State> {
|
|||||||
return regex.test(asset.name)
|
return regex.test(asset.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPlatform(): string {
|
||||||
|
if (typeof window === 'undefined') return 'linux'
|
||||||
|
const userAgent = window.navigator.userAgent.toLowerCase()
|
||||||
|
if (userAgent.includes('mac')) return 'darwin'
|
||||||
|
if (userAgent.includes('win')) return 'win32'
|
||||||
|
return 'linux'
|
||||||
|
}
|
||||||
|
|
||||||
private renderDownloads() {
|
private renderDownloads() {
|
||||||
const latestUpdate = this.state.newerVersions[0]
|
const latestUpdate = this.state.newerVersions[0]
|
||||||
if (!latestUpdate || !latestUpdate.assets) {
|
if (!latestUpdate || !latestUpdate.assets) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { connect, Provider } from 'react-redux'
|
|||||||
import { ThemeProvider } from '@material-ui/styles'
|
import { ThemeProvider } from '@material-ui/styles'
|
||||||
import './utils/tracking'
|
import './utils/tracking'
|
||||||
import { themes } from './theme'
|
import { themes } from './theme'
|
||||||
|
import { BrowserAuthWrapper } from './components/BrowserAuthWrapper'
|
||||||
|
|
||||||
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
|
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
|
||||||
const store = createStore(reducers, composeEnhancers(applyMiddleware(reduxThunk, batchDispatchMiddleware)))
|
const store = createStore(reducers, composeEnhancers(applyMiddleware(reduxThunk, batchDispatchMiddleware)))
|
||||||
@@ -33,7 +34,9 @@ const Application = connect(mapStateToProps)(ApplicationRenderer)
|
|||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
<BrowserAuthWrapper>
|
||||||
<Application />
|
<Application />
|
||||||
|
</BrowserAuthWrapper>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
document.getElementById('app')
|
document.getElementById('app')
|
||||||
)
|
)
|
||||||
|
|||||||
12
app/src/mocks/electron.ts
Normal file
12
app/src/mocks/electron.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Mock electron module for browser environment
|
||||||
|
export const shell = {
|
||||||
|
openExternal: (url: string) => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
shell,
|
||||||
|
}
|
||||||
102
app/webpack.browser.config.js
Normal file
102
app/webpack.browser.config.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// Browser-specific webpack configuration
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
|
const webpack = require('webpack')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
app: './src/index.tsx',
|
||||||
|
bugtracking: './src/utils/bugtracking.ts',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
chunkFilename: '[name].bundle.js',
|
||||||
|
filename: '[name].bundle.js',
|
||||||
|
path: `${__dirname}/build`,
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: false,
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'all',
|
||||||
|
minSize: 30000,
|
||||||
|
minChunks: 1,
|
||||||
|
maxAsyncRequests: 5,
|
||||||
|
maxInitialRequests: 3,
|
||||||
|
automaticNameDelimiter: '~',
|
||||||
|
cacheGroups: {
|
||||||
|
vendors: {
|
||||||
|
test: /[\\/]node_modules[\\/](react|react-dom|@material-ui|popper\.js|react|react-redux|prop-types|jss|redux|scheduler|react-transition-group)[\\/]/,
|
||||||
|
name: 'vendors',
|
||||||
|
chunks: 'all',
|
||||||
|
priority: -10,
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
name: 'default',
|
||||||
|
minChunks: 2,
|
||||||
|
priority: -20,
|
||||||
|
reuseExistingChunk: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtimeChunk: 'single',
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
hot: true,
|
||||||
|
liveReload: true,
|
||||||
|
},
|
||||||
|
target: 'web', // Changed from 'electron-renderer' to 'web'
|
||||||
|
mode: 'production',
|
||||||
|
devtool: 'source-map',
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.mjs', '.m.js', '.tsx', '.js', '.json'],
|
||||||
|
modules: ['node_modules', path.resolve(__dirname, 'node_modules')],
|
||||||
|
alias: {
|
||||||
|
electron: require.resolve('./src/mocks/electron.ts'),
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
// Browser fallbacks for Node.js modules
|
||||||
|
path: require.resolve('path-browserify'),
|
||||||
|
fs: false,
|
||||||
|
crypto: false,
|
||||||
|
url: require.resolve('url/'),
|
||||||
|
os: require.resolve('os-browserify/browser'),
|
||||||
|
events: require.resolve('events/'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'ts-loader',
|
||||||
|
options: {
|
||||||
|
transpileOnly: true, // Skip type checking, we already did it with tsc
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: ['style-loader', 'css-loader'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jpg|gif)$/i,
|
||||||
|
type: 'asset/resource',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({ template: './index.html', file: './build/index.html', inject: false }),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.BROWSER_MODE': JSON.stringify('true'),
|
||||||
|
}),
|
||||||
|
new webpack.NormalModuleReplacementPlugin(/EventSystem[\\/]EventBus$/, resource => {
|
||||||
|
console.log('Replacing EventBus:', resource.request);
|
||||||
|
resource.request = resource.request.replace(/EventBus$/, 'BrowserEventBus');
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
externals: {},
|
||||||
|
cache: false,
|
||||||
|
}
|
||||||
359
app/yarn.lock
359
app/yarn.lock
@@ -188,6 +188,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817"
|
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817"
|
||||||
integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==
|
integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==
|
||||||
|
|
||||||
|
"@socket.io/component-emitter@~3.1.0":
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
|
||||||
|
integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==
|
||||||
|
|
||||||
"@types/body-parser@*":
|
"@types/body-parser@*":
|
||||||
version "1.19.5"
|
version "1.19.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4"
|
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4"
|
||||||
@@ -668,10 +673,12 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/socket.io-client@^1.4.32":
|
"@types/socket.io-client@^3.0.0":
|
||||||
version "1.4.36"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/socket.io-client/-/socket.io-client-1.4.36.tgz#e4f1ca065f84c20939e9850e70222202bd76ff3f"
|
resolved "https://registry.yarnpkg.com/@types/socket.io-client/-/socket.io-client-3.0.0.tgz#d0b8ea22121b7c1df68b6a923002f9c8e3cefb42"
|
||||||
integrity sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag==
|
integrity sha512-s+IPvFoEIjKA3RdJz/Z2dGR4gLgysKi8owcnrVwNjgvc01Lk68LJDDsG2GRqegFITcxmvCMYM7bhMpwEMlHmDg==
|
||||||
|
dependencies:
|
||||||
|
socket.io-client "*"
|
||||||
|
|
||||||
"@types/sockjs@^0.3.36":
|
"@types/sockjs@^0.3.36":
|
||||||
version "0.3.36"
|
version "0.3.36"
|
||||||
@@ -873,11 +880,6 @@ acorn@^8.0.4, acorn@^8.7.1, acorn@^8.8.2:
|
|||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
|
||||||
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
|
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
|
||||||
|
|
||||||
after@0.8.2:
|
|
||||||
version "0.8.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
|
|
||||||
integrity sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==
|
|
||||||
|
|
||||||
ajv-formats@^2.1.1:
|
ajv-formats@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
|
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
|
||||||
@@ -967,11 +969,6 @@ array-flatten@1.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
|
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
|
||||||
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
|
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
|
||||||
|
|
||||||
arraybuffer.slice@~0.0.7:
|
|
||||||
version "0.0.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
|
|
||||||
integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==
|
|
||||||
|
|
||||||
assertion-error@^1.1.0:
|
assertion-error@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
|
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
|
||||||
@@ -998,21 +995,11 @@ axios@^0.28.0:
|
|||||||
form-data "^4.0.0"
|
form-data "^4.0.0"
|
||||||
proxy-from-env "^1.1.0"
|
proxy-from-env "^1.1.0"
|
||||||
|
|
||||||
backo2@1.0.2:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
|
|
||||||
integrity sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==
|
|
||||||
|
|
||||||
balanced-match@^1.0.0:
|
balanced-match@^1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||||
|
|
||||||
base64-arraybuffer@0.1.4:
|
|
||||||
version "0.1.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
|
|
||||||
integrity sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==
|
|
||||||
|
|
||||||
batch@0.6.1:
|
batch@0.6.1:
|
||||||
version "0.6.1"
|
version "0.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
|
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
|
||||||
@@ -1028,11 +1015,6 @@ binary-extensions@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
||||||
|
|
||||||
blob@0.0.5:
|
|
||||||
version "0.0.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
|
|
||||||
integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==
|
|
||||||
|
|
||||||
body-parser@1.20.2:
|
body-parser@1.20.2:
|
||||||
version "1.20.2"
|
version "1.20.2"
|
||||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
|
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
|
||||||
@@ -1115,6 +1097,14 @@ bytes@3.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||||
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
||||||
|
|
||||||
|
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
|
||||||
|
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
|
||||||
call-bind@^1.0.2, call-bind@^1.0.6, call-bind@^1.0.7:
|
call-bind@^1.0.2, call-bind@^1.0.6, call-bind@^1.0.7:
|
||||||
version "1.0.7"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
|
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
|
||||||
@@ -1126,6 +1116,14 @@ call-bind@^1.0.2, call-bind@^1.0.6, call-bind@^1.0.7:
|
|||||||
get-intrinsic "^1.2.4"
|
get-intrinsic "^1.2.4"
|
||||||
set-function-length "^1.2.1"
|
set-function-length "^1.2.1"
|
||||||
|
|
||||||
|
call-bound@^1.0.2:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
|
||||||
|
integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers "^1.0.2"
|
||||||
|
get-intrinsic "^1.3.0"
|
||||||
|
|
||||||
camel-case@^4.1.2:
|
camel-case@^4.1.2:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a"
|
resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a"
|
||||||
@@ -1301,21 +1299,6 @@ compare-versions@^3.5.0:
|
|||||||
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
|
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
|
||||||
integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
|
integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
|
||||||
|
|
||||||
component-bind@1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
|
|
||||||
integrity sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==
|
|
||||||
|
|
||||||
component-emitter@~1.3.0:
|
|
||||||
version "1.3.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17"
|
|
||||||
integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==
|
|
||||||
|
|
||||||
component-inherit@0.0.3:
|
|
||||||
version "0.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
|
|
||||||
integrity sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==
|
|
||||||
|
|
||||||
compressible@~2.0.16:
|
compressible@~2.0.16:
|
||||||
version "2.0.18"
|
version "2.0.18"
|
||||||
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
|
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
|
||||||
@@ -1819,12 +1802,12 @@ debug@4.3.4, debug@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "2.1.2"
|
ms "2.1.2"
|
||||||
|
|
||||||
debug@~3.1.0:
|
debug@~4.3.1, debug@~4.3.2:
|
||||||
version "3.1.0"
|
version "4.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
|
||||||
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
|
integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
ms "2.0.0"
|
ms "^2.1.3"
|
||||||
|
|
||||||
decamelize@^4.0.0:
|
decamelize@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
@@ -2005,6 +1988,15 @@ dot-prop@^5.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-obj "^2.0.0"
|
is-obj "^2.0.0"
|
||||||
|
|
||||||
|
dunder-proto@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
|
||||||
|
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers "^1.0.1"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
gopd "^1.2.0"
|
||||||
|
|
||||||
duplexer@^0.1.2:
|
duplexer@^0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
|
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
|
||||||
@@ -2045,33 +2037,21 @@ encodeurl@~1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||||
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
|
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
|
||||||
|
|
||||||
engine.io-client@~3.5.0:
|
engine.io-client@~6.6.1:
|
||||||
version "3.5.3"
|
version "6.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.5.3.tgz#3254f61fdbd53503dc9a6f9d46a52528871ca0d7"
|
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.6.3.tgz#815393fa24f30b8e6afa8f77ccca2f28146be6de"
|
||||||
integrity sha512-qsgyc/CEhJ6cgMUwxRRtOndGVhIu5hpL5tR4umSpmX/MvkFoIxUTM7oFMDQumHNzlNLwSVy6qhstFPoWTf7dOw==
|
integrity sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==
|
||||||
dependencies:
|
dependencies:
|
||||||
component-emitter "~1.3.0"
|
"@socket.io/component-emitter" "~3.1.0"
|
||||||
component-inherit "0.0.3"
|
debug "~4.3.1"
|
||||||
debug "~3.1.0"
|
engine.io-parser "~5.2.1"
|
||||||
engine.io-parser "~2.2.0"
|
ws "~8.17.1"
|
||||||
has-cors "1.1.0"
|
xmlhttprequest-ssl "~2.1.1"
|
||||||
indexof "0.0.1"
|
|
||||||
parseqs "0.0.6"
|
|
||||||
parseuri "0.0.6"
|
|
||||||
ws "~7.4.2"
|
|
||||||
xmlhttprequest-ssl "~1.6.2"
|
|
||||||
yeast "0.1.2"
|
|
||||||
|
|
||||||
engine.io-parser@~2.2.0:
|
engine.io-parser@~5.2.1:
|
||||||
version "2.2.1"
|
version "5.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.1.tgz#57ce5611d9370ee94f99641b589f94c97e4f5da7"
|
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f"
|
||||||
integrity sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==
|
integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==
|
||||||
dependencies:
|
|
||||||
after "0.8.2"
|
|
||||||
arraybuffer.slice "~0.0.7"
|
|
||||||
base64-arraybuffer "0.1.4"
|
|
||||||
blob "0.0.5"
|
|
||||||
has-binary2 "~1.0.2"
|
|
||||||
|
|
||||||
enhanced-resolve@^5.0.0:
|
enhanced-resolve@^5.0.0:
|
||||||
version "5.15.1"
|
version "5.15.1"
|
||||||
@@ -2106,6 +2086,11 @@ es-define-property@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic "^1.2.4"
|
get-intrinsic "^1.2.4"
|
||||||
|
|
||||||
|
es-define-property@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
|
||||||
|
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
|
||||||
|
|
||||||
es-errors@^1.3.0:
|
es-errors@^1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||||
@@ -2116,6 +2101,13 @@ es-module-lexer@^1.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.4.1.tgz#41ea21b43908fe6a287ffcbe4300f790555331f5"
|
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.4.1.tgz#41ea21b43908fe6a287ffcbe4300f790555331f5"
|
||||||
integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==
|
integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==
|
||||||
|
|
||||||
|
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
|
||||||
|
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
|
||||||
escalade@^3.1.1:
|
escalade@^3.1.1:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27"
|
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27"
|
||||||
@@ -2166,7 +2158,7 @@ eventemitter3@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||||
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
||||||
|
|
||||||
events@^3.2.0:
|
events@^3.2.0, events@^3.3.0:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||||
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
|
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
|
||||||
@@ -2367,6 +2359,30 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
|
|||||||
has-symbols "^1.0.3"
|
has-symbols "^1.0.3"
|
||||||
hasown "^2.0.0"
|
hasown "^2.0.0"
|
||||||
|
|
||||||
|
get-intrinsic@^1.2.5, get-intrinsic@^1.3.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
|
||||||
|
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers "^1.0.2"
|
||||||
|
es-define-property "^1.0.1"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
es-object-atoms "^1.1.1"
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
get-proto "^1.0.1"
|
||||||
|
gopd "^1.2.0"
|
||||||
|
has-symbols "^1.1.0"
|
||||||
|
hasown "^2.0.2"
|
||||||
|
math-intrinsics "^1.1.0"
|
||||||
|
|
||||||
|
get-proto@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
|
||||||
|
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
||||||
|
dependencies:
|
||||||
|
dunder-proto "^1.0.1"
|
||||||
|
es-object-atoms "^1.0.0"
|
||||||
|
|
||||||
get-stream@^6.0.0:
|
get-stream@^6.0.0:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
|
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
|
||||||
@@ -2428,6 +2444,11 @@ gopd@^1.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic "^1.1.3"
|
get-intrinsic "^1.1.3"
|
||||||
|
|
||||||
|
gopd@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
|
||||||
|
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
||||||
|
|
||||||
graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6:
|
graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6:
|
||||||
version "4.2.11"
|
version "4.2.11"
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||||
@@ -2450,18 +2471,6 @@ handle-thing@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
|
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
|
||||||
integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==
|
integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==
|
||||||
|
|
||||||
has-binary2@~1.0.2:
|
|
||||||
version "1.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d"
|
|
||||||
integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==
|
|
||||||
dependencies:
|
|
||||||
isarray "2.0.1"
|
|
||||||
|
|
||||||
has-cors@1.1.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
|
|
||||||
integrity sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==
|
|
||||||
|
|
||||||
has-flag@^4.0.0:
|
has-flag@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||||
@@ -2484,6 +2493,11 @@ has-symbols@^1.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
|
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
|
||||||
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
|
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
|
||||||
|
|
||||||
|
has-symbols@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
|
||||||
|
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
||||||
|
|
||||||
has-tostringtag@^1.0.0:
|
has-tostringtag@^1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
|
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
|
||||||
@@ -2498,6 +2512,13 @@ hasown@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.2"
|
function-bind "^1.1.2"
|
||||||
|
|
||||||
|
hasown@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
|
||||||
|
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||||
|
dependencies:
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
|
||||||
he@1.2.0, he@^1.2.0:
|
he@1.2.0, he@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||||
@@ -2662,11 +2683,6 @@ in-viewport@^3.6.0:
|
|||||||
resolved "https://registry.yarnpkg.com/in-viewport/-/in-viewport-3.6.0.tgz#c59b4cdcaa41adb5bf5b8fe390c7d34259891f4a"
|
resolved "https://registry.yarnpkg.com/in-viewport/-/in-viewport-3.6.0.tgz#c59b4cdcaa41adb5bf5b8fe390c7d34259891f4a"
|
||||||
integrity sha512-MhaJ7Pr3NhUyAfpULysTZZBUAYfJAX1O8PccW2gvXlbQduMrJz7qQQ5yzC7SAr/0g5LbeRk432yNjsLMCnYzJg==
|
integrity sha512-MhaJ7Pr3NhUyAfpULysTZZBUAYfJAX1O8PccW2gvXlbQduMrJz7qQQ5yzC7SAr/0g5LbeRk432yNjsLMCnYzJg==
|
||||||
|
|
||||||
indexof@0.0.1:
|
|
||||||
version "0.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
|
|
||||||
integrity sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==
|
|
||||||
|
|
||||||
inflight@^1.0.4:
|
inflight@^1.0.4:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||||
@@ -2835,11 +2851,6 @@ is-wsl@^3.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-inside-container "^1.0.0"
|
is-inside-container "^1.0.0"
|
||||||
|
|
||||||
isarray@2.0.1:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
|
|
||||||
integrity sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==
|
|
||||||
|
|
||||||
isarray@~1.0.0:
|
isarray@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||||
@@ -3116,6 +3127,11 @@ lru-cache@^6.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
|
math-intrinsics@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
||||||
|
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||||
|
|
||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
@@ -3270,7 +3286,7 @@ ms@2.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3, ms@^2.1.3:
|
||||||
version "2.1.3"
|
version "2.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||||
@@ -3350,6 +3366,11 @@ object-inspect@^1.13.1:
|
|||||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
|
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
|
||||||
integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
|
integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
|
||||||
|
|
||||||
|
object-inspect@^1.13.3:
|
||||||
|
version "1.13.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213"
|
||||||
|
integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==
|
||||||
|
|
||||||
object-is@^1.1.5:
|
object-is@^1.1.5:
|
||||||
version "1.1.6"
|
version "1.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07"
|
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07"
|
||||||
@@ -3409,6 +3430,11 @@ opener@^1.5.2:
|
|||||||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
||||||
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
|
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
|
||||||
|
|
||||||
|
os-browserify@^0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
|
||||||
|
integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==
|
||||||
|
|
||||||
p-limit@^2.2.0:
|
p-limit@^2.2.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
|
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
|
||||||
@@ -3464,16 +3490,6 @@ parse-duration@^0.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/parse-duration/-/parse-duration-0.1.3.tgz#c2c4d45d49513d544e129b2a5a07b9473545d19a"
|
resolved "https://registry.yarnpkg.com/parse-duration/-/parse-duration-0.1.3.tgz#c2c4d45d49513d544e129b2a5a07b9473545d19a"
|
||||||
integrity sha512-hMOZHfUmjxO5hMKn7Eft+ckP2M4nV4yzauLXiw3PndpkASnx5r8pDAMcOAiqxoemqWjMWmz4fOHQM6n6WwETXw==
|
integrity sha512-hMOZHfUmjxO5hMKn7Eft+ckP2M4nV4yzauLXiw3PndpkASnx5r8pDAMcOAiqxoemqWjMWmz4fOHQM6n6WwETXw==
|
||||||
|
|
||||||
parseqs@0.0.6:
|
|
||||||
version "0.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
|
|
||||||
integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==
|
|
||||||
|
|
||||||
parseuri@0.0.6:
|
|
||||||
version "0.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
|
|
||||||
integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==
|
|
||||||
|
|
||||||
parseurl@~1.3.2, parseurl@~1.3.3:
|
parseurl@~1.3.2, parseurl@~1.3.3:
|
||||||
version "1.3.3"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||||
@@ -3487,6 +3503,11 @@ pascal-case@^3.1.2:
|
|||||||
no-case "^3.0.4"
|
no-case "^3.0.4"
|
||||||
tslib "^2.0.3"
|
tslib "^2.0.3"
|
||||||
|
|
||||||
|
path-browserify@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
|
||||||
|
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
|
||||||
|
|
||||||
path-exists@^4.0.0:
|
path-exists@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
|
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
|
||||||
@@ -3656,6 +3677,11 @@ proxy-from-env@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||||
|
|
||||||
|
punycode@^1.4.1:
|
||||||
|
version "1.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
||||||
|
integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==
|
||||||
|
|
||||||
punycode@^2.1.0:
|
punycode@^2.1.0:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||||
@@ -3668,6 +3694,13 @@ qs@6.11.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
side-channel "^1.0.4"
|
side-channel "^1.0.4"
|
||||||
|
|
||||||
|
qs@^6.12.3:
|
||||||
|
version "6.14.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930"
|
||||||
|
integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==
|
||||||
|
dependencies:
|
||||||
|
side-channel "^1.1.0"
|
||||||
|
|
||||||
raf-schd@^4.0.2:
|
raf-schd@^4.0.2:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
|
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
|
||||||
@@ -4171,6 +4204,35 @@ shell-quote@^1.8.1:
|
|||||||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
|
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
|
||||||
integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
|
integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
|
||||||
|
|
||||||
|
side-channel-list@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
|
||||||
|
integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
object-inspect "^1.13.3"
|
||||||
|
|
||||||
|
side-channel-map@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42"
|
||||||
|
integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==
|
||||||
|
dependencies:
|
||||||
|
call-bound "^1.0.2"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
get-intrinsic "^1.2.5"
|
||||||
|
object-inspect "^1.13.3"
|
||||||
|
|
||||||
|
side-channel-weakmap@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea"
|
||||||
|
integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==
|
||||||
|
dependencies:
|
||||||
|
call-bound "^1.0.2"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
get-intrinsic "^1.2.5"
|
||||||
|
object-inspect "^1.13.3"
|
||||||
|
side-channel-map "^1.0.1"
|
||||||
|
|
||||||
side-channel@^1.0.4:
|
side-channel@^1.0.4:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
|
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
|
||||||
@@ -4181,6 +4243,17 @@ side-channel@^1.0.4:
|
|||||||
get-intrinsic "^1.2.4"
|
get-intrinsic "^1.2.4"
|
||||||
object-inspect "^1.13.1"
|
object-inspect "^1.13.1"
|
||||||
|
|
||||||
|
side-channel@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
|
||||||
|
integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
object-inspect "^1.13.3"
|
||||||
|
side-channel-list "^1.0.0"
|
||||||
|
side-channel-map "^1.0.1"
|
||||||
|
side-channel-weakmap "^1.0.2"
|
||||||
|
|
||||||
signal-exit@^3.0.3:
|
signal-exit@^3.0.3:
|
||||||
version "3.0.7"
|
version "3.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
|
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
|
||||||
@@ -4200,31 +4273,23 @@ sirv@^2.0.3:
|
|||||||
mrmime "^2.0.0"
|
mrmime "^2.0.0"
|
||||||
totalist "^3.0.0"
|
totalist "^3.0.0"
|
||||||
|
|
||||||
socket.io-client@^2.2.0:
|
socket.io-client@*, socket.io-client@^4.8.1:
|
||||||
version "2.5.0"
|
version "4.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.5.0.tgz#34f486f3640dde9c2211fce885ac2746f9baf5cb"
|
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.8.1.tgz#1941eca135a5490b94281d0323fe2a35f6f291cb"
|
||||||
integrity sha512-lOO9clmdgssDykiOmVQQitwBAF3I6mYcQAo7hQ7AM6Ny5X7fp8hIJ3HcQs3Rjz4SoggoxA1OgrQyY8EgTbcPYw==
|
integrity sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
backo2 "1.0.2"
|
"@socket.io/component-emitter" "~3.1.0"
|
||||||
component-bind "1.0.0"
|
debug "~4.3.2"
|
||||||
component-emitter "~1.3.0"
|
engine.io-client "~6.6.1"
|
||||||
debug "~3.1.0"
|
socket.io-parser "~4.2.4"
|
||||||
engine.io-client "~3.5.0"
|
|
||||||
has-binary2 "~1.0.2"
|
|
||||||
indexof "0.0.1"
|
|
||||||
parseqs "0.0.6"
|
|
||||||
parseuri "0.0.6"
|
|
||||||
socket.io-parser "~3.3.0"
|
|
||||||
to-array "0.1.4"
|
|
||||||
|
|
||||||
socket.io-parser@~3.3.0:
|
socket.io-parser@~4.2.4:
|
||||||
version "3.3.3"
|
version "4.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.3.tgz#3a8b84823eba87f3f7624e64a8aaab6d6318a72f"
|
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83"
|
||||||
integrity sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==
|
integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==
|
||||||
dependencies:
|
dependencies:
|
||||||
component-emitter "~1.3.0"
|
"@socket.io/component-emitter" "~3.1.0"
|
||||||
debug "~3.1.0"
|
debug "~4.3.1"
|
||||||
isarray "2.0.1"
|
|
||||||
|
|
||||||
sockjs@^0.3.24:
|
sockjs@^0.3.24:
|
||||||
version "0.3.24"
|
version "0.3.24"
|
||||||
@@ -4441,11 +4506,6 @@ tiny-warning@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||||
|
|
||||||
to-array@0.1.4:
|
|
||||||
version "0.1.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
|
|
||||||
integrity sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==
|
|
||||||
|
|
||||||
to-regex-range@^5.0.1:
|
to-regex-range@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
||||||
@@ -4527,6 +4587,14 @@ uri-js@^4.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode "^2.1.0"
|
punycode "^2.1.0"
|
||||||
|
|
||||||
|
url@^0.11.4:
|
||||||
|
version "0.11.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/url/-/url-0.11.4.tgz#adca77b3562d56b72746e76b330b7f27b6721f3c"
|
||||||
|
integrity sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==
|
||||||
|
dependencies:
|
||||||
|
punycode "^1.4.1"
|
||||||
|
qs "^6.12.3"
|
||||||
|
|
||||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
@@ -4775,15 +4843,15 @@ ws@^8.16.0:
|
|||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea"
|
||||||
integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==
|
integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==
|
||||||
|
|
||||||
ws@~7.4.2:
|
ws@~8.17.1:
|
||||||
version "7.4.6"
|
version "8.17.1"
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
|
||||||
integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
|
integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
|
||||||
|
|
||||||
xmlhttprequest-ssl@~1.6.2:
|
xmlhttprequest-ssl@~2.1.1:
|
||||||
version "1.6.3"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz#03b713873b01659dfa2c1c5d056065b27ddc2de6"
|
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz#e9e8023b3f29ef34b97a859f584c5e6c61418e23"
|
||||||
integrity sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==
|
integrity sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==
|
||||||
|
|
||||||
y18n@^5.0.5:
|
y18n@^5.0.5:
|
||||||
version "5.0.8"
|
version "5.0.8"
|
||||||
@@ -4828,11 +4896,6 @@ yargs@16.2.0:
|
|||||||
y18n "^5.0.5"
|
y18n "^5.0.5"
|
||||||
yargs-parser "^20.2.2"
|
yargs-parser "^20.2.2"
|
||||||
|
|
||||||
yeast@0.1.2:
|
|
||||||
version "0.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
|
|
||||||
integrity sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==
|
|
||||||
|
|
||||||
yocto-queue@^0.1.0:
|
yocto-queue@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"postinstall": "yarn build"
|
"postinstall": "yarn build"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "CC-BY-ND-4.0",
|
"license": "CC-BY-ND-4.0",
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import FileAsync from 'lowdb/adapters/FileAsync'
|
|||||||
import fs from 'fs-extra'
|
import fs from 'fs-extra'
|
||||||
import lowdb from 'lowdb'
|
import lowdb from 'lowdb'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { backendRpc } from '../../events'
|
import { Rpc } from '../../events/EventSystem/Rpc'
|
||||||
import { storageClearEvent, storageLoadEvent, storageStoreEvent } from '../../events/StorageEvents'
|
import { storageClearEvent, storageLoadEvent, storageStoreEvent } from '../../events/StorageEvents'
|
||||||
|
|
||||||
export default class ConfigStorage {
|
export default class ConfigStorage {
|
||||||
private file: string
|
private file: string
|
||||||
private database: any
|
private database: any
|
||||||
constructor(file: string) {
|
private rpc: Rpc
|
||||||
|
|
||||||
|
constructor(file: string, rpc: Rpc) {
|
||||||
this.file = file
|
this.file = file
|
||||||
|
this.rpc = rpc
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDb() {
|
private async getDb() {
|
||||||
@@ -26,13 +29,13 @@ export default class ConfigStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async init() {
|
public async init() {
|
||||||
backendRpc.on(storageStoreEvent, async event => {
|
this.rpc.on(storageStoreEvent, async event => {
|
||||||
const db = await this.getDb()
|
const db = await this.getDb()
|
||||||
await db.set(event.store, event.data).write()
|
await db.set(event.store, event.data).write()
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
backendRpc.on(storageLoadEvent, async event => {
|
this.rpc.on(storageLoadEvent, async event => {
|
||||||
const db = await this.getDb()
|
const db = await this.getDb()
|
||||||
const data = await db.get(event.store).value()
|
const data = await db.get(event.store).value()
|
||||||
return {
|
return {
|
||||||
@@ -41,7 +44,7 @@ export default class ConfigStorage {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
backendRpc.on(storageClearEvent, async event => {
|
this.rpc.on(storageClearEvent, async event => {
|
||||||
const db = await this.getDb()
|
const db = await this.getDb()
|
||||||
const keys = await db.keys().value()
|
const keys = await db.keys().value()
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
|
|||||||
@@ -4,15 +4,20 @@ import {
|
|||||||
AddMqttConnection,
|
AddMqttConnection,
|
||||||
MqttMessage,
|
MqttMessage,
|
||||||
addMqttConnectionEvent,
|
addMqttConnectionEvent,
|
||||||
backendEvents,
|
|
||||||
makeConnectionMessageEvent,
|
makeConnectionMessageEvent,
|
||||||
makeConnectionStateEvent,
|
makeConnectionStateEvent,
|
||||||
makePublishEvent,
|
makePublishEvent,
|
||||||
removeConnection,
|
removeConnection,
|
||||||
} from '../../events'
|
} from '../../events'
|
||||||
|
import { EventBusInterface } from '../../events/EventSystem/EventBusInterface'
|
||||||
|
|
||||||
export class ConnectionManager {
|
export class ConnectionManager {
|
||||||
private connections: { [s: string]: DataSource<any> } = {}
|
private connections: { [s: string]: DataSource<any> } = {}
|
||||||
|
private backendEvents: EventBusInterface
|
||||||
|
|
||||||
|
constructor(backendEvents: EventBusInterface) {
|
||||||
|
this.backendEvents = backendEvents
|
||||||
|
}
|
||||||
|
|
||||||
private handleConnectionRequest = (event: AddMqttConnection) => {
|
private handleConnectionRequest = (event: AddMqttConnection) => {
|
||||||
const connectionId = event.id
|
const connectionId = event.id
|
||||||
@@ -28,12 +33,12 @@ export class ConnectionManager {
|
|||||||
|
|
||||||
const connectionStateEvent = makeConnectionStateEvent(connectionId)
|
const connectionStateEvent = makeConnectionStateEvent(connectionId)
|
||||||
connection.stateMachine.onUpdate.subscribe(state => {
|
connection.stateMachine.onUpdate.subscribe(state => {
|
||||||
backendEvents.emit(connectionStateEvent, state)
|
this.backendEvents.emit(connectionStateEvent, state)
|
||||||
})
|
})
|
||||||
|
|
||||||
connection.connect(options)
|
connection.connect(options)
|
||||||
this.handleNewMessagesForConnection(connectionId, connection)
|
this.handleNewMessagesForConnection(connectionId, connection)
|
||||||
backendEvents.subscribe(makePublishEvent(connectionId), (msg: MqttMessage) => {
|
this.backendEvents.subscribe(makePublishEvent(connectionId), (msg: MqttMessage) => {
|
||||||
this.connections[connectionId].publish(msg)
|
this.connections[connectionId].publish(msg)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -49,7 +54,7 @@ export class ConnectionManager {
|
|||||||
let decoded_payload = null
|
let decoded_payload = null
|
||||||
decoded_payload = Base64Message.fromBuffer(buffer)
|
decoded_payload = Base64Message.fromBuffer(buffer)
|
||||||
|
|
||||||
backendEvents.emit(messageEvent, {
|
this.backendEvents.emit(messageEvent, {
|
||||||
topic,
|
topic,
|
||||||
payload: decoded_payload,
|
payload: decoded_payload,
|
||||||
qos: packet.qos,
|
qos: packet.qos,
|
||||||
@@ -60,8 +65,8 @@ export class ConnectionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public manageConnections() {
|
public manageConnections() {
|
||||||
backendEvents.subscribe(addMqttConnectionEvent, this.handleConnectionRequest)
|
this.backendEvents.subscribe(addMqttConnectionEvent, this.handleConnectionRequest)
|
||||||
backendEvents.subscribe(removeConnection, (connectionId: string) => {
|
this.backendEvents.subscribe(removeConnection, (connectionId: string) => {
|
||||||
this.removeConnection(connectionId)
|
this.removeConnection(connectionId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -69,7 +74,7 @@ export class ConnectionManager {
|
|||||||
public removeConnection(connectionId: string) {
|
public removeConnection(connectionId: string) {
|
||||||
const connection = this.connections[connectionId]
|
const connection = this.connections[connectionId]
|
||||||
if (connection) {
|
if (connection) {
|
||||||
backendEvents.unsubscribeAll(makePublishEvent(connectionId))
|
this.backendEvents.unsubscribeAll(makePublishEvent(connectionId))
|
||||||
connection.disconnect()
|
connection.disconnect()
|
||||||
delete this.connections[connectionId]
|
delete this.connections[connectionId]
|
||||||
connection.stateMachine.onUpdate.removeAllListeners()
|
connection.stateMachine.onUpdate.removeAllListeners()
|
||||||
|
|||||||
@@ -2,96 +2,3 @@
|
|||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
|
|
||||||
version "1.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
|
|
||||||
integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==
|
|
||||||
|
|
||||||
"@protobufjs/base64@^1.1.2":
|
|
||||||
version "1.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
|
|
||||||
integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
|
|
||||||
|
|
||||||
"@protobufjs/codegen@^2.0.4":
|
|
||||||
version "2.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
|
|
||||||
integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
|
|
||||||
|
|
||||||
"@protobufjs/eventemitter@^1.1.0":
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
|
|
||||||
integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==
|
|
||||||
|
|
||||||
"@protobufjs/fetch@^1.1.0":
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
|
|
||||||
integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==
|
|
||||||
dependencies:
|
|
||||||
"@protobufjs/aspromise" "^1.1.1"
|
|
||||||
"@protobufjs/inquire" "^1.1.0"
|
|
||||||
|
|
||||||
"@protobufjs/float@^1.0.2":
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
|
|
||||||
integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==
|
|
||||||
|
|
||||||
"@protobufjs/inquire@^1.1.0":
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
|
|
||||||
integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==
|
|
||||||
|
|
||||||
"@protobufjs/path@^1.1.2":
|
|
||||||
version "1.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
|
|
||||||
integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==
|
|
||||||
|
|
||||||
"@protobufjs/pool@^1.1.0":
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
|
|
||||||
integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==
|
|
||||||
|
|
||||||
"@protobufjs/utf8@^1.1.0":
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
|
||||||
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
|
|
||||||
|
|
||||||
"@types/long@^4.0.1":
|
|
||||||
version "4.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
|
|
||||||
integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
|
|
||||||
|
|
||||||
"@types/node@>=13.7.0":
|
|
||||||
version "20.12.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.2.tgz#9facdd11102f38b21b4ebedd9d7999663343d72e"
|
|
||||||
integrity sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==
|
|
||||||
dependencies:
|
|
||||||
undici-types "~5.26.4"
|
|
||||||
|
|
||||||
long@^4.0.0:
|
|
||||||
version "4.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
|
|
||||||
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
|
|
||||||
|
|
||||||
protobufjs@^6.11.4:
|
|
||||||
version "6.11.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.4.tgz#29a412c38bf70d89e537b6d02d904a6f448173aa"
|
|
||||||
integrity sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==
|
|
||||||
dependencies:
|
|
||||||
"@protobufjs/aspromise" "^1.1.2"
|
|
||||||
"@protobufjs/base64" "^1.1.2"
|
|
||||||
"@protobufjs/codegen" "^2.0.4"
|
|
||||||
"@protobufjs/eventemitter" "^1.1.0"
|
|
||||||
"@protobufjs/fetch" "^1.1.0"
|
|
||||||
"@protobufjs/float" "^1.0.2"
|
|
||||||
"@protobufjs/inquire" "^1.1.0"
|
|
||||||
"@protobufjs/path" "^1.1.2"
|
|
||||||
"@protobufjs/pool" "^1.1.0"
|
|
||||||
"@protobufjs/utf8" "^1.1.0"
|
|
||||||
"@types/long" "^4.0.1"
|
|
||||||
"@types/node" ">=13.7.0"
|
|
||||||
long "^4.0.0"
|
|
||||||
|
|
||||||
undici-types@~5.26.4:
|
|
||||||
version "5.26.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
|
||||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
|
||||||
|
|||||||
29
events/EventSystem/BrowserEventBus.ts
Normal file
29
events/EventSystem/BrowserEventBus.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Browser-specific EventBus implementation using Socket.io
|
||||||
|
import io from 'socket.io-client'
|
||||||
|
import { SocketIOClientEventBus } from './SocketIOClientEventBus'
|
||||||
|
import { Rpc } from './Rpc'
|
||||||
|
|
||||||
|
// Get auth from sessionStorage or use empty (will show login dialog)
|
||||||
|
const username = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('mqtt-explorer-username') || '' : ''
|
||||||
|
const password = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('mqtt-explorer-password') || '' : ''
|
||||||
|
|
||||||
|
// Connect to the server (same origin in browser mode)
|
||||||
|
const socket = io({
|
||||||
|
auth: {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
reconnection: true,
|
||||||
|
reconnectionDelay: 1000,
|
||||||
|
reconnectionDelayMax: 5000,
|
||||||
|
reconnectionAttempts: Infinity,
|
||||||
|
transports: ['websocket', 'polling'],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const rendererEvents = new SocketIOClientEventBus(socket)
|
||||||
|
export const rendererRpc = new Rpc(rendererEvents)
|
||||||
|
|
||||||
|
// In browser mode, the backend is on the server
|
||||||
|
// For compatibility, export same instances (renderer communicates with server backend via socket)
|
||||||
|
export const backendEvents = rendererEvents
|
||||||
|
export const backendRpc = rendererRpc
|
||||||
@@ -1,24 +1,54 @@
|
|||||||
import { IpcMain } from 'electron'
|
import { IpcMain, WebContents } from 'electron'
|
||||||
import { Event } from '../Events'
|
import { Event } from '../Events'
|
||||||
import { EventBusInterface } from './EventBusInterface'
|
import { EventBusInterface } from './EventBusInterface'
|
||||||
|
|
||||||
export class IpcMainEventBus implements EventBusInterface {
|
export class IpcMainEventBus implements EventBusInterface {
|
||||||
private ipc: IpcMain
|
private ipc: IpcMain
|
||||||
private client: any
|
private clients: Map<number, WebContents> = new Map() // webContentsId -> WebContents
|
||||||
|
private connectionOwners: Map<string, number> = new Map() // connectionId -> webContentsId
|
||||||
|
private currentClient: WebContents | undefined
|
||||||
|
|
||||||
constructor(ipc: IpcMain) {
|
constructor(ipc: IpcMain) {
|
||||||
this.ipc = ipc
|
this.ipc = ipc
|
||||||
}
|
}
|
||||||
|
|
||||||
public subscribe<MessageType>(subscribeEvent: Event<MessageType>, callback: (msg: MessageType) => void) {
|
public subscribe<MessageType>(subscribeEvent: Event<MessageType>, callback: (msg: MessageType) => void) {
|
||||||
console.log('subscribing', subscribeEvent.topic)
|
|
||||||
this.ipc.on(subscribeEvent.topic, (event: any, arg: any) => {
|
this.ipc.on(subscribeEvent.topic, (event: any, arg: any) => {
|
||||||
this.client = event.sender
|
const sender = event.sender as WebContents
|
||||||
|
this.currentClient = sender
|
||||||
|
|
||||||
|
// Track the client (O(1) operation)
|
||||||
|
if (!this.clients.has(sender.id)) {
|
||||||
|
this.clients.set(sender.id, sender)
|
||||||
|
|
||||||
|
// Clean up when window is closed
|
||||||
|
sender.once('destroyed', () => {
|
||||||
|
this.clients.delete(sender.id)
|
||||||
|
|
||||||
|
// Clean up owned connections
|
||||||
|
for (const [connectionId, webContentsId] of this.connectionOwners.entries()) {
|
||||||
|
if (webContentsId === sender.id) {
|
||||||
|
this.connectionOwners.delete(connectionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track connection ownership
|
||||||
|
if (subscribeEvent.topic === 'connection/add/mqtt' && arg?.id) {
|
||||||
|
this.connectionOwners.set(arg.id, sender.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove connection ownership
|
||||||
|
if (subscribeEvent.topic === 'connection/remove' && typeof arg === 'string') {
|
||||||
|
this.connectionOwners.delete(arg)
|
||||||
|
}
|
||||||
|
|
||||||
callback(arg)
|
callback(arg)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
|
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
|
||||||
console.log('unsubscribeAll', event.topic)
|
|
||||||
this.ipc.removeAllListeners(event.topic)
|
this.ipc.removeAllListeners(event.topic)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,8 +57,44 @@ export class IpcMainEventBus implements EventBusInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
|
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
|
||||||
if (!this.client.isDestroyed()) {
|
const topic = event.topic
|
||||||
this.client.send(event.topic, msg)
|
|
||||||
|
// RPC responses go only to the requesting client
|
||||||
|
if (topic.includes('/response/')) {
|
||||||
|
if (this.currentClient && !this.currentClient.isDestroyed()) {
|
||||||
|
this.currentClient.send(topic, msg)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection-specific events - optimized with early pattern match
|
||||||
|
if (topic.startsWith('conn/')) {
|
||||||
|
const parts = topic.split('/')
|
||||||
|
let connectionId: string | undefined
|
||||||
|
|
||||||
|
if (parts.length === 2) {
|
||||||
|
connectionId = parts[1]
|
||||||
|
} else if (parts.length === 3 && (parts[1] === 'state' || parts[1] === 'publish')) {
|
||||||
|
connectionId = parts[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionId) {
|
||||||
|
const ownerWebContentsId = this.connectionOwners.get(connectionId)
|
||||||
|
if (ownerWebContentsId !== undefined) {
|
||||||
|
const ownerClient = this.clients.get(ownerWebContentsId)
|
||||||
|
if (ownerClient && !ownerClient.isDestroyed()) {
|
||||||
|
ownerClient.send(topic, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other events go to all clients
|
||||||
|
this.clients.forEach(client => {
|
||||||
|
if (!client.isDestroyed()) {
|
||||||
|
client.send(topic, msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
events/EventSystem/IpcMainEventBusV2.ts
Normal file
61
events/EventSystem/IpcMainEventBusV2.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { IpcMain } from 'electron'
|
||||||
|
import { Event } from '../Events'
|
||||||
|
import { EventBusInterface } from './EventBusInterface'
|
||||||
|
import { MessageCodec } from './MessageCodec'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced IPC Main Event Bus with Protobuf support
|
||||||
|
*
|
||||||
|
* This version uses binary serialization for better performance
|
||||||
|
* while maintaining backward compatibility with the old JSON-based system.
|
||||||
|
*/
|
||||||
|
export class IpcMainEventBusV2 implements EventBusInterface {
|
||||||
|
private ipc: IpcMain
|
||||||
|
private client: any
|
||||||
|
private useBinary: boolean
|
||||||
|
|
||||||
|
constructor(ipc: IpcMain, useBinary: boolean = true) {
|
||||||
|
this.ipc = ipc
|
||||||
|
this.useBinary = useBinary
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribe<MessageType>(subscribeEvent: Event<MessageType>, callback: (msg: MessageType) => void) {
|
||||||
|
console.log('subscribing', subscribeEvent.topic, this.useBinary ? '(binary)' : '(json)')
|
||||||
|
this.ipc.on(subscribeEvent.topic, (event: any, arg: any) => {
|
||||||
|
this.client = event.sender
|
||||||
|
|
||||||
|
if (this.useBinary && arg instanceof Uint8Array) {
|
||||||
|
// Binary message - decode it
|
||||||
|
const { data } = MessageCodec.decodeWithPayload<MessageType>(arg)
|
||||||
|
callback(data)
|
||||||
|
} else {
|
||||||
|
// Regular JSON message
|
||||||
|
callback(arg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
|
||||||
|
console.log('unsubscribeAll', event.topic)
|
||||||
|
this.ipc.removeAllListeners(event.topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
|
||||||
|
throw new Error('Not implemented') // Todo: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
|
||||||
|
if (!this.client || this.client.isDestroyed()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.useBinary) {
|
||||||
|
// Encode as binary
|
||||||
|
const binary = MessageCodec.encode(event.topic, msg)
|
||||||
|
this.client.send(event.topic, binary)
|
||||||
|
} else {
|
||||||
|
// Send as JSON (legacy)
|
||||||
|
this.client.send(event.topic, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
events/EventSystem/IpcRendererEventBusV2.ts
Normal file
65
events/EventSystem/IpcRendererEventBusV2.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { CallbackStore } from './CallbackStore'
|
||||||
|
import { EventBusInterface } from './EventBusInterface'
|
||||||
|
import { Event } from '../Events'
|
||||||
|
import { IpcRenderer } from 'electron'
|
||||||
|
import { MessageCodec } from './MessageCodec'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced IPC Renderer Event Bus with Protobuf support
|
||||||
|
*
|
||||||
|
* This version uses binary serialization for better performance
|
||||||
|
* while maintaining backward compatibility with the old JSON-based system.
|
||||||
|
*/
|
||||||
|
export class IpcRendererEventBusV2 implements EventBusInterface {
|
||||||
|
private ipc: IpcRenderer
|
||||||
|
private callbacks: Array<CallbackStore> = []
|
||||||
|
private useBinary: boolean
|
||||||
|
|
||||||
|
constructor(ipc: IpcRenderer, useBinary: boolean = true) {
|
||||||
|
this.ipc = ipc
|
||||||
|
this.useBinary = useBinary
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribe<MessageType>(event: Event<MessageType>, callback: (msg: MessageType) => void) {
|
||||||
|
const wrappedCallback = (_: any, arg: any) => {
|
||||||
|
if (this.useBinary && arg instanceof Uint8Array) {
|
||||||
|
// Binary message - decode it
|
||||||
|
const { data } = MessageCodec.decodeWithPayload<MessageType>(arg)
|
||||||
|
callback(data)
|
||||||
|
} else {
|
||||||
|
// Regular JSON message
|
||||||
|
callback(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('subscribing', event.topic, this.useBinary ? '(binary)' : '(json)')
|
||||||
|
this.ipc.on(event.topic, wrappedCallback)
|
||||||
|
this.callbacks.push({
|
||||||
|
callback,
|
||||||
|
wrappedCallback,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
|
||||||
|
this.ipc.removeAllListeners(event.topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
|
||||||
|
const item = this.callbacks.find(store => store.callback === callback)
|
||||||
|
if (!item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.ipc.removeListener(event.topic, item.wrappedCallback)
|
||||||
|
this.callbacks = this.callbacks.filter(a => a !== item)
|
||||||
|
}
|
||||||
|
|
||||||
|
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
|
||||||
|
if (this.useBinary) {
|
||||||
|
// Encode as binary
|
||||||
|
const binary = MessageCodec.encode(event.topic, msg)
|
||||||
|
this.ipc.send(event.topic, binary)
|
||||||
|
} else {
|
||||||
|
// Send as JSON (legacy)
|
||||||
|
this.ipc.send(event.topic, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
events/EventSystem/MessageCodec.ts
Normal file
74
events/EventSystem/MessageCodec.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Binary Message Codec using Protobuf
|
||||||
|
*
|
||||||
|
* This provides efficient binary serialization for IPC messages,
|
||||||
|
* avoiding JSON stringify/parse overhead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as protobuf from 'protobufjs'
|
||||||
|
|
||||||
|
// Define message schema
|
||||||
|
const messageSchema = {
|
||||||
|
nested: {
|
||||||
|
mqtt: {
|
||||||
|
nested: {
|
||||||
|
Envelope: {
|
||||||
|
fields: {
|
||||||
|
topic: { type: 'string', id: 1 },
|
||||||
|
payload: { type: 'bytes', id: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create root from JSON schema
|
||||||
|
const root = protobuf.Root.fromJSON(messageSchema)
|
||||||
|
const Envelope = root.lookupType('mqtt.Envelope')
|
||||||
|
|
||||||
|
export interface BinaryMessage {
|
||||||
|
topic: string
|
||||||
|
payload: Uint8Array
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MessageCodec {
|
||||||
|
/**
|
||||||
|
* Encode a message to binary format
|
||||||
|
*/
|
||||||
|
public static encode(topic: string, data: any): Uint8Array {
|
||||||
|
// Serialize the payload to JSON, then to bytes
|
||||||
|
const jsonString = JSON.stringify(data)
|
||||||
|
const payloadBytes = new TextEncoder().encode(jsonString)
|
||||||
|
|
||||||
|
// Create protobuf envelope
|
||||||
|
const message = Envelope.create({
|
||||||
|
topic,
|
||||||
|
payload: payloadBytes,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Encode to binary
|
||||||
|
return Envelope.encode(message).finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a binary message
|
||||||
|
*/
|
||||||
|
public static decode(binary: Uint8Array): BinaryMessage {
|
||||||
|
const message = Envelope.decode(binary) as any
|
||||||
|
return {
|
||||||
|
topic: message.topic,
|
||||||
|
payload: message.payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode and parse payload as JSON
|
||||||
|
*/
|
||||||
|
public static decodeWithPayload<T>(binary: Uint8Array): { topic: string; data: T } {
|
||||||
|
const { topic, payload } = this.decode(binary)
|
||||||
|
const jsonString = new TextDecoder().decode(payload)
|
||||||
|
const data = JSON.parse(jsonString)
|
||||||
|
return { topic, data }
|
||||||
|
}
|
||||||
|
}
|
||||||
42
events/EventSystem/SocketIOClientEventBus.ts
Normal file
42
events/EventSystem/SocketIOClientEventBus.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Socket } from 'socket.io-client'
|
||||||
|
import { CallbackStore } from './CallbackStore'
|
||||||
|
import { EventBusInterface } from './EventBusInterface'
|
||||||
|
import { Event } from '../Events'
|
||||||
|
|
||||||
|
export class SocketIOClientEventBus implements EventBusInterface {
|
||||||
|
private socket: Socket
|
||||||
|
private callbacks: Array<CallbackStore> = []
|
||||||
|
|
||||||
|
constructor(socket: Socket) {
|
||||||
|
this.socket = socket
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribe<MessageType>(event: Event<MessageType>, callback: (msg: MessageType) => void) {
|
||||||
|
const wrappedCallback = (arg: any) => {
|
||||||
|
callback(arg)
|
||||||
|
}
|
||||||
|
console.log('subscribing', event.topic)
|
||||||
|
this.socket.on(event.topic, wrappedCallback)
|
||||||
|
this.callbacks.push({
|
||||||
|
callback,
|
||||||
|
wrappedCallback,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
|
||||||
|
this.socket.removeAllListeners(event.topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
|
||||||
|
const item = this.callbacks.find(store => store.callback === callback)
|
||||||
|
if (!item) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.socket.off(event.topic, item.wrappedCallback)
|
||||||
|
this.callbacks = this.callbacks.filter(a => a !== item)
|
||||||
|
}
|
||||||
|
|
||||||
|
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
|
||||||
|
this.socket.emit(event.topic, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
274
events/EventSystem/SocketIOServerEventBus.ts
Normal file
274
events/EventSystem/SocketIOServerEventBus.ts
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import { Server as SocketIOServer, Socket } from 'socket.io'
|
||||||
|
import { Event } from '../Events'
|
||||||
|
import { EventBusInterface } from './EventBusInterface'
|
||||||
|
import Debug from 'debug'
|
||||||
|
|
||||||
|
const debug = Debug('mqtt-explorer:socketio')
|
||||||
|
const debugConnect = Debug('mqtt-explorer:socketio:connect')
|
||||||
|
const debugDisconnect = Debug('mqtt-explorer:socketio:disconnect')
|
||||||
|
const debugSubscriptions = Debug('mqtt-explorer:socketio:subscriptions')
|
||||||
|
const debugConnections = Debug('mqtt-explorer:socketio:connections')
|
||||||
|
const debugEmit = Debug('mqtt-explorer:socketio:emit')
|
||||||
|
|
||||||
|
interface SocketSubscription {
|
||||||
|
topic: string
|
||||||
|
handler: (arg: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SocketIOServerEventBus implements EventBusInterface {
|
||||||
|
private io: SocketIOServer
|
||||||
|
private clients: Map<string, Socket> = new Map() // socketId -> Socket
|
||||||
|
|
||||||
|
// Global handlers that apply to ALL sockets (like RPC endpoints)
|
||||||
|
private globalHandlers: Map<string, (socket: Socket, arg: any) => void> = new Map()
|
||||||
|
|
||||||
|
// Per-socket subscriptions for cleanup
|
||||||
|
private socketSubscriptions: Map<string, SocketSubscription[]> = new Map()
|
||||||
|
|
||||||
|
// Track which socket is currently processing a request
|
||||||
|
private currentSocket: Socket | undefined
|
||||||
|
|
||||||
|
// Map connectionId -> socketId to route messages to correct client
|
||||||
|
private connectionOwners: Map<string, string> = new Map()
|
||||||
|
|
||||||
|
// Track which connections to close when a socket disconnects
|
||||||
|
private socketConnections: Map<string, Set<string>> = new Map()
|
||||||
|
|
||||||
|
constructor(io: SocketIOServer) {
|
||||||
|
this.io = io
|
||||||
|
|
||||||
|
// Register connection handler once
|
||||||
|
this.io.on('connection', socket => {
|
||||||
|
debugConnect('Client connected: %s', socket.id)
|
||||||
|
this.clients.set(socket.id, socket)
|
||||||
|
this.socketSubscriptions.set(socket.id, [])
|
||||||
|
this.socketConnections.set(socket.id, new Set())
|
||||||
|
|
||||||
|
// Register all global handlers on this socket
|
||||||
|
this.globalHandlers.forEach((handler, topic) => {
|
||||||
|
this.registerHandlerOnSocket(socket, topic, handler)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Log connection metrics
|
||||||
|
this.logConnectionMetrics('connect', socket.id)
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
debugDisconnect('Client disconnected: %s', socket.id)
|
||||||
|
this.cleanupSocket(socket)
|
||||||
|
this.clients.delete(socket.id)
|
||||||
|
this.logConnectionMetrics('disconnect', socket.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private logConnectionMetrics(event: 'connect' | 'disconnect', socketId: string) {
|
||||||
|
const totalClients = this.clients.size
|
||||||
|
const totalSubscriptions = Array.from(this.socketSubscriptions.values()).reduce((sum, subs) => sum + subs.length, 0)
|
||||||
|
const totalConnections = this.connectionOwners.size
|
||||||
|
const socketSubs = this.socketSubscriptions.get(socketId)?.length || 0
|
||||||
|
const socketConns = this.socketConnections.get(socketId)?.size || 0
|
||||||
|
|
||||||
|
debug(
|
||||||
|
'[%s] clients=%d subscriptions=%d mqttConns=%d | socket[%s]: subs=%d conns=%d',
|
||||||
|
event,
|
||||||
|
totalClients,
|
||||||
|
totalSubscriptions,
|
||||||
|
|
||||||
|
totalConnections,
|
||||||
|
socketId.substring(0, 8),
|
||||||
|
socketSubs,
|
||||||
|
socketConns
|
||||||
|
)
|
||||||
|
|
||||||
|
debugSubscriptions(
|
||||||
|
'Total subscriptions: %d across %d sockets (avg: %d per socket)',
|
||||||
|
totalSubscriptions,
|
||||||
|
totalClients,
|
||||||
|
totalClients > 0 ? Math.round(totalSubscriptions / totalClients) : 0
|
||||||
|
)
|
||||||
|
|
||||||
|
debugConnections(
|
||||||
|
'MQTT connections: %d total, %d owned by socket %s',
|
||||||
|
totalConnections,
|
||||||
|
socketConns,
|
||||||
|
socketId.substring(0, 8)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerHandlerOnSocket(socket: Socket, topic: string, handler: (socket: Socket, arg: any) => void) {
|
||||||
|
const wrappedHandler = (arg: any) => {
|
||||||
|
this.currentSocket = socket
|
||||||
|
|
||||||
|
// Track connection ownership when a connection is added
|
||||||
|
if (topic === 'connection/add/mqtt' && arg?.id) {
|
||||||
|
this.connectionOwners.set(arg.id, socket.id)
|
||||||
|
const socketConns = this.socketConnections.get(socket.id)
|
||||||
|
if (socketConns) {
|
||||||
|
socketConns.add(arg.id)
|
||||||
|
}
|
||||||
|
debugConnections(
|
||||||
|
'Connection %s owned by socket %s (total: %d)',
|
||||||
|
arg.id,
|
||||||
|
socket.id.substring(0, 8),
|
||||||
|
socketConns?.size || 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove connection ownership when a connection is removed
|
||||||
|
if (topic === 'connection/remove' && typeof arg === 'string') {
|
||||||
|
this.connectionOwners.delete(arg)
|
||||||
|
const socketConns = this.socketConnections.get(socket.id)
|
||||||
|
if (socketConns) {
|
||||||
|
socketConns.delete(arg)
|
||||||
|
}
|
||||||
|
debugConnections(
|
||||||
|
'Connection %s removed (socket %s remaining: %d)',
|
||||||
|
arg,
|
||||||
|
socket.id.substring(0, 8),
|
||||||
|
socketConns?.size || 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(socket, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on(topic, wrappedHandler)
|
||||||
|
|
||||||
|
// Track subscription for cleanup
|
||||||
|
const subscriptions = this.socketSubscriptions.get(socket.id)
|
||||||
|
if (subscriptions) {
|
||||||
|
subscriptions.push({ topic, handler: wrappedHandler })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanupSocket(socket: Socket) {
|
||||||
|
debugDisconnect('Cleaning up socket %s', socket.id)
|
||||||
|
|
||||||
|
// Remove all event listeners for this socket
|
||||||
|
const subscriptions = this.socketSubscriptions.get(socket.id)
|
||||||
|
if (subscriptions) {
|
||||||
|
subscriptions.forEach(({ topic, handler }) => {
|
||||||
|
socket.off(topic, handler)
|
||||||
|
})
|
||||||
|
this.socketSubscriptions.delete(socket.id)
|
||||||
|
debugSubscriptions('Removed %d subscriptions for socket %s', subscriptions.length, socket.id.substring(0, 8))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all MQTT connections owned by this socket
|
||||||
|
const ownedConnections = this.socketConnections.get(socket.id)
|
||||||
|
if (ownedConnections && ownedConnections.size > 0) {
|
||||||
|
debugConnections(
|
||||||
|
'Socket %s owned %d connections, requesting cleanup',
|
||||||
|
socket.id.substring(0, 8),
|
||||||
|
ownedConnections.size
|
||||||
|
)
|
||||||
|
|
||||||
|
// Emit connection/remove for each owned connection
|
||||||
|
// This will be handled by ConnectionManager to actually close the MQTT connection
|
||||||
|
ownedConnections.forEach(connectionId => {
|
||||||
|
debugConnections('Auto-closing connection %s (owner disconnected)', connectionId)
|
||||||
|
// Simulate a remove request from this socket
|
||||||
|
const removeHandler = this.globalHandlers.get('connection/remove')
|
||||||
|
if (removeHandler) {
|
||||||
|
this.currentSocket = socket
|
||||||
|
removeHandler(socket, connectionId)
|
||||||
|
}
|
||||||
|
this.connectionOwners.delete(connectionId)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.socketConnections.delete(socket.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from clients set
|
||||||
|
this.clients.delete(socket.id)
|
||||||
|
|
||||||
|
// Clear current socket if it was this one
|
||||||
|
if (this.currentSocket === socket) {
|
||||||
|
this.currentSocket = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
debugDisconnect('Cleanup complete for socket %s', socket.id.substring(0, 8))
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribe<MessageType>(subscribeEvent: Event<MessageType>, callback: (msg: MessageType) => void) {
|
||||||
|
const handler = (socket: Socket, arg: any) => {
|
||||||
|
this.currentSocket = socket
|
||||||
|
callback(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store as global handler
|
||||||
|
this.globalHandlers.set(subscribeEvent.topic, handler)
|
||||||
|
|
||||||
|
// Register on all currently connected clients
|
||||||
|
this.clients.forEach(client => {
|
||||||
|
this.registerHandlerOnSocket(client, subscribeEvent.topic, handler)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
|
||||||
|
// Remove from global handlers
|
||||||
|
this.globalHandlers.delete(event.topic)
|
||||||
|
|
||||||
|
// Remove from all sockets
|
||||||
|
this.clients.forEach(client => {
|
||||||
|
const subscriptions = this.socketSubscriptions.get(client.id)
|
||||||
|
if (subscriptions) {
|
||||||
|
const toRemove = subscriptions.filter(s => s.topic === event.topic)
|
||||||
|
toRemove.forEach(({ handler }) => {
|
||||||
|
client.off(event.topic, handler)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update subscriptions list
|
||||||
|
this.socketSubscriptions.set(
|
||||||
|
client.id,
|
||||||
|
subscriptions.filter(s => s.topic !== event.topic)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
|
||||||
|
throw new Error('Not implemented - use unsubscribeAll instead')
|
||||||
|
}
|
||||||
|
|
||||||
|
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
|
||||||
|
const topic = event.topic
|
||||||
|
|
||||||
|
// Check if this is an RPC response (contains /response/ in topic)
|
||||||
|
if (topic.includes('/response/')) {
|
||||||
|
if (this.currentSocket && this.currentSocket.connected) {
|
||||||
|
this.currentSocket.emit(topic, msg)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a connection-specific event - optimized with early pattern match
|
||||||
|
// Patterns: conn/${connectionId}, conn/state/${connectionId}, conn/publish/${connectionId}
|
||||||
|
if (topic.startsWith('conn/')) {
|
||||||
|
const parts = topic.split('/')
|
||||||
|
let connectionId: string | undefined
|
||||||
|
|
||||||
|
if (parts.length === 2) {
|
||||||
|
// conn/${connectionId}
|
||||||
|
connectionId = parts[1]
|
||||||
|
} else if (parts.length === 3 && (parts[1] === 'state' || parts[1] === 'publish')) {
|
||||||
|
// conn/state/${connectionId} or conn/publish/${connectionId}
|
||||||
|
connectionId = parts[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionId) {
|
||||||
|
const ownerSocketId = this.connectionOwners.get(connectionId)
|
||||||
|
if (ownerSocketId) {
|
||||||
|
const ownerSocket = this.clients.get(ownerSocketId)
|
||||||
|
if (ownerSocket && ownerSocket.connected) {
|
||||||
|
ownerSocket.emit(topic, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other events go to all clients
|
||||||
|
this.io.emit(topic, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
71
events/EventsV2.ts
Normal file
71
events/EventsV2.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Simplified Event System V2
|
||||||
|
*
|
||||||
|
* This provides a simpler, more type-safe way to define and use events.
|
||||||
|
* Instead of factory functions like makeConnectionStateEvent(id),
|
||||||
|
* you can now use: Events.connectionState(id)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Base64MessageDTO } from '../backend/src/Model/Base64Message'
|
||||||
|
import { DataSourceState, MqttOptions } from '../backend/src/DataSource'
|
||||||
|
import { UpdateInfo } from 'builder-util-runtime'
|
||||||
|
import { RpcEvent } from './EventSystem/Rpc'
|
||||||
|
|
||||||
|
export type EventV2<MessageType> = {
|
||||||
|
topic: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple event definitions (no parameters)
|
||||||
|
export const Events = {
|
||||||
|
// Connection management
|
||||||
|
addMqttConnection: { topic: 'connection/add/mqtt' } as EventV2<AddMqttConnectionV2>,
|
||||||
|
removeConnection: { topic: 'connection/remove' } as EventV2<string>,
|
||||||
|
updateAvailable: { topic: 'app/update/available' } as EventV2<UpdateInfo>,
|
||||||
|
|
||||||
|
// Parameterized events (for connection-specific events)
|
||||||
|
connectionState: (connectionId: string) => ({ topic: `conn/state/${connectionId}` }) as EventV2<DataSourceState>,
|
||||||
|
connectionMessage: (connectionId: string) => ({ topic: `conn/${connectionId}` }) as EventV2<MqttMessageV2>,
|
||||||
|
publish: (connectionId: string) => ({ topic: `conn/publish/${connectionId}` }) as EventV2<MqttMessageV2>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPC Events - type-safe request/response patterns
|
||||||
|
export const RpcEvents = {
|
||||||
|
getAppVersion: { topic: 'getAppVersion' } as RpcEvent<void, string>,
|
||||||
|
writeToFile: { topic: 'writeFile' } as RpcEvent<{ filePath: string; data: string; encoding?: string }, void>,
|
||||||
|
readFromFile: { topic: 'readFromFile' } as RpcEvent<{ filePath: string; encoding?: string }, Buffer>,
|
||||||
|
openDialog: { topic: 'openDialog' } as RpcEvent<OpenDialogOptionsV2, OpenDialogReturnValueV2>,
|
||||||
|
saveDialog: { topic: 'saveDialog' } as RpcEvent<SaveDialogOptionsV2, SaveDialogReturnValueV2>,
|
||||||
|
uploadCertificate: { topic: 'uploadCertificate' } as RpcEvent<CertificateUploadRequest, CertificateUploadResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type definitions
|
||||||
|
export interface AddMqttConnectionV2 {
|
||||||
|
id: string
|
||||||
|
options: MqttOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MqttMessageV2 {
|
||||||
|
topic: string
|
||||||
|
payload: Base64MessageDTO | null
|
||||||
|
qos: 0 | 1 | 2
|
||||||
|
retain: boolean
|
||||||
|
messageId: number | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CertificateUploadRequest {
|
||||||
|
filename: string
|
||||||
|
data: string // base64 encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CertificateUploadResponse {
|
||||||
|
name: string
|
||||||
|
data: string // base64 encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Electron dialog types (re-exported for convenience)
|
||||||
|
import { OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
||||||
|
|
||||||
|
export type OpenDialogOptionsV2 = OpenDialogOptions
|
||||||
|
export type OpenDialogReturnValueV2 = OpenDialogReturnValue
|
||||||
|
export type SaveDialogOptionsV2 = SaveDialogOptions
|
||||||
|
export type SaveDialogReturnValueV2 = SaveDialogReturnValue
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
import { OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
||||||
import { RpcEvent } from './EventSystem/Rpc'
|
import { RpcEvent } from './EventSystem/Rpc'
|
||||||
|
|
||||||
|
// Legacy functions - use RpcEvents from EventsV2.ts for new code
|
||||||
export function makeOpenDialogRpc(): RpcEvent<OpenDialogOptions, OpenDialogReturnValue> {
|
export function makeOpenDialogRpc(): RpcEvent<OpenDialogOptions, OpenDialogReturnValue> {
|
||||||
return {
|
return {
|
||||||
topic: 'openDialog',
|
topic: 'openDialog',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './Events'
|
export * from './Events'
|
||||||
|
export * from './EventsV2'
|
||||||
export * from './EventSystem/EventDispatcher'
|
export * from './EventSystem/EventDispatcher'
|
||||||
export * from './EventSystem/EventBus'
|
export * from './EventSystem/EventBus'
|
||||||
export * from './EventSystem/EventBusInterface'
|
export * from './EventSystem/EventBusInterface'
|
||||||
|
|||||||
19
package.json
19
package.json
@@ -4,11 +4,12 @@
|
|||||||
"description": "Explore your message queues",
|
"description": "Explore your message queues",
|
||||||
"main": "dist/src/electron.js",
|
"main": "dist/src/electron.js",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
|
"start:server": "npx tsc && node dist/src/server.js",
|
||||||
"test": "yarn test:app && yarn test:backend",
|
"test": "yarn test:app && yarn test:backend",
|
||||||
"test:app": "cd app && yarn test",
|
"test:app": "cd app && yarn test",
|
||||||
"test:backend": "cd backend && yarn test",
|
"test:backend": "cd backend && yarn test",
|
||||||
@@ -18,6 +19,9 @@
|
|||||||
"dev": "npm-run-all --parallel dev:*",
|
"dev": "npm-run-all --parallel dev:*",
|
||||||
"dev:app": "cd app && npm run dev",
|
"dev:app": "cd app && npm run dev",
|
||||||
"dev:electron": "tsc && electron . --development",
|
"dev:electron": "tsc && electron . --development",
|
||||||
|
"dev:server": "npm-run-all --parallel dev:server:*",
|
||||||
|
"dev:server:app": "cd app && npx webpack-dev-server --config webpack.browser.config.js --mode development --progress",
|
||||||
|
"dev:server:backend": "tsc && node dist/src/server.js",
|
||||||
"lint": "npm-run-all --parallel lint:prettier lint:tslint lint:spellcheck",
|
"lint": "npm-run-all --parallel lint:prettier lint:tslint lint:spellcheck",
|
||||||
"lint:fix": "npm-run-all lint:tslint:fix lint:prettier:fix",
|
"lint:fix": "npm-run-all lint:tslint:fix lint:prettier:fix",
|
||||||
"lint:prettier": "prettier --check \"**/*.ts{x,}\"",
|
"lint:prettier": "prettier --check \"**/*.ts{x,}\"",
|
||||||
@@ -26,6 +30,7 @@
|
|||||||
"lint:tslint:fix": "tslint -p ./ --fix",
|
"lint:tslint:fix": "tslint -p ./ --fix",
|
||||||
"lint:spellcheck": "cspell -e ./build -e \"node_modules\" \"**/*.ts{x,}\"",
|
"lint:spellcheck": "cspell -e ./build -e \"node_modules\" \"**/*.ts{x,}\"",
|
||||||
"build": "tsc && cd app && yarn run build && cd ..",
|
"build": "tsc && cd app && yarn run build && cd ..",
|
||||||
|
"build:server": "npx tsc && cd app && npx webpack --config webpack.browser.config.js --mode production && cd ..",
|
||||||
"prepare-release": "ts-node scripts/prepare-release.ts",
|
"prepare-release": "ts-node scripts/prepare-release.ts",
|
||||||
"package": "ts-node package.ts",
|
"package": "ts-node package.ts",
|
||||||
"ui-test": "./scripts/uiTests.sh",
|
"ui-test": "./scripts/uiTests.sh",
|
||||||
@@ -84,15 +89,18 @@
|
|||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/commit-analyzer": "^12.0.0",
|
"@semantic-release/commit-analyzer": "^12.0.0",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/chai": "^4.1.7",
|
"@types/chai": "^4.1.7",
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
"@types/fs-extra": "8",
|
"@types/fs-extra": "8",
|
||||||
"@types/lowdb": "^1.0.6",
|
"@types/lowdb": "^1.0.6",
|
||||||
"@types/mime": "^2.0.0",
|
"@types/mime": "^2.0.0",
|
||||||
"@types/mocha": "^7.0.2",
|
"@types/mocha": "^7.0.2",
|
||||||
"@types/mustache": "4",
|
"@types/mustache": "4",
|
||||||
"@types/node": "^12.6.8",
|
"@types/node": "^25.0.3",
|
||||||
"@types/semver": "7",
|
"@types/semver": "7",
|
||||||
"@types/sha1": "^1.1.1",
|
"@types/sha1": "^1.1.1",
|
||||||
|
"@types/socket.io": "^3.0.2",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"builder-util-runtime": "^9",
|
"builder-util-runtime": "^9",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
@@ -120,18 +128,23 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"about-window": "^1.12.1",
|
"about-window": "^1.12.1",
|
||||||
"axios": "^0.28.0",
|
"axios": "^0.28.0",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
|
"debug": "^4.3.4",
|
||||||
"dot-prop": "^5.0.0",
|
"dot-prop": "^5.0.0",
|
||||||
"electron-log": "4.4.6",
|
"electron-log": "4.4.6",
|
||||||
"electron-updater": "^4.6",
|
"electron-updater": "^4.6",
|
||||||
|
"express": "^5.2.1",
|
||||||
"fs-extra": "9",
|
"fs-extra": "9",
|
||||||
"js-base64": "^3.7.2",
|
"js-base64": "^3.7.2",
|
||||||
"json-to-ast": "^2.1.0",
|
"json-to-ast": "^2.1.0",
|
||||||
"lowdb": "^1.0.0",
|
"lowdb": "^1.0.0",
|
||||||
"mime": "^2.4.4",
|
"mime": "^2.4.4",
|
||||||
"mqtt": "^4.3.6",
|
"mqtt": "^4.3.6",
|
||||||
|
"protobufjs": "^8.0.0",
|
||||||
"sha1": "^1.1.1",
|
"sha1": "^1.1.1",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
"sparkplug-payload": "^1.0.3",
|
"sparkplug-payload": "^1.0.3",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^13.0.0",
|
||||||
"yarn-run-all": "^3.1.1"
|
"yarn-run-all": "^3.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
94
src/AuthManager.ts
Normal file
94
src/AuthManager.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as bcrypt from 'bcryptjs'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
export interface Credentials {
|
||||||
|
username: string
|
||||||
|
passwordHash: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthManager {
|
||||||
|
private credentialsPath: string
|
||||||
|
private credentials: Credentials | undefined
|
||||||
|
|
||||||
|
constructor(credentialsPath: string) {
|
||||||
|
this.credentialsPath = credentialsPath
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
// Try to get credentials from environment variables
|
||||||
|
const envUsername = process.env.MQTT_EXPLORER_USERNAME
|
||||||
|
const envPassword = process.env.MQTT_EXPLORER_PASSWORD
|
||||||
|
|
||||||
|
if (envUsername && envPassword) {
|
||||||
|
// Use environment credentials
|
||||||
|
console.log('Using credentials from environment variables')
|
||||||
|
console.log('Username:', envUsername)
|
||||||
|
this.credentials = {
|
||||||
|
username: envUsername,
|
||||||
|
passwordHash: await bcrypt.hash(envPassword, 10),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load from file
|
||||||
|
if (fs.existsSync(this.credentialsPath)) {
|
||||||
|
try {
|
||||||
|
const data = fs.readFileSync(this.credentialsPath, 'utf8')
|
||||||
|
this.credentials = JSON.parse(data)
|
||||||
|
console.log('Loaded credentials from', this.credentialsPath)
|
||||||
|
console.log('Username:', this.credentials!.username)
|
||||||
|
return
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load credentials from file:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new credentials
|
||||||
|
const username = `user-${uuidv4().substring(0, 8)}`
|
||||||
|
const password = uuidv4()
|
||||||
|
|
||||||
|
console.log('='.repeat(60))
|
||||||
|
console.log('Generated new credentials:')
|
||||||
|
console.log('Username:', username)
|
||||||
|
console.log('Password:', password)
|
||||||
|
console.log('='.repeat(60))
|
||||||
|
console.log('Please save these credentials. They will be persisted to:')
|
||||||
|
console.log(this.credentialsPath)
|
||||||
|
console.log('='.repeat(60))
|
||||||
|
|
||||||
|
this.credentials = {
|
||||||
|
username,
|
||||||
|
passwordHash: await bcrypt.hash(password, 10),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to file
|
||||||
|
try {
|
||||||
|
const dir = path.dirname(this.credentialsPath)
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true })
|
||||||
|
}
|
||||||
|
fs.writeFileSync(this.credentialsPath, JSON.stringify(this.credentials, null, 2))
|
||||||
|
console.log('Credentials saved successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save credentials:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async verifyCredentials(username: string, password: string): Promise<boolean> {
|
||||||
|
if (!this.credentials) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username !== this.credentials.username) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return bcrypt.compare(password, this.credentials.passwordHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUsername(): string | undefined {
|
||||||
|
return this.credentials?.username
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,8 @@ import {
|
|||||||
import { shouldAutoUpdate, handleAutoUpdate } from './autoUpdater'
|
import { shouldAutoUpdate, handleAutoUpdate } from './autoUpdater'
|
||||||
import { registerCrashReporter } from './registerCrashReporter'
|
import { registerCrashReporter } from './registerCrashReporter'
|
||||||
import { makeOpenDialogRpc, makeSaveDialogRpc } from '../events/OpenDialogRequest'
|
import { makeOpenDialogRpc, makeSaveDialogRpc } from '../events/OpenDialogRequest'
|
||||||
import { backendRpc, getAppVersion, writeToFile, readFromFile } from '../events'
|
import { backendRpc, backendEvents, getAppVersion, writeToFile, readFromFile } from '../events'
|
||||||
|
import { RpcEvents } from '../events/EventsV2'
|
||||||
|
|
||||||
registerCrashReporter()
|
registerCrashReporter()
|
||||||
|
|
||||||
@@ -49,21 +50,35 @@ app.whenReady().then(() => {
|
|||||||
backendRpc.on(getAppVersion, async () => app.getVersion())
|
backendRpc.on(getAppVersion, async () => app.getVersion())
|
||||||
|
|
||||||
backendRpc.on(writeToFile, async ({ filePath, data, encoding }) => {
|
backendRpc.on(writeToFile, async ({ filePath, data, encoding }) => {
|
||||||
await fsPromise.writeFile(filePath, Buffer.from(data, 'base64'), { encoding })
|
await fsPromise.writeFile(filePath, Buffer.from(data, 'base64'), { encoding: encoding as BufferEncoding })
|
||||||
})
|
})
|
||||||
|
|
||||||
backendRpc.on(readFromFile, async ({ filePath, encoding }) => {
|
backendRpc.on(readFromFile, async ({ filePath, encoding }) => {
|
||||||
return fsPromise.readFile(filePath, { encoding })
|
if (encoding) {
|
||||||
|
const content = await fsPromise.readFile(filePath, { encoding: encoding as BufferEncoding })
|
||||||
|
return Buffer.from(content)
|
||||||
|
}
|
||||||
|
return fsPromise.readFile(filePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Certificate upload handler - works for both Electron and browser mode via IPC
|
||||||
|
backendRpc.on(RpcEvents.uploadCertificate, async ({ filename, data }) => {
|
||||||
|
// In Electron, we just return the data as-is since it's already read
|
||||||
|
// The client will use it directly
|
||||||
|
return {
|
||||||
|
name: filename,
|
||||||
|
data,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
autoUpdater.logger = log
|
autoUpdater.logger = log
|
||||||
log.info('App starting...')
|
log.info('App starting...')
|
||||||
|
|
||||||
const connectionManager = new ConnectionManager()
|
const connectionManager = new ConnectionManager(backendEvents)
|
||||||
connectionManager.manageConnections()
|
connectionManager.manageConnections()
|
||||||
|
|
||||||
const configStorage = new ConfigStorage(path.join(app.getPath('userData'), 'settings.json'))
|
const configStorage = new ConfigStorage(path.join(app.getPath('userData'), 'settings.json'), backendRpc)
|
||||||
configStorage.init()
|
configStorage.init()
|
||||||
|
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
// Keep a global reference of the window object, if you don't, the window will
|
||||||
|
|||||||
177
src/server.ts
Normal file
177
src/server.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import * as http from 'http'
|
||||||
|
import * as path from 'path'
|
||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { promises as fsPromise } from 'fs'
|
||||||
|
import { Request, Response } from 'express'
|
||||||
|
import { AuthManager } from './AuthManager'
|
||||||
|
import { ConnectionManager } from '../backend/src/index'
|
||||||
|
import ConfigStorage from '../backend/src/ConfigStorage'
|
||||||
|
import { SocketIOServerEventBus } from '../events/EventSystem/SocketIOServerEventBus'
|
||||||
|
import { Rpc } from '../events/EventSystem/Rpc'
|
||||||
|
import { makeOpenDialogRpc, makeSaveDialogRpc } from '../events/OpenDialogRequest'
|
||||||
|
import { getAppVersion, writeToFile, readFromFile } from '../events'
|
||||||
|
import { RpcEvents } from '../events/EventsV2'
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000
|
||||||
|
const CREDENTIALS_PATH = path.join(process.cwd(), 'data', 'credentials.json')
|
||||||
|
|
||||||
|
async function startServer() {
|
||||||
|
// Initialize authentication
|
||||||
|
const authManager = new AuthManager(CREDENTIALS_PATH)
|
||||||
|
await authManager.initialize()
|
||||||
|
|
||||||
|
// Create Express app
|
||||||
|
const app = express()
|
||||||
|
const server = http.createServer(app)
|
||||||
|
const io = new Server(server, {
|
||||||
|
cors: {
|
||||||
|
origin: '*',
|
||||||
|
methods: ['GET', 'POST'],
|
||||||
|
},
|
||||||
|
allowEIO3: true, // Allow Engine.IO v3 clients (backwards compatibility)
|
||||||
|
transports: ['websocket', 'polling'], // Support both transports
|
||||||
|
pingTimeout: 60000, // Increase ping timeout
|
||||||
|
pingInterval: 25000, // Ping interval
|
||||||
|
})
|
||||||
|
|
||||||
|
// Authentication middleware for Socket.io
|
||||||
|
io.use(async (socket, next) => {
|
||||||
|
const { username, password } = socket.handshake.auth
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return next(new Error('Authentication required'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await authManager.verifyCredentials(username, password)
|
||||||
|
if (!isValid) {
|
||||||
|
return next(new Error('Invalid credentials'))
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Client authenticated:', username)
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialize backend event bus with Socket.io
|
||||||
|
const backendEvents = new SocketIOServerEventBus(io)
|
||||||
|
const backendRpc = new Rpc(backendEvents)
|
||||||
|
|
||||||
|
// Initialize connection manager
|
||||||
|
const connectionManager = new ConnectionManager(backendEvents)
|
||||||
|
connectionManager.manageConnections()
|
||||||
|
|
||||||
|
// Initialize config storage
|
||||||
|
const configStorage = new ConfigStorage(path.join(process.cwd(), 'data', 'settings.json'), backendRpc)
|
||||||
|
configStorage.init()
|
||||||
|
|
||||||
|
// Setup RPC handlers for file operations
|
||||||
|
backendRpc.on(makeOpenDialogRpc(), async request => {
|
||||||
|
// In browser mode, file selection is handled client-side via upload
|
||||||
|
// Return empty result as this will be handled differently
|
||||||
|
return { canceled: true, filePaths: [] }
|
||||||
|
})
|
||||||
|
|
||||||
|
backendRpc.on(makeSaveDialogRpc(), async request => {
|
||||||
|
// In browser mode, file saving is handled client-side via download
|
||||||
|
return { canceled: true, filePath: undefined }
|
||||||
|
})
|
||||||
|
|
||||||
|
backendRpc.on(getAppVersion, async () => {
|
||||||
|
// Return version from package.json
|
||||||
|
try {
|
||||||
|
const packageJsonPath = path.join(__dirname, '..', '..', 'package.json')
|
||||||
|
const packageJsonData = await fsPromise.readFile(packageJsonPath, 'utf8')
|
||||||
|
const packageJson = JSON.parse(packageJsonData)
|
||||||
|
return packageJson.version
|
||||||
|
} catch (e) {
|
||||||
|
return '0.0.0'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
backendRpc.on(writeToFile, async ({ filePath, data, encoding }) => {
|
||||||
|
// In browser mode, we store files in the server's data directory
|
||||||
|
const dataDir = path.join(process.cwd(), 'data', 'uploads')
|
||||||
|
const safePath = path.join(dataDir, path.basename(filePath))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fsPromise.mkdir(dataDir, { recursive: true })
|
||||||
|
if (encoding) {
|
||||||
|
await fsPromise.writeFile(safePath, Buffer.from(data, 'base64'), { encoding: encoding as BufferEncoding })
|
||||||
|
} else {
|
||||||
|
await fsPromise.writeFile(safePath, Buffer.from(data, 'base64'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error writing file:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
backendRpc.on(readFromFile, async ({ filePath, encoding }) => {
|
||||||
|
// In browser mode, files are read from the server's data directory
|
||||||
|
const dataDir = path.join(process.cwd(), 'data', 'uploads')
|
||||||
|
const safePath = path.join(dataDir, path.basename(filePath))
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (encoding) {
|
||||||
|
const content = await fsPromise.readFile(safePath, { encoding: encoding as BufferEncoding })
|
||||||
|
return Buffer.from(content)
|
||||||
|
}
|
||||||
|
return await fsPromise.readFile(safePath)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading file:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Certificate upload handler - via IPC for consistency
|
||||||
|
backendRpc.on(RpcEvents.uploadCertificate, async ({ filename, data }) => {
|
||||||
|
// Store certificate on server for browser mode
|
||||||
|
const dataDir = path.join(process.cwd(), 'data', 'certificates')
|
||||||
|
await fsPromise.mkdir(dataDir, { recursive: true })
|
||||||
|
|
||||||
|
const safePath = path.join(dataDir, path.basename(filename))
|
||||||
|
await fsPromise.writeFile(safePath, Buffer.from(data, 'base64'))
|
||||||
|
|
||||||
|
console.log('Certificate uploaded:', filename)
|
||||||
|
|
||||||
|
// Return the certificate data for client to use
|
||||||
|
return {
|
||||||
|
name: filename,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Serve static files
|
||||||
|
app.use(express.static(path.join(__dirname, '..', '..', 'app', 'build')))
|
||||||
|
|
||||||
|
// Serve index.html for all other routes (SPA)
|
||||||
|
app.use((req: Request, res: Response) => {
|
||||||
|
res.sendFile(path.join(__dirname, '..', '..', 'app', 'index.html'))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log('='.repeat(60))
|
||||||
|
console.log(`MQTT Explorer server running on http://localhost:${PORT}`)
|
||||||
|
console.log('='.repeat(60))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle graceful shutdown
|
||||||
|
process.on('SIGTERM' as any, () => {
|
||||||
|
console.log('SIGTERM received, closing connections...')
|
||||||
|
connectionManager.closeAllConnections()
|
||||||
|
server.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on('SIGINT' as any, () => {
|
||||||
|
console.log('SIGINT received, closing connections...')
|
||||||
|
connectionManager.closeAllConnections()
|
||||||
|
server.close()
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer().catch(error => {
|
||||||
|
console.error('Failed to start server:', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -32,7 +32,7 @@ const cleanUp = async (scenes: SceneBuilder, electronApp: ElectronApplication) =
|
|||||||
await electronApp.close()
|
await electronApp.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on('unhandledRejection', (error: Error | any) => {
|
process.on('unhandledRejection' as any, (error: Error | any) => {
|
||||||
console.error('unhandledRejection', error.message, error.stack)
|
console.error('unhandledRejection', error.message, error.stack)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { clearSearch, searchTree } from './scenarios/searchTree'
|
|||||||
import { connectTo } from './scenarios/connect'
|
import { connectTo } from './scenarios/connect'
|
||||||
import { reconnect } from './scenarios/reconnect'
|
import { reconnect } from './scenarios/reconnect'
|
||||||
|
|
||||||
process.on('unhandledRejection', (error: Error | any) => {
|
process.on('unhandledRejection' as any, (error: Error | any) => {
|
||||||
console.error('unhandledRejection', error.message, error.stack)
|
console.error('unhandledRejection', error.message, error.stack)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/electron.ts",
|
"src/electron.ts",
|
||||||
|
"src/server.ts",
|
||||||
|
"src/AuthManager.ts",
|
||||||
"src/spec/electron.ts",
|
"src/spec/electron.ts",
|
||||||
"src/spec/demoVideo.ts",
|
"src/spec/demoVideo.ts",
|
||||||
"src/spec/leakTest.ts",
|
"src/spec/leakTest.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user