├── .vscode └── settings.json ├── .gitignore ├── frontend ├── src │ ├── index.css │ ├── vite-env.d.ts │ ├── main.tsx │ ├── components │ │ ├── CodeEditor.tsx │ │ └── WebSocketConfig.tsx │ ├── utils │ │ └── WebSocketManager.ts │ └── App.tsx ├── README.md ├── tsconfig.json ├── tailwind.config.js ├── .gitignore ├── vite.config.ts ├── eslint.config.js ├── tsconfig.node.json ├── tsconfig.app.json ├── package.json ├── public │ └── vite.svg └── index.html ├── .dockerignore ├── doc └── img.png ├── Dockerfile ├── Cargo.toml ├── .github └── workflows │ ├── ci.yml │ └── cd.yml ├── LICENSE-MIT ├── src ├── main.rs ├── broadcast.rs ├── single.rs └── tests.rs ├── README.md ├── static └── index.html ├── LICENSE-APACHE └── Cargo.lock /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | test.md 4 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | ```bash 2 | npm run dev 3 | ``` -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | target/ 3 | .idea 4 | frontend/node_modules -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /doc/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timzaak/notir/HEAD/doc/img.png -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client' 2 | import App from './App.tsx' 3 | import './index.css' 4 | 5 | ReactDOM.createRoot(document.getElementById('root')!).render( 6 | , 7 | ) 8 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .vite -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Frontend build stage 2 | FROM node:20 AS frontend_builder 3 | WORKDIR /app/frontend 4 | COPY frontend/ ./ 5 | RUN npm install 6 | RUN npm run build 7 | 8 | # Rust builder stage 9 | FROM rust:1.89-alpine3.22 AS builder 10 | RUN apk add --no-cache musl-dev make 11 | WORKDIR /app 12 | COPY . . 13 | COPY --from=frontend_builder /app/frontend/dist ./static 14 | RUN cargo build --release 15 | 16 | # Final image 17 | FROM alpine:3.22 18 | WORKDIR /app 19 | COPY --from=builder /app/target/release/notir . 20 | EXPOSE 5800 21 | CMD ["./notir"] 22 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import tailwindcss from '@tailwindcss/vite' 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | react(), 9 | tailwindcss(), 10 | ], 11 | server: { 12 | proxy: { 13 | '/version': { 14 | target: 'http://localhost:5800', 15 | changeOrigin: true, 16 | }, 17 | '/single/sub': { 18 | target: 'http://localhost:5800', 19 | ws: true, 20 | }, 21 | '/broad/sub': { 22 | target: 'http://localhost:5800', 23 | ws: true, 24 | }, 25 | } 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "notir" 3 | version = "0.1.7" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | futures-util = "0.3" 8 | rust-embed = ">= 6, <= 9" 9 | salvo = { version = "0.84.2", features = ["websocket", "serve-static", "compression"] } 10 | tokio = { version = "1", features = ["rt-multi-thread", "macros"] } 11 | tracing = "0.1" 12 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 13 | serde = { version = "1.0", features = ["derive"] } 14 | tokio-stream = { version = "0.1", features = ["net"] } 15 | dashmap = "6.1" 16 | bytes = "1.10" 17 | nanoid = "0.4" 18 | clap = { version = "4.5.47", features = ["derive"] } 19 | 20 | [dev-dependencies] 21 | serde_json = "1.0" 22 | -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | import { globalIgnores } from 'eslint/config' 7 | 8 | export default tseslint.config([ 9 | globalIgnores(['dist']), 10 | { 11 | files: ['**/*.{ts,tsx}'], 12 | extends: [ 13 | js.configs.recommended, 14 | tseslint.configs.recommended, 15 | reactHooks.configs['recommended-latest'], 16 | reactRefresh.configs.vite, 17 | ], 18 | languageOptions: { 19 | ecmaVersion: 2020, 20 | globals: globals.browser, 21 | }, 22 | }, 23 | ]) 24 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2023", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "erasableSyntaxOnly": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | test: 12 | name: Test 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Install Rust 19 | uses: dtolnay/rust-toolchain@stable 20 | with: 21 | toolchain: 1.89 22 | components: rustfmt, clippy 23 | 24 | - name: Cache cargo registry 25 | uses: actions/cache@v4 26 | with: 27 | path: | 28 | ~/.cargo/registry 29 | ~/.cargo/git 30 | target 31 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 32 | restore-keys: | 33 | ${{ runner.os }}-cargo- 34 | 35 | - name: Check formatting 36 | run: cargo fmt --all -- --check 37 | 38 | - name: Run clippy 39 | run: cargo clippy --all-targets --all-features -- -D warnings 40 | 41 | - name: Build 42 | run: cargo build 43 | 44 | - name: Run tests 45 | run: cargo test -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Timzaak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@codemirror/commands": "^6.8.1", 14 | "@codemirror/lang-javascript": "^6.2.4", 15 | "@codemirror/state": "^6.5.2", 16 | "@codemirror/theme-one-dark": "^6.1.2", 17 | "@codemirror/view": "^6.38.0", 18 | "@tailwindcss/vite": "^4.1.11", 19 | "@uiw/react-codemirror": "^4.24.0", 20 | "codemirror": "^6.0.2", 21 | "react": "^19.1.0", 22 | "react-dom": "^19.1.0" 23 | }, 24 | "devDependencies": { 25 | "@eslint/js": "^9.30.1", 26 | "@types/react": "^19.1.8", 27 | "@types/react-dom": "^19.1.6", 28 | "@vitejs/plugin-react": "^4.6.0", 29 | "autoprefixer": "^10.4.21", 30 | "eslint": "^9.30.1", 31 | "eslint-plugin-react-hooks": "^5.2.0", 32 | "eslint-plugin-react-refresh": "^0.4.20", 33 | "globals": "^16.3.0", 34 | "postcss": "^8.5.6", 35 | "tailwindcss": "^4.1.11", 36 | "typescript": "~5.8.3", 37 | "typescript-eslint": "^8.35.1", 38 | "vite": "^7.0.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: cd 2 | on: 3 | push: 4 | tags: 5 | - 'v*.*.*' # Trigger on version tags like v1.0.0 6 | env: 7 | CARGO_TERM_COLOR: always 8 | IMAGE_NAME: ghcr.io/${{ github.repository }} 9 | 10 | jobs: 11 | release_image: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | packages: write 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | - name: Log in to the GitHub Container Registry 20 | uses: docker/login-action@v3 21 | with: 22 | registry: ghcr.io 23 | username: ${{ github.repository_owner }} 24 | password: ${{ secrets.GITHUB_TOKEN }} 25 | - name: Get the version from the tag 26 | id: get_version 27 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 28 | - name: Build and push Docker image 29 | uses: docker/build-push-action@v5 30 | with: 31 | context: . 32 | file: Dockerfile 33 | push: true 34 | tags: | 35 | ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest 36 | ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ steps.get_version.outputs.VERSION }} 37 | 38 | -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Notir | Notification via WebSocket 7 | 8 | 53 | 54 | 55 |
56 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /frontend/src/components/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CodeMirror from '@uiw/react-codemirror'; 3 | import { javascript } from '@codemirror/lang-javascript'; 4 | // import { githubLight } from '@uiw/codemirror-theme-github'; 5 | 6 | interface CodeEditorProps { 7 | code: string; 8 | setCode: (code: string) => void; 9 | submitCode: () => void; 10 | resetCode: () => void; 11 | isLoading?: boolean; 12 | } 13 | 14 | const CodeEditor: React.FC = ({ code, setCode, submitCode, resetCode, isLoading }) => { 15 | const handleCodeChange = (value: string) => { 16 | setCode(value); 17 | }; 18 | 19 | const handleSubmit = () => { 20 | submitCode(); 21 | }; 22 | 23 | const handleReset = () => { 24 | resetCode(); 25 | }; 26 | 27 | return ( 28 |
29 |

WebSocket Message Handler Code

30 | 38 | 45 | 52 |

53 | Edit the JavaScript function body above to customize how WebSocket messages are handled, it now outputs receiving data to dev console. 54 | The function will receive: event (MessageEvent), arrayBufferToBase64 (function), sendMessage (function). 55 |

56 |
57 | ); 58 | }; 59 | 60 | export default CodeEditor; 61 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::single::ONLINE_USERS; 2 | use clap::Parser; 3 | use rust_embed::RustEmbed; 4 | use salvo::prelude::*; 5 | use salvo::serve_static::static_embed; 6 | use serde::Serialize; 7 | use tracing_subscriber::EnvFilter; 8 | 9 | mod broadcast; 10 | mod single; 11 | 12 | #[cfg(test)] 13 | mod tests; 14 | 15 | #[derive(RustEmbed)] 16 | #[folder = "static"] 17 | struct Assets; 18 | 19 | /// A lightweight WebSocket server for real-time messaging. 20 | #[derive(Parser, Debug)] 21 | #[command(author, version, about, long_about = None)] 22 | struct Cli { 23 | /// The port to listen on. 24 | #[arg(short, long, default_value_t = 5800)] 25 | port: u16, 26 | } 27 | 28 | #[handler] 29 | async fn health(res: &mut Response) { 30 | res.status_code(StatusCode::OK); 31 | } 32 | 33 | #[handler] 34 | async fn version() -> String { 35 | env!("CARGO_PKG_VERSION").to_string() 36 | } 37 | 38 | #[derive(Serialize)] 39 | struct ConnectionCount { 40 | count: usize, 41 | } 42 | 43 | #[handler] 44 | pub async fn connections(req: &mut Request, res: &mut Response) { 45 | let string_uid = req.query::("id").unwrap_or_default(); 46 | if string_uid.is_empty() { 47 | res.status_code(StatusCode::BAD_REQUEST); 48 | return; 49 | } 50 | let count = ONLINE_USERS 51 | .get(&string_uid) 52 | .map(|conns| conns.len()) 53 | .unwrap_or(0); 54 | res.render(Json(ConnectionCount { count })); 55 | } 56 | 57 | #[tokio::main] 58 | async fn main() { 59 | let cli = Cli::parse(); 60 | // Initialize logging subsystem 61 | let default_filter = "info,salvo_core::server=warn"; 62 | tracing_subscriber::fmt() 63 | .with_env_filter( 64 | EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter)), 65 | ) 66 | .init(); 67 | 68 | // Bind server to port 5800 69 | let acceptor = TcpListener::new(format!("0.0.0.0:{}", cli.port)) 70 | .bind() 71 | .await; 72 | let static_files = Router::with_hoop(Compression::new().enable_gzip(CompressionLevel::Fastest)) 73 | .path("{*path}") 74 | .get(static_embed::().fallback("index.html")); 75 | 76 | let router = Router::new() 77 | .push(Router::with_path("single/sub").goal(single::user_connected)) 78 | .push(Router::with_path("single/pub").post(single::publish_message)) 79 | .push(Router::with_path("broad/sub").goal(broadcast::broadcast_subscribe)) 80 | .push(Router::with_path("broad/pub").post(broadcast::broadcast_publish)) 81 | .push(Router::with_path("connections").goal(connections)) 82 | .push(Router::with_path("health").goal(health)) 83 | .push(Router::with_path("version").goal(version)) 84 | .push(static_files); 85 | 86 | println!( 87 | "Notir server start, binding: {:?}", 88 | acceptor.local_addr().unwrap() 89 | ); 90 | 91 | Server::new(acceptor).serve(router).await; 92 | } 93 | -------------------------------------------------------------------------------- /frontend/src/utils/WebSocketManager.ts: -------------------------------------------------------------------------------- 1 | export interface WebSocketConfig { 2 | enableReconnect: boolean; 3 | reconnectInterval: number; // in milliseconds 4 | maxReconnectAttempts: number; 5 | mode: 'single' | 'broad'; // WebSocket connection mode 6 | } 7 | 8 | export class WebSocketManager { 9 | private ws: WebSocket | null = null; 10 | private url: string; 11 | private config: WebSocketConfig; 12 | private reconnectAttempts = 0; 13 | private reconnectTimer: number | null = null; 14 | private isManualClose = false; 15 | 16 | // Event handlers 17 | private onOpenHandler?: () => void; 18 | private onMessageHandler?: (event: MessageEvent) => void; 19 | private onCloseHandler?: (event: CloseEvent) => void; 20 | private onErrorHandler?: (error: Event) => void; 21 | 22 | constructor(url: string, config: WebSocketConfig) { 23 | this.url = url; 24 | this.config = config; 25 | } 26 | 27 | setConfig(config: WebSocketConfig) { 28 | this.config = config 29 | } 30 | 31 | 32 | connect(): void { 33 | if (this.ws?.readyState === WebSocket.OPEN) { 34 | return; 35 | } 36 | 37 | this.isManualClose = false; 38 | this.ws = new WebSocket(this.url); 39 | 40 | this.ws.onopen = () => { 41 | this.reconnectAttempts = 0; 42 | this.onOpenHandler?.(); 43 | }; 44 | 45 | this.ws.onmessage = (event) => { 46 | this.onMessageHandler?.(event); 47 | }; 48 | 49 | this.ws.onclose = (event) => { 50 | this.onCloseHandler?.(event); 51 | if (!this.isManualClose && this.config.enableReconnect && this.reconnectAttempts < this.config.maxReconnectAttempts) { 52 | this.scheduleReconnect(); 53 | } 54 | }; 55 | 56 | this.ws.onerror = (error) => { 57 | this.onErrorHandler?.(error); 58 | }; 59 | } 60 | 61 | private scheduleReconnect(): void { 62 | if (this.reconnectTimer) { 63 | clearTimeout(this.reconnectTimer); 64 | } 65 | 66 | this.reconnectTimer = setTimeout(() => { 67 | this.reconnectAttempts++; 68 | // console.debug(`WebSocket reconnect attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts}`); 69 | this.connect(); 70 | }, this.config.reconnectInterval); 71 | } 72 | 73 | send(message: string | ArrayBufferLike | Blob | ArrayBufferView): void { 74 | if (this.ws?.readyState === WebSocket.OPEN) { 75 | this.ws.send(message); 76 | } else { 77 | // console.error("WebSocket is not connected."); 78 | } 79 | } 80 | 81 | close(): void { 82 | this.isManualClose = true; 83 | if (this.reconnectTimer) { 84 | clearTimeout(this.reconnectTimer); 85 | this.reconnectTimer = null; 86 | } 87 | this.ws?.close(); 88 | } 89 | 90 | get readyState(): number | undefined { 91 | return this.ws?.readyState; 92 | } 93 | 94 | // Event handler setters 95 | onOpen(handler: () => void): void { 96 | this.onOpenHandler = handler; 97 | } 98 | 99 | onMessage(handler: (event: MessageEvent) => void): void { 100 | this.onMessageHandler = handler; 101 | } 102 | 103 | onClose(handler: (event: CloseEvent) => void): void { 104 | this.onCloseHandler = handler; 105 | } 106 | 107 | onError(handler: (error: Event) => void): void { 108 | this.onErrorHandler = handler; 109 | } 110 | } -------------------------------------------------------------------------------- /frontend/src/components/WebSocketConfig.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import type { WebSocketConfig } from '../utils/WebSocketManager'; 3 | 4 | interface WebSocketConfigProps { 5 | config: WebSocketConfig; 6 | onConfigChange: (config: WebSocketConfig) => void; 7 | } 8 | 9 | const WebSocketConfigComponent: React.FC = ({ 10 | config, 11 | onConfigChange, 12 | }) => { 13 | const [showNotification, setShowNotification] = useState(false); 14 | 15 | const handleConfigChange = (field: keyof WebSocketConfig, value: boolean | number | string) => { 16 | if (field === 'enableReconnect') { 17 | const savedConfig = localStorage.getItem('wsConfig'); 18 | if (savedConfig) { 19 | try { 20 | const parsedConfig = JSON.parse(savedConfig); 21 | if (parsedConfig.enableReconnect !== value) { 22 | setShowNotification(true); 23 | } else { 24 | setShowNotification(false); 25 | } 26 | } catch { 27 | // ignore 28 | } 29 | } 30 | } 31 | 32 | onConfigChange({ 33 | ...config, 34 | [field]: value 35 | }); 36 | }; 37 | 38 | return ( 39 |
40 |

WebSocket Config

41 |
42 | 43 |
44 | 47 | 56 |
57 | 58 |
59 | 62 |
63 | handleConfigChange('enableReconnect', e.target.checked)} 68 | className="text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2 disabled:opacity-50" 69 | /> 70 |
71 |
72 | 73 |
74 | 77 | handleConfigChange('reconnectInterval', parseInt(e.target.value) || 3000)} 85 | className="px-3 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:bg-gray-100" 86 | /> 87 |
88 | 89 |
90 | 93 | handleConfigChange('maxReconnectAttempts', parseInt(e.target.value) || 5)} 100 | className="px-3 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:bg-gray-100" 101 | /> 102 |
103 |
104 | {showNotification && ( 105 |
106 |

Need refresh the page for the auto connect change to take effect.

107 |
108 | )} 109 |
110 | ); 111 | }; 112 | 113 | export default WebSocketConfigComponent; 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notir 2 | 3 | [![Release](https://img.shields.io/github/v/release/timzaak/notir)](https://github.com/timzaak/notir/pkgs/container/notir) 4 | 5 | `Notir` is a lightweight WebSocket server built with Rust using the Salvo web framework and Tokio. It allows users to connect via WebSockets, subscribe to a real-time message feed, and publish messages to other connected clients. 6 | 7 | Feel free to open an issue anytime, for any reason. 8 | 9 | ## Features 10 | 11 | - WebSocket communication for real-time messaging. 12 | - Simple publish/subscribe model. 13 | - Containerized with Docker for easy deployment. 14 | 15 | ## Getting Started 16 | 17 | ### Quick Try 18 | 19 | It has been deployed on the public server, you can try it out right away: 20 | ``` 21 | http://notir.fornetcode.com:5800?id=${uuid} 22 | ``` 23 | Please change `uuid` to whatever you want, and now you can publish 24 | messages to the server like this: 25 | 26 | ```bash 27 | # Single mode - Point-to-point messaging 28 | curl -X POST http://notir.fornetcode.com:5800/single/pub?id=${uuid} \ 29 | -H 'Content-Type: application/json' \ 30 | -d '{"msg": "hello world"}' 31 | 32 | # Single mode with PingPong - Two-way communication 33 | curl -X POST http://notir.fornetcode.com:5800/single/pub?id=${uuid}&mode=ping_pong \ 34 | -H 'Content-Type: application/json' \ 35 | -d '{"msg": "hello world"}' 36 | 37 | # Broadcast mode - Message to all subscribers of a channel 38 | curl -X POST http://notir.fornetcode.com:5800/broad/pub?id=${uuid} \ 39 | -H 'Content-Type: application/json' \ 40 | -d '{"msg": "broadcast message"}' 41 | ``` 42 | 43 | Usage screenshot 44 | 45 | ### Self Hosted 46 | 47 | The easiest way to run `Notir` is by using the pre-built Docker image available 48 | on GitHub Container Registry. 49 | 50 | ```bash 51 | docker run -d -p 5800:5800 --name notir ghcr.io/timzaak/notir:latest 52 | 53 | #The server will start on port 5800 by default. You can specify a different port using the `--port` or `-p` flag. 54 | 55 | docker run -d -p 8698:8698 --name notir ghcr.io/timzaak/notir:latest -- --port 8698 56 | ``` 57 | 58 | ## API Endpoints 59 | 60 | ### Single Mode (Point-to-Point Communication) 61 | 62 | - `WS /single/sub?id=`: 63 | - Establishes a WebSocket connection for a user to subscribe to messages. 64 | - Query Parameters: 65 | - `id` (required): A unique string identifier for the client. Cannot be 66 | empty. 67 | - Upgrades the connection to WebSocket. Messages from other users will be 68 | pushed to this WebSocket connection. 69 | - Supports bidirectional communication and heartbeat mechanism. 70 | 71 | - `POST /single/pub?id=&mode=`: 72 | - Publishes a message to a specific connected client. 73 | - Query Parameters: 74 | - `id` (required): The unique string identifier of the target client. Cannot 75 | be empty. 76 | - `mode` (optional): The mode of communication. Can be `shot` or 77 | `ping_pong`, defaults to `shot`. 78 | - `shot`: One-way message delivery, no response expected. 79 | - `ping_pong`: Two-way communication, waits for client response within 5 80 | seconds. 81 | - Request Body: The message content. 82 | - If the `Content-Type` header is `application/json` or starts with `text/` 83 | (e.g., `text/plain`), the message is treated as a `UTF-8` text message. 84 | - Otherwise, the message is treated as binary. 85 | - Responses: 86 | - `200 OK`: If the message was successfully sent to the target user's 87 | channel. 88 | - `400 Bad Request`: If the `id` query parameter is missing or empty, or if 89 | a `text/*` body contains invalid UTF-8. 90 | - `404 Not Found`: If the specified `user_id` is not currently connected. 91 | - `408 Request Timeout`: If using `ping_pong` mode and no response received 92 | within 5 seconds. 93 | 94 | ### Broadcast Mode (One-to-Many Communication) 95 | 96 | - `WS /broad/sub?id=`: 97 | - Establishes a WebSocket connection to subscribe to broadcast messages for a 98 | specific channel. 99 | - Query Parameters: 100 | - `id` (required): The broadcast channel identifier. Cannot be empty. 101 | - Multiple clients can subscribe to the same broadcast channel. 102 | - Only receives messages from `broad/pub`, ignores client-sent messages 103 | (except pong responses). 104 | - Supports heartbeat mechanism for connection health monitoring. 105 | 106 | - `POST /broad/pub?id=`: 107 | - Broadcasts a message to all clients subscribed to the specified channel. 108 | - Query Parameters: 109 | - `id` (required): The broadcast channel identifier. Cannot be empty. 110 | - Request Body: The message content. 111 | - If the `Content-Type` header is `application/json` or starts with `text/` 112 | (e.g., `text/plain`), the message is treated as a `UTF-8` text message. 113 | - Otherwise, the message is treated as binary. 114 | - Responses: 115 | - `200 OK`: Always returns success, regardless of whether there are active 116 | subscribers. 117 | - `400 Bad Request`: If the `id` query parameter is missing or empty, or if 118 | a `text/*` body contains invalid UTF-8. 119 | 120 | ### General Endpoints 121 | 122 | - `GET /health`: Health check endpoint, returns `200 OK` if the service is 123 | running. 124 | - `GET /version`: Returns the current version of the service. 125 | - `GET /connections?id=`: Returns the number of active WebSocket connections for a given user ID. 126 | 127 | ## License 128 | 129 | This project is dual-licensed under either: 130 | 131 | - **Apache License 2.0** ([LICENSE-APACHE](LICENSE-APACHE) or 132 | http://www.apache.org/licenses/LICENSE-2.0) 133 | - **MIT License** ([LICENSE-MIT](LICENSE-MIT) or 134 | http://opensource.org/licenses/MIT) 135 | 136 | You may choose either license at your option. 137 | -------------------------------------------------------------------------------- /src/broadcast.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::LazyLock; 3 | use std::sync::atomic::{AtomicU64, Ordering}; 4 | 5 | use salvo::prelude::*; 6 | use salvo::websocket::{Message, WebSocket, WebSocketUpgrade}; 7 | 8 | use futures_util::{FutureExt, StreamExt}; 9 | use salvo::http::Mime; 10 | use salvo::http::headers::ContentType; 11 | use tokio::sync::{RwLock, mpsc}; 12 | use tokio::time::{Duration, interval}; 13 | use tokio_stream::wrappers::UnboundedReceiverStream; 14 | 15 | // 为每个连接生成唯一ID 16 | static CONNECTION_COUNTER: AtomicU64 = AtomicU64::new(0); 17 | 18 | #[derive(Debug, Clone)] 19 | pub(crate) struct Connection { 20 | pub connection_id: u64, 21 | pub sender: mpsc::UnboundedSender>, 22 | } 23 | 24 | type BroadcastUsers = RwLock>>; 25 | 26 | pub static BROADCAST_USERS: LazyLock = LazyLock::new(BroadcastUsers::default); 27 | 28 | #[handler] 29 | pub async fn broadcast_subscribe(req: &mut Request, res: &mut Response) -> Result<(), StatusError> { 30 | let string_uid = req 31 | .query::("id") 32 | .ok_or_else(|| StatusError::bad_request().detail("Missing 'id' query parameter"))?; 33 | if string_uid.is_empty() { 34 | return Err(StatusError::bad_request().detail("'id' query parameter cannot be empty")); 35 | } 36 | WebSocketUpgrade::new() 37 | .upgrade(req, res, |ws| handle_broadcast_socket(ws, string_uid)) 38 | .await 39 | } 40 | 41 | async fn handle_broadcast_socket(ws: WebSocket, my_id: String) { 42 | let connection_id = CONNECTION_COUNTER.fetch_add(1, Ordering::SeqCst); 43 | tracing::info!( 44 | "new broadcast user: {} (connection_id: {})", 45 | my_id, 46 | connection_id 47 | ); 48 | 49 | let (user_ws_tx, mut user_ws_rx) = ws.split(); 50 | 51 | let (tx, rx) = mpsc::unbounded_channel(); 52 | let rx = UnboundedReceiverStream::new(rx); 53 | let fut = rx.forward(user_ws_tx).map(|_result| { 54 | // if let Err(e) = result { 55 | // tracing::error!(error = e, "websocket send error"); 56 | // } 57 | }); 58 | tokio::task::spawn(fut); 59 | 60 | let my_id_clone_for_task = my_id.clone(); 61 | let tx_clone_for_ping = tx.clone(); 62 | let my_id_clone_for_ping = my_id.clone(); 63 | 64 | // 心跳任务 65 | let ping_task = async move { 66 | let mut ping_interval = interval(Duration::from_secs(30)); 67 | ping_interval.tick().await; // 跳过第一次触发 68 | 69 | loop { 70 | ping_interval.tick().await; 71 | if tx_clone_for_ping.send(Ok(Message::ping(vec![]))).is_err() { 72 | tracing::debug!( 73 | "Failed to send ping to broadcast subscriber {my_id_clone_for_ping}, connection likely closed" 74 | ); 75 | break; 76 | } 77 | tracing::debug!("Sent ping to broadcast subscriber: {my_id_clone_for_ping}"); 78 | } 79 | }; 80 | tokio::task::spawn(ping_task); 81 | 82 | let fut = async move { 83 | // 将连接添加到广播用户池 84 | { 85 | let mut users_map = BROADCAST_USERS.write().await; 86 | let connection = Connection { 87 | connection_id, 88 | sender: tx, 89 | }; 90 | users_map 91 | .entry(my_id_clone_for_task.clone()) 92 | .or_default() 93 | .push(connection); 94 | } 95 | 96 | // 处理接收到的消息(忽略所有消息,只处理 pong) 97 | while let Some(result) = user_ws_rx.next().await { 98 | match result { 99 | Ok(msg) => { 100 | if msg.is_pong() { 101 | tracing::debug!( 102 | "Received pong from broadcast subscriber: {} (connection_id: {}), ignoring", 103 | my_id_clone_for_task, 104 | connection_id 105 | ); 106 | continue; 107 | } 108 | // 忽略其他所有消息 109 | tracing::debug!( 110 | "Ignoring message from broadcast subscriber: {} (connection_id: {})", 111 | my_id_clone_for_task, 112 | connection_id 113 | ); 114 | } 115 | Err(e) => { 116 | tracing::warn!( 117 | "WebSocket error for broadcast subscriber {} (connection_id: {}): {:?}", 118 | my_id_clone_for_task, 119 | connection_id, 120 | e 121 | ); 122 | break; 123 | } 124 | }; 125 | } 126 | 127 | broadcast_user_disconnected(my_id_clone_for_task, connection_id).await; 128 | }; 129 | tokio::task::spawn(fut); 130 | } 131 | 132 | async fn broadcast_user_disconnected(my_id: String, connection_id: u64) { 133 | tracing::info!( 134 | "broadcast subscriber disconnected: {} (connection_id: {})", 135 | my_id, 136 | connection_id 137 | ); 138 | 139 | let mut users_map = BROADCAST_USERS.write().await; 140 | if let Some(connections) = users_map.get_mut(&my_id) { 141 | connections.retain(|conn| conn.connection_id != connection_id); 142 | 143 | // 如果没有连接了,移除整个条目 144 | if connections.is_empty() { 145 | users_map.remove(&my_id); 146 | } 147 | } 148 | } 149 | 150 | #[handler] 151 | pub async fn broadcast_publish(req: &mut Request, res: &mut Response) { 152 | let string_uid = req.query::("id").unwrap_or_default(); 153 | if string_uid.is_empty() { 154 | res.status_code(StatusCode::BAD_REQUEST); 155 | res.body("Missing 'id' query parameter for /broad/pub"); 156 | return; 157 | } 158 | 159 | let content_type = req 160 | .content_type() 161 | .unwrap_or_else(|| Mime::from(ContentType::octet_stream())); 162 | let body_bytes = match req.payload().await { 163 | Ok(bytes) => bytes, 164 | Err(e) => { 165 | tracing::error!("Failed to read payload for /broad/pub: {}", e); 166 | res.status_code(StatusCode::INTERNAL_SERVER_ERROR); 167 | res.body("Failed to read request body"); 168 | return; 169 | } 170 | }; 171 | 172 | // 构造消息 173 | let content_type_str = content_type.to_string(); 174 | let msg = if content_type_str.starts_with("application/json") 175 | || content_type_str.starts_with("text/") 176 | { 177 | match String::from_utf8(body_bytes.to_vec()) { 178 | Ok(text_payload) => Message::text(text_payload), 179 | Err(_) => { 180 | res.status_code(StatusCode::BAD_REQUEST); 181 | res.body("Invalid UTF-8 in body"); 182 | return; 183 | } 184 | } 185 | } else { 186 | Message::binary(body_bytes.to_vec()) 187 | }; 188 | 189 | // 发送给所有订阅此 id 的连接 190 | let users_map = BROADCAST_USERS.read().await; 191 | if let Some(connections) = users_map.get(&string_uid) { 192 | let mut failed_connection_ids = Vec::new(); 193 | 194 | for connection in connections.iter() { 195 | if connection.sender.send(Ok(msg.clone())).is_err() { 196 | failed_connection_ids.push(connection.connection_id); 197 | tracing::warn!( 198 | "Failed to send broadcast message to user {} (connection_id: {}), connection will be removed", 199 | string_uid, 200 | connection.connection_id 201 | ); 202 | } 203 | } 204 | 205 | // 清理失败的连接 206 | if !failed_connection_ids.is_empty() { 207 | drop(users_map); 208 | let mut users_map = BROADCAST_USERS.write().await; 209 | if let Some(connections) = users_map.get_mut(&string_uid) { 210 | connections.retain(|conn| !failed_connection_ids.contains(&conn.connection_id)); 211 | 212 | // 如果没有连接了,移除整个条目 213 | if connections.is_empty() { 214 | users_map.remove(&string_uid); 215 | } 216 | } 217 | } 218 | } 219 | 220 | res.status_code(StatusCode::OK); 221 | } 222 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Notir | Notification via WebSocket 7 | 67 | 68 | 69 |
70 |

NOTIR

71 |
72 |

Current Mode: Unknown

73 |
74 |
75 |

Checking for ID in URL...

76 |
77 |
78 |

79 | Press Ctrl+Shift+J (Windows/Linux) or Cmd+Option+J (Mac) to 80 | open the Developer Console to see messages. 81 |

82 |
83 |
84 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /src/single.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::sync::LazyLock; 3 | 4 | use salvo::prelude::*; 5 | use salvo::websocket::{Message, WebSocket, WebSocketUpgrade}; 6 | 7 | use bytes::Bytes; 8 | use dashmap::DashMap; 9 | use futures_util::{FutureExt, StreamExt}; 10 | use nanoid::nanoid; 11 | use serde::Deserialize; 12 | use tokio::sync::{mpsc, oneshot}; 13 | use tokio::time::{Duration, interval, timeout}; 14 | use tokio_stream::wrappers::UnboundedReceiverStream; 15 | 16 | #[derive(Deserialize, Debug, Default)] 17 | #[serde(rename_all = "snake_case")] 18 | pub enum Mode { 19 | #[default] 20 | Shot, 21 | PingPong, 22 | } 23 | 24 | type Users = DashMap>>>; 25 | type CallbackChannels = DashMap)>>; 26 | 27 | pub static ONLINE_USERS: LazyLock = LazyLock::new(Users::default); 28 | pub static CALLBACK_CHANNELS: LazyLock = LazyLock::new(CallbackChannels::default); 29 | 30 | #[handler] 31 | pub async fn user_connected(req: &mut Request, res: &mut Response) -> Result<(), StatusError> { 32 | let string_uid = req 33 | .query::("id") 34 | .ok_or_else(|| StatusError::bad_request().detail("Missing 'id' query parameter"))?; 35 | if string_uid.is_empty() { 36 | return Err(StatusError::bad_request().detail("'id' query parameter cannot be empty")); 37 | } 38 | WebSocketUpgrade::new() 39 | .upgrade(req, res, |ws| handle_socket(ws, string_uid)) 40 | .await 41 | } 42 | 43 | async fn handle_socket(ws: WebSocket, my_id: String) { 44 | tracing::info!("new single user: {}", my_id); 45 | let conn_id = nanoid!(); 46 | 47 | let (user_ws_tx, mut user_ws_rx) = ws.split(); 48 | 49 | let (tx, rx) = mpsc::unbounded_channel(); 50 | let rx = UnboundedReceiverStream::new(rx); 51 | tokio::task::spawn(rx.forward(user_ws_tx).map(|result| { 52 | if let Err(e) = result { 53 | tracing::error!(error = ?e, "websocket send error"); 54 | } 55 | })); 56 | 57 | let my_id_clone = my_id.clone(); 58 | let conn_id_clone = conn_id.clone(); 59 | let tx_clone = tx.clone(); 60 | let ping_task = async move { 61 | let mut ping_interval = interval(Duration::from_secs(30)); 62 | ping_interval.tick().await; 63 | 64 | loop { 65 | ping_interval.tick().await; 66 | if tx_clone.send(Ok(Message::ping(vec![]))).is_err() { 67 | tracing::debug!( 68 | "Failed to send ping to user {}, connection {}, likely closed", 69 | my_id_clone, 70 | conn_id_clone 71 | ); 72 | break; 73 | } 74 | tracing::debug!( 75 | "Sent ping to user: {}, connection: {}", 76 | my_id_clone, 77 | conn_id_clone 78 | ); 79 | } 80 | }; 81 | tokio::task::spawn(ping_task); 82 | 83 | ONLINE_USERS 84 | .entry(my_id.clone()) 85 | .or_default() 86 | .insert(conn_id.clone(), tx); 87 | while let Some(result) = user_ws_rx.next().await { 88 | match result { 89 | Ok(msg) => { 90 | if msg.is_pong() { 91 | tracing::debug!("Received pong from user: {}, ignoring", my_id); 92 | continue; 93 | } 94 | let data: Bytes = msg.as_bytes().to_vec().into(); 95 | if let Some(mut entry) = CALLBACK_CHANNELS.get_mut(&my_id) 96 | && let Some((_id, tx)) = entry.pop_front() 97 | && let Err(e) = tx.send(data) 98 | { 99 | tracing::error!( 100 | "Failed to send message to callback channel for user {}: {:?}", 101 | my_id, 102 | e 103 | ); 104 | } 105 | } 106 | Err(e) => { 107 | tracing::warn!("WebSocket error for user {}: {:?}", my_id, e); 108 | break; 109 | } 110 | }; 111 | } 112 | 113 | user_disconnected(my_id, conn_id).await; 114 | } 115 | 116 | pub async fn user_disconnected(my_id: String, conn_id: String) { 117 | tracing::info!("subscriber disconnected: user {}, conn {}", my_id, conn_id); 118 | if let Some(user_conns) = ONLINE_USERS.get_mut(&my_id) { 119 | user_conns.remove(&conn_id); 120 | if user_conns.is_empty() { 121 | drop(user_conns); 122 | ONLINE_USERS.remove(&my_id); 123 | CALLBACK_CHANNELS.remove(&my_id); 124 | } 125 | } 126 | } 127 | 128 | #[handler] 129 | pub async fn publish_message(req: &mut Request, res: &mut Response) { 130 | let string_uid = req.query::("id").unwrap_or_default(); 131 | if string_uid.is_empty() { 132 | res.status_code(StatusCode::BAD_REQUEST); 133 | res.body("Missing 'id' query parameter for /pub"); 134 | return; 135 | } 136 | let mode = req.query::("mode").unwrap_or_default(); 137 | 138 | let content_type_str = req 139 | .content_type() 140 | .map(|ct| ct.to_string()) 141 | .unwrap_or_default(); 142 | let body_bytes = match req.payload().await { 143 | Ok(bytes) => bytes, 144 | Err(e) => { 145 | tracing::error!("Failed to read payload for /pub: {}", e); 146 | res.status_code(StatusCode::INTERNAL_SERVER_ERROR); 147 | res.body("Failed to read request body"); 148 | return; 149 | } 150 | }; 151 | 152 | match mode { 153 | Mode::Shot => { 154 | if let Some(user_conns) = ONLINE_USERS.get(&string_uid) { 155 | let msg = if content_type_str.starts_with("application/json") 156 | || content_type_str.starts_with("text/") 157 | { 158 | match String::from_utf8(body_bytes.to_vec()) { 159 | Ok(text_payload) => Message::text(text_payload), 160 | Err(_) => { 161 | res.status_code(StatusCode::BAD_REQUEST); 162 | res.body("Invalid UTF-8 in body"); 163 | return; 164 | } 165 | } 166 | } else { 167 | Message::binary(body_bytes.to_vec()) 168 | }; 169 | 170 | let mut disconnected_conns = Vec::new(); 171 | for conn in user_conns.iter() { 172 | if conn.value().send(Ok(msg.clone())).is_err() { 173 | disconnected_conns.push(conn.key().clone()); 174 | } 175 | } 176 | 177 | if !disconnected_conns.is_empty() { 178 | for conn_id in disconnected_conns { 179 | user_conns.remove(&conn_id); 180 | } 181 | if user_conns.is_empty() { 182 | ONLINE_USERS.remove(&string_uid); 183 | } 184 | } 185 | res.status_code(StatusCode::OK); 186 | } else { 187 | res.status_code(StatusCode::NOT_FOUND); 188 | res.body("subscriber id not found"); 189 | } 190 | } 191 | Mode::PingPong => { 192 | let id = nanoid!(); 193 | let (tx, rx) = oneshot::channel(); 194 | if let Some(user_conns) = ONLINE_USERS.get(&string_uid) { 195 | let msg = if content_type_str.starts_with("application/json") 196 | || content_type_str.starts_with("text/") 197 | { 198 | match String::from_utf8(body_bytes.to_vec()) { 199 | Ok(text_payload) => Message::text(text_payload), 200 | Err(_) => { 201 | res.status_code(StatusCode::BAD_REQUEST); 202 | res.body("Invalid UTF-8 in body"); 203 | return; 204 | } 205 | } 206 | } else { 207 | Message::binary(body_bytes.to_vec()) 208 | }; 209 | 210 | CALLBACK_CHANNELS 211 | .entry(string_uid.clone()) 212 | .or_default() 213 | .push_back((id.clone(), tx)); 214 | 215 | let mut disconnected_conns = Vec::new(); 216 | let mut sent = false; 217 | for conn in user_conns.iter() { 218 | if conn.value().send(Ok(msg.clone())).is_ok() { 219 | sent = true; 220 | break; 221 | } else { 222 | disconnected_conns.push(conn.key().clone()); 223 | } 224 | } 225 | if !disconnected_conns.is_empty() { 226 | for conn_id in disconnected_conns { 227 | user_conns.remove(&conn_id); 228 | } 229 | if user_conns.is_empty() { 230 | ONLINE_USERS.remove(&string_uid); 231 | } 232 | } 233 | if !sent { 234 | res.status_code(StatusCode::NOT_FOUND); 235 | res.body("subscriber disconnected during send"); 236 | return; 237 | } 238 | } else { 239 | res.status_code(StatusCode::NOT_FOUND); 240 | res.body("subscriber id not found"); 241 | return; 242 | } 243 | 244 | match timeout(Duration::from_secs(5), rx).await { 245 | Ok(Ok(response)) => { 246 | res.headers_mut().insert( 247 | salvo::http::header::CONTENT_TYPE, 248 | "application/octet-stream".parse().unwrap(), 249 | ); 250 | res.write_body(response).ok(); 251 | } 252 | Ok(Err(_)) => { 253 | res.status_code(StatusCode::NO_CONTENT); 254 | } 255 | Err(_) => { 256 | if let Some(mut entry) = CALLBACK_CHANNELS.get_mut(&string_uid) { 257 | entry.retain(|(callback_id, _)| callback_id != &id); 258 | } 259 | res.status_code(StatusCode::REQUEST_TIMEOUT); 260 | res.body("Request timeout after 5 seconds"); 261 | } 262 | } 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef, useCallback } from 'react'; 2 | import CodeEditor from './components/CodeEditor'; 3 | import WebSocketConfigComponent from './components/WebSocketConfig'; 4 | import { type WebSocketConfig, WebSocketManager } from './utils/WebSocketManager'; 5 | 6 | const arrayBufferToBase64 = (buffer: ArrayBuffer): string => { 7 | const bytes = new Uint8Array(buffer); 8 | let binary = ''; 9 | for (let i = 0; i < bytes.byteLength; i++) { 10 | binary += String.fromCharCode(bytes[i]); 11 | } 12 | return window.btoa(binary); 13 | }; 14 | 15 | const defaultWsMessageHandler = (event: MessageEvent) => { 16 | const { data } = event; 17 | 18 | if (typeof data === 'string') { 19 | console.log(data); 20 | } else if (data instanceof ArrayBuffer) { 21 | console.log(arrayBufferToBase64(data)); 22 | } else if (data instanceof Blob) { 23 | const reader = new FileReader(); 24 | reader.onload = () => { 25 | console.log(arrayBufferToBase64(reader.result as ArrayBuffer)); 26 | }; 27 | reader.readAsArrayBuffer(data); 28 | } else { 29 | console.warn("Received unknown message type:", data); 30 | } 31 | }; 32 | 33 | const defaultEditorCode = `// event: MessageEvent, arrayBufferToBase64: (buffer: ArrayBuffer) => string, sendMessage: (message: string | ArrayBufferLike | Blob | ArrayBufferView) => void 34 | (event, arrayBufferToBase64, sendMessage) => { 35 | const { data } = event; 36 | 37 | if (typeof data === 'string') { 38 | console.log(data); 39 | // Attention: sendMessage only works when publish message via /single/pub?id=\${userId}&mod=ping_pong 40 | // sendMessage('response'); 41 | } else if (data instanceof ArrayBuffer) { 42 | console.log(arrayBufferToBase64(data)); 43 | } else if (data instanceof Blob) { 44 | const reader = new FileReader(); 45 | reader.onload = () => { 46 | console.log(arrayBufferToBase64(reader.result)); 47 | }; 48 | reader.readAsArrayBuffer(data); 49 | } else { 50 | console.warn("Received unknown message type:", data); 51 | } 52 | } 53 | `; 54 | 55 | function App() { 56 | const [statusMessage, setStatusMessage] = useState('Checking for ID in URL...'); 57 | const [editorCode, setEditorCode] = useState(() => 58 | localStorage.getItem('wsMessageHandlerCode') || defaultEditorCode 59 | ); 60 | const [wsMessageHandler, setWsMessageHandler] = useState<(event: MessageEvent) => void>(() => defaultWsMessageHandler); 61 | const [isApplyingCode, setIsApplyingCode] = useState(false); 62 | const [versionInfo, setVersionInfo] = useState(''); 63 | 64 | const [wsConfig, setWsConfig] = useState(() => { 65 | const savedConfig = localStorage.getItem('wsConfig'); 66 | if (savedConfig) { 67 | try { 68 | const parsed = JSON.parse(savedConfig); 69 | // Ensure mode field exists with default value 70 | return { 71 | enableReconnect: parsed.enableReconnect || false, 72 | reconnectInterval: parsed.reconnectInterval || 5000, 73 | maxReconnectAttempts: parsed.maxReconnectAttempts || 5, 74 | mode: parsed.mode || 'single' 75 | }; 76 | } catch { 77 | console.warn('Failed to parse saved WebSocket config, using defaults'); 78 | } 79 | } 80 | return { 81 | enableReconnect: false, 82 | reconnectInterval: 5000, 83 | maxReconnectAttempts: 5, 84 | mode: 'single' 85 | }; 86 | }); 87 | 88 | const wsManager = useRef(null); 89 | const wsMessageHandlerRef = useRef(wsMessageHandler); 90 | 91 | useEffect(() => { 92 | wsMessageHandlerRef.current = wsMessageHandler; 93 | }, [wsMessageHandler]); 94 | 95 | // 处理WebSocket配置变更 96 | const handleConfigChange = useCallback((newConfig: WebSocketConfig) => { 97 | const oldMode = wsConfig.mode; 98 | setWsConfig(newConfig); 99 | localStorage.setItem('wsConfig', JSON.stringify(newConfig)); 100 | 101 | // 如果模式改变,刷新页面 102 | if (oldMode !== newConfig.mode) { 103 | window.location.reload(); 104 | } 105 | }, [wsConfig.mode]); 106 | 107 | // 编译并应用 WebSocket 消息处理器代码 108 | const compileAndApplyCode = useCallback(async (codeToApply: string, isInitialLoad = false) => { 109 | const statusMsg = isInitialLoad ? "Initializing WebSocket handler..." : "Applying new code..."; 110 | setStatusMessage(statusMsg); 111 | 112 | if (!isInitialLoad) { 113 | setIsApplyingCode(true); 114 | await new Promise(resolve => setTimeout(resolve, 50)); 115 | } 116 | 117 | try { 118 | const dynamicHandler = new Function('event', 'arrayBufferToBase64', 'sendMessage', 119 | `(${codeToApply})(event, arrayBufferToBase64, sendMessage)` 120 | ); 121 | 122 | const createSendMessage = () => (message: string | ArrayBufferLike | Blob | ArrayBufferView) => { 123 | if (wsManager.current?.readyState === WebSocket.OPEN) { 124 | wsManager.current.send(message); 125 | } else { 126 | console.error("WebSocket is not connected."); 127 | } 128 | }; 129 | 130 | setWsMessageHandler(() => (event: MessageEvent) => { 131 | try { 132 | dynamicHandler(event, arrayBufferToBase64, createSendMessage()); 133 | } catch (e) { 134 | console.error(`Error executing WebSocket handler:`, e); 135 | setStatusMessage(`Error in custom message handler. Using default handler.`); 136 | defaultWsMessageHandler(event); 137 | } 138 | }); 139 | 140 | if (!isInitialLoad) { 141 | localStorage.setItem('wsMessageHandlerCode', codeToApply); 142 | setStatusMessage("WebSocket message handler updated successfully!"); 143 | } 144 | } catch (error) { 145 | const errorMessage = error instanceof Error ? error.message : String(error); 146 | console.error(`Error compiling WebSocket handler:`, error); 147 | setStatusMessage(`Error compiling code: ${errorMessage}. ${isInitialLoad ? 'Using default handler.' : 'Previous handler remains active.'}`); 148 | 149 | if (isInitialLoad) { 150 | setWsMessageHandler(() => defaultWsMessageHandler); 151 | } 152 | } finally { 153 | if (!isInitialLoad) { 154 | setIsApplyingCode(false); 155 | } 156 | } 157 | }, []); 158 | 159 | // Effect for applying initial code on mount 160 | useEffect(() => { 161 | const initialCode = localStorage.getItem('wsMessageHandlerCode') || defaultEditorCode; 162 | setEditorCode(initialCode); 163 | compileAndApplyCode(initialCode, true); 164 | }, [compileAndApplyCode]); // compileAndApplyCode is stable due to useCallback 165 | 166 | const handleCodeSubmit = () => { 167 | compileAndApplyCode(editorCode, false); 168 | } 169 | 170 | const resetCode = () => { 171 | setEditorCode(defaultEditorCode); 172 | } 173 | 174 | useEffect(() => { 175 | const params = new URLSearchParams(window.location.search); 176 | const id = params.get('id') || (import.meta.env.DEV ? 'test' : null); 177 | 178 | if (!id) { 179 | setStatusMessage('Error: No ID found in URL query string. Please append ?id=your_id to the URL.'); 180 | return; 181 | } 182 | 183 | setStatusMessage(`Attempting to connect WebSocket with ID: ${id}`); 184 | 185 | wsManager.current?.close() 186 | 187 | // 根据配置的模式选择不同的 WebSocket URL 188 | const wsUrl = wsConfig.mode === 'broad' ? `/broad/sub?id=${id}` : `/single/sub?id=${id}`; 189 | wsManager.current = new WebSocketManager(wsUrl, wsConfig); 190 | 191 | wsManager.current.onOpen(() => { 192 | setStatusMessage(`Connected with ID: ${id}`); 193 | }); 194 | 195 | wsManager.current.onMessage((event) => wsMessageHandlerRef.current(event)); 196 | 197 | wsManager.current.onClose((event) => { 198 | setStatusMessage(`Disconnected. ID: ${id}. Error Code: ${event.code}, Date: ${new Date()}`); 199 | }); 200 | 201 | wsManager.current.onError((error) => { 202 | setStatusMessage(`WebSocket Error with ID: ${id}. See console for details.`); 203 | console.error(`WebSocket Error with ID: ${id}:`, error); 204 | }); 205 | 206 | wsManager.current.connect(); 207 | 208 | return () => { 209 | wsManager.current?.close(); 210 | }; 211 | }, [wsConfig.mode]); // 添加 wsConfig.mode 作为依赖 212 | 213 | useEffect(() => { 214 | // const httpPrefix = import.meta.env.DEV ? `http://localhost:5800/`: `/`; 215 | fetch(`/version`) 216 | .then(response => response.text()) 217 | .then(data => setVersionInfo(data)) 218 | .catch(error => console.error('Error fetching version:', error)); 219 | }, []); 220 | 221 | const statusIsError = statusMessage.toLowerCase().includes('error'); 222 | const statusMessageClasses = ` 223 | p-3 mb-5 rounded-md shadow-md text-left 224 | ${statusIsError 225 | ? 'bg-red-100 text-red-700 border border-red-300' 226 | : 'bg-blue-100 text-blue-800 border border-blue-300' 227 | } 228 | `; 229 | 230 | return ( 231 |
232 |

NOTIR

233 |
{statusMessage}
234 |
235 |

236 | Press Ctrl+Shift+J (Windows/Linux) or Cmd+Option+J (Mac) to see message. 237 |

238 |
239 | 240 | 247 |
248 | 252 | 269 |
270 | ); 271 | } 272 | 273 | export default App; 274 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2025] [Timzaak] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod test { 3 | use crate::broadcast::{BROADCAST_USERS, Connection}; 4 | use crate::single::{CALLBACK_CHANNELS, Mode, ONLINE_USERS, user_disconnected}; 5 | use bytes::Bytes; 6 | use std::time::Duration; 7 | use tokio::sync::mpsc; 8 | use tokio::time::timeout; 9 | 10 | #[tokio::test] 11 | async fn test_single_shot_mode() { 12 | // 测试 Mode::Shot 的反序列化 13 | let mode_json = r#""shot""#; 14 | let mode: Mode = serde_json::from_str(mode_json).unwrap(); 15 | assert!(matches!(mode, Mode::Shot)); 16 | 17 | // 测试用户连接和消息发送逻辑 18 | let user_id = "test_user_shot".to_string(); 19 | let (tx, mut rx) = mpsc::unbounded_channel(); 20 | 21 | // 模拟用户连接 22 | let conn_id = "test_conn".to_string(); 23 | ONLINE_USERS 24 | .entry(user_id.clone()) 25 | .or_default() 26 | .insert(conn_id.clone(), tx); 27 | 28 | // 验证用户已连接 29 | assert!(ONLINE_USERS.contains_key(&user_id), "用户应该已连接"); 30 | 31 | // 模拟发送消息到用户 32 | let test_message = "Hello from single shot test"; 33 | if let Some(user_conns) = ONLINE_USERS.get(&user_id) 34 | && let Some(conn) = user_conns.get(&conn_id) 35 | { 36 | let msg = salvo::websocket::Message::text(test_message); 37 | assert!(conn.send(Ok(msg)).is_ok(), "消息发送应该成功"); 38 | } 39 | 40 | // 验证消息接收 41 | let received = timeout(Duration::from_secs(1), rx.recv()).await; 42 | assert!(received.is_ok(), "应该接收到消息"); 43 | 44 | // 清理 45 | ONLINE_USERS.remove(&user_id); 46 | } 47 | 48 | #[tokio::test] 49 | async fn test_ping_pong_mode() { 50 | // 测试 Mode::PingPong 的反序列化 51 | let mode_json = r#""ping_pong""#; 52 | let mode: Mode = serde_json::from_str(mode_json).unwrap(); 53 | assert!(matches!(mode, Mode::PingPong)); 54 | 55 | // 测试ping-pong模式的回调机制 56 | let user_id = "test_user_pingpong".to_string(); 57 | let (tx, _rx) = mpsc::unbounded_channel(); 58 | 59 | // 模拟用户连接 60 | let conn_id = "test_conn".to_string(); 61 | ONLINE_USERS 62 | .entry(user_id.clone()) 63 | .or_default() 64 | .insert(conn_id.clone(), tx); 65 | 66 | // 创建回调通道 67 | let (callback_tx, callback_rx) = tokio::sync::oneshot::channel(); 68 | let callback_id = nanoid::nanoid!(); 69 | 70 | // 添加回调到队列 71 | { 72 | let mut entry = CALLBACK_CHANNELS.entry(user_id.clone()).or_default(); 73 | entry.push_back((callback_id.clone(), callback_tx)); 74 | } 75 | 76 | // 验证回调通道已添加 77 | { 78 | let entry = CALLBACK_CHANNELS.get(&user_id).unwrap(); 79 | assert_eq!(entry.len(), 1, "应该有一个回调通道"); 80 | } 81 | 82 | // 模拟客户端回复 83 | let response_data = Bytes::from("Pong response from client"); 84 | let user_id_clone = user_id.clone(); 85 | tokio::spawn(async move { 86 | tokio::time::sleep(Duration::from_millis(10)).await; 87 | if let Some(mut entry) = CALLBACK_CHANNELS.get_mut(&user_id_clone) 88 | && let Some((_id, tx)) = entry.pop_front() 89 | { 90 | let _ = tx.send(response_data); 91 | } 92 | }); 93 | 94 | // 等待回调响应 95 | let result = timeout(Duration::from_secs(1), callback_rx).await; 96 | assert!(result.is_ok(), "应该接收到回调响应"); 97 | 98 | let response = result.unwrap().unwrap(); 99 | assert_eq!( 100 | response, 101 | Bytes::from("Pong response from client"), 102 | "回调响应内容应该匹配" 103 | ); 104 | 105 | // 清理 106 | ONLINE_USERS.remove(&user_id); 107 | CALLBACK_CHANNELS.remove(&user_id); 108 | } 109 | 110 | #[tokio::test] 111 | async fn test_single_shot_user_not_found() { 112 | // 测试向不存在的用户发送消息 113 | let non_existent_user = "non_existent_user".to_string(); 114 | 115 | // 确保用户不存在 116 | assert!( 117 | !ONLINE_USERS.contains_key(&non_existent_user), 118 | "用户不应该存在" 119 | ); 120 | 121 | // 模拟查找不存在的用户 122 | let user_found = ONLINE_USERS.get(&non_existent_user).is_some(); 123 | 124 | assert!(!user_found, "不存在的用户查找应该返回false"); 125 | } 126 | 127 | #[tokio::test] 128 | async fn test_ping_pong_timeout() { 129 | // 测试ping-pong模式的超时机制 130 | let user_id = "test_user_timeout".to_string(); 131 | let (tx, _rx) = mpsc::unbounded_channel(); 132 | 133 | // 模拟用户连接 134 | let conn_id = "test_conn".to_string(); 135 | ONLINE_USERS 136 | .entry(user_id.clone()) 137 | .or_default() 138 | .insert(conn_id.clone(), tx); 139 | 140 | // 创建回调通道但不回复 141 | let (callback_tx, callback_rx) = tokio::sync::oneshot::channel::(); 142 | let callback_id = nanoid::nanoid!(); 143 | 144 | // 添加回调到队列 145 | { 146 | let mut entry = CALLBACK_CHANNELS.entry(user_id.clone()).or_default(); 147 | entry.push_back((callback_id.clone(), callback_tx)); 148 | } 149 | 150 | // 模拟5秒超时 151 | let result = timeout(Duration::from_millis(100), callback_rx).await; // 使用较短的超时进行测试 152 | assert!(result.is_err(), "应该发生超时"); 153 | 154 | // 验证超时后回调通道被清理 155 | { 156 | let mut entry = CALLBACK_CHANNELS.entry(user_id.clone()).or_default(); 157 | entry.retain(|(id, _)| id != &callback_id); 158 | } 159 | 160 | // 清理 161 | ONLINE_USERS.remove(&user_id); 162 | CALLBACK_CHANNELS.remove(&user_id); 163 | } 164 | 165 | #[tokio::test] 166 | async fn test_mode_deserialization() { 167 | // 测试默认模式 168 | let default_mode = Mode::default(); 169 | assert!(matches!(default_mode, Mode::Shot)); 170 | 171 | // 测试从字符串反序列化 172 | let shot_mode: Mode = serde_json::from_str(r#""shot""#).unwrap(); 173 | assert!(matches!(shot_mode, Mode::Shot)); 174 | 175 | let ping_pong_mode: Mode = serde_json::from_str(r#""ping_pong""#).unwrap(); 176 | assert!(matches!(ping_pong_mode, Mode::PingPong)); 177 | } 178 | 179 | #[tokio::test] 180 | async fn test_user_disconnection() { 181 | // 测试用户断开连接的清理逻辑 182 | let user_id = "test_disconnect_user".to_string(); 183 | let (tx, _rx) = mpsc::unbounded_channel(); 184 | 185 | // 模拟用户连接 186 | let conn_id = "test_conn".to_string(); 187 | ONLINE_USERS 188 | .entry(user_id.clone()) 189 | .or_default() 190 | .insert(conn_id.clone(), tx); 191 | 192 | // 添加一些回调通道 193 | { 194 | let mut entry = CALLBACK_CHANNELS.entry(user_id.clone()).or_default(); 195 | let (callback_tx, _) = tokio::sync::oneshot::channel::(); 196 | entry.push_back(("test_callback".to_string(), callback_tx)); 197 | } 198 | 199 | // 验证用户和回调通道存在 200 | assert!(ONLINE_USERS.contains_key(&user_id), "用户应该存在"); 201 | assert!(CALLBACK_CHANNELS.contains_key(&user_id), "回调通道应该存在"); 202 | 203 | // 模拟用户断开连接 204 | user_disconnected(user_id.clone(), conn_id).await; 205 | 206 | // 验证清理完成 207 | assert!(!ONLINE_USERS.contains_key(&user_id), "用户应该被移除"); 208 | assert!( 209 | !CALLBACK_CHANNELS.contains_key(&user_id), 210 | "回调通道应该被移除" 211 | ); 212 | } 213 | 214 | // ========== Broadcast 模块测试 ========== 215 | 216 | #[tokio::test] 217 | async fn test_broadcast_users_pool() { 218 | // 测试广播用户连接池的基本操作 219 | let user_id = "test_broadcast_user".to_string(); 220 | let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); 221 | 222 | // 添加用户到广播池 223 | { 224 | let mut users_map = BROADCAST_USERS.write().await; 225 | let connection = Connection { 226 | connection_id: 1, 227 | sender: tx, 228 | }; 229 | users_map 230 | .entry(user_id.clone()) 231 | .or_default() 232 | .push(connection); 233 | } 234 | 235 | // 验证用户已添加 236 | { 237 | let users_map = BROADCAST_USERS.read().await; 238 | assert!(users_map.contains_key(&user_id), "用户应该在广播池中"); 239 | assert_eq!(users_map.get(&user_id).unwrap().len(), 1, "应该有一个连接"); 240 | } 241 | 242 | // 清理 243 | { 244 | let mut users_map = BROADCAST_USERS.write().await; 245 | users_map.remove(&user_id); 246 | } 247 | } 248 | 249 | #[tokio::test] 250 | async fn test_broadcast_multiple_connections() { 251 | // 测试同一用户的多个连接 252 | let user_id = "test_multi_broadcast_user".to_string(); 253 | let (tx1, _rx1) = tokio::sync::mpsc::unbounded_channel(); 254 | let (tx2, _rx2) = tokio::sync::mpsc::unbounded_channel(); 255 | 256 | // 添加多个连接到同一用户 257 | { 258 | let mut users_map = BROADCAST_USERS.write().await; 259 | let entry = users_map.entry(user_id.clone()).or_default(); 260 | entry.push(Connection { 261 | connection_id: 1, 262 | sender: tx1, 263 | }); 264 | entry.push(Connection { 265 | connection_id: 2, 266 | sender: tx2, 267 | }); 268 | } 269 | 270 | // 验证多个连接 271 | { 272 | let users_map = BROADCAST_USERS.read().await; 273 | assert!(users_map.contains_key(&user_id), "用户应该在广播池中"); 274 | assert_eq!(users_map.get(&user_id).unwrap().len(), 2, "应该有两个连接"); 275 | } 276 | 277 | // 清理 278 | { 279 | let mut users_map = BROADCAST_USERS.write().await; 280 | users_map.remove(&user_id); 281 | } 282 | } 283 | 284 | #[tokio::test] 285 | async fn test_broadcast_message_distribution() { 286 | // 测试消息分发到多个连接 287 | let user_id = "test_message_dist_user".to_string(); 288 | let (tx1, mut rx1) = tokio::sync::mpsc::unbounded_channel(); 289 | let (tx2, mut rx2) = tokio::sync::mpsc::unbounded_channel(); 290 | 291 | // 添加连接到广播池 292 | { 293 | let mut users_map = BROADCAST_USERS.write().await; 294 | let entry = users_map.entry(user_id.clone()).or_default(); 295 | entry.push(Connection { 296 | connection_id: 1, 297 | sender: tx1, 298 | }); 299 | entry.push(Connection { 300 | connection_id: 2, 301 | sender: tx2, 302 | }); 303 | } 304 | 305 | // 模拟消息分发 306 | let test_message = salvo::websocket::Message::text("Broadcast test message"); 307 | { 308 | let users_map = BROADCAST_USERS.read().await; 309 | if let Some(connections) = users_map.get(&user_id) { 310 | for connection in connections { 311 | let _ = connection.sender.send(Ok(test_message.clone())); 312 | } 313 | } 314 | } 315 | 316 | // 验证两个连接都收到消息 317 | let msg1 = timeout(Duration::from_secs(1), rx1.recv()).await; 318 | let msg2 = timeout(Duration::from_secs(1), rx2.recv()).await; 319 | 320 | assert!(msg1.is_ok(), "第一个连接应该收到消息"); 321 | assert!(msg2.is_ok(), "第二个连接应该收到消息"); 322 | 323 | // 清理 324 | { 325 | let mut users_map = BROADCAST_USERS.write().await; 326 | users_map.remove(&user_id); 327 | } 328 | } 329 | 330 | #[tokio::test] 331 | async fn test_broadcast_failed_connection_cleanup() { 332 | // 测试失败连接的清理机制 333 | let user_id = "test_cleanup_user".to_string(); 334 | let (tx1, rx1) = tokio::sync::mpsc::unbounded_channel(); 335 | let (tx2, _rx2) = tokio::sync::mpsc::unbounded_channel(); 336 | 337 | // 添加连接到广播池 338 | { 339 | let mut users_map = BROADCAST_USERS.write().await; 340 | let entry = users_map.entry(user_id.clone()).or_default(); 341 | entry.push(Connection { 342 | connection_id: 1, 343 | sender: tx1, 344 | }); 345 | entry.push(Connection { 346 | connection_id: 2, 347 | sender: tx2, 348 | }); 349 | } 350 | 351 | // 关闭第一个接收器,模拟连接断开 352 | drop(rx1); 353 | 354 | // 模拟发送消息,第一个连接会失败 355 | let test_message = salvo::websocket::Message::text("Test cleanup message"); 356 | let mut failed_connections = Vec::new(); 357 | 358 | { 359 | let users_map = BROADCAST_USERS.read().await; 360 | if let Some(connections) = users_map.get(&user_id) { 361 | for (index, connection) in connections.iter().enumerate() { 362 | if connection.sender.send(Ok(test_message.clone())).is_err() { 363 | failed_connections.push(index); 364 | } 365 | } 366 | } 367 | } 368 | 369 | // 验证有失败的连接 370 | assert_eq!(failed_connections.len(), 1, "应该有一个失败的连接"); 371 | 372 | // 模拟清理失败的连接 373 | { 374 | let mut users_map = BROADCAST_USERS.write().await; 375 | if let Some(connections) = users_map.get_mut(&user_id) { 376 | for &index in failed_connections.iter().rev() { 377 | if index < connections.len() { 378 | connections.remove(index); 379 | } 380 | } 381 | } 382 | } 383 | 384 | // 验证清理后只剩一个连接 385 | { 386 | let users_map = BROADCAST_USERS.read().await; 387 | assert_eq!( 388 | users_map.get(&user_id).unwrap().len(), 389 | 1, 390 | "应该只剩一个连接" 391 | ); 392 | } 393 | 394 | // 清理 395 | { 396 | let mut users_map = BROADCAST_USERS.write().await; 397 | users_map.remove(&user_id); 398 | } 399 | } 400 | 401 | #[tokio::test] 402 | async fn test_broadcast_empty_user_cleanup() { 403 | // 测试当用户没有连接时的清理 404 | let user_id = "test_empty_cleanup_user".to_string(); 405 | let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); 406 | 407 | // 添加连接 408 | { 409 | let mut users_map = BROADCAST_USERS.write().await; 410 | let connection = Connection { 411 | connection_id: 1, 412 | sender: tx, 413 | }; 414 | users_map 415 | .entry(user_id.clone()) 416 | .or_default() 417 | .push(connection); 418 | } 419 | 420 | // 关闭接收器 421 | drop(rx); 422 | 423 | // 模拟发送消息失败并清理 424 | { 425 | let users_map = BROADCAST_USERS.read().await; 426 | if let Some(connections) = users_map.get(&user_id) { 427 | let mut failed_connections = Vec::new(); 428 | for (index, connection) in connections.iter().enumerate() { 429 | if connection 430 | .sender 431 | .send(Ok(salvo::websocket::Message::text("test"))) 432 | .is_err() 433 | { 434 | failed_connections.push(index); 435 | } 436 | } 437 | 438 | drop(users_map); 439 | let mut users_map = BROADCAST_USERS.write().await; 440 | if let Some(connections) = users_map.get_mut(&user_id) { 441 | for &index in failed_connections.iter().rev() { 442 | if index < connections.len() { 443 | connections.remove(index); 444 | } 445 | } 446 | // 如果没有连接了,移除整个条目 447 | if connections.is_empty() { 448 | users_map.remove(&user_id); 449 | } 450 | } 451 | } 452 | } 453 | 454 | // 验证用户已被完全移除 455 | { 456 | let users_map = BROADCAST_USERS.read().await; 457 | assert!(!users_map.contains_key(&user_id), "空用户应该被移除"); 458 | } 459 | } 460 | 461 | #[tokio::test] 462 | async fn test_broadcast_message_types() { 463 | // 测试不同类型的消息处理 464 | let user_id = "test_message_types_user".to_string(); 465 | let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); 466 | 467 | // 添加连接 468 | { 469 | let mut users_map = BROADCAST_USERS.write().await; 470 | let connection = Connection { 471 | connection_id: 1, 472 | sender: tx, 473 | }; 474 | users_map 475 | .entry(user_id.clone()) 476 | .or_default() 477 | .push(connection); 478 | } 479 | 480 | // 测试文本消息 481 | let text_msg = salvo::websocket::Message::text("Hello World"); 482 | { 483 | let users_map = BROADCAST_USERS.read().await; 484 | if let Some(connections) = users_map.get(&user_id) { 485 | for connection in connections { 486 | let _ = connection.sender.send(Ok(text_msg.clone())); 487 | } 488 | } 489 | } 490 | 491 | let received_text = timeout(Duration::from_secs(1), rx.recv()).await; 492 | assert!(received_text.is_ok(), "应该收到文本消息"); 493 | 494 | // 测试二进制消息 495 | let binary_msg = salvo::websocket::Message::binary(vec![1, 2, 3, 4]); 496 | { 497 | let users_map = BROADCAST_USERS.read().await; 498 | if let Some(connections) = users_map.get(&user_id) { 499 | for connection in connections { 500 | let _ = connection.sender.send(Ok(binary_msg.clone())); 501 | } 502 | } 503 | } 504 | 505 | let received_binary = timeout(Duration::from_secs(1), rx.recv()).await; 506 | assert!(received_binary.is_ok(), "应该收到二进制消息"); 507 | 508 | // 测试ping消息 509 | let ping_msg = salvo::websocket::Message::ping(vec![]); 510 | { 511 | let users_map = BROADCAST_USERS.read().await; 512 | if let Some(connections) = users_map.get(&user_id) { 513 | for connection in connections { 514 | let _ = connection.sender.send(Ok(ping_msg.clone())); 515 | } 516 | } 517 | } 518 | 519 | let received_ping = timeout(Duration::from_secs(1), rx.recv()).await; 520 | assert!(received_ping.is_ok(), "应该收到ping消息"); 521 | 522 | // 清理 523 | { 524 | let mut users_map = BROADCAST_USERS.write().await; 525 | users_map.remove(&user_id); 526 | } 527 | } 528 | 529 | #[tokio::test] 530 | async fn test_broadcast_concurrent_access() { 531 | // 测试并发访问广播池 532 | let user_id = "test_concurrent_user".to_string(); 533 | let mut handles = Vec::new(); 534 | 535 | // 启动多个任务并发添加连接 536 | for i in 0..10 { 537 | let user_id_clone = user_id.clone(); 538 | let handle = tokio::spawn(async move { 539 | let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); 540 | let mut users_map = BROADCAST_USERS.write().await; 541 | let connection = Connection { 542 | connection_id: i as u64, 543 | sender: tx, 544 | }; 545 | users_map.entry(user_id_clone).or_default().push(connection); 546 | i 547 | }); 548 | handles.push(handle); 549 | } 550 | 551 | // 等待所有任务完成 552 | for handle in handles { 553 | handle.await.unwrap(); 554 | } 555 | 556 | // 验证所有连接都已添加 557 | { 558 | let users_map = BROADCAST_USERS.read().await; 559 | assert!(users_map.contains_key(&user_id), "用户应该存在"); 560 | assert_eq!(users_map.get(&user_id).unwrap().len(), 10, "应该有10个连接"); 561 | } 562 | 563 | // 清理 564 | { 565 | let mut users_map = BROADCAST_USERS.write().await; 566 | users_map.remove(&user_id); 567 | } 568 | } 569 | 570 | #[tokio::test] 571 | async fn test_broadcast_connection_disconnect_scenario() { 572 | // 测试具体场景:两个连接,一个断开后,另一个仍能接收消息 573 | let user_id = "test_disconnect_scenario".to_string(); 574 | let (tx1, mut rx1) = tokio::sync::mpsc::unbounded_channel(); 575 | let (tx2, rx2) = tokio::sync::mpsc::unbounded_channel(); 576 | 577 | // 添加两个连接到广播池 578 | { 579 | let mut users_map = BROADCAST_USERS.write().await; 580 | let entry = users_map.entry(user_id.clone()).or_default(); 581 | entry.push(Connection { 582 | connection_id: 1, 583 | sender: tx1, 584 | }); 585 | entry.push(Connection { 586 | connection_id: 2, 587 | sender: tx2, 588 | }); 589 | } 590 | 591 | // 验证两个连接都已添加 592 | { 593 | let users_map = BROADCAST_USERS.read().await; 594 | assert_eq!(users_map.get(&user_id).unwrap().len(), 2, "应该有两个连接"); 595 | } 596 | 597 | // 模拟第二个连接断开(关闭接收器) 598 | drop(rx2); 599 | 600 | // 模拟通过 /broad/pub 发送消息的逻辑 601 | let test_message = salvo::websocket::Message::text("Test message after disconnect"); 602 | let mut failed_connection_ids = Vec::new(); 603 | 604 | // 尝试发送消息给所有连接 605 | { 606 | let users_map = BROADCAST_USERS.read().await; 607 | if let Some(connections) = users_map.get(&user_id) { 608 | for connection in connections.iter() { 609 | if connection.sender.send(Ok(test_message.clone())).is_err() { 610 | failed_connection_ids.push(connection.connection_id); 611 | tracing::debug!( 612 | "Failed to send message to connection_id: {}", 613 | connection.connection_id 614 | ); 615 | } 616 | } 617 | } 618 | } 619 | 620 | // 验证有一个连接失败 621 | assert_eq!(failed_connection_ids.len(), 1, "应该有一个失败的连接"); 622 | assert_eq!(failed_connection_ids[0], 2, "失败的应该是 connection_id 2"); 623 | 624 | // 清理失败的连接(模拟 broadcast_publish 中的清理逻辑) 625 | { 626 | let mut users_map = BROADCAST_USERS.write().await; 627 | if let Some(connections) = users_map.get_mut(&user_id) { 628 | connections.retain(|conn| !failed_connection_ids.contains(&conn.connection_id)); 629 | 630 | // 如果没有连接了,移除整个条目 631 | if connections.is_empty() { 632 | users_map.remove(&user_id); 633 | } 634 | } 635 | } 636 | 637 | // 验证清理后只剩一个连接 638 | { 639 | let users_map = BROADCAST_USERS.read().await; 640 | assert!(users_map.contains_key(&user_id), "用户应该仍然存在"); 641 | assert_eq!( 642 | users_map.get(&user_id).unwrap().len(), 643 | 1, 644 | "应该只剩一个连接" 645 | ); 646 | assert_eq!( 647 | users_map.get(&user_id).unwrap()[0].connection_id, 648 | 1, 649 | "剩余的应该是 connection_id 1" 650 | ); 651 | } 652 | 653 | // 验证剩余连接能收到之前发送的消息 654 | let received_msg = timeout(Duration::from_millis(100), rx1.recv()).await; 655 | assert!(received_msg.is_ok(), "剩余连接应该能收到消息"); 656 | 657 | // 再次发送消息,验证剩余连接仍然工作正常 658 | let second_message = salvo::websocket::Message::text("Second test message"); 659 | { 660 | let users_map = BROADCAST_USERS.read().await; 661 | if let Some(connections) = users_map.get(&user_id) { 662 | for connection in connections.iter() { 663 | let send_result = connection.sender.send(Ok(second_message.clone())); 664 | assert!(send_result.is_ok(), "发送给剩余连接应该成功"); 665 | } 666 | } 667 | } 668 | 669 | // 验证第二条消息也能正常接收 670 | let received_second_msg = timeout(Duration::from_millis(100), rx1.recv()).await; 671 | assert!(received_second_msg.is_ok(), "剩余连接应该能收到第二条消息"); 672 | 673 | // 清理 674 | { 675 | let mut users_map = BROADCAST_USERS.write().await; 676 | users_map.remove(&user_id); 677 | } 678 | } 679 | 680 | #[tokio::test] 681 | async fn test_broadcast_all_connections_disconnect() { 682 | // 测试所有连接都断开的场景 683 | let user_id = "test_all_disconnect".to_string(); 684 | let (tx1, rx1) = tokio::sync::mpsc::unbounded_channel(); 685 | let (tx2, rx2) = tokio::sync::mpsc::unbounded_channel(); 686 | 687 | // 添加两个连接 688 | { 689 | let mut users_map = BROADCAST_USERS.write().await; 690 | let entry = users_map.entry(user_id.clone()).or_default(); 691 | entry.push(Connection { 692 | connection_id: 1, 693 | sender: tx1, 694 | }); 695 | entry.push(Connection { 696 | connection_id: 2, 697 | sender: tx2, 698 | }); 699 | } 700 | 701 | // 断开所有连接 702 | drop(rx1); 703 | drop(rx2); 704 | 705 | // 尝试发送消息 706 | let test_message = salvo::websocket::Message::text("Message to disconnected connections"); 707 | let mut failed_connection_ids = Vec::new(); 708 | 709 | { 710 | let users_map = BROADCAST_USERS.read().await; 711 | if let Some(connections) = users_map.get(&user_id) { 712 | for connection in connections.iter() { 713 | if connection.sender.send(Ok(test_message.clone())).is_err() { 714 | failed_connection_ids.push(connection.connection_id); 715 | } 716 | } 717 | } 718 | } 719 | 720 | // 验证所有连接都失败 721 | assert_eq!(failed_connection_ids.len(), 2, "所有连接都应该失败"); 722 | 723 | // 清理失败的连接 724 | { 725 | let mut users_map = BROADCAST_USERS.write().await; 726 | if let Some(connections) = users_map.get_mut(&user_id) { 727 | connections.retain(|conn| !failed_connection_ids.contains(&conn.connection_id)); 728 | 729 | // 如果没有连接了,移除整个条目 730 | if connections.is_empty() { 731 | users_map.remove(&user_id); 732 | } 733 | } 734 | } 735 | 736 | // 验证用户条目已被完全移除 737 | { 738 | let users_map = BROADCAST_USERS.read().await; 739 | assert!(!users_map.contains_key(&user_id), "用户条目应该被完全移除"); 740 | } 741 | } 742 | 743 | #[tokio::test] 744 | async fn test_broadcast_partial_disconnect_multiple_users() { 745 | // 测试多个用户,部分连接断开的复杂场景 746 | let user1_id = "user1".to_string(); 747 | let user2_id = "user2".to_string(); 748 | 749 | let (user1_tx1, mut user1_rx1) = tokio::sync::mpsc::unbounded_channel(); 750 | let (user1_tx2, user1_rx2) = tokio::sync::mpsc::unbounded_channel(); // 这个会断开 751 | let (user2_tx1, mut user2_rx1) = tokio::sync::mpsc::unbounded_channel(); 752 | let (user2_tx2, mut user2_rx2) = tokio::sync::mpsc::unbounded_channel(); 753 | 754 | // 添加连接 755 | { 756 | let mut users_map = BROADCAST_USERS.write().await; 757 | 758 | // 用户1的连接 759 | let user1_entry = users_map.entry(user1_id.clone()).or_default(); 760 | user1_entry.push(Connection { 761 | connection_id: 1, 762 | sender: user1_tx1, 763 | }); 764 | user1_entry.push(Connection { 765 | connection_id: 2, 766 | sender: user1_tx2, 767 | }); 768 | 769 | // 用户2的连接 770 | let user2_entry = users_map.entry(user2_id.clone()).or_default(); 771 | user2_entry.push(Connection { 772 | connection_id: 3, 773 | sender: user2_tx1, 774 | }); 775 | user2_entry.push(Connection { 776 | connection_id: 4, 777 | sender: user2_tx2, 778 | }); 779 | } 780 | 781 | // 断开用户1的第二个连接 782 | drop(user1_rx2); 783 | 784 | // 给用户1发送消息 785 | let message_for_user1 = salvo::websocket::Message::text("Message for user1"); 786 | let mut failed_connection_ids = Vec::new(); 787 | 788 | { 789 | let users_map = BROADCAST_USERS.read().await; 790 | if let Some(connections) = users_map.get(&user1_id) { 791 | for connection in connections.iter() { 792 | if connection 793 | .sender 794 | .send(Ok(message_for_user1.clone())) 795 | .is_err() 796 | { 797 | failed_connection_ids.push(connection.connection_id); 798 | } 799 | } 800 | } 801 | } 802 | 803 | // 清理用户1的失败连接 804 | { 805 | let mut users_map = BROADCAST_USERS.write().await; 806 | if let Some(connections) = users_map.get_mut(&user1_id) { 807 | connections.retain(|conn| !failed_connection_ids.contains(&conn.connection_id)); 808 | } 809 | } 810 | 811 | // 验证用户1只剩一个连接,用户2仍有两个连接 812 | { 813 | let users_map = BROADCAST_USERS.read().await; 814 | assert_eq!( 815 | users_map.get(&user1_id).unwrap().len(), 816 | 1, 817 | "用户1应该只剩一个连接" 818 | ); 819 | assert_eq!( 820 | users_map.get(&user2_id).unwrap().len(), 821 | 2, 822 | "用户2应该仍有两个连接" 823 | ); 824 | } 825 | 826 | // 验证用户1的剩余连接能收到消息 827 | let received_msg = timeout(Duration::from_millis(100), user1_rx1.recv()).await; 828 | assert!(received_msg.is_ok(), "用户1的剩余连接应该能收到消息"); 829 | 830 | // 给用户2发送消息,验证不受影响 831 | let message_for_user2 = salvo::websocket::Message::text("Message for user2"); 832 | { 833 | let users_map = BROADCAST_USERS.read().await; 834 | if let Some(connections) = users_map.get(&user2_id) { 835 | for connection in connections.iter() { 836 | let send_result = connection.sender.send(Ok(message_for_user2.clone())); 837 | assert!(send_result.is_ok(), "用户2的连接发送应该成功"); 838 | } 839 | } 840 | } 841 | 842 | // 验证用户2的两个连接都能收到消息 843 | let user2_msg1 = timeout(Duration::from_millis(100), user2_rx1.recv()).await; 844 | let user2_msg2 = timeout(Duration::from_millis(100), user2_rx2.recv()).await; 845 | assert!(user2_msg1.is_ok(), "用户2的第一个连接应该能收到消息"); 846 | assert!(user2_msg2.is_ok(), "用户2的第二个连接应该能收到消息"); 847 | 848 | // 清理 849 | { 850 | let mut users_map = BROADCAST_USERS.write().await; 851 | users_map.remove(&user1_id); 852 | users_map.remove(&user2_id); 853 | } 854 | } 855 | } 856 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 | 20 | [[package]] 21 | name = "aead" 22 | version = "0.5.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" 25 | dependencies = [ 26 | "crypto-common", 27 | "generic-array", 28 | ] 29 | 30 | [[package]] 31 | name = "aes" 32 | version = "0.8.4" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" 35 | dependencies = [ 36 | "cfg-if", 37 | "cipher", 38 | "cpufeatures", 39 | ] 40 | 41 | [[package]] 42 | name = "aes-gcm" 43 | version = "0.10.3" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" 46 | dependencies = [ 47 | "aead", 48 | "aes", 49 | "cipher", 50 | "ctr", 51 | "ghash", 52 | "subtle", 53 | ] 54 | 55 | [[package]] 56 | name = "aho-corasick" 57 | version = "1.1.3" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 60 | dependencies = [ 61 | "memchr", 62 | ] 63 | 64 | [[package]] 65 | name = "alloc-no-stdlib" 66 | version = "2.0.4" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 69 | 70 | [[package]] 71 | name = "alloc-stdlib" 72 | version = "0.2.2" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 75 | dependencies = [ 76 | "alloc-no-stdlib", 77 | ] 78 | 79 | [[package]] 80 | name = "anstream" 81 | version = "0.6.20" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" 84 | dependencies = [ 85 | "anstyle", 86 | "anstyle-parse", 87 | "anstyle-query", 88 | "anstyle-wincon", 89 | "colorchoice", 90 | "is_terminal_polyfill", 91 | "utf8parse", 92 | ] 93 | 94 | [[package]] 95 | name = "anstyle" 96 | version = "1.0.11" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 99 | 100 | [[package]] 101 | name = "anstyle-parse" 102 | version = "0.2.7" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 105 | dependencies = [ 106 | "utf8parse", 107 | ] 108 | 109 | [[package]] 110 | name = "anstyle-query" 111 | version = "1.1.4" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 114 | dependencies = [ 115 | "windows-sys 0.60.2", 116 | ] 117 | 118 | [[package]] 119 | name = "anstyle-wincon" 120 | version = "3.0.10" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 123 | dependencies = [ 124 | "anstyle", 125 | "once_cell_polyfill", 126 | "windows-sys 0.60.2", 127 | ] 128 | 129 | [[package]] 130 | name = "async-trait" 131 | version = "0.1.88" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" 134 | dependencies = [ 135 | "proc-macro2", 136 | "quote", 137 | "syn", 138 | ] 139 | 140 | [[package]] 141 | name = "atomic-waker" 142 | version = "1.1.2" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 145 | 146 | [[package]] 147 | name = "autocfg" 148 | version = "1.5.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 151 | 152 | [[package]] 153 | name = "backtrace" 154 | version = "0.3.75" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 157 | dependencies = [ 158 | "addr2line", 159 | "cfg-if", 160 | "libc", 161 | "miniz_oxide", 162 | "object", 163 | "rustc-demangle", 164 | "windows-targets 0.52.6", 165 | ] 166 | 167 | [[package]] 168 | name = "base16ct" 169 | version = "0.2.0" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 172 | 173 | [[package]] 174 | name = "base64" 175 | version = "0.22.1" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 178 | 179 | [[package]] 180 | name = "base64ct" 181 | version = "1.8.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 184 | 185 | [[package]] 186 | name = "bitflags" 187 | version = "2.9.1" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 190 | 191 | [[package]] 192 | name = "block-buffer" 193 | version = "0.10.4" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 196 | dependencies = [ 197 | "generic-array", 198 | ] 199 | 200 | [[package]] 201 | name = "brotli" 202 | version = "8.0.1" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" 205 | dependencies = [ 206 | "alloc-no-stdlib", 207 | "alloc-stdlib", 208 | "brotli-decompressor", 209 | ] 210 | 211 | [[package]] 212 | name = "brotli-decompressor" 213 | version = "5.0.0" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" 216 | dependencies = [ 217 | "alloc-no-stdlib", 218 | "alloc-stdlib", 219 | ] 220 | 221 | [[package]] 222 | name = "bumpalo" 223 | version = "3.19.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 226 | 227 | [[package]] 228 | name = "bytes" 229 | version = "1.10.1" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 232 | 233 | [[package]] 234 | name = "cc" 235 | version = "1.2.29" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" 238 | dependencies = [ 239 | "jobserver", 240 | "libc", 241 | "shlex", 242 | ] 243 | 244 | [[package]] 245 | name = "cfg-if" 246 | version = "1.0.1" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 249 | 250 | [[package]] 251 | name = "cfg_aliases" 252 | version = "0.2.1" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 255 | 256 | [[package]] 257 | name = "chardetng" 258 | version = "0.1.17" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea" 261 | dependencies = [ 262 | "cfg-if", 263 | "encoding_rs", 264 | "memchr", 265 | ] 266 | 267 | [[package]] 268 | name = "cipher" 269 | version = "0.4.4" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 272 | dependencies = [ 273 | "crypto-common", 274 | "inout", 275 | ] 276 | 277 | [[package]] 278 | name = "clap" 279 | version = "4.5.47" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" 282 | dependencies = [ 283 | "clap_builder", 284 | "clap_derive", 285 | ] 286 | 287 | [[package]] 288 | name = "clap_builder" 289 | version = "4.5.47" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" 292 | dependencies = [ 293 | "anstream", 294 | "anstyle", 295 | "clap_lex", 296 | "strsim", 297 | ] 298 | 299 | [[package]] 300 | name = "clap_derive" 301 | version = "4.5.47" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" 304 | dependencies = [ 305 | "heck", 306 | "proc-macro2", 307 | "quote", 308 | "syn", 309 | ] 310 | 311 | [[package]] 312 | name = "clap_lex" 313 | version = "0.7.5" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 316 | 317 | [[package]] 318 | name = "colorchoice" 319 | version = "1.0.4" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 322 | 323 | [[package]] 324 | name = "const-oid" 325 | version = "0.9.6" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 328 | 329 | [[package]] 330 | name = "content_inspector" 331 | version = "0.2.4" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" 334 | dependencies = [ 335 | "memchr", 336 | ] 337 | 338 | [[package]] 339 | name = "cookie" 340 | version = "0.18.1" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" 343 | dependencies = [ 344 | "aes-gcm", 345 | "base64", 346 | "hmac", 347 | "percent-encoding", 348 | "rand 0.8.5", 349 | "sha2", 350 | "subtle", 351 | "time", 352 | "version_check", 353 | ] 354 | 355 | [[package]] 356 | name = "core-foundation" 357 | version = "0.9.4" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 360 | dependencies = [ 361 | "core-foundation-sys", 362 | "libc", 363 | ] 364 | 365 | [[package]] 366 | name = "core-foundation" 367 | version = "0.10.1" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 370 | dependencies = [ 371 | "core-foundation-sys", 372 | "libc", 373 | ] 374 | 375 | [[package]] 376 | name = "core-foundation-sys" 377 | version = "0.8.7" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 380 | 381 | [[package]] 382 | name = "cpufeatures" 383 | version = "0.2.17" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 386 | dependencies = [ 387 | "libc", 388 | ] 389 | 390 | [[package]] 391 | name = "crc32fast" 392 | version = "1.4.2" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 395 | dependencies = [ 396 | "cfg-if", 397 | ] 398 | 399 | [[package]] 400 | name = "crossbeam-utils" 401 | version = "0.8.21" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 404 | 405 | [[package]] 406 | name = "crypto-bigint" 407 | version = "0.5.5" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 410 | dependencies = [ 411 | "generic-array", 412 | "rand_core 0.6.4", 413 | "subtle", 414 | "zeroize", 415 | ] 416 | 417 | [[package]] 418 | name = "crypto-common" 419 | version = "0.1.6" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 422 | dependencies = [ 423 | "generic-array", 424 | "rand_core 0.6.4", 425 | "typenum", 426 | ] 427 | 428 | [[package]] 429 | name = "ctr" 430 | version = "0.9.2" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" 433 | dependencies = [ 434 | "cipher", 435 | ] 436 | 437 | [[package]] 438 | name = "curve25519-dalek" 439 | version = "4.1.3" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" 442 | dependencies = [ 443 | "cfg-if", 444 | "cpufeatures", 445 | "curve25519-dalek-derive", 446 | "digest", 447 | "fiat-crypto", 448 | "rustc_version", 449 | "subtle", 450 | "zeroize", 451 | ] 452 | 453 | [[package]] 454 | name = "curve25519-dalek-derive" 455 | version = "0.1.1" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" 458 | dependencies = [ 459 | "proc-macro2", 460 | "quote", 461 | "syn", 462 | ] 463 | 464 | [[package]] 465 | name = "dashmap" 466 | version = "6.1.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" 469 | dependencies = [ 470 | "cfg-if", 471 | "crossbeam-utils", 472 | "hashbrown 0.14.5", 473 | "lock_api", 474 | "once_cell", 475 | "parking_lot_core", 476 | ] 477 | 478 | [[package]] 479 | name = "der" 480 | version = "0.7.10" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 483 | dependencies = [ 484 | "const-oid", 485 | "pem-rfc7468", 486 | "zeroize", 487 | ] 488 | 489 | [[package]] 490 | name = "deranged" 491 | version = "0.4.0" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 494 | dependencies = [ 495 | "powerfmt", 496 | "serde", 497 | ] 498 | 499 | [[package]] 500 | name = "digest" 501 | version = "0.10.7" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 504 | dependencies = [ 505 | "block-buffer", 506 | "const-oid", 507 | "crypto-common", 508 | "subtle", 509 | ] 510 | 511 | [[package]] 512 | name = "displaydoc" 513 | version = "0.2.5" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 516 | dependencies = [ 517 | "proc-macro2", 518 | "quote", 519 | "syn", 520 | ] 521 | 522 | [[package]] 523 | name = "ecdsa" 524 | version = "0.16.9" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 527 | dependencies = [ 528 | "der", 529 | "digest", 530 | "elliptic-curve", 531 | "rfc6979", 532 | "signature", 533 | "spki", 534 | ] 535 | 536 | [[package]] 537 | name = "ed25519" 538 | version = "2.2.3" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" 541 | dependencies = [ 542 | "pkcs8", 543 | "signature", 544 | ] 545 | 546 | [[package]] 547 | name = "ed25519-dalek" 548 | version = "2.2.0" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" 551 | dependencies = [ 552 | "curve25519-dalek", 553 | "ed25519", 554 | "serde", 555 | "sha2", 556 | "subtle", 557 | "zeroize", 558 | ] 559 | 560 | [[package]] 561 | name = "elliptic-curve" 562 | version = "0.13.8" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 565 | dependencies = [ 566 | "base16ct", 567 | "crypto-bigint", 568 | "digest", 569 | "ff", 570 | "generic-array", 571 | "group", 572 | "hkdf", 573 | "pem-rfc7468", 574 | "pkcs8", 575 | "rand_core 0.6.4", 576 | "sec1", 577 | "subtle", 578 | "zeroize", 579 | ] 580 | 581 | [[package]] 582 | name = "encoding_rs" 583 | version = "0.8.35" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 586 | dependencies = [ 587 | "cfg-if", 588 | ] 589 | 590 | [[package]] 591 | name = "enumflags2" 592 | version = "0.7.12" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" 595 | dependencies = [ 596 | "enumflags2_derive", 597 | ] 598 | 599 | [[package]] 600 | name = "enumflags2_derive" 601 | version = "0.7.12" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" 604 | dependencies = [ 605 | "proc-macro2", 606 | "quote", 607 | "syn", 608 | ] 609 | 610 | [[package]] 611 | name = "equivalent" 612 | version = "1.0.2" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 615 | 616 | [[package]] 617 | name = "errno" 618 | version = "0.3.13" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 621 | dependencies = [ 622 | "libc", 623 | "windows-sys 0.60.2", 624 | ] 625 | 626 | [[package]] 627 | name = "etag" 628 | version = "4.0.0" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "4b3d0661a2ccddc26cba0b834e9b717959ed6fdd76c7129ee159c170a875bf44" 631 | dependencies = [ 632 | "str-buf", 633 | "xxhash-rust", 634 | ] 635 | 636 | [[package]] 637 | name = "fastrand" 638 | version = "2.3.0" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 641 | 642 | [[package]] 643 | name = "ff" 644 | version = "0.13.1" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" 647 | dependencies = [ 648 | "rand_core 0.6.4", 649 | "subtle", 650 | ] 651 | 652 | [[package]] 653 | name = "fiat-crypto" 654 | version = "0.2.9" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" 657 | 658 | [[package]] 659 | name = "flate2" 660 | version = "1.1.1" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 663 | dependencies = [ 664 | "crc32fast", 665 | "miniz_oxide", 666 | ] 667 | 668 | [[package]] 669 | name = "fnv" 670 | version = "1.0.7" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 673 | 674 | [[package]] 675 | name = "form_urlencoded" 676 | version = "1.2.1" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 679 | dependencies = [ 680 | "percent-encoding", 681 | ] 682 | 683 | [[package]] 684 | name = "futures-channel" 685 | version = "0.3.31" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 688 | dependencies = [ 689 | "futures-core", 690 | ] 691 | 692 | [[package]] 693 | name = "futures-core" 694 | version = "0.3.31" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 697 | 698 | [[package]] 699 | name = "futures-io" 700 | version = "0.3.31" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 703 | 704 | [[package]] 705 | name = "futures-macro" 706 | version = "0.3.31" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 709 | dependencies = [ 710 | "proc-macro2", 711 | "quote", 712 | "syn", 713 | ] 714 | 715 | [[package]] 716 | name = "futures-sink" 717 | version = "0.3.31" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 720 | 721 | [[package]] 722 | name = "futures-task" 723 | version = "0.3.31" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 726 | 727 | [[package]] 728 | name = "futures-util" 729 | version = "0.3.31" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 732 | dependencies = [ 733 | "futures-core", 734 | "futures-io", 735 | "futures-macro", 736 | "futures-sink", 737 | "futures-task", 738 | "memchr", 739 | "pin-project-lite", 740 | "pin-utils", 741 | "slab", 742 | ] 743 | 744 | [[package]] 745 | name = "generic-array" 746 | version = "0.14.7" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 749 | dependencies = [ 750 | "typenum", 751 | "version_check", 752 | "zeroize", 753 | ] 754 | 755 | [[package]] 756 | name = "getrandom" 757 | version = "0.2.16" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 760 | dependencies = [ 761 | "cfg-if", 762 | "js-sys", 763 | "libc", 764 | "wasi 0.11.1+wasi-snapshot-preview1", 765 | "wasm-bindgen", 766 | ] 767 | 768 | [[package]] 769 | name = "getrandom" 770 | version = "0.3.3" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 773 | dependencies = [ 774 | "cfg-if", 775 | "js-sys", 776 | "libc", 777 | "r-efi", 778 | "wasi 0.14.2+wasi-0.2.4", 779 | "wasm-bindgen", 780 | ] 781 | 782 | [[package]] 783 | name = "ghash" 784 | version = "0.5.1" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" 787 | dependencies = [ 788 | "opaque-debug", 789 | "polyval", 790 | ] 791 | 792 | [[package]] 793 | name = "gimli" 794 | version = "0.31.1" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 797 | 798 | [[package]] 799 | name = "group" 800 | version = "0.13.0" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 803 | dependencies = [ 804 | "ff", 805 | "rand_core 0.6.4", 806 | "subtle", 807 | ] 808 | 809 | [[package]] 810 | name = "h2" 811 | version = "0.4.11" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" 814 | dependencies = [ 815 | "atomic-waker", 816 | "bytes", 817 | "fnv", 818 | "futures-core", 819 | "futures-sink", 820 | "http", 821 | "indexmap", 822 | "slab", 823 | "tokio", 824 | "tokio-util", 825 | "tracing", 826 | ] 827 | 828 | [[package]] 829 | name = "hashbrown" 830 | version = "0.14.5" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 833 | 834 | [[package]] 835 | name = "hashbrown" 836 | version = "0.15.4" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" 839 | 840 | [[package]] 841 | name = "headers" 842 | version = "0.4.1" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" 845 | dependencies = [ 846 | "base64", 847 | "bytes", 848 | "headers-core", 849 | "http", 850 | "httpdate", 851 | "mime", 852 | "sha1", 853 | ] 854 | 855 | [[package]] 856 | name = "headers-core" 857 | version = "0.3.0" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" 860 | dependencies = [ 861 | "http", 862 | ] 863 | 864 | [[package]] 865 | name = "heck" 866 | version = "0.5.0" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 869 | 870 | [[package]] 871 | name = "hex" 872 | version = "0.4.3" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 875 | 876 | [[package]] 877 | name = "hkdf" 878 | version = "0.12.4" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 881 | dependencies = [ 882 | "hmac", 883 | ] 884 | 885 | [[package]] 886 | name = "hmac" 887 | version = "0.12.1" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 890 | dependencies = [ 891 | "digest", 892 | ] 893 | 894 | [[package]] 895 | name = "http" 896 | version = "1.3.1" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 899 | dependencies = [ 900 | "bytes", 901 | "fnv", 902 | "itoa", 903 | ] 904 | 905 | [[package]] 906 | name = "http-body" 907 | version = "1.0.1" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 910 | dependencies = [ 911 | "bytes", 912 | "http", 913 | ] 914 | 915 | [[package]] 916 | name = "http-body-util" 917 | version = "0.1.3" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 920 | dependencies = [ 921 | "bytes", 922 | "futures-core", 923 | "http", 924 | "http-body", 925 | "pin-project-lite", 926 | ] 927 | 928 | [[package]] 929 | name = "httparse" 930 | version = "1.10.1" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 933 | 934 | [[package]] 935 | name = "httpdate" 936 | version = "1.0.3" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 939 | 940 | [[package]] 941 | name = "hyper" 942 | version = "1.6.0" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 945 | dependencies = [ 946 | "bytes", 947 | "futures-channel", 948 | "futures-util", 949 | "h2", 950 | "http", 951 | "http-body", 952 | "httparse", 953 | "httpdate", 954 | "itoa", 955 | "pin-project-lite", 956 | "smallvec", 957 | "tokio", 958 | "want", 959 | ] 960 | 961 | [[package]] 962 | name = "hyper-rustls" 963 | version = "0.27.7" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 966 | dependencies = [ 967 | "http", 968 | "hyper", 969 | "hyper-util", 970 | "log", 971 | "rustls", 972 | "rustls-native-certs", 973 | "rustls-pki-types", 974 | "tokio", 975 | "tokio-rustls", 976 | "tower-service", 977 | "webpki-roots", 978 | ] 979 | 980 | [[package]] 981 | name = "hyper-util" 982 | version = "0.1.15" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" 985 | dependencies = [ 986 | "base64", 987 | "bytes", 988 | "futures-channel", 989 | "futures-core", 990 | "futures-util", 991 | "http", 992 | "http-body", 993 | "hyper", 994 | "ipnet", 995 | "libc", 996 | "percent-encoding", 997 | "pin-project-lite", 998 | "socket2", 999 | "system-configuration", 1000 | "tokio", 1001 | "tower-service", 1002 | "tracing", 1003 | "windows-registry", 1004 | ] 1005 | 1006 | [[package]] 1007 | name = "icu_collections" 1008 | version = "2.0.0" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 1011 | dependencies = [ 1012 | "displaydoc", 1013 | "potential_utf", 1014 | "yoke", 1015 | "zerofrom", 1016 | "zerovec", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "icu_locale_core" 1021 | version = "2.0.0" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 1024 | dependencies = [ 1025 | "displaydoc", 1026 | "litemap", 1027 | "tinystr", 1028 | "writeable", 1029 | "zerovec", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "icu_normalizer" 1034 | version = "2.0.0" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 1037 | dependencies = [ 1038 | "displaydoc", 1039 | "icu_collections", 1040 | "icu_normalizer_data", 1041 | "icu_properties", 1042 | "icu_provider", 1043 | "smallvec", 1044 | "zerovec", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "icu_normalizer_data" 1049 | version = "2.0.0" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 1052 | 1053 | [[package]] 1054 | name = "icu_properties" 1055 | version = "2.0.1" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 1058 | dependencies = [ 1059 | "displaydoc", 1060 | "icu_collections", 1061 | "icu_locale_core", 1062 | "icu_properties_data", 1063 | "icu_provider", 1064 | "potential_utf", 1065 | "zerotrie", 1066 | "zerovec", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "icu_properties_data" 1071 | version = "2.0.1" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 1074 | 1075 | [[package]] 1076 | name = "icu_provider" 1077 | version = "2.0.0" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 1080 | dependencies = [ 1081 | "displaydoc", 1082 | "icu_locale_core", 1083 | "stable_deref_trait", 1084 | "tinystr", 1085 | "writeable", 1086 | "yoke", 1087 | "zerofrom", 1088 | "zerotrie", 1089 | "zerovec", 1090 | ] 1091 | 1092 | [[package]] 1093 | name = "idna" 1094 | version = "1.0.3" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 1097 | dependencies = [ 1098 | "idna_adapter", 1099 | "smallvec", 1100 | "utf8_iter", 1101 | ] 1102 | 1103 | [[package]] 1104 | name = "idna_adapter" 1105 | version = "1.2.1" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 1108 | dependencies = [ 1109 | "icu_normalizer", 1110 | "icu_properties", 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "indexmap" 1115 | version = "2.10.0" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 1118 | dependencies = [ 1119 | "equivalent", 1120 | "hashbrown 0.15.4", 1121 | ] 1122 | 1123 | [[package]] 1124 | name = "inout" 1125 | version = "0.1.4" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" 1128 | dependencies = [ 1129 | "generic-array", 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "io-uring" 1134 | version = "0.7.8" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" 1137 | dependencies = [ 1138 | "bitflags", 1139 | "cfg-if", 1140 | "libc", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "ipnet" 1145 | version = "2.11.0" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1148 | 1149 | [[package]] 1150 | name = "iri-string" 1151 | version = "0.7.8" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 1154 | dependencies = [ 1155 | "memchr", 1156 | "serde", 1157 | ] 1158 | 1159 | [[package]] 1160 | name = "is_terminal_polyfill" 1161 | version = "1.70.1" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 1164 | 1165 | [[package]] 1166 | name = "itoa" 1167 | version = "1.0.15" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1170 | 1171 | [[package]] 1172 | name = "jobserver" 1173 | version = "0.1.33" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 1176 | dependencies = [ 1177 | "getrandom 0.3.3", 1178 | "libc", 1179 | ] 1180 | 1181 | [[package]] 1182 | name = "js-sys" 1183 | version = "0.3.77" 1184 | source = "registry+https://github.com/rust-lang/crates.io-index" 1185 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 1186 | dependencies = [ 1187 | "once_cell", 1188 | "wasm-bindgen", 1189 | ] 1190 | 1191 | [[package]] 1192 | name = "jsonwebtoken" 1193 | version = "10.2.0" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e" 1196 | dependencies = [ 1197 | "base64", 1198 | "ed25519-dalek", 1199 | "getrandom 0.2.16", 1200 | "hmac", 1201 | "js-sys", 1202 | "p256", 1203 | "p384", 1204 | "pem", 1205 | "rand 0.8.5", 1206 | "rsa", 1207 | "serde", 1208 | "serde_json", 1209 | "sha2", 1210 | "signature", 1211 | "simple_asn1", 1212 | ] 1213 | 1214 | [[package]] 1215 | name = "lazy_static" 1216 | version = "1.5.0" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1219 | dependencies = [ 1220 | "spin", 1221 | ] 1222 | 1223 | [[package]] 1224 | name = "libc" 1225 | version = "0.2.174" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 1228 | 1229 | [[package]] 1230 | name = "libm" 1231 | version = "0.2.15" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 1234 | 1235 | [[package]] 1236 | name = "linux-raw-sys" 1237 | version = "0.9.4" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 1240 | 1241 | [[package]] 1242 | name = "litemap" 1243 | version = "0.8.0" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 1246 | 1247 | [[package]] 1248 | name = "lock_api" 1249 | version = "0.4.13" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 1252 | dependencies = [ 1253 | "autocfg", 1254 | "scopeguard", 1255 | ] 1256 | 1257 | [[package]] 1258 | name = "log" 1259 | version = "0.4.27" 1260 | source = "registry+https://github.com/rust-lang/crates.io-index" 1261 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 1262 | 1263 | [[package]] 1264 | name = "lru-slab" 1265 | version = "0.1.2" 1266 | source = "registry+https://github.com/rust-lang/crates.io-index" 1267 | checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 1268 | 1269 | [[package]] 1270 | name = "matchers" 1271 | version = "0.1.0" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 1274 | dependencies = [ 1275 | "regex-automata 0.1.10", 1276 | ] 1277 | 1278 | [[package]] 1279 | name = "memchr" 1280 | version = "2.7.5" 1281 | source = "registry+https://github.com/rust-lang/crates.io-index" 1282 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 1283 | 1284 | [[package]] 1285 | name = "mime" 1286 | version = "0.3.17" 1287 | source = "registry+https://github.com/rust-lang/crates.io-index" 1288 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1289 | 1290 | [[package]] 1291 | name = "mime-infer" 1292 | version = "4.0.0" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "0d72eb6076a2d5a9c624f9282d5a4d3428303c7c6671067f8ef812d91a002710" 1295 | dependencies = [ 1296 | "mime", 1297 | "unicase", 1298 | ] 1299 | 1300 | [[package]] 1301 | name = "miniz_oxide" 1302 | version = "0.8.9" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 1305 | dependencies = [ 1306 | "adler2", 1307 | ] 1308 | 1309 | [[package]] 1310 | name = "mio" 1311 | version = "1.0.4" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 1314 | dependencies = [ 1315 | "libc", 1316 | "wasi 0.11.1+wasi-snapshot-preview1", 1317 | "windows-sys 0.59.0", 1318 | ] 1319 | 1320 | [[package]] 1321 | name = "multer" 1322 | version = "3.1.0" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" 1325 | dependencies = [ 1326 | "bytes", 1327 | "encoding_rs", 1328 | "futures-util", 1329 | "http", 1330 | "httparse", 1331 | "memchr", 1332 | "mime", 1333 | "spin", 1334 | "version_check", 1335 | ] 1336 | 1337 | [[package]] 1338 | name = "multimap" 1339 | version = "0.10.1" 1340 | source = "registry+https://github.com/rust-lang/crates.io-index" 1341 | checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" 1342 | dependencies = [ 1343 | "serde", 1344 | ] 1345 | 1346 | [[package]] 1347 | name = "nanoid" 1348 | version = "0.4.0" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" 1351 | dependencies = [ 1352 | "rand 0.8.5", 1353 | ] 1354 | 1355 | [[package]] 1356 | name = "nix" 1357 | version = "0.30.1" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 1360 | dependencies = [ 1361 | "bitflags", 1362 | "cfg-if", 1363 | "cfg_aliases", 1364 | "libc", 1365 | ] 1366 | 1367 | [[package]] 1368 | name = "notir" 1369 | version = "0.1.7" 1370 | dependencies = [ 1371 | "bytes", 1372 | "clap", 1373 | "dashmap", 1374 | "futures-util", 1375 | "nanoid", 1376 | "rust-embed", 1377 | "salvo", 1378 | "serde", 1379 | "serde_json", 1380 | "tokio", 1381 | "tokio-stream", 1382 | "tracing", 1383 | "tracing-subscriber", 1384 | ] 1385 | 1386 | [[package]] 1387 | name = "nu-ansi-term" 1388 | version = "0.46.0" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 1391 | dependencies = [ 1392 | "overload", 1393 | "winapi", 1394 | ] 1395 | 1396 | [[package]] 1397 | name = "num-bigint" 1398 | version = "0.4.6" 1399 | source = "registry+https://github.com/rust-lang/crates.io-index" 1400 | checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 1401 | dependencies = [ 1402 | "num-integer", 1403 | "num-traits", 1404 | ] 1405 | 1406 | [[package]] 1407 | name = "num-bigint-dig" 1408 | version = "0.8.6" 1409 | source = "registry+https://github.com/rust-lang/crates.io-index" 1410 | checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" 1411 | dependencies = [ 1412 | "lazy_static", 1413 | "libm", 1414 | "num-integer", 1415 | "num-iter", 1416 | "num-traits", 1417 | "rand 0.8.5", 1418 | "smallvec", 1419 | "zeroize", 1420 | ] 1421 | 1422 | [[package]] 1423 | name = "num-conv" 1424 | version = "0.1.0" 1425 | source = "registry+https://github.com/rust-lang/crates.io-index" 1426 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 1427 | 1428 | [[package]] 1429 | name = "num-integer" 1430 | version = "0.1.46" 1431 | source = "registry+https://github.com/rust-lang/crates.io-index" 1432 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 1433 | dependencies = [ 1434 | "num-traits", 1435 | ] 1436 | 1437 | [[package]] 1438 | name = "num-iter" 1439 | version = "0.1.45" 1440 | source = "registry+https://github.com/rust-lang/crates.io-index" 1441 | checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 1442 | dependencies = [ 1443 | "autocfg", 1444 | "num-integer", 1445 | "num-traits", 1446 | ] 1447 | 1448 | [[package]] 1449 | name = "num-traits" 1450 | version = "0.2.19" 1451 | source = "registry+https://github.com/rust-lang/crates.io-index" 1452 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1453 | dependencies = [ 1454 | "autocfg", 1455 | "libm", 1456 | ] 1457 | 1458 | [[package]] 1459 | name = "object" 1460 | version = "0.36.7" 1461 | source = "registry+https://github.com/rust-lang/crates.io-index" 1462 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1463 | dependencies = [ 1464 | "memchr", 1465 | ] 1466 | 1467 | [[package]] 1468 | name = "once_cell" 1469 | version = "1.21.3" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1472 | 1473 | [[package]] 1474 | name = "once_cell_polyfill" 1475 | version = "1.70.1" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 1478 | 1479 | [[package]] 1480 | name = "opaque-debug" 1481 | version = "0.3.1" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" 1484 | 1485 | [[package]] 1486 | name = "openssl-probe" 1487 | version = "0.1.6" 1488 | source = "registry+https://github.com/rust-lang/crates.io-index" 1489 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1490 | 1491 | [[package]] 1492 | name = "overload" 1493 | version = "0.1.1" 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" 1495 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1496 | 1497 | [[package]] 1498 | name = "p256" 1499 | version = "0.13.2" 1500 | source = "registry+https://github.com/rust-lang/crates.io-index" 1501 | checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" 1502 | dependencies = [ 1503 | "ecdsa", 1504 | "elliptic-curve", 1505 | "primeorder", 1506 | "sha2", 1507 | ] 1508 | 1509 | [[package]] 1510 | name = "p384" 1511 | version = "0.13.1" 1512 | source = "registry+https://github.com/rust-lang/crates.io-index" 1513 | checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" 1514 | dependencies = [ 1515 | "ecdsa", 1516 | "elliptic-curve", 1517 | "primeorder", 1518 | "sha2", 1519 | ] 1520 | 1521 | [[package]] 1522 | name = "parking_lot" 1523 | version = "0.12.4" 1524 | source = "registry+https://github.com/rust-lang/crates.io-index" 1525 | checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 1526 | dependencies = [ 1527 | "lock_api", 1528 | "parking_lot_core", 1529 | ] 1530 | 1531 | [[package]] 1532 | name = "parking_lot_core" 1533 | version = "0.9.11" 1534 | source = "registry+https://github.com/rust-lang/crates.io-index" 1535 | checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 1536 | dependencies = [ 1537 | "cfg-if", 1538 | "libc", 1539 | "redox_syscall", 1540 | "smallvec", 1541 | "windows-targets 0.52.6", 1542 | ] 1543 | 1544 | [[package]] 1545 | name = "path-slash" 1546 | version = "0.2.1" 1547 | source = "registry+https://github.com/rust-lang/crates.io-index" 1548 | checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" 1549 | 1550 | [[package]] 1551 | name = "pem" 1552 | version = "3.0.5" 1553 | source = "registry+https://github.com/rust-lang/crates.io-index" 1554 | checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" 1555 | dependencies = [ 1556 | "base64", 1557 | "serde", 1558 | ] 1559 | 1560 | [[package]] 1561 | name = "pem-rfc7468" 1562 | version = "0.7.0" 1563 | source = "registry+https://github.com/rust-lang/crates.io-index" 1564 | checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 1565 | dependencies = [ 1566 | "base64ct", 1567 | ] 1568 | 1569 | [[package]] 1570 | name = "percent-encoding" 1571 | version = "2.3.1" 1572 | source = "registry+https://github.com/rust-lang/crates.io-index" 1573 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1574 | 1575 | [[package]] 1576 | name = "pin-project" 1577 | version = "1.1.10" 1578 | source = "registry+https://github.com/rust-lang/crates.io-index" 1579 | checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" 1580 | dependencies = [ 1581 | "pin-project-internal", 1582 | ] 1583 | 1584 | [[package]] 1585 | name = "pin-project-internal" 1586 | version = "1.1.10" 1587 | source = "registry+https://github.com/rust-lang/crates.io-index" 1588 | checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" 1589 | dependencies = [ 1590 | "proc-macro2", 1591 | "quote", 1592 | "syn", 1593 | ] 1594 | 1595 | [[package]] 1596 | name = "pin-project-lite" 1597 | version = "0.2.16" 1598 | source = "registry+https://github.com/rust-lang/crates.io-index" 1599 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1600 | 1601 | [[package]] 1602 | name = "pin-utils" 1603 | version = "0.1.0" 1604 | source = "registry+https://github.com/rust-lang/crates.io-index" 1605 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1606 | 1607 | [[package]] 1608 | name = "pkcs1" 1609 | version = "0.7.5" 1610 | source = "registry+https://github.com/rust-lang/crates.io-index" 1611 | checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 1612 | dependencies = [ 1613 | "der", 1614 | "pkcs8", 1615 | "spki", 1616 | ] 1617 | 1618 | [[package]] 1619 | name = "pkcs8" 1620 | version = "0.10.2" 1621 | source = "registry+https://github.com/rust-lang/crates.io-index" 1622 | checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 1623 | dependencies = [ 1624 | "der", 1625 | "spki", 1626 | ] 1627 | 1628 | [[package]] 1629 | name = "pkg-config" 1630 | version = "0.3.32" 1631 | source = "registry+https://github.com/rust-lang/crates.io-index" 1632 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1633 | 1634 | [[package]] 1635 | name = "polyval" 1636 | version = "0.6.2" 1637 | source = "registry+https://github.com/rust-lang/crates.io-index" 1638 | checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" 1639 | dependencies = [ 1640 | "cfg-if", 1641 | "cpufeatures", 1642 | "opaque-debug", 1643 | "universal-hash", 1644 | ] 1645 | 1646 | [[package]] 1647 | name = "potential_utf" 1648 | version = "0.1.2" 1649 | source = "registry+https://github.com/rust-lang/crates.io-index" 1650 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 1651 | dependencies = [ 1652 | "zerovec", 1653 | ] 1654 | 1655 | [[package]] 1656 | name = "powerfmt" 1657 | version = "0.2.0" 1658 | source = "registry+https://github.com/rust-lang/crates.io-index" 1659 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1660 | 1661 | [[package]] 1662 | name = "ppv-lite86" 1663 | version = "0.2.21" 1664 | source = "registry+https://github.com/rust-lang/crates.io-index" 1665 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1666 | dependencies = [ 1667 | "zerocopy", 1668 | ] 1669 | 1670 | [[package]] 1671 | name = "primeorder" 1672 | version = "0.13.6" 1673 | source = "registry+https://github.com/rust-lang/crates.io-index" 1674 | checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" 1675 | dependencies = [ 1676 | "elliptic-curve", 1677 | ] 1678 | 1679 | [[package]] 1680 | name = "proc-macro-crate" 1681 | version = "3.3.0" 1682 | source = "registry+https://github.com/rust-lang/crates.io-index" 1683 | checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" 1684 | dependencies = [ 1685 | "toml_edit", 1686 | ] 1687 | 1688 | [[package]] 1689 | name = "proc-macro2" 1690 | version = "1.0.95" 1691 | source = "registry+https://github.com/rust-lang/crates.io-index" 1692 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 1693 | dependencies = [ 1694 | "unicode-ident", 1695 | ] 1696 | 1697 | [[package]] 1698 | name = "quinn" 1699 | version = "0.11.8" 1700 | source = "registry+https://github.com/rust-lang/crates.io-index" 1701 | checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" 1702 | dependencies = [ 1703 | "bytes", 1704 | "cfg_aliases", 1705 | "pin-project-lite", 1706 | "quinn-proto", 1707 | "quinn-udp", 1708 | "rustc-hash", 1709 | "rustls", 1710 | "socket2", 1711 | "thiserror 2.0.12", 1712 | "tokio", 1713 | "tracing", 1714 | "web-time", 1715 | ] 1716 | 1717 | [[package]] 1718 | name = "quinn-proto" 1719 | version = "0.11.12" 1720 | source = "registry+https://github.com/rust-lang/crates.io-index" 1721 | checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" 1722 | dependencies = [ 1723 | "bytes", 1724 | "getrandom 0.3.3", 1725 | "lru-slab", 1726 | "rand 0.9.1", 1727 | "ring", 1728 | "rustc-hash", 1729 | "rustls", 1730 | "rustls-pki-types", 1731 | "slab", 1732 | "thiserror 2.0.12", 1733 | "tinyvec", 1734 | "tracing", 1735 | "web-time", 1736 | ] 1737 | 1738 | [[package]] 1739 | name = "quinn-udp" 1740 | version = "0.5.13" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" 1743 | dependencies = [ 1744 | "cfg_aliases", 1745 | "libc", 1746 | "once_cell", 1747 | "socket2", 1748 | "tracing", 1749 | "windows-sys 0.59.0", 1750 | ] 1751 | 1752 | [[package]] 1753 | name = "quote" 1754 | version = "1.0.40" 1755 | source = "registry+https://github.com/rust-lang/crates.io-index" 1756 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1757 | dependencies = [ 1758 | "proc-macro2", 1759 | ] 1760 | 1761 | [[package]] 1762 | name = "r-efi" 1763 | version = "5.3.0" 1764 | source = "registry+https://github.com/rust-lang/crates.io-index" 1765 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 1766 | 1767 | [[package]] 1768 | name = "rand" 1769 | version = "0.8.5" 1770 | source = "registry+https://github.com/rust-lang/crates.io-index" 1771 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1772 | dependencies = [ 1773 | "libc", 1774 | "rand_chacha 0.3.1", 1775 | "rand_core 0.6.4", 1776 | ] 1777 | 1778 | [[package]] 1779 | name = "rand" 1780 | version = "0.9.1" 1781 | source = "registry+https://github.com/rust-lang/crates.io-index" 1782 | checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 1783 | dependencies = [ 1784 | "rand_chacha 0.9.0", 1785 | "rand_core 0.9.3", 1786 | ] 1787 | 1788 | [[package]] 1789 | name = "rand_chacha" 1790 | version = "0.3.1" 1791 | source = "registry+https://github.com/rust-lang/crates.io-index" 1792 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1793 | dependencies = [ 1794 | "ppv-lite86", 1795 | "rand_core 0.6.4", 1796 | ] 1797 | 1798 | [[package]] 1799 | name = "rand_chacha" 1800 | version = "0.9.0" 1801 | source = "registry+https://github.com/rust-lang/crates.io-index" 1802 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1803 | dependencies = [ 1804 | "ppv-lite86", 1805 | "rand_core 0.9.3", 1806 | ] 1807 | 1808 | [[package]] 1809 | name = "rand_core" 1810 | version = "0.6.4" 1811 | source = "registry+https://github.com/rust-lang/crates.io-index" 1812 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1813 | dependencies = [ 1814 | "getrandom 0.2.16", 1815 | ] 1816 | 1817 | [[package]] 1818 | name = "rand_core" 1819 | version = "0.9.3" 1820 | source = "registry+https://github.com/rust-lang/crates.io-index" 1821 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1822 | dependencies = [ 1823 | "getrandom 0.3.3", 1824 | ] 1825 | 1826 | [[package]] 1827 | name = "redox_syscall" 1828 | version = "0.5.13" 1829 | source = "registry+https://github.com/rust-lang/crates.io-index" 1830 | checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" 1831 | dependencies = [ 1832 | "bitflags", 1833 | ] 1834 | 1835 | [[package]] 1836 | name = "regex" 1837 | version = "1.11.1" 1838 | source = "registry+https://github.com/rust-lang/crates.io-index" 1839 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1840 | dependencies = [ 1841 | "aho-corasick", 1842 | "memchr", 1843 | "regex-automata 0.4.9", 1844 | "regex-syntax 0.8.5", 1845 | ] 1846 | 1847 | [[package]] 1848 | name = "regex-automata" 1849 | version = "0.1.10" 1850 | source = "registry+https://github.com/rust-lang/crates.io-index" 1851 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1852 | dependencies = [ 1853 | "regex-syntax 0.6.29", 1854 | ] 1855 | 1856 | [[package]] 1857 | name = "regex-automata" 1858 | version = "0.4.9" 1859 | source = "registry+https://github.com/rust-lang/crates.io-index" 1860 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1861 | dependencies = [ 1862 | "aho-corasick", 1863 | "memchr", 1864 | "regex-syntax 0.8.5", 1865 | ] 1866 | 1867 | [[package]] 1868 | name = "regex-syntax" 1869 | version = "0.6.29" 1870 | source = "registry+https://github.com/rust-lang/crates.io-index" 1871 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1872 | 1873 | [[package]] 1874 | name = "regex-syntax" 1875 | version = "0.8.5" 1876 | source = "registry+https://github.com/rust-lang/crates.io-index" 1877 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1878 | 1879 | [[package]] 1880 | name = "reqwest" 1881 | version = "0.12.22" 1882 | source = "registry+https://github.com/rust-lang/crates.io-index" 1883 | checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" 1884 | dependencies = [ 1885 | "base64", 1886 | "bytes", 1887 | "encoding_rs", 1888 | "futures-core", 1889 | "futures-util", 1890 | "h2", 1891 | "http", 1892 | "http-body", 1893 | "http-body-util", 1894 | "hyper", 1895 | "hyper-rustls", 1896 | "hyper-util", 1897 | "js-sys", 1898 | "log", 1899 | "mime", 1900 | "percent-encoding", 1901 | "pin-project-lite", 1902 | "quinn", 1903 | "rustls", 1904 | "rustls-pki-types", 1905 | "serde", 1906 | "serde_json", 1907 | "serde_urlencoded", 1908 | "sync_wrapper", 1909 | "tokio", 1910 | "tokio-rustls", 1911 | "tokio-util", 1912 | "tower", 1913 | "tower-http", 1914 | "tower-service", 1915 | "url", 1916 | "wasm-bindgen", 1917 | "wasm-bindgen-futures", 1918 | "wasm-streams", 1919 | "web-sys", 1920 | "webpki-roots", 1921 | ] 1922 | 1923 | [[package]] 1924 | name = "rfc6979" 1925 | version = "0.4.0" 1926 | source = "registry+https://github.com/rust-lang/crates.io-index" 1927 | checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 1928 | dependencies = [ 1929 | "hmac", 1930 | "subtle", 1931 | ] 1932 | 1933 | [[package]] 1934 | name = "ring" 1935 | version = "0.17.14" 1936 | source = "registry+https://github.com/rust-lang/crates.io-index" 1937 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1938 | dependencies = [ 1939 | "cc", 1940 | "cfg-if", 1941 | "getrandom 0.2.16", 1942 | "libc", 1943 | "untrusted", 1944 | "windows-sys 0.52.0", 1945 | ] 1946 | 1947 | [[package]] 1948 | name = "rsa" 1949 | version = "0.9.9" 1950 | source = "registry+https://github.com/rust-lang/crates.io-index" 1951 | checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" 1952 | dependencies = [ 1953 | "const-oid", 1954 | "digest", 1955 | "num-bigint-dig", 1956 | "num-integer", 1957 | "num-traits", 1958 | "pkcs1", 1959 | "pkcs8", 1960 | "rand_core 0.6.4", 1961 | "signature", 1962 | "spki", 1963 | "subtle", 1964 | "zeroize", 1965 | ] 1966 | 1967 | [[package]] 1968 | name = "rust-embed" 1969 | version = "8.7.2" 1970 | source = "registry+https://github.com/rust-lang/crates.io-index" 1971 | checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" 1972 | dependencies = [ 1973 | "rust-embed-impl", 1974 | "rust-embed-utils", 1975 | "walkdir", 1976 | ] 1977 | 1978 | [[package]] 1979 | name = "rust-embed-impl" 1980 | version = "8.7.2" 1981 | source = "registry+https://github.com/rust-lang/crates.io-index" 1982 | checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" 1983 | dependencies = [ 1984 | "proc-macro2", 1985 | "quote", 1986 | "rust-embed-utils", 1987 | "syn", 1988 | "walkdir", 1989 | ] 1990 | 1991 | [[package]] 1992 | name = "rust-embed-utils" 1993 | version = "8.7.2" 1994 | source = "registry+https://github.com/rust-lang/crates.io-index" 1995 | checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" 1996 | dependencies = [ 1997 | "sha2", 1998 | "walkdir", 1999 | ] 2000 | 2001 | [[package]] 2002 | name = "rustc-demangle" 2003 | version = "0.1.25" 2004 | source = "registry+https://github.com/rust-lang/crates.io-index" 2005 | checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" 2006 | 2007 | [[package]] 2008 | name = "rustc-hash" 2009 | version = "2.1.1" 2010 | source = "registry+https://github.com/rust-lang/crates.io-index" 2011 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 2012 | 2013 | [[package]] 2014 | name = "rustc_version" 2015 | version = "0.4.1" 2016 | source = "registry+https://github.com/rust-lang/crates.io-index" 2017 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 2018 | dependencies = [ 2019 | "semver", 2020 | ] 2021 | 2022 | [[package]] 2023 | name = "rustix" 2024 | version = "1.0.7" 2025 | source = "registry+https://github.com/rust-lang/crates.io-index" 2026 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 2027 | dependencies = [ 2028 | "bitflags", 2029 | "errno", 2030 | "libc", 2031 | "linux-raw-sys", 2032 | "windows-sys 0.59.0", 2033 | ] 2034 | 2035 | [[package]] 2036 | name = "rustls" 2037 | version = "0.23.28" 2038 | source = "registry+https://github.com/rust-lang/crates.io-index" 2039 | checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" 2040 | dependencies = [ 2041 | "log", 2042 | "once_cell", 2043 | "ring", 2044 | "rustls-pki-types", 2045 | "rustls-webpki", 2046 | "subtle", 2047 | "zeroize", 2048 | ] 2049 | 2050 | [[package]] 2051 | name = "rustls-native-certs" 2052 | version = "0.8.1" 2053 | source = "registry+https://github.com/rust-lang/crates.io-index" 2054 | checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" 2055 | dependencies = [ 2056 | "openssl-probe", 2057 | "rustls-pki-types", 2058 | "schannel", 2059 | "security-framework", 2060 | ] 2061 | 2062 | [[package]] 2063 | name = "rustls-pemfile" 2064 | version = "2.2.0" 2065 | source = "registry+https://github.com/rust-lang/crates.io-index" 2066 | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 2067 | dependencies = [ 2068 | "rustls-pki-types", 2069 | ] 2070 | 2071 | [[package]] 2072 | name = "rustls-pki-types" 2073 | version = "1.12.0" 2074 | source = "registry+https://github.com/rust-lang/crates.io-index" 2075 | checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 2076 | dependencies = [ 2077 | "web-time", 2078 | "zeroize", 2079 | ] 2080 | 2081 | [[package]] 2082 | name = "rustls-webpki" 2083 | version = "0.103.3" 2084 | source = "registry+https://github.com/rust-lang/crates.io-index" 2085 | checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" 2086 | dependencies = [ 2087 | "ring", 2088 | "rustls-pki-types", 2089 | "untrusted", 2090 | ] 2091 | 2092 | [[package]] 2093 | name = "rustversion" 2094 | version = "1.0.21" 2095 | source = "registry+https://github.com/rust-lang/crates.io-index" 2096 | checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 2097 | 2098 | [[package]] 2099 | name = "ryu" 2100 | version = "1.0.20" 2101 | source = "registry+https://github.com/rust-lang/crates.io-index" 2102 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 2103 | 2104 | [[package]] 2105 | name = "salvo" 2106 | version = "0.84.2" 2107 | source = "registry+https://github.com/rust-lang/crates.io-index" 2108 | checksum = "44fde3137cad708fa63089a3fe789aa4b864c88608d77067547355c78b5bef5b" 2109 | dependencies = [ 2110 | "salvo-compression", 2111 | "salvo-jwt-auth", 2112 | "salvo-proxy", 2113 | "salvo-serve-static", 2114 | "salvo_core", 2115 | "salvo_extra", 2116 | ] 2117 | 2118 | [[package]] 2119 | name = "salvo-compression" 2120 | version = "0.84.2" 2121 | source = "registry+https://github.com/rust-lang/crates.io-index" 2122 | checksum = "79d3d65aeddfce63bbcf137c3982374bf147070613abeba309200595918bbdec" 2123 | dependencies = [ 2124 | "brotli", 2125 | "bytes", 2126 | "flate2", 2127 | "futures-util", 2128 | "indexmap", 2129 | "salvo_core", 2130 | "tokio", 2131 | "tokio-util", 2132 | "tracing", 2133 | "zstd", 2134 | ] 2135 | 2136 | [[package]] 2137 | name = "salvo-jwt-auth" 2138 | version = "0.84.2" 2139 | source = "registry+https://github.com/rust-lang/crates.io-index" 2140 | checksum = "a51330d67b4898e8504d5603e60ab1048046c665b2712ae2cbf8a4630b875e7a" 2141 | dependencies = [ 2142 | "base64", 2143 | "bytes", 2144 | "http-body-util", 2145 | "hyper-rustls", 2146 | "hyper-util", 2147 | "jsonwebtoken", 2148 | "salvo_core", 2149 | "serde", 2150 | "serde_json", 2151 | "thiserror 2.0.12", 2152 | "tokio", 2153 | "tracing", 2154 | ] 2155 | 2156 | [[package]] 2157 | name = "salvo-proxy" 2158 | version = "0.84.2" 2159 | source = "registry+https://github.com/rust-lang/crates.io-index" 2160 | checksum = "d453a98c7c4edf67ae4055f7853b6cbb6cf885c8994f0c00e7b91830fda5da93" 2161 | dependencies = [ 2162 | "fastrand", 2163 | "futures-util", 2164 | "hyper", 2165 | "hyper-rustls", 2166 | "hyper-util", 2167 | "percent-encoding", 2168 | "reqwest", 2169 | "salvo_core", 2170 | "tokio", 2171 | "tracing", 2172 | ] 2173 | 2174 | [[package]] 2175 | name = "salvo-serde-util" 2176 | version = "0.84.2" 2177 | source = "registry+https://github.com/rust-lang/crates.io-index" 2178 | checksum = "aacd87fb1e706eab8e24692b6b45aa9851ab53eec53b3cd4750f7b9577923e43" 2179 | dependencies = [ 2180 | "proc-macro2", 2181 | "quote", 2182 | "syn", 2183 | ] 2184 | 2185 | [[package]] 2186 | name = "salvo-serve-static" 2187 | version = "0.84.2" 2188 | source = "registry+https://github.com/rust-lang/crates.io-index" 2189 | checksum = "faca6f3a87a86fc208f2c4c524e5e494f33ee4343f6c03bc1818cc66b3d7ebd7" 2190 | dependencies = [ 2191 | "hex", 2192 | "mime", 2193 | "mime-infer", 2194 | "path-slash", 2195 | "percent-encoding", 2196 | "rust-embed", 2197 | "salvo_core", 2198 | "serde", 2199 | "serde_json", 2200 | "time", 2201 | "tokio", 2202 | "tracing", 2203 | ] 2204 | 2205 | [[package]] 2206 | name = "salvo_core" 2207 | version = "0.84.2" 2208 | source = "registry+https://github.com/rust-lang/crates.io-index" 2209 | checksum = "792d0f9aad788fcb8c7057c1d390ec26385db04a4a68de9ec802f5a7a8f83d99" 2210 | dependencies = [ 2211 | "async-trait", 2212 | "base64", 2213 | "brotli", 2214 | "bytes", 2215 | "chardetng", 2216 | "content_inspector", 2217 | "cookie", 2218 | "encoding_rs", 2219 | "enumflags2", 2220 | "flate2", 2221 | "form_urlencoded", 2222 | "futures-channel", 2223 | "futures-util", 2224 | "headers", 2225 | "http", 2226 | "http-body-util", 2227 | "hyper", 2228 | "hyper-rustls", 2229 | "hyper-util", 2230 | "indexmap", 2231 | "mime", 2232 | "mime-infer", 2233 | "multer", 2234 | "multimap", 2235 | "nix", 2236 | "parking_lot", 2237 | "percent-encoding", 2238 | "pin-project", 2239 | "rand 0.9.1", 2240 | "regex", 2241 | "rustls-pemfile", 2242 | "salvo_macros", 2243 | "serde", 2244 | "serde-xml-rs", 2245 | "serde_json", 2246 | "serde_urlencoded", 2247 | "sync_wrapper", 2248 | "tempfile", 2249 | "thiserror 2.0.12", 2250 | "tokio", 2251 | "tokio-rustls", 2252 | "tokio-util", 2253 | "tracing", 2254 | "url", 2255 | "zstd", 2256 | ] 2257 | 2258 | [[package]] 2259 | name = "salvo_extra" 2260 | version = "0.84.2" 2261 | source = "registry+https://github.com/rust-lang/crates.io-index" 2262 | checksum = "7b80ea9c6c391fd892218974d8a456397d3c1feaef4a35ae3bd3923042539d8a" 2263 | dependencies = [ 2264 | "base64", 2265 | "etag", 2266 | "futures-util", 2267 | "http-body-util", 2268 | "hyper", 2269 | "pin-project", 2270 | "salvo_core", 2271 | "serde", 2272 | "serde_json", 2273 | "tokio", 2274 | "tokio-tungstenite", 2275 | "tower", 2276 | "tracing", 2277 | "ulid", 2278 | ] 2279 | 2280 | [[package]] 2281 | name = "salvo_macros" 2282 | version = "0.84.2" 2283 | source = "registry+https://github.com/rust-lang/crates.io-index" 2284 | checksum = "40591758c26d9143043b1ae4640e936dcdb3c7c16f787a1c46c12af1643d7c45" 2285 | dependencies = [ 2286 | "proc-macro-crate", 2287 | "proc-macro2", 2288 | "quote", 2289 | "regex", 2290 | "salvo-serde-util", 2291 | "syn", 2292 | ] 2293 | 2294 | [[package]] 2295 | name = "same-file" 2296 | version = "1.0.6" 2297 | source = "registry+https://github.com/rust-lang/crates.io-index" 2298 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 2299 | dependencies = [ 2300 | "winapi-util", 2301 | ] 2302 | 2303 | [[package]] 2304 | name = "schannel" 2305 | version = "0.1.27" 2306 | source = "registry+https://github.com/rust-lang/crates.io-index" 2307 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 2308 | dependencies = [ 2309 | "windows-sys 0.59.0", 2310 | ] 2311 | 2312 | [[package]] 2313 | name = "scopeguard" 2314 | version = "1.2.0" 2315 | source = "registry+https://github.com/rust-lang/crates.io-index" 2316 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 2317 | 2318 | [[package]] 2319 | name = "sec1" 2320 | version = "0.7.3" 2321 | source = "registry+https://github.com/rust-lang/crates.io-index" 2322 | checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 2323 | dependencies = [ 2324 | "base16ct", 2325 | "der", 2326 | "generic-array", 2327 | "pkcs8", 2328 | "subtle", 2329 | "zeroize", 2330 | ] 2331 | 2332 | [[package]] 2333 | name = "security-framework" 2334 | version = "3.2.0" 2335 | source = "registry+https://github.com/rust-lang/crates.io-index" 2336 | checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" 2337 | dependencies = [ 2338 | "bitflags", 2339 | "core-foundation 0.10.1", 2340 | "core-foundation-sys", 2341 | "libc", 2342 | "security-framework-sys", 2343 | ] 2344 | 2345 | [[package]] 2346 | name = "security-framework-sys" 2347 | version = "2.14.0" 2348 | source = "registry+https://github.com/rust-lang/crates.io-index" 2349 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 2350 | dependencies = [ 2351 | "core-foundation-sys", 2352 | "libc", 2353 | ] 2354 | 2355 | [[package]] 2356 | name = "semver" 2357 | version = "1.0.27" 2358 | source = "registry+https://github.com/rust-lang/crates.io-index" 2359 | checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 2360 | 2361 | [[package]] 2362 | name = "serde" 2363 | version = "1.0.219" 2364 | source = "registry+https://github.com/rust-lang/crates.io-index" 2365 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 2366 | dependencies = [ 2367 | "serde_derive", 2368 | ] 2369 | 2370 | [[package]] 2371 | name = "serde-xml-rs" 2372 | version = "0.8.1" 2373 | source = "registry+https://github.com/rust-lang/crates.io-index" 2374 | checksum = "53630160a98edebde0123eb4dfd0fce6adff091b2305db3154a9e920206eb510" 2375 | dependencies = [ 2376 | "log", 2377 | "serde", 2378 | "thiserror 1.0.69", 2379 | "xml-rs", 2380 | ] 2381 | 2382 | [[package]] 2383 | name = "serde_derive" 2384 | version = "1.0.219" 2385 | source = "registry+https://github.com/rust-lang/crates.io-index" 2386 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 2387 | dependencies = [ 2388 | "proc-macro2", 2389 | "quote", 2390 | "syn", 2391 | ] 2392 | 2393 | [[package]] 2394 | name = "serde_json" 2395 | version = "1.0.140" 2396 | source = "registry+https://github.com/rust-lang/crates.io-index" 2397 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 2398 | dependencies = [ 2399 | "itoa", 2400 | "memchr", 2401 | "ryu", 2402 | "serde", 2403 | ] 2404 | 2405 | [[package]] 2406 | name = "serde_urlencoded" 2407 | version = "0.7.1" 2408 | source = "registry+https://github.com/rust-lang/crates.io-index" 2409 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 2410 | dependencies = [ 2411 | "form_urlencoded", 2412 | "itoa", 2413 | "ryu", 2414 | "serde", 2415 | ] 2416 | 2417 | [[package]] 2418 | name = "sha1" 2419 | version = "0.10.6" 2420 | source = "registry+https://github.com/rust-lang/crates.io-index" 2421 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 2422 | dependencies = [ 2423 | "cfg-if", 2424 | "cpufeatures", 2425 | "digest", 2426 | ] 2427 | 2428 | [[package]] 2429 | name = "sha2" 2430 | version = "0.10.9" 2431 | source = "registry+https://github.com/rust-lang/crates.io-index" 2432 | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 2433 | dependencies = [ 2434 | "cfg-if", 2435 | "cpufeatures", 2436 | "digest", 2437 | ] 2438 | 2439 | [[package]] 2440 | name = "sharded-slab" 2441 | version = "0.1.7" 2442 | source = "registry+https://github.com/rust-lang/crates.io-index" 2443 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 2444 | dependencies = [ 2445 | "lazy_static", 2446 | ] 2447 | 2448 | [[package]] 2449 | name = "shlex" 2450 | version = "1.3.0" 2451 | source = "registry+https://github.com/rust-lang/crates.io-index" 2452 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 2453 | 2454 | [[package]] 2455 | name = "signature" 2456 | version = "2.2.0" 2457 | source = "registry+https://github.com/rust-lang/crates.io-index" 2458 | checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 2459 | dependencies = [ 2460 | "digest", 2461 | "rand_core 0.6.4", 2462 | ] 2463 | 2464 | [[package]] 2465 | name = "simple_asn1" 2466 | version = "0.6.3" 2467 | source = "registry+https://github.com/rust-lang/crates.io-index" 2468 | checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" 2469 | dependencies = [ 2470 | "num-bigint", 2471 | "num-traits", 2472 | "thiserror 2.0.12", 2473 | "time", 2474 | ] 2475 | 2476 | [[package]] 2477 | name = "slab" 2478 | version = "0.4.10" 2479 | source = "registry+https://github.com/rust-lang/crates.io-index" 2480 | checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" 2481 | 2482 | [[package]] 2483 | name = "smallvec" 2484 | version = "1.15.1" 2485 | source = "registry+https://github.com/rust-lang/crates.io-index" 2486 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 2487 | 2488 | [[package]] 2489 | name = "socket2" 2490 | version = "0.5.10" 2491 | source = "registry+https://github.com/rust-lang/crates.io-index" 2492 | checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 2493 | dependencies = [ 2494 | "libc", 2495 | "windows-sys 0.52.0", 2496 | ] 2497 | 2498 | [[package]] 2499 | name = "spin" 2500 | version = "0.9.8" 2501 | source = "registry+https://github.com/rust-lang/crates.io-index" 2502 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 2503 | 2504 | [[package]] 2505 | name = "spki" 2506 | version = "0.7.3" 2507 | source = "registry+https://github.com/rust-lang/crates.io-index" 2508 | checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 2509 | dependencies = [ 2510 | "base64ct", 2511 | "der", 2512 | ] 2513 | 2514 | [[package]] 2515 | name = "stable_deref_trait" 2516 | version = "1.2.0" 2517 | source = "registry+https://github.com/rust-lang/crates.io-index" 2518 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 2519 | 2520 | [[package]] 2521 | name = "str-buf" 2522 | version = "3.0.3" 2523 | source = "registry+https://github.com/rust-lang/crates.io-index" 2524 | checksum = "0ceb97b7225c713c2fd4db0153cb6b3cab244eb37900c3f634ed4d43310d8c34" 2525 | 2526 | [[package]] 2527 | name = "strsim" 2528 | version = "0.11.1" 2529 | source = "registry+https://github.com/rust-lang/crates.io-index" 2530 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 2531 | 2532 | [[package]] 2533 | name = "subtle" 2534 | version = "2.6.1" 2535 | source = "registry+https://github.com/rust-lang/crates.io-index" 2536 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 2537 | 2538 | [[package]] 2539 | name = "syn" 2540 | version = "2.0.104" 2541 | source = "registry+https://github.com/rust-lang/crates.io-index" 2542 | checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 2543 | dependencies = [ 2544 | "proc-macro2", 2545 | "quote", 2546 | "unicode-ident", 2547 | ] 2548 | 2549 | [[package]] 2550 | name = "sync_wrapper" 2551 | version = "1.0.2" 2552 | source = "registry+https://github.com/rust-lang/crates.io-index" 2553 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 2554 | dependencies = [ 2555 | "futures-core", 2556 | ] 2557 | 2558 | [[package]] 2559 | name = "synstructure" 2560 | version = "0.13.2" 2561 | source = "registry+https://github.com/rust-lang/crates.io-index" 2562 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 2563 | dependencies = [ 2564 | "proc-macro2", 2565 | "quote", 2566 | "syn", 2567 | ] 2568 | 2569 | [[package]] 2570 | name = "system-configuration" 2571 | version = "0.6.1" 2572 | source = "registry+https://github.com/rust-lang/crates.io-index" 2573 | checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2574 | dependencies = [ 2575 | "bitflags", 2576 | "core-foundation 0.9.4", 2577 | "system-configuration-sys", 2578 | ] 2579 | 2580 | [[package]] 2581 | name = "system-configuration-sys" 2582 | version = "0.6.0" 2583 | source = "registry+https://github.com/rust-lang/crates.io-index" 2584 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 2585 | dependencies = [ 2586 | "core-foundation-sys", 2587 | "libc", 2588 | ] 2589 | 2590 | [[package]] 2591 | name = "tempfile" 2592 | version = "3.20.0" 2593 | source = "registry+https://github.com/rust-lang/crates.io-index" 2594 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 2595 | dependencies = [ 2596 | "fastrand", 2597 | "getrandom 0.3.3", 2598 | "once_cell", 2599 | "rustix", 2600 | "windows-sys 0.59.0", 2601 | ] 2602 | 2603 | [[package]] 2604 | name = "thiserror" 2605 | version = "1.0.69" 2606 | source = "registry+https://github.com/rust-lang/crates.io-index" 2607 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 2608 | dependencies = [ 2609 | "thiserror-impl 1.0.69", 2610 | ] 2611 | 2612 | [[package]] 2613 | name = "thiserror" 2614 | version = "2.0.12" 2615 | source = "registry+https://github.com/rust-lang/crates.io-index" 2616 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 2617 | dependencies = [ 2618 | "thiserror-impl 2.0.12", 2619 | ] 2620 | 2621 | [[package]] 2622 | name = "thiserror-impl" 2623 | version = "1.0.69" 2624 | source = "registry+https://github.com/rust-lang/crates.io-index" 2625 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 2626 | dependencies = [ 2627 | "proc-macro2", 2628 | "quote", 2629 | "syn", 2630 | ] 2631 | 2632 | [[package]] 2633 | name = "thiserror-impl" 2634 | version = "2.0.12" 2635 | source = "registry+https://github.com/rust-lang/crates.io-index" 2636 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 2637 | dependencies = [ 2638 | "proc-macro2", 2639 | "quote", 2640 | "syn", 2641 | ] 2642 | 2643 | [[package]] 2644 | name = "thread_local" 2645 | version = "1.1.9" 2646 | source = "registry+https://github.com/rust-lang/crates.io-index" 2647 | checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 2648 | dependencies = [ 2649 | "cfg-if", 2650 | ] 2651 | 2652 | [[package]] 2653 | name = "time" 2654 | version = "0.3.41" 2655 | source = "registry+https://github.com/rust-lang/crates.io-index" 2656 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 2657 | dependencies = [ 2658 | "deranged", 2659 | "itoa", 2660 | "num-conv", 2661 | "powerfmt", 2662 | "serde", 2663 | "time-core", 2664 | "time-macros", 2665 | ] 2666 | 2667 | [[package]] 2668 | name = "time-core" 2669 | version = "0.1.4" 2670 | source = "registry+https://github.com/rust-lang/crates.io-index" 2671 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 2672 | 2673 | [[package]] 2674 | name = "time-macros" 2675 | version = "0.2.22" 2676 | source = "registry+https://github.com/rust-lang/crates.io-index" 2677 | checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 2678 | dependencies = [ 2679 | "num-conv", 2680 | "time-core", 2681 | ] 2682 | 2683 | [[package]] 2684 | name = "tinystr" 2685 | version = "0.8.1" 2686 | source = "registry+https://github.com/rust-lang/crates.io-index" 2687 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 2688 | dependencies = [ 2689 | "displaydoc", 2690 | "zerovec", 2691 | ] 2692 | 2693 | [[package]] 2694 | name = "tinyvec" 2695 | version = "1.9.0" 2696 | source = "registry+https://github.com/rust-lang/crates.io-index" 2697 | checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 2698 | dependencies = [ 2699 | "tinyvec_macros", 2700 | ] 2701 | 2702 | [[package]] 2703 | name = "tinyvec_macros" 2704 | version = "0.1.1" 2705 | source = "registry+https://github.com/rust-lang/crates.io-index" 2706 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 2707 | 2708 | [[package]] 2709 | name = "tokio" 2710 | version = "1.46.1" 2711 | source = "registry+https://github.com/rust-lang/crates.io-index" 2712 | checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" 2713 | dependencies = [ 2714 | "backtrace", 2715 | "bytes", 2716 | "io-uring", 2717 | "libc", 2718 | "mio", 2719 | "pin-project-lite", 2720 | "slab", 2721 | "socket2", 2722 | "tokio-macros", 2723 | "windows-sys 0.52.0", 2724 | ] 2725 | 2726 | [[package]] 2727 | name = "tokio-macros" 2728 | version = "2.5.0" 2729 | source = "registry+https://github.com/rust-lang/crates.io-index" 2730 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 2731 | dependencies = [ 2732 | "proc-macro2", 2733 | "quote", 2734 | "syn", 2735 | ] 2736 | 2737 | [[package]] 2738 | name = "tokio-rustls" 2739 | version = "0.26.2" 2740 | source = "registry+https://github.com/rust-lang/crates.io-index" 2741 | checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 2742 | dependencies = [ 2743 | "rustls", 2744 | "tokio", 2745 | ] 2746 | 2747 | [[package]] 2748 | name = "tokio-stream" 2749 | version = "0.1.17" 2750 | source = "registry+https://github.com/rust-lang/crates.io-index" 2751 | checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 2752 | dependencies = [ 2753 | "futures-core", 2754 | "pin-project-lite", 2755 | "tokio", 2756 | ] 2757 | 2758 | [[package]] 2759 | name = "tokio-tungstenite" 2760 | version = "0.28.0" 2761 | source = "registry+https://github.com/rust-lang/crates.io-index" 2762 | checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" 2763 | dependencies = [ 2764 | "futures-util", 2765 | "log", 2766 | "tokio", 2767 | "tungstenite", 2768 | ] 2769 | 2770 | [[package]] 2771 | name = "tokio-util" 2772 | version = "0.7.15" 2773 | source = "registry+https://github.com/rust-lang/crates.io-index" 2774 | checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" 2775 | dependencies = [ 2776 | "bytes", 2777 | "futures-core", 2778 | "futures-sink", 2779 | "pin-project-lite", 2780 | "tokio", 2781 | ] 2782 | 2783 | [[package]] 2784 | name = "toml_datetime" 2785 | version = "0.6.11" 2786 | source = "registry+https://github.com/rust-lang/crates.io-index" 2787 | checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 2788 | 2789 | [[package]] 2790 | name = "toml_edit" 2791 | version = "0.22.27" 2792 | source = "registry+https://github.com/rust-lang/crates.io-index" 2793 | checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 2794 | dependencies = [ 2795 | "indexmap", 2796 | "toml_datetime", 2797 | "winnow", 2798 | ] 2799 | 2800 | [[package]] 2801 | name = "tower" 2802 | version = "0.5.2" 2803 | source = "registry+https://github.com/rust-lang/crates.io-index" 2804 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 2805 | dependencies = [ 2806 | "futures-core", 2807 | "futures-util", 2808 | "pin-project-lite", 2809 | "sync_wrapper", 2810 | "tokio", 2811 | "tokio-util", 2812 | "tower-layer", 2813 | "tower-service", 2814 | "tracing", 2815 | ] 2816 | 2817 | [[package]] 2818 | name = "tower-http" 2819 | version = "0.6.6" 2820 | source = "registry+https://github.com/rust-lang/crates.io-index" 2821 | checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 2822 | dependencies = [ 2823 | "bitflags", 2824 | "bytes", 2825 | "futures-util", 2826 | "http", 2827 | "http-body", 2828 | "iri-string", 2829 | "pin-project-lite", 2830 | "tower", 2831 | "tower-layer", 2832 | "tower-service", 2833 | ] 2834 | 2835 | [[package]] 2836 | name = "tower-layer" 2837 | version = "0.3.3" 2838 | source = "registry+https://github.com/rust-lang/crates.io-index" 2839 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 2840 | 2841 | [[package]] 2842 | name = "tower-service" 2843 | version = "0.3.3" 2844 | source = "registry+https://github.com/rust-lang/crates.io-index" 2845 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 2846 | 2847 | [[package]] 2848 | name = "tracing" 2849 | version = "0.1.41" 2850 | source = "registry+https://github.com/rust-lang/crates.io-index" 2851 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 2852 | dependencies = [ 2853 | "pin-project-lite", 2854 | "tracing-attributes", 2855 | "tracing-core", 2856 | ] 2857 | 2858 | [[package]] 2859 | name = "tracing-attributes" 2860 | version = "0.1.30" 2861 | source = "registry+https://github.com/rust-lang/crates.io-index" 2862 | checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" 2863 | dependencies = [ 2864 | "proc-macro2", 2865 | "quote", 2866 | "syn", 2867 | ] 2868 | 2869 | [[package]] 2870 | name = "tracing-core" 2871 | version = "0.1.34" 2872 | source = "registry+https://github.com/rust-lang/crates.io-index" 2873 | checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 2874 | dependencies = [ 2875 | "once_cell", 2876 | "valuable", 2877 | ] 2878 | 2879 | [[package]] 2880 | name = "tracing-log" 2881 | version = "0.2.0" 2882 | source = "registry+https://github.com/rust-lang/crates.io-index" 2883 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 2884 | dependencies = [ 2885 | "log", 2886 | "once_cell", 2887 | "tracing-core", 2888 | ] 2889 | 2890 | [[package]] 2891 | name = "tracing-subscriber" 2892 | version = "0.3.19" 2893 | source = "registry+https://github.com/rust-lang/crates.io-index" 2894 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 2895 | dependencies = [ 2896 | "matchers", 2897 | "nu-ansi-term", 2898 | "once_cell", 2899 | "regex", 2900 | "sharded-slab", 2901 | "smallvec", 2902 | "thread_local", 2903 | "tracing", 2904 | "tracing-core", 2905 | "tracing-log", 2906 | ] 2907 | 2908 | [[package]] 2909 | name = "try-lock" 2910 | version = "0.2.5" 2911 | source = "registry+https://github.com/rust-lang/crates.io-index" 2912 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2913 | 2914 | [[package]] 2915 | name = "tungstenite" 2916 | version = "0.28.0" 2917 | source = "registry+https://github.com/rust-lang/crates.io-index" 2918 | checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" 2919 | dependencies = [ 2920 | "bytes", 2921 | "log", 2922 | "rand 0.9.1", 2923 | "thiserror 2.0.12", 2924 | "utf-8", 2925 | ] 2926 | 2927 | [[package]] 2928 | name = "typenum" 2929 | version = "1.18.0" 2930 | source = "registry+https://github.com/rust-lang/crates.io-index" 2931 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 2932 | 2933 | [[package]] 2934 | name = "ulid" 2935 | version = "1.2.1" 2936 | source = "registry+https://github.com/rust-lang/crates.io-index" 2937 | checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" 2938 | dependencies = [ 2939 | "rand 0.9.1", 2940 | "web-time", 2941 | ] 2942 | 2943 | [[package]] 2944 | name = "unicase" 2945 | version = "2.8.1" 2946 | source = "registry+https://github.com/rust-lang/crates.io-index" 2947 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 2948 | 2949 | [[package]] 2950 | name = "unicode-ident" 2951 | version = "1.0.18" 2952 | source = "registry+https://github.com/rust-lang/crates.io-index" 2953 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 2954 | 2955 | [[package]] 2956 | name = "universal-hash" 2957 | version = "0.5.1" 2958 | source = "registry+https://github.com/rust-lang/crates.io-index" 2959 | checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" 2960 | dependencies = [ 2961 | "crypto-common", 2962 | "subtle", 2963 | ] 2964 | 2965 | [[package]] 2966 | name = "untrusted" 2967 | version = "0.9.0" 2968 | source = "registry+https://github.com/rust-lang/crates.io-index" 2969 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2970 | 2971 | [[package]] 2972 | name = "url" 2973 | version = "2.5.4" 2974 | source = "registry+https://github.com/rust-lang/crates.io-index" 2975 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 2976 | dependencies = [ 2977 | "form_urlencoded", 2978 | "idna", 2979 | "percent-encoding", 2980 | ] 2981 | 2982 | [[package]] 2983 | name = "utf-8" 2984 | version = "0.7.6" 2985 | source = "registry+https://github.com/rust-lang/crates.io-index" 2986 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 2987 | 2988 | [[package]] 2989 | name = "utf8_iter" 2990 | version = "1.0.4" 2991 | source = "registry+https://github.com/rust-lang/crates.io-index" 2992 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 2993 | 2994 | [[package]] 2995 | name = "utf8parse" 2996 | version = "0.2.2" 2997 | source = "registry+https://github.com/rust-lang/crates.io-index" 2998 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 2999 | 3000 | [[package]] 3001 | name = "valuable" 3002 | version = "0.1.1" 3003 | source = "registry+https://github.com/rust-lang/crates.io-index" 3004 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 3005 | 3006 | [[package]] 3007 | name = "version_check" 3008 | version = "0.9.5" 3009 | source = "registry+https://github.com/rust-lang/crates.io-index" 3010 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 3011 | 3012 | [[package]] 3013 | name = "walkdir" 3014 | version = "2.5.0" 3015 | source = "registry+https://github.com/rust-lang/crates.io-index" 3016 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 3017 | dependencies = [ 3018 | "same-file", 3019 | "winapi-util", 3020 | ] 3021 | 3022 | [[package]] 3023 | name = "want" 3024 | version = "0.3.1" 3025 | source = "registry+https://github.com/rust-lang/crates.io-index" 3026 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 3027 | dependencies = [ 3028 | "try-lock", 3029 | ] 3030 | 3031 | [[package]] 3032 | name = "wasi" 3033 | version = "0.11.1+wasi-snapshot-preview1" 3034 | source = "registry+https://github.com/rust-lang/crates.io-index" 3035 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 3036 | 3037 | [[package]] 3038 | name = "wasi" 3039 | version = "0.14.2+wasi-0.2.4" 3040 | source = "registry+https://github.com/rust-lang/crates.io-index" 3041 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 3042 | dependencies = [ 3043 | "wit-bindgen-rt", 3044 | ] 3045 | 3046 | [[package]] 3047 | name = "wasm-bindgen" 3048 | version = "0.2.100" 3049 | source = "registry+https://github.com/rust-lang/crates.io-index" 3050 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 3051 | dependencies = [ 3052 | "cfg-if", 3053 | "once_cell", 3054 | "rustversion", 3055 | "wasm-bindgen-macro", 3056 | ] 3057 | 3058 | [[package]] 3059 | name = "wasm-bindgen-backend" 3060 | version = "0.2.100" 3061 | source = "registry+https://github.com/rust-lang/crates.io-index" 3062 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 3063 | dependencies = [ 3064 | "bumpalo", 3065 | "log", 3066 | "proc-macro2", 3067 | "quote", 3068 | "syn", 3069 | "wasm-bindgen-shared", 3070 | ] 3071 | 3072 | [[package]] 3073 | name = "wasm-bindgen-futures" 3074 | version = "0.4.50" 3075 | source = "registry+https://github.com/rust-lang/crates.io-index" 3076 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 3077 | dependencies = [ 3078 | "cfg-if", 3079 | "js-sys", 3080 | "once_cell", 3081 | "wasm-bindgen", 3082 | "web-sys", 3083 | ] 3084 | 3085 | [[package]] 3086 | name = "wasm-bindgen-macro" 3087 | version = "0.2.100" 3088 | source = "registry+https://github.com/rust-lang/crates.io-index" 3089 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 3090 | dependencies = [ 3091 | "quote", 3092 | "wasm-bindgen-macro-support", 3093 | ] 3094 | 3095 | [[package]] 3096 | name = "wasm-bindgen-macro-support" 3097 | version = "0.2.100" 3098 | source = "registry+https://github.com/rust-lang/crates.io-index" 3099 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 3100 | dependencies = [ 3101 | "proc-macro2", 3102 | "quote", 3103 | "syn", 3104 | "wasm-bindgen-backend", 3105 | "wasm-bindgen-shared", 3106 | ] 3107 | 3108 | [[package]] 3109 | name = "wasm-bindgen-shared" 3110 | version = "0.2.100" 3111 | source = "registry+https://github.com/rust-lang/crates.io-index" 3112 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 3113 | dependencies = [ 3114 | "unicode-ident", 3115 | ] 3116 | 3117 | [[package]] 3118 | name = "wasm-streams" 3119 | version = "0.4.2" 3120 | source = "registry+https://github.com/rust-lang/crates.io-index" 3121 | checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" 3122 | dependencies = [ 3123 | "futures-util", 3124 | "js-sys", 3125 | "wasm-bindgen", 3126 | "wasm-bindgen-futures", 3127 | "web-sys", 3128 | ] 3129 | 3130 | [[package]] 3131 | name = "web-sys" 3132 | version = "0.3.77" 3133 | source = "registry+https://github.com/rust-lang/crates.io-index" 3134 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 3135 | dependencies = [ 3136 | "js-sys", 3137 | "wasm-bindgen", 3138 | ] 3139 | 3140 | [[package]] 3141 | name = "web-time" 3142 | version = "1.1.0" 3143 | source = "registry+https://github.com/rust-lang/crates.io-index" 3144 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 3145 | dependencies = [ 3146 | "js-sys", 3147 | "wasm-bindgen", 3148 | ] 3149 | 3150 | [[package]] 3151 | name = "webpki-roots" 3152 | version = "1.0.1" 3153 | source = "registry+https://github.com/rust-lang/crates.io-index" 3154 | checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" 3155 | dependencies = [ 3156 | "rustls-pki-types", 3157 | ] 3158 | 3159 | [[package]] 3160 | name = "winapi" 3161 | version = "0.3.9" 3162 | source = "registry+https://github.com/rust-lang/crates.io-index" 3163 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 3164 | dependencies = [ 3165 | "winapi-i686-pc-windows-gnu", 3166 | "winapi-x86_64-pc-windows-gnu", 3167 | ] 3168 | 3169 | [[package]] 3170 | name = "winapi-i686-pc-windows-gnu" 3171 | version = "0.4.0" 3172 | source = "registry+https://github.com/rust-lang/crates.io-index" 3173 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 3174 | 3175 | [[package]] 3176 | name = "winapi-util" 3177 | version = "0.1.9" 3178 | source = "registry+https://github.com/rust-lang/crates.io-index" 3179 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 3180 | dependencies = [ 3181 | "windows-sys 0.59.0", 3182 | ] 3183 | 3184 | [[package]] 3185 | name = "winapi-x86_64-pc-windows-gnu" 3186 | version = "0.4.0" 3187 | source = "registry+https://github.com/rust-lang/crates.io-index" 3188 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 3189 | 3190 | [[package]] 3191 | name = "windows-link" 3192 | version = "0.1.3" 3193 | source = "registry+https://github.com/rust-lang/crates.io-index" 3194 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 3195 | 3196 | [[package]] 3197 | name = "windows-registry" 3198 | version = "0.5.3" 3199 | source = "registry+https://github.com/rust-lang/crates.io-index" 3200 | checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" 3201 | dependencies = [ 3202 | "windows-link", 3203 | "windows-result", 3204 | "windows-strings", 3205 | ] 3206 | 3207 | [[package]] 3208 | name = "windows-result" 3209 | version = "0.3.4" 3210 | source = "registry+https://github.com/rust-lang/crates.io-index" 3211 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 3212 | dependencies = [ 3213 | "windows-link", 3214 | ] 3215 | 3216 | [[package]] 3217 | name = "windows-strings" 3218 | version = "0.4.2" 3219 | source = "registry+https://github.com/rust-lang/crates.io-index" 3220 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 3221 | dependencies = [ 3222 | "windows-link", 3223 | ] 3224 | 3225 | [[package]] 3226 | name = "windows-sys" 3227 | version = "0.52.0" 3228 | source = "registry+https://github.com/rust-lang/crates.io-index" 3229 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 3230 | dependencies = [ 3231 | "windows-targets 0.52.6", 3232 | ] 3233 | 3234 | [[package]] 3235 | name = "windows-sys" 3236 | version = "0.59.0" 3237 | source = "registry+https://github.com/rust-lang/crates.io-index" 3238 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 3239 | dependencies = [ 3240 | "windows-targets 0.52.6", 3241 | ] 3242 | 3243 | [[package]] 3244 | name = "windows-sys" 3245 | version = "0.60.2" 3246 | source = "registry+https://github.com/rust-lang/crates.io-index" 3247 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 3248 | dependencies = [ 3249 | "windows-targets 0.53.2", 3250 | ] 3251 | 3252 | [[package]] 3253 | name = "windows-targets" 3254 | version = "0.52.6" 3255 | source = "registry+https://github.com/rust-lang/crates.io-index" 3256 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 3257 | dependencies = [ 3258 | "windows_aarch64_gnullvm 0.52.6", 3259 | "windows_aarch64_msvc 0.52.6", 3260 | "windows_i686_gnu 0.52.6", 3261 | "windows_i686_gnullvm 0.52.6", 3262 | "windows_i686_msvc 0.52.6", 3263 | "windows_x86_64_gnu 0.52.6", 3264 | "windows_x86_64_gnullvm 0.52.6", 3265 | "windows_x86_64_msvc 0.52.6", 3266 | ] 3267 | 3268 | [[package]] 3269 | name = "windows-targets" 3270 | version = "0.53.2" 3271 | source = "registry+https://github.com/rust-lang/crates.io-index" 3272 | checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" 3273 | dependencies = [ 3274 | "windows_aarch64_gnullvm 0.53.0", 3275 | "windows_aarch64_msvc 0.53.0", 3276 | "windows_i686_gnu 0.53.0", 3277 | "windows_i686_gnullvm 0.53.0", 3278 | "windows_i686_msvc 0.53.0", 3279 | "windows_x86_64_gnu 0.53.0", 3280 | "windows_x86_64_gnullvm 0.53.0", 3281 | "windows_x86_64_msvc 0.53.0", 3282 | ] 3283 | 3284 | [[package]] 3285 | name = "windows_aarch64_gnullvm" 3286 | version = "0.52.6" 3287 | source = "registry+https://github.com/rust-lang/crates.io-index" 3288 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 3289 | 3290 | [[package]] 3291 | name = "windows_aarch64_gnullvm" 3292 | version = "0.53.0" 3293 | source = "registry+https://github.com/rust-lang/crates.io-index" 3294 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 3295 | 3296 | [[package]] 3297 | name = "windows_aarch64_msvc" 3298 | version = "0.52.6" 3299 | source = "registry+https://github.com/rust-lang/crates.io-index" 3300 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 3301 | 3302 | [[package]] 3303 | name = "windows_aarch64_msvc" 3304 | version = "0.53.0" 3305 | source = "registry+https://github.com/rust-lang/crates.io-index" 3306 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 3307 | 3308 | [[package]] 3309 | name = "windows_i686_gnu" 3310 | version = "0.52.6" 3311 | source = "registry+https://github.com/rust-lang/crates.io-index" 3312 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 3313 | 3314 | [[package]] 3315 | name = "windows_i686_gnu" 3316 | version = "0.53.0" 3317 | source = "registry+https://github.com/rust-lang/crates.io-index" 3318 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 3319 | 3320 | [[package]] 3321 | name = "windows_i686_gnullvm" 3322 | version = "0.52.6" 3323 | source = "registry+https://github.com/rust-lang/crates.io-index" 3324 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 3325 | 3326 | [[package]] 3327 | name = "windows_i686_gnullvm" 3328 | version = "0.53.0" 3329 | source = "registry+https://github.com/rust-lang/crates.io-index" 3330 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 3331 | 3332 | [[package]] 3333 | name = "windows_i686_msvc" 3334 | version = "0.52.6" 3335 | source = "registry+https://github.com/rust-lang/crates.io-index" 3336 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 3337 | 3338 | [[package]] 3339 | name = "windows_i686_msvc" 3340 | version = "0.53.0" 3341 | source = "registry+https://github.com/rust-lang/crates.io-index" 3342 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 3343 | 3344 | [[package]] 3345 | name = "windows_x86_64_gnu" 3346 | version = "0.52.6" 3347 | source = "registry+https://github.com/rust-lang/crates.io-index" 3348 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 3349 | 3350 | [[package]] 3351 | name = "windows_x86_64_gnu" 3352 | version = "0.53.0" 3353 | source = "registry+https://github.com/rust-lang/crates.io-index" 3354 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 3355 | 3356 | [[package]] 3357 | name = "windows_x86_64_gnullvm" 3358 | version = "0.52.6" 3359 | source = "registry+https://github.com/rust-lang/crates.io-index" 3360 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 3361 | 3362 | [[package]] 3363 | name = "windows_x86_64_gnullvm" 3364 | version = "0.53.0" 3365 | source = "registry+https://github.com/rust-lang/crates.io-index" 3366 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 3367 | 3368 | [[package]] 3369 | name = "windows_x86_64_msvc" 3370 | version = "0.52.6" 3371 | source = "registry+https://github.com/rust-lang/crates.io-index" 3372 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 3373 | 3374 | [[package]] 3375 | name = "windows_x86_64_msvc" 3376 | version = "0.53.0" 3377 | source = "registry+https://github.com/rust-lang/crates.io-index" 3378 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 3379 | 3380 | [[package]] 3381 | name = "winnow" 3382 | version = "0.7.11" 3383 | source = "registry+https://github.com/rust-lang/crates.io-index" 3384 | checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" 3385 | dependencies = [ 3386 | "memchr", 3387 | ] 3388 | 3389 | [[package]] 3390 | name = "wit-bindgen-rt" 3391 | version = "0.39.0" 3392 | source = "registry+https://github.com/rust-lang/crates.io-index" 3393 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 3394 | dependencies = [ 3395 | "bitflags", 3396 | ] 3397 | 3398 | [[package]] 3399 | name = "writeable" 3400 | version = "0.6.1" 3401 | source = "registry+https://github.com/rust-lang/crates.io-index" 3402 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 3403 | 3404 | [[package]] 3405 | name = "xml-rs" 3406 | version = "0.8.26" 3407 | source = "registry+https://github.com/rust-lang/crates.io-index" 3408 | checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" 3409 | 3410 | [[package]] 3411 | name = "xxhash-rust" 3412 | version = "0.8.15" 3413 | source = "registry+https://github.com/rust-lang/crates.io-index" 3414 | checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" 3415 | 3416 | [[package]] 3417 | name = "yoke" 3418 | version = "0.8.0" 3419 | source = "registry+https://github.com/rust-lang/crates.io-index" 3420 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 3421 | dependencies = [ 3422 | "serde", 3423 | "stable_deref_trait", 3424 | "yoke-derive", 3425 | "zerofrom", 3426 | ] 3427 | 3428 | [[package]] 3429 | name = "yoke-derive" 3430 | version = "0.8.0" 3431 | source = "registry+https://github.com/rust-lang/crates.io-index" 3432 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 3433 | dependencies = [ 3434 | "proc-macro2", 3435 | "quote", 3436 | "syn", 3437 | "synstructure", 3438 | ] 3439 | 3440 | [[package]] 3441 | name = "zerocopy" 3442 | version = "0.8.26" 3443 | source = "registry+https://github.com/rust-lang/crates.io-index" 3444 | checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" 3445 | dependencies = [ 3446 | "zerocopy-derive", 3447 | ] 3448 | 3449 | [[package]] 3450 | name = "zerocopy-derive" 3451 | version = "0.8.26" 3452 | source = "registry+https://github.com/rust-lang/crates.io-index" 3453 | checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" 3454 | dependencies = [ 3455 | "proc-macro2", 3456 | "quote", 3457 | "syn", 3458 | ] 3459 | 3460 | [[package]] 3461 | name = "zerofrom" 3462 | version = "0.1.6" 3463 | source = "registry+https://github.com/rust-lang/crates.io-index" 3464 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 3465 | dependencies = [ 3466 | "zerofrom-derive", 3467 | ] 3468 | 3469 | [[package]] 3470 | name = "zerofrom-derive" 3471 | version = "0.1.6" 3472 | source = "registry+https://github.com/rust-lang/crates.io-index" 3473 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 3474 | dependencies = [ 3475 | "proc-macro2", 3476 | "quote", 3477 | "syn", 3478 | "synstructure", 3479 | ] 3480 | 3481 | [[package]] 3482 | name = "zeroize" 3483 | version = "1.8.1" 3484 | source = "registry+https://github.com/rust-lang/crates.io-index" 3485 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 3486 | 3487 | [[package]] 3488 | name = "zerotrie" 3489 | version = "0.2.2" 3490 | source = "registry+https://github.com/rust-lang/crates.io-index" 3491 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 3492 | dependencies = [ 3493 | "displaydoc", 3494 | "yoke", 3495 | "zerofrom", 3496 | ] 3497 | 3498 | [[package]] 3499 | name = "zerovec" 3500 | version = "0.11.2" 3501 | source = "registry+https://github.com/rust-lang/crates.io-index" 3502 | checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 3503 | dependencies = [ 3504 | "yoke", 3505 | "zerofrom", 3506 | "zerovec-derive", 3507 | ] 3508 | 3509 | [[package]] 3510 | name = "zerovec-derive" 3511 | version = "0.11.1" 3512 | source = "registry+https://github.com/rust-lang/crates.io-index" 3513 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 3514 | dependencies = [ 3515 | "proc-macro2", 3516 | "quote", 3517 | "syn", 3518 | ] 3519 | 3520 | [[package]] 3521 | name = "zstd" 3522 | version = "0.13.3" 3523 | source = "registry+https://github.com/rust-lang/crates.io-index" 3524 | checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" 3525 | dependencies = [ 3526 | "zstd-safe", 3527 | ] 3528 | 3529 | [[package]] 3530 | name = "zstd-safe" 3531 | version = "7.2.4" 3532 | source = "registry+https://github.com/rust-lang/crates.io-index" 3533 | checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" 3534 | dependencies = [ 3535 | "zstd-sys", 3536 | ] 3537 | 3538 | [[package]] 3539 | name = "zstd-sys" 3540 | version = "2.0.15+zstd.1.5.7" 3541 | source = "registry+https://github.com/rust-lang/crates.io-index" 3542 | checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" 3543 | dependencies = [ 3544 | "cc", 3545 | "pkg-config", 3546 | ] 3547 | --------------------------------------------------------------------------------