feat: migrate sse to websocket

This commit is contained in:
Xinrea
2025-09-26 21:30:55 +08:00
parent 1f2508aae9
commit e6159555f3
10 changed files with 415 additions and 136 deletions

View File

@@ -30,7 +30,8 @@
"@tauri-apps/plugin-sql": "~2",
"lucide-svelte": "^0.479.0",
"marked": "^16.1.1",
"qrcode": "^1.5.4"
"qrcode": "^1.5.4",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.0.0",

154
src-tauri/Cargo.lock generated
View File

@@ -124,6 +124,15 @@ version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "ashpd"
version = "0.11.0"
@@ -442,7 +451,7 @@ dependencies = [
"hyper 1.6.0",
"hyper-util",
"itoa",
"matchit",
"matchit 0.7.3",
"memchr",
"mime",
"multer",
@@ -575,6 +584,7 @@ dependencies = [
"serde_derive",
"serde_json",
"simplelog",
"socketioxide",
"sqlx",
"srtparse",
"sysinfo",
@@ -592,6 +602,7 @@ dependencies = [
"tauri-utils",
"thiserror 2.0.12",
"tokio",
"tokio-stream",
"tokio-util",
"toml 0.7.8",
"tower-http 0.5.2",
@@ -1313,7 +1324,7 @@ dependencies = [
"serde_json",
"thiserror 2.0.12",
"tokio",
"tokio-tungstenite",
"tokio-tungstenite 0.27.0",
"tonic-build",
"url",
"urlencoding",
@@ -1539,6 +1550,17 @@ dependencies = [
"serde",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "derive_more"
version = "0.99.20"
@@ -1777,6 +1799,45 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
[[package]]
name = "engineioxide"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88a4ef9fd57bc7e9fbe59550d3cba88536fbe47fba05ab33088edc4b09d3267a"
dependencies = [
"base64 0.22.1",
"bytes",
"engineioxide-core",
"futures-core",
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.6.0",
"hyper-util",
"pin-project-lite",
"serde",
"serde_json",
"smallvec",
"thiserror 2.0.12",
"tokio",
"tokio-tungstenite 0.26.2",
"tower-layer",
"tower-service",
]
[[package]]
name = "engineioxide-core"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04e5d58eb7374df380cbb53ef65f9c35f544c9c217528adb1458c8df05978475"
dependencies = [
"base64 0.22.1",
"bytes",
"rand 0.9.1",
"serde",
]
[[package]]
name = "enumflags2"
version = "0.6.4"
@@ -3509,6 +3570,12 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "matchit"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f926ade0c4e170215ae43342bf13b9310a437609c81f29f86c5df6657582ef9"
[[package]]
name = "md-5"
version = "0.10.6"
@@ -5790,6 +5857,58 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "socketioxide"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "476190583b592f1e3d55584269600f2d3c4f18af36adad03c41c27e82dcb6bd5"
dependencies = [
"bytes",
"engineioxide",
"futures-core",
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
"hyper 1.6.0",
"matchit 0.8.6",
"pin-project-lite",
"serde",
"socketioxide-core",
"socketioxide-parser-common",
"thiserror 2.0.12",
"tokio",
"tower-layer",
"tower-service",
]
[[package]]
name = "socketioxide-core"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b07b95089a961994921d23dd6e70792a06f5daa250b5ec8919f6f9de371d2cc5"
dependencies = [
"arbitrary",
"bytes",
"engineioxide-core",
"futures-core",
"serde",
"smallvec",
"thiserror 2.0.12",
]
[[package]]
name = "socketioxide-parser-common"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fe3b57122bf9c17fe8c2f364e1d307983068396cfb1b0407ec897de411f8033"
dependencies = [
"bytes",
"itoa",
"serde",
"serde_json",
"socketioxide-core",
]
[[package]]
name = "softbuffer"
version = "0.4.6"
@@ -7009,6 +7128,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite 0.26.2",
]
[[package]]
name = "tokio-tungstenite"
version = "0.27.0"
@@ -7020,7 +7151,7 @@ dependencies = [
"native-tls",
"tokio",
"tokio-native-tls",
"tungstenite",
"tungstenite 0.27.0",
]
[[package]]
@@ -7296,6 +7427,23 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"bytes",
"data-encoding",
"http 1.3.1",
"httparse",
"log",
"rand 0.9.1",
"sha1",
"thiserror 2.0.12",
"utf-8",
]
[[package]]
name = "tungstenite"
version = "0.27.0"

View File

@@ -55,12 +55,14 @@ tower-http = { version = "0.5", features = ["cors", "fs"] }
futures-core = "0.3"
futures = "0.3"
tokio-util = { version = "0.7", features = ["io"] }
tokio-stream = "0.1"
clap = { version = "4.5.37", features = ["derive"] }
url = "2.5.4"
srtparse = "0.2.0"
thiserror = "2"
deno_core = "0.355"
sanitize-filename = "0.6.0"
socketioxide = "0.17.2"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem

View File

@@ -1,4 +1,7 @@
use std::fmt::{self, Display};
use std::{
fmt::{self, Display},
path::PathBuf,
};
use crate::{
config::Config,
@@ -35,7 +38,7 @@ use crate::{
},
AccountInfo,
},
progress::progress_manager::Event,
http_server::websocket,
recorder::{
bilibili::{
client::{QrInfo, QrStatus},
@@ -48,19 +51,15 @@ use crate::{
recorder_manager::{ClipRangeParams, RecorderList},
state::State,
};
use axum::extract::Query;
use axum::{
body::Body,
extract::{DefaultBodyLimit, Json, Multipart, Path},
http::{Request, StatusCode},
middleware::{self, Next},
response::{IntoResponse, Response, Sse},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Router,
};
use axum::{extract::Query, response::sse};
use futures::stream::{self, Stream};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tower_http::cors::{Any, CorsLayer};
use tower_http::services::ServeDir;
@@ -1628,67 +1627,6 @@ async fn handler_hls(
Ok(response)
}
// 字符串转义工具函数
fn escape_sse_string(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('"', "\\\"")
}
async fn handler_sse(
state: axum::extract::State<State>,
) -> Sse<impl Stream<Item = Result<sse::Event, axum::Error>>> {
let rx = state.progress_manager.subscribe();
let stream = stream::unfold(rx, move |mut rx| async move {
match rx.recv().await {
Ok(event) => {
let sse_event = match event {
Event::ProgressUpdate { id, content } => {
sse::Event::default().event("progress-update").data(format!(
r#"{{"id":"{}","content":"{}"}}"#,
id,
escape_sse_string(&content)
))
}
Event::ProgressFinished {
id,
success,
message,
} => sse::Event::default()
.event("progress-finished")
.data(format!(
r#"{{"id":"{}","success":{},"message":"{}"}}"#,
id,
success,
escape_sse_string(&message)
)),
Event::DanmuReceived { room, ts, content } => sse::Event::default()
.event(format!("danmu:{}", room))
.data(format!(
r#"{{"ts":"{}","content":"{}"}}"#,
ts,
escape_sse_string(&content)
)),
};
Some((Ok(sse_event), rx))
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => None,
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
// 跳过丢失的事件,继续处理
Some((Ok(sse::Event::default().event("keep-alive").data("")), rx))
}
}
});
Sse::new(stream).keep_alive(
sse::KeepAlive::new()
.interval(std::time::Duration::from_secs(30))
.text("keep-alive"),
)
}
const MAX_BODY_SIZE: usize = 10 * 1024 * 1024 * 1024;
pub async fn start_api_server(state: State) {
@@ -1864,11 +1802,13 @@ pub async fn start_api_server(state: State) {
.route("/api/upload_file", post(handler_upload_file))
.route("/api/image/:video_id", get(handler_image_base64))
.route("/hls/*uri", get(handler_hls))
.route("/api/sse", get(handler_sse))
.nest_service("/output", ServeDir::new(output_path))
.nest_service("/cache", ServeDir::new(cache_path));
let websocket_layer = websocket::create_websocket_server(state.clone()).await;
let router = app
.layer(websocket_layer)
.layer(cors)
.layer(DefaultBodyLimit::max(MAX_BODY_SIZE))
.with_state(state);

View File

@@ -0,0 +1,2 @@
pub mod api_server;
pub mod websocket;

View File

@@ -0,0 +1,101 @@
use serde_json::{json, Value};
use socketioxide::{
extract::{Data, SocketRef},
layer::SocketIoLayer,
SocketIo,
};
use tokio::sync::broadcast;
use crate::progress::progress_manager::Event;
use crate::state::State;
pub async fn create_websocket_server(state: State) -> SocketIoLayer {
let (layer, io) = SocketIo::new_layer();
// Clone the state for the namespace handler
let state_clone = state.clone();
io.ns("/ws", move |socket: SocketRef| {
let state = state_clone.clone();
// Subscribe to progress events
let mut rx = state.progress_manager.subscribe();
// Spawn a task to handle progress events for this socket
let socket_clone = socket.clone();
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(event) => {
let (event_type, message) = match event {
Event::ProgressUpdate { id, content } => (
"progress",
json!({
"event": "progress-update",
"data": {
"id": id,
"content": content
}
}),
),
Event::ProgressFinished {
id,
success,
message,
} => (
"progress",
json!({
"event": "progress-finished",
"data": {
"id": id,
"success": success,
"message": message
}
}),
),
Event::DanmuReceived { room, ts, content } => (
"danmu",
json!({
"event": "danmu-received",
"data": {
"room": room,
"ts": ts,
"content": content
}
}),
),
};
if let Err(e) = socket_clone.emit(event_type, &message) {
log::warn!("Failed to emit progress event to WebSocket client: {}", e);
break;
}
}
Err(broadcast::error::RecvError::Closed) => {
log::info!("Progress channel closed, stopping WebSocket progress stream");
break;
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
log::warn!("WebSocket client lagged, skipped {} events", skipped);
}
}
}
});
// Handle client messages
socket.on("message", |socket: SocketRef, Data::<Value>(data)| {
log::debug!("Received WebSocket message: {:?}", data);
// Echo back the message for testing
socket.emit("echo", &data).ok();
});
// Handle client disconnect
socket.on_disconnect(|socket: SocketRef| {
log::info!("WebSocket client disconnected: {}", socket.id);
});
log::info!("WebSocket client connected: {}", socket.id);
});
layer
}

View File

@@ -682,6 +682,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(v) => log::info!("Checked ffmpeg version: {v}"),
}
http_server::start_api_server(state).await;
http_server::api_server::start_api_server(state).await;
Ok(())
}

View File

@@ -1,11 +1,5 @@
<script lang="ts">
import {
invoke,
TAURI_ENV,
ENDPOINT,
listen,
onConnectionRestore,
} from "../invoker";
import { invoke, TAURI_ENV, ENDPOINT, listen } from "../invoker";
import { Upload, X, CheckCircle } from "lucide-svelte";
import { createEventDispatcher, onDestroy } from "svelte";
import { open } from "@tauri-apps/plugin-dialog";
@@ -120,11 +114,6 @@
}
}
// 注册连接恢复回调
if (!TAURI_ENV) {
onConnectionRestore(checkTaskStatus);
}
onDestroy(() => {
progressUpdateListener?.then((fn) => fn());
progressFinishedListener?.then((fn) => fn());

View File

@@ -5,6 +5,7 @@ import { convertFileSrc as tauri_convert } from "@tauri-apps/api/core";
import { listen as tauri_listen } from "@tauri-apps/api/event";
import { open as tauri_open } from "@tauri-apps/plugin-shell";
import { onOpenUrl as tauri_onOpenUrl } from "@tauri-apps/plugin-deep-link";
import { io, Socket } from "socket.io-client";
declare global {
interface Window {
@@ -171,60 +172,87 @@ async function get_cover(coverType: string, coverPath: string) {
return `${ENDPOINT}/${coverType}/${coverPath}`;
}
let event_source: EventSource | null = null;
let reconnectTimeout: number | null = null;
const MAX_RECONNECT_ATTEMPTS = 5;
let reconnectAttempts = 0;
let socket: Socket | null = null;
// 连接恢复回调列表
const connectionRestoreCallbacks: Array<() => void> = [];
// Socket.IO 事件监听器映射
const eventListeners: Map<string, Array<(data: any) => void>> = new Map();
function createEventSource() {
function createSocket() {
if (TAURI_ENV) return;
if (event_source) {
event_source.close();
if (socket) {
socket.disconnect();
}
event_source = new EventSource(`${ENDPOINT}/api/sse`);
event_source.onopen = () => {
reconnectAttempts = 0;
// 构建 Socket.IO URL
console.log("endpoint:", ENDPOINT);
const socketUrl = ENDPOINT;
socket = io(`${socketUrl}/ws`, {
transports: ["websocket", "polling"],
autoConnect: true,
reconnection: true,
});
// 触发连接恢复回调
connectionRestoreCallbacks.forEach((callback) => {
try {
callback();
} catch (e) {
console.error("[SSE] Connection restore callback error:", e);
}
});
};
socket.on("connect", () => {
console.log("[Socket.IO] Connected to server");
});
event_source.onerror = (error) => {
// 只有在连接真正关闭时才进行重连
if (
event_source.readyState === EventSource.CLOSED &&
reconnectAttempts < MAX_RECONNECT_ATTEMPTS
) {
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000);
socket.on("disconnect", (reason) => {
console.log("[Socket.IO] Disconnected from server:", reason);
});
reconnectTimeout = window.setTimeout(() => {
createEventSource();
}, delay);
} else {
console.error("[SSE] Max reconnection attempts reached, giving up");
socket.on("connect_error", (error) => {
console.error("[Socket.IO] Connection error:", error);
});
// 监听服务器发送的事件
socket.on("progress", (data) => {
const eventType = data.event || "message";
// 触发对应的事件监听器
const listeners = eventListeners.get(eventType);
if (listeners) {
listeners.forEach((callback) => {
try {
callback({
type: eventType,
payload: data.data,
});
} catch (e) {
console.error(
`[Socket.IO] Event listener error for ${eventType}:`,
e
);
}
});
}
};
}
});
// 注册连接恢复回调
function onConnectionRestore(callback: () => void) {
connectionRestoreCallbacks.push(callback);
socket.on("danmu", (data) => {
const eventType = data.event || "message";
// 触发对应的事件监听器
const listeners = eventListeners.get(eventType);
if (listeners) {
listeners.forEach((callback) => {
try {
callback({
type: eventType,
payload: data.data,
});
} catch (e) {
console.error(
`[Socket.IO] Event listener error for ${eventType}:`,
e
);
}
});
}
});
}
if (!TAURI_ENV) {
createEventSource();
createSocket();
}
async function listen<T>(event: string, callback: (data: any) => void) {
@@ -232,13 +260,26 @@ async function listen<T>(event: string, callback: (data: any) => void) {
return await tauri_listen(event, callback);
}
event_source.addEventListener(event, (event_data) => {
const data = JSON.parse(event_data.data);
callback({
type: event,
payload: data,
});
});
// 将事件监听器添加到映射中
if (!eventListeners.has(event)) {
eventListeners.set(event, []);
}
eventListeners.get(event)!.push(callback);
// 返回一个清理函数
return () => {
const listeners = eventListeners.get(event);
if (listeners) {
const index = listeners.indexOf(callback);
if (index > -1) {
listeners.splice(index, 1);
}
// 如果没有监听器了,删除这个事件
if (listeners.length === 0) {
eventListeners.delete(event);
}
}
};
}
async function open(url: string) {
@@ -273,6 +314,5 @@ export {
log,
close_window,
onOpenUrl,
onConnectionRestore,
get_cover,
};

View File

@@ -886,6 +886,11 @@
resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224"
integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==
"@socket.io/component-emitter@~3.1.0":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==
"@sveltejs/vite-plugin-svelte-inspector@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz#c99fcb73aaa845a3e2c0563409aeb3ee0b863add"
@@ -2193,6 +2198,13 @@ debug@^4.3.4, debug@^4.4.0:
dependencies:
ms "^2.1.3"
debug@~4.3.1, debug@~4.3.2:
version "4.3.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
dependencies:
ms "^2.1.3"
decamelize@1.2.0, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -2279,6 +2291,22 @@ emoji-regex@^9.2.2:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
engine.io-client@~6.6.1:
version "6.6.3"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.6.3.tgz#815393fa24f30b8e6afa8f77ccca2f28146be6de"
integrity sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
engine.io-parser "~5.2.1"
ws "~8.17.1"
xmlhttprequest-ssl "~2.1.1"
engine.io-parser@~5.2.1:
version "5.2.3"
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f"
integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==
entities@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
@@ -3478,6 +3506,24 @@ simple-wcswidth@^1.0.1:
resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz#66722f37629d5203f9b47c5477b1225b85d6525b"
integrity sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==
socket.io-client@^4.8.1:
version "4.8.1"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.8.1.tgz#1941eca135a5490b94281d0323fe2a35f6f291cb"
integrity sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.2"
engine.io-client "~6.6.1"
socket.io-parser "~4.2.4"
socket.io-parser@~4.2.4:
version "4.2.4"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83"
integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
sorcery@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.11.1.tgz#7cac27ae9c9549b3cd1e4bb85317f7b2dc7b7e22"
@@ -4059,6 +4105,16 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@~8.17.1:
version "8.17.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
xmlhttprequest-ssl@~2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz#e9e8023b3f29ef34b97a859f584c5e6c61418e23"
integrity sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==
y18n@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"