├── .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 |
43 | {isLoading ? 'Applying...' : 'Apply and Save Code'}
44 |
45 |
50 | Reset to Default
51 |
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 |
45 | Mode
46 |
47 | handleConfigChange('mode', e.target.value as 'single' | 'broad')}
51 | 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"
52 | >
53 | Single
54 | Broad
55 |
56 |
57 |
58 |
59 |
60 | Auto Connect
61 |
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 |
75 | Interval(ms)
76 |
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 |
91 | Max Retries
92 |
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 | [](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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------