️ feat: Optimize websocket binding method, optimize download speed event current limit

This commit is contained in:
jarvis2f 2025-01-12 14:04:50 +08:00
parent 23c5758e0f
commit d3f81c9a14
9 changed files with 234 additions and 158 deletions

View file

@ -110,7 +110,7 @@ public class AutoDownloadVerticle extends AbstractVerticle {
});
vertx.eventBus().consumer(EventEnum.SETTING_UPDATE.address(SettingKey.autoDownloadLimit.name()), message -> {
log.debug("Auto download limit update: %s".formatted(message.body()));
this.limit = Convert.toInt(message.body());
this.limit = Convert.toInt(message.body(), DEFAULT_LIMIT);
});
vertx.eventBus().consumer(EventEnum.MESSAGE_RECEIVED.address(), message -> {
log.trace("Auto download message received: %s".formatted(message.body()));

View file

@ -23,6 +23,14 @@ public enum EventEnum {
* body = JSONObject with "telegramId", "chatId", "messageId"
*/
MESSAGE_RECEIVED,
/**
* suffix = null <br>
* body = JSONObject with "telegramId", "payload"
*
* @see telegram.files.EventPayload
*/
TELEGRAM_EVENT,
;
public String address() {

View file

@ -13,6 +13,7 @@ import io.vertx.core.http.CookieSameSite;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.healthchecks.HealthCheckHandler;
@ -55,6 +56,7 @@ public class HttpVerticle extends AbstractVerticle {
initHttpServer()
.compose(r -> initTelegramVerticles())
.compose(r -> initAutoDownloadVerticle())
.compose(r -> initEventConsumer())
.onSuccess(startPromise::complete)
.onFailure(startPromise::fail);
}
@ -216,12 +218,37 @@ public class HttpVerticle extends AbstractVerticle {
.mapEmpty();
}
private Future<Void> initEventConsumer() {
vertx.eventBus().consumer(EventEnum.TELEGRAM_EVENT.address(), message -> {
log.debug("Received telegram event: %s".formatted(message.body()));
JsonObject jsonObject = (JsonObject) message.body();
String telegramId = jsonObject.getString("telegramId");
EventPayload payload = jsonObject.getJsonObject("payload").mapTo(EventPayload.class);
sessionTelegramVerticles.entrySet().stream()
.filter(e -> Objects.equals(Convert.toStr(e.getValue().getId()), telegramId))
.map(Map.Entry::getKey)
.forEach(sessionId -> {
String wsHandlerId = clients.get(sessionId);
if (StrUtil.isNotBlank(wsHandlerId)) {
vertx.eventBus().send(wsHandlerId, Json.encode(payload));
}
});
});
return Future.succeededFuture();
}
private void handleWebSocket(RoutingContext ctx) {
String sessionId = ctx.session().id();
String telegramId = ctx.request().getParam("telegramId");
ctx.request().toWebSocket()
.onSuccess(ws -> {
log.debug("Upgraded to WebSocket. SessionId: %s".formatted(sessionId));
clients.put(sessionId, ws.textHandlerID());
if (StrUtil.isNotBlank(telegramId) && !handleTelegramChange(sessionId, telegramId)) {
log.debug("Failed to change telegram verticle. SessionId: %s".formatted(sessionId));
}
long timerId = vertx.setPeriodic(30000, id -> {
if (!ws.isClosed()) {
@ -237,9 +264,7 @@ public class HttpVerticle extends AbstractVerticle {
log.debug("WebSocket closed. SessionId: %s".formatted(sessionId));
});
ws.textMessageHandler(text -> {
log.debug("Received WebSocket message: " + text);
});
ws.textMessageHandler(text -> log.debug("Received WebSocket message: " + text));
})
.onFailure(err -> log.warn("Failed to upgrade to WebSocket: %s".formatted(err.getMessage())));
}
@ -303,7 +328,6 @@ public class HttpVerticle extends AbstractVerticle {
String proxyName = jsonObject.getString("proxyName");
TelegramVerticle newTelegramVerticle = new TelegramVerticle(DataVerticle.telegramRepository.getRootPath());
newTelegramVerticle.bindHttpSession(sessionId);
newTelegramVerticle.setProxy(proxyName);
sessionTelegramVerticles.put(sessionId, newTelegramVerticle);
telegramVerticles.add(newTelegramVerticle);
@ -356,11 +380,10 @@ public class HttpVerticle extends AbstractVerticle {
String query = ctx.request().getParam("query");
String chatId = ctx.request().getParam("chatId");
getTelegramVerticle(telegramId)
.ifPresentOrElse(telegramVerticle -> {
telegramVerticle.getChats(Convert.toLong(chatId), query)
.onSuccess(ctx::json)
.onFailure(ctx::fail);
}, () -> ctx.fail(404));
.ifPresentOrElse(telegramVerticle ->
telegramVerticle.getChats(Convert.toLong(chatId), query)
.onSuccess(ctx::json)
.onFailure(ctx::fail), () -> ctx.fail(404));
}
private void handleTelegramFiles(RoutingContext ctx) {
@ -427,16 +450,24 @@ public class HttpVerticle extends AbstractVerticle {
private void handleTelegramChange(RoutingContext ctx) {
String sessionId = ctx.session().id();
String telegramId = ctx.request().getParam("telegramId");
if (handleTelegramChange(sessionId, telegramId)) {
ctx.end();
} else {
ctx.fail(400);
}
}
private boolean handleTelegramChange(String sessionId, String telegramId) {
if (StrUtil.isBlank(telegramId)) {
sessionTelegramVerticles.remove(sessionId);
ctx.end();
return true;
}
getTelegramVerticle(telegramId)
.ifPresentOrElse(telegramVerticle -> {
telegramVerticle.bindHttpSession(sessionId);
sessionTelegramVerticles.put(sessionId, telegramVerticle);
ctx.end();
}, () -> ctx.fail(404));
Optional<TelegramVerticle> optionalTelegramVerticle = getTelegramVerticle(telegramId);
if (optionalTelegramVerticle.isEmpty()) {
return false;
}
sessionTelegramVerticles.put(sessionId, optionalTelegramVerticle.get());
return true;
}
private void handleTelegramToggleProxy(RoutingContext ctx) {
@ -478,13 +509,11 @@ public class HttpVerticle extends AbstractVerticle {
ctx.fail(400);
return;
}
String sessionId = ctx.session().id();
TelegramVerticle telegramVerticle = getTelegramVerticle(ctx);
if (telegramVerticle == null) {
return;
}
JsonObject params = ctx.body().asJsonObject();
telegramVerticle.bindHttpSession(sessionId);
telegramVerticle.execute(method, params == null ? null : params.getMap())
.onSuccess(code -> ctx.json(JsonObject.of("code", code)))
.onFailure(ctx::fail);
@ -637,10 +666,6 @@ public class HttpVerticle extends AbstractVerticle {
.findFirst();
}
public static String getWSHandlerId(String sessionId) {
return clients.get(sessionId);
}
private TelegramVerticle getTelegramVerticle(RoutingContext ctx) {
String sessionId = ctx.session().id();
TelegramVerticle telegramVerticle = sessionTelegramVerticles.get(sessionId);

View file

@ -42,8 +42,6 @@ public class TelegramVerticle extends AbstractVerticle {
private Client client;
private volatile String httpSessionId;
public boolean authorized = false;
public TdApi.AuthorizationState lastAuthorizationState;
@ -62,6 +60,10 @@ public class TelegramVerticle extends AbstractVerticle {
private long avgSpeedPersistenceTimerId;
private long lastFileEventTime;
private long lastFileDownloadEventTime;
static {
Client.setLogMessageHandler(0, new LogMessageHandler());
@ -137,10 +139,6 @@ public class TelegramVerticle extends AbstractVerticle {
return true;
}
public void bindHttpSession(String httpSessionId) {
this.httpSessionId = httpSessionId;
}
public Future<JsonObject> getTelegramAccount() {
return Future.future(promise -> {
if (!authorized) {
@ -441,11 +439,6 @@ public class TelegramVerticle extends AbstractVerticle {
return this.execute(new TdApi.ToggleDownloadIsPaused(fileId, isPaused));
})
.onSuccess(r ->
sendHttpEvent(EventPayload.build(EventPayload.TYPE_FILE_STATUS, new JsonObject()
.put("fileId", fileId)
.put("downloadStatus", isPaused ? FileRecord.DownloadStatus.paused : FileRecord.DownloadStatus.downloading)
)))
.mapEmpty();
}
@ -644,14 +637,8 @@ public class TelegramVerticle extends AbstractVerticle {
}
private void sendHttpEvent(EventPayload payload) {
if (this.httpSessionId == null) return;
String address = HttpVerticle.getWSHandlerId(httpSessionId);
if (address == null) {
log.debug("[%s] Can not found websocket textHandlerID for session id:%s".formatted(getRootId(), httpSessionId));
return;
}
vertx.eventBus().send(address, Json.encode(payload));
vertx.eventBus().send(EventEnum.TELEGRAM_EVENT.address(),
JsonObject.of("telegramId", this.getId(), "payload", JsonObject.mapFrom(payload)));
}
private void handleAuthorizationResult(TdApi.Object object) {
@ -797,6 +784,7 @@ public class TelegramVerticle extends AbstractVerticle {
if (file != null) {
String localPath = null;
Long completionDate = null;
long downloadedSize = file.local == null ? 0 : file.local.downloadedSize;
if (file.local != null && file.local.isDownloadingCompleted) {
localPath = file.local.path;
completionDate = System.currentTimeMillis();
@ -814,16 +802,23 @@ public class TelegramVerticle extends AbstractVerticle {
.put("downloadStatus", r.getString("downloadStatus"))
.put("localPath", r.getString("localPath"))
.put("completionDate", r.getLong("completionDate"))
.put("downloadedSize", downloadedSize)
));
});
}
sendHttpEvent(EventPayload.build(EventPayload.TYPE_FILE, updateFile));
if (lastFileEventTime == 0 || System.currentTimeMillis() - lastFileEventTime > 1000) {
sendHttpEvent(EventPayload.build(EventPayload.TYPE_FILE, updateFile));
lastFileEventTime = System.currentTimeMillis();
}
}
private void onFileDownloadsUpdated(TdApi.UpdateFileDownloads updateFileDownloads) {
log.trace("[%s] Receive file downloads update: %s".formatted(getRootId(), updateFileDownloads));
avgSpeed.update(updateFileDownloads.downloadedSize, System.currentTimeMillis());
sendHttpEvent(EventPayload.build(EventPayload.TYPE_FILE_DOWNLOAD, updateFileDownloads));
if (lastFileDownloadEventTime == 0 || System.currentTimeMillis() - lastFileDownloadEventTime > 1000) {
sendHttpEvent(EventPayload.build(EventPayload.TYPE_FILE_DOWNLOAD, updateFileDownloads));
lastFileDownloadEventTime = System.currentTimeMillis();
}
}
private void onMessageReceived(TdApi.Message message) {

View file

@ -171,8 +171,8 @@ export default function AccountCreator({
if (!authState && !isMethodExecuting) {
return (
<div className="flex items-center justify-center space-x-2 text-xl">
<p>Waiting for the telegram account to be initialized, please wait</p>
<div className="flex flex-col items-center justify-center space-y-2 rounded bg-gray-50 p-2">
<p>Waiting for the telegram account to be initialized, please wait.</p>
<p>If it takes too long, please refresh the page or try again later.</p>
<Ellipsis className="h-4 w-4 animate-pulse" />
</div>

View file

@ -25,110 +25,141 @@ interface FileExtraProps {
rowHeight: RowHeight;
}
export default function FileExtra({ file, rowHeight }: FileExtraProps) {
function FileName({ file }: { file: TelegramFile }) {
if (!file.fileName) {
return null;
}
return (
<SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}>
<p className="flex items-center gap-2">
<Mountain className="h-4 w-4" />
<span className="rounded px-1 text-sm hover:bg-gray-100">
{file.fileName}
</span>
</p>
</SpoiledWrapper>
);
}
function FileCaption({ file, rowHeight }: FileExtraProps) {
if (!file.caption) {
return null;
}
return (
<SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}>
<Tooltip>
<TooltipTrigger>
<div className="flex items-center gap-2">
<Captions className="h-4 w-4 flex-shrink-0" />
<p
className={cn(
rowHeight !== "l" && "line-clamp-1",
"overflow-hidden truncate text-ellipsis text-wrap text-start text-sm",
)}
>
{file.caption}
</p>
</div>
</TooltipTrigger>
<TooltipContent>
<p
className="max-w-80 text-wrap rounded p-2"
dangerouslySetInnerHTML={{
__html: file.caption.replaceAll("\n", "<br />"),
}}
></p>
</TooltipContent>
</Tooltip>
</SpoiledWrapper>
);
}
function FilePath({ file }: { file: TelegramFile }) {
const [, copyToClipboard] = useCopyToClipboard();
if (!file.localPath) {
return null;
}
return (
<Tooltip>
<TooltipTrigger>
<div className="flex items-center gap-2 text-sm">
<FileCheck className="h-4 w-4 flex-shrink-0" />
<p
className="group line-clamp-1 cursor-pointer overflow-hidden truncate text-ellipsis text-wrap rounded px-1 hover:bg-gray-100"
onClick={() => copyToClipboard(file.localPath)}
>
{file.localPath.split("/").pop()}
<Copy className="ml-1 inline-flex h-4 w-4 opacity-0 group-hover:opacity-100" />
</p>
</div>
</TooltipTrigger>
<TooltipContent>
<div className="max-w-80 text-wrap rounded p-2">{file.localPath}</div>
</TooltipContent>
</Tooltip>
);
}
function FileTime({ file }: { file: TelegramFile }) {
return (
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<p className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<span className="rounded px-1 text-sm text-muted-foreground hover:bg-gray-100">
{formatDistanceToNow(new Date(file.date * 1000), {
addSuffix: true,
})}
</span>
</p>
</TooltipTrigger>
<TooltipContent>
<div className="max-w-80 text-wrap rounded p-2">
{`Message received at ${new Date(file.date * 1000).toLocaleString()}`}
</div>
</TooltipContent>
</Tooltip>
{file.completionDate && (
<Tooltip>
<TooltipTrigger asChild>
<p className="flex items-center gap-2">
<ClockArrowDown className="h-4 w-4" />
<span className="rounded px-1 text-sm text-muted-foreground hover:bg-gray-100">
{formatDistanceToNow(new Date(file.completionDate), {
addSuffix: true,
})}
</span>
</p>
</TooltipTrigger>
<TooltipContent>
<div className="max-w-80 text-wrap rounded p-2">
{`File downloaded at ${new Date(file.completionDate).toLocaleString()}`}
</div>
</TooltipContent>
</Tooltip>
)}
</div>
);
}
export default function FileExtra({ file, rowHeight }: FileExtraProps) {
if (rowHeight === "s") {
return (
<TooltipProvider>
{file.fileName ? <FileName file={file} /> : <FileTime file={file} />}
</TooltipProvider>
);
}
return (
<div className="relative flex flex-col space-y-1 overflow-hidden">
<TooltipProvider>
{file.fileName && (
<SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}>
<p className="flex items-center gap-2">
<Mountain className="h-4 w-4" />
<span className="rounded px-1 text-sm hover:bg-gray-100">
{file.fileName}
</span>
</p>
</SpoiledWrapper>
)}
{rowHeight !== "s" && file.caption && (
<SpoiledWrapper hasSensitiveContent={file.hasSensitiveContent}>
<Tooltip>
<TooltipTrigger>
<div className="flex items-center gap-2">
<Captions className="h-4 w-4 flex-shrink-0" />
<p
className={cn(
rowHeight !== "l" && "line-clamp-1",
"overflow-hidden truncate text-ellipsis text-wrap text-start text-sm",
)}
>
{file.caption}
</p>
</div>
</TooltipTrigger>
<TooltipContent>
<p
className="max-w-80 text-wrap rounded p-2"
dangerouslySetInnerHTML={{
__html: file.caption.replaceAll("\n", "<br />"),
}}
></p>
</TooltipContent>
</Tooltip>
</SpoiledWrapper>
)}
{rowHeight !== "s" && file.localPath && (
<Tooltip>
<TooltipTrigger>
<div className="flex items-center gap-2 text-sm">
<FileCheck className="h-4 w-4 flex-shrink-0" />
<p
className="group line-clamp-1 cursor-pointer overflow-hidden truncate text-ellipsis text-wrap rounded px-1 hover:bg-gray-100"
onClick={() => copyToClipboard(file.localPath)}
>
{file.localPath.split("/").pop()}
<Copy className="ml-1 inline-flex h-4 w-4 opacity-0 group-hover:opacity-100" />
</p>
</div>
</TooltipTrigger>
<TooltipContent>
<div className="max-w-80 text-wrap rounded p-2">
{file.localPath}
</div>
</TooltipContent>
</Tooltip>
)}
{rowHeight !== "s" && (
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<p className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<span className="rounded px-1 text-sm text-muted-foreground hover:bg-gray-100">
{formatDistanceToNow(new Date(file.date * 1000), {
addSuffix: true,
})}
</span>
</p>
</TooltipTrigger>
<TooltipContent>
<div className="max-w-80 text-wrap rounded p-2">
{`Message received at ${new Date(file.date * 1000).toLocaleString()}`}
</div>
</TooltipContent>
</Tooltip>
{file.completionDate && (
<Tooltip>
<TooltipTrigger asChild>
<p className="flex items-center gap-2">
<ClockArrowDown className="h-4 w-4" />
<span className="rounded px-1 text-sm text-muted-foreground hover:bg-gray-100">
{formatDistanceToNow(new Date(file.completionDate), {
addSuffix: true,
})}
</span>
</p>
</TooltipTrigger>
<TooltipContent>
<div className="max-w-80 text-wrap rounded p-2">
{`File downloaded at ${new Date(file.completionDate).toLocaleString()}`}
</div>
</TooltipContent>
</Tooltip>
)}
</div>
)}
<FileName file={file} />
<FileCaption file={file} rowHeight={rowHeight} />
<FilePath file={file} />
<FileTime file={file} />
</TooltipProvider>
</div>
);

View file

@ -14,9 +14,14 @@ export function useFileSpeed(fileId: number) {
lastTimestamp: 0,
});
const [debounceSpeed] = useDebounce(downloadSpeed.speed, 1000, {
const [debounceSpeed] = useDebounce(downloadSpeed.speed, 300, {
leading: true,
maxWait: 2000,
maxWait: 1000,
});
const [debounceProgress] = useDebounce(downloadProgress, 300, {
leading: true,
maxWait: 1000,
});
useEffect(() => {
@ -94,7 +99,7 @@ export function useFileSpeed(fileId: number) {
}, []);
return {
downloadProgress,
downloadProgress: debounceProgress,
downloadSpeed: debounceSpeed,
};
}

View file

@ -8,7 +8,7 @@ import useSWRInfinite from "swr/infinite";
import { useWebsocket } from "@/hooks/use-websocket";
import { WebSocketMessageType } from "@/lib/websocket-types";
import useLocalStorage from "@/hooks/use-local-storage";
import {useDebounce} from "use-debounce";
import { useDebounce } from "use-debounce";
const DEFAULT_FILTERS: FileFilter = {
search: "",
@ -31,6 +31,7 @@ export function useFiles(accountId: string, chatId: string) {
downloadStatus: DownloadStatus;
localPath: string;
completionDate: number;
downloadedSize: number;
}
>
>({});
@ -76,6 +77,7 @@ export function useFiles(accountId: string, chatId: string) {
downloadStatus: DownloadStatus;
localPath: string;
completionDate: number;
downloadedSize: number;
};
setLatestFileStatus((prev) => ({
@ -84,7 +86,10 @@ export function useFiles(accountId: string, chatId: string) {
downloadStatus:
data.downloadStatus ?? prev[data.fileId]?.downloadStatus,
localPath: data.localPath ?? prev[data.fileId]?.localPath,
completionDate: data.completionDate ?? prev[data.fileId]?.completionDate,
completionDate:
data.completionDate ?? prev[data.fileId]?.completionDate,
downloadedSize:
data.downloadedSize ?? prev[data.fileId]?.downloadedSize,
},
}));
}, [lastJsonMessage]);
@ -101,6 +106,8 @@ export function useFiles(accountId: string, chatId: string) {
localPath: latestFilesStatus[file.id]?.localPath ?? file.localPath,
completionDate:
latestFilesStatus[file.id]?.completionDate ?? file.completionDate,
downloadedSize:
latestFilesStatus[file.id]?.downloadedSize ?? file.downloadedSize,
});
});
});

View file

@ -17,6 +17,7 @@ import {
import { useToast } from "./use-toast";
import { useDebounce } from "use-debounce";
import { getWsUrl } from "@/lib/api";
import { useParams } from "next/navigation";
const WS_URL = `${getWsUrl()}`;
@ -39,6 +40,7 @@ interface WebSocketProviderProps {
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
children,
}) => {
const params = useParams();
const [isReady, setIsReady] = useState(false);
const [accountDownloadSpeed, setAccountDownloadSpeed] = useState({
speed: 0,
@ -46,17 +48,20 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
lastTimestamp: 0,
});
const { toast } = useToast();
const [debounceSpeed] = useDebounce(accountDownloadSpeed.speed, 1000, {
const [debounceSpeed] = useDebounce(accountDownloadSpeed.speed, 300, {
leading: true,
maxWait: 2000,
maxWait: 1000,
});
const { sendMessage, lastJsonMessage, readyState } =
useWebSocket<WebSocketMessage>(WS_URL, {
shouldReconnect: (closeEvent) => true,
reconnectAttempts: 3,
reconnectInterval: 3000,
});
useWebSocket<WebSocketMessage>(
`${WS_URL}?telegramId=${(params.accountId as string) ?? ""}`,
{
shouldReconnect: (closeEvent) => true,
reconnectAttempts: 3,
reconnectInterval: 3000,
},
);
const connectionStatus = {
[ReadyState.CONNECTING]: "Connecting",
@ -106,7 +111,7 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
lastDownloadedSize: downloadedSize,
};
const timeDiff = (timestamp - prev.lastTimestamp) / 1000;
if (prev.lastTimestamp === 0 || timeDiff <= 0) {
if (prev.lastTimestamp === 0 || timeDiff <= 0 || downloadedSize <= prev.lastDownloadedSize) {
return {
...state,
speed: prev.speed,