Add browser support with Socket.io transport, authentication, performance-optimized IPC, and CI/CD (#925)

This commit is contained in:
Copilot
2025-12-20 02:35:34 +01:00
committed by GitHub
parent 8285627c5f
commit 91df6de4d4
42 changed files with 2805 additions and 290 deletions

View 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"
}

View 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

View File

@@ -0,0 +1,4 @@
# Mosquitto configuration for development
listener 1883
allow_anonymous true
persistence false

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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
View 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
View 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

View File

@@ -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

View File

@@ -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",

View 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}</>
}

View File

@@ -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))

View File

@@ -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"

View 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>
)
}

View File

@@ -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) {

View File

@@ -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
View 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,
}

View 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,
}

View File

@@ -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"

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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==

View 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

View File

@@ -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)
}
})
}
}

View 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)
}
}
}

View 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)
}
}
}

View 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 }
}
}

View 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)
}
}

View 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
View 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

View File

@@ -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',

View File

@@ -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'

View File

@@ -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
View 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
}
}

View File

@@ -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
View 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)
})

View File

@@ -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)
}) })

View File

@@ -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)
}) })

View File

@@ -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",

660
yarn.lock

File diff suppressed because it is too large Load Diff