fix: debounced payloads to prevent excessive payload freezes
This commit is contained in:
parent
bb84df1091
commit
063e9f8837
3 changed files with 133 additions and 21 deletions
|
|
@ -31,7 +31,7 @@ export type Filters = { game_versions: string[] | null; loaders: GameLoader[] |
|
|||
|
||||
export type GameLoader = "vanilla" | "forge" | "neoforge" | "quilt" | "fabric" | "legacyfabric"
|
||||
|
||||
export type IOError = { type: "InvalidAbsolutePath"; data: string } | { type: "FileNotFoundInZip"; data: string } | { type: "IOErrorWrapper"; data: string } | { type: "IOError"; data: string } | { type: "DeserializeError"; data: string } | { type: "AsyncZipError"; data: string }
|
||||
export type IOError = { type: "InvalidAbsolutePath"; data: string } | { type: "FileNotFoundInZip"; data: string } | { type: "IOErrorWrapper"; data: string } | { type: "IOError"; data: string } | { type: "DeserializeError"; data: string } | { type: "AsyncZipError"; data: string } | { type: "TempFileError"; data: string }
|
||||
|
||||
export type Icon = string
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ export type SettingProfileModel = { name: string; java_id: number | null; res: R
|
|||
|
||||
export type Settings = { global_game_settings: SettingProfileModel; allow_parallel_running_clusters: boolean; enable_gamemode: boolean; discord_enabled: boolean; seen_onboarding: boolean; max_concurrent_requests: number; settings_version: number; native_window_frame: boolean; show_tanstack_dev_tools: boolean }
|
||||
|
||||
export type SettingsOsExtra = Record<string, never>
|
||||
export type SettingsOsExtra = { enable_gamemode: boolean | null }
|
||||
|
||||
export type SkinVariant = "classic" | "slim"
|
||||
|
||||
|
|
@ -255,13 +255,8 @@ export type VersionType =
|
|||
*/
|
||||
"old_beta"
|
||||
|
||||
const ARGS_MAP = { 'oneclient':'{"getBundlesFor":["cluster_id"],"openDevTools":[],"getClustersGroupedByMajor":[]}', 'events':'{"ingress":["event"],"process":["event"],"message":["event"]}', 'core':'{"removeUser":["uuid"],"updateClusterProfile":["name","profile"],"readSettings":[],"getRunningProcesses":[],"isClusterRunning":["cluster_id"],"openMsaLogin":[],"writeSettings":["setting"],"removeCluster":["id"],"getPackageBody":["provider","body"],"getClusterById":["id"],"getLogs":["id"],"getLoadersForVersion":["mc_version"],"setDefaultUser":["uuid"],"fetchMinecraftProfile":["uuid"],"getScreenshots":["id"],"getGameVersions":[],"searchPackages":["provider","query"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"changeSkin":["access_token","skin_url","skin_variant"],"removeCape":["access_token"],"convertUsernameUUID":["username_uuid"],"getProfileOrDefault":["name"],"updateClusterById":["id","request"],"getWorlds":["id"],"getGlobalProfile":[],"getUsersFromAuthor":["provider","author"],"changeCape":["access_token","cape_uuid"],"createCluster":["options"],"getClusters":[],"launchCluster":["id","uuid"],"getMultiplePackages":["provider","slugs"],"getPackage":["provider","slug"],"getUsers":[],"installModpack":["modpack","cluster_id"],"createSettingsProfile":["name"],"getLogByName":["id","name"],"getUser":["uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"fetchLoggedInProfile":["access_token"],"getDefaultUser":["fallback"],"open":["input"],"killProcess":["pid"],"getRunningProcessesByClusterId":["cluster_id"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}' }
|
||||
export type Router = { 'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
'core': { getClusters: () => Promise<ClusterModel[]>,
|
||||
const ARGS_MAP = { 'core':'{"open":["input"],"fetchLoggedInProfile":["access_token"],"convertUsernameUUID":["username_uuid"],"getGlobalProfile":[],"getRunningProcessesByClusterId":["cluster_id"],"getClusterById":["id"],"readSettings":[],"getClusters":[],"updateClusterProfile":["name","profile"],"createCluster":["options"],"getGameVersions":[],"writeSettings":["setting"],"getLoadersForVersion":["mc_version"],"getUser":["uuid"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"changeCape":["access_token","cape_uuid"],"updateClusterById":["id","request"],"fetchMinecraftProfile":["uuid"],"getPackage":["provider","slug"],"getLogByName":["id","name"],"launchCluster":["id","uuid"],"getWorlds":["id"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"removeUser":["uuid"],"getMultiplePackages":["provider","slugs"],"installModpack":["modpack","cluster_id"],"getRunningProcesses":[],"getLogs":["id"],"searchPackages":["provider","query"],"getPackageBody":["provider","body"],"changeSkin":["access_token","skin_url","skin_variant"],"removeCape":["access_token"],"removeCluster":["id"],"isClusterRunning":["cluster_id"],"killProcess":["pid"],"getUsersFromAuthor":["provider","author"],"createSettingsProfile":["name"],"getDefaultUser":["fallback"],"getUsers":[],"getProfileOrDefault":["name"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getScreenshots":["id"],"setDefaultUser":["uuid"],"openMsaLogin":[]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}', 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}' }
|
||||
export type Router = { 'core': { getClusters: () => Promise<ClusterModel[]>,
|
||||
getClusterById: (id: number) => Promise<ClusterModel | null>,
|
||||
removeCluster: (id: number) => Promise<null>,
|
||||
createCluster: (options: CreateCluster) => Promise<ClusterModel>,
|
||||
|
|
@ -307,7 +302,12 @@ convertUsernameUUID: (usernameUuid: string) => Promise<MowojangProfile>,
|
|||
open: (input: string) => Promise<null> },
|
||||
'oneclient': { openDevTools: () => Promise<void>,
|
||||
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
|
||||
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]> } };
|
||||
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> } };
|
||||
|
||||
|
||||
export type { InferCommandOutput }
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { bindings } from '@/main';
|
|||
import { toast, toastUpdate } from '@/utils/toast';
|
||||
import { Button } from '@onelauncher/common/components';
|
||||
import { AlertCircleIcon, AlertTriangleIcon, CheckCircleIcon, InfoCircleIcon, XCloseIcon } from '@untitled-theme/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { cssTransition, ToastContainer } from 'react-toastify';
|
||||
import { tv } from 'tailwind-variants';
|
||||
import { Spinner } from '../Spinner';
|
||||
|
|
@ -126,14 +126,24 @@ const ingressNames: Record<(IngressType & string) | (keyof UnionToIntersection<I
|
|||
};
|
||||
|
||||
export function Toasts() {
|
||||
// runs twice in dev mode because of react strict mode
|
||||
// the double notifications are NOT a bug!!!
|
||||
const toastIngressMapRef = useRef(new Map<string, ToastId>());
|
||||
const listenerRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (listenerRef.current)
|
||||
return;
|
||||
|
||||
listenerRef.current = true;
|
||||
|
||||
let unlisten: (() => void) | undefined;
|
||||
const toastIngressMap: Map<string, ToastId> = new Map();
|
||||
|
||||
bindings.events.ingress.on((e) => {
|
||||
const ingressType = typeof e.ingress_type === 'string' ? e.ingress_type : Object.keys(e.ingress_type).at(0) ?? 'Info';
|
||||
const toastIngressMap = toastIngressMapRef.current;
|
||||
|
||||
const ingressType = typeof e.ingress_type === 'string'
|
||||
? e.ingress_type
|
||||
: Object.keys(e.ingress_type)[0] ?? 'Info';
|
||||
|
||||
const title = ingressNames[ingressType as keyof typeof ingressNames] ?? ingressType;
|
||||
|
||||
const existingToastId = toastIngressMap.get(e.id);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
use std::collections::{HashMap, VecDeque};
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
use tokio::time::Instant;
|
||||
use tracing::{error, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::LauncherResult;
|
||||
use crate::api::ingress::IngressPayload;
|
||||
use crate::api::proxy::LauncherProxy;
|
||||
use crate::api::proxy::event::LauncherEvent;
|
||||
use crate::api::proxy::message::MessageLevel;
|
||||
|
|
@ -9,8 +18,10 @@ use crate::api::tauri::LauncherEventEmitter;
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct ProxyTauri {
|
||||
emitter: LauncherEventEmitter<tauri::Wry>,
|
||||
emitter: Arc<LauncherEventEmitter<tauri::Wry>>,
|
||||
handle: AppHandle,
|
||||
|
||||
ingress_queue: Arc<IngressQueue>,
|
||||
}
|
||||
|
||||
impl ProxyTauri {
|
||||
|
|
@ -23,9 +34,27 @@ impl ProxyTauri {
|
|||
);
|
||||
|
||||
Self {
|
||||
emitter: LauncherEventEmitter::new(handle.clone()),
|
||||
emitter: Arc::new(LauncherEventEmitter::new(handle.clone())),
|
||||
handle,
|
||||
ingress_queue: IngressQueue::new(),
|
||||
}
|
||||
.init()
|
||||
}
|
||||
|
||||
fn init(self) -> Self {
|
||||
let ingress_queue = Arc::clone(&self.ingress_queue);
|
||||
let emitter = Arc::clone(&self.emitter);
|
||||
|
||||
// spawn the processor on another thread
|
||||
tokio::spawn(async move {
|
||||
ingress_queue
|
||||
.process_queue(move |payload| {
|
||||
let _ = emitter.ingress(payload);
|
||||
})
|
||||
.await;
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,10 +70,14 @@ impl LauncherProxy for ProxyTauri {
|
|||
}
|
||||
|
||||
Ok(match event {
|
||||
LauncherEvent::Ingress(ingress) => self.emitter.ingress(ingress),
|
||||
LauncherEvent::Message(message) => self.emitter.message(message),
|
||||
LauncherEvent::Process(process) => self.emitter.process(process),
|
||||
}?)
|
||||
LauncherEvent::Ingress(ingress) => {
|
||||
self.ingress_queue.push(ingress).await;
|
||||
|
||||
()
|
||||
}
|
||||
LauncherEvent::Message(message) => self.emitter.message(message)?,
|
||||
LauncherEvent::Process(process) => self.emitter.process(process)?,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
|
|
@ -67,3 +100,72 @@ impl LauncherProxy for ProxyTauri {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IngressQueue {
|
||||
queue: Mutex<VecDeque<IngressPayload>>,
|
||||
last_sent: Mutex<HashMap<Uuid, Instant>>,
|
||||
notify: Notify,
|
||||
debounce: Duration,
|
||||
}
|
||||
|
||||
impl Debug for IngressQueue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("IngressQueue")
|
||||
.field("debounce", &self.debounce)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl IngressQueue {
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
queue: Mutex::new(VecDeque::new()),
|
||||
last_sent: Mutex::new(HashMap::new()),
|
||||
notify: Notify::new(),
|
||||
debounce: Duration::from_millis(100),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn push(&self, payload: IngressPayload) {
|
||||
let now = Instant::now();
|
||||
|
||||
// check last sent time for debouncing
|
||||
let mut last_sent = self.last_sent.lock().await;
|
||||
if let Some(&instant) = last_sent.get(&payload.id) {
|
||||
if now.duration_since(instant) < self.debounce {
|
||||
// skip update or merge with existing queued message
|
||||
let mut queue = self.queue.lock().await;
|
||||
if let Some(existing) = queue.iter_mut().find(|q| q.id == payload.id) {
|
||||
existing.percent = payload.percent.or(existing.percent);
|
||||
existing.message = payload.message;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
last_sent.insert(payload.id, now);
|
||||
|
||||
let mut queue = self.queue.lock().await;
|
||||
queue.push_back(payload);
|
||||
self.notify.notify_one(); // wake processor
|
||||
}
|
||||
|
||||
pub async fn process_queue<F>(self: Arc<Self>, mut sender: F)
|
||||
where
|
||||
F: FnMut(IngressPayload) + Send + 'static,
|
||||
{
|
||||
loop {
|
||||
let queued = {
|
||||
let mut queue = self.queue.lock().await;
|
||||
queue.pop_front()
|
||||
};
|
||||
|
||||
if let Some(payload) = queued {
|
||||
sender(payload);
|
||||
} else {
|
||||
// wait until next push, no CPU spin
|
||||
self.notify.notified().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue