perf(*): bound list queries, batch fetches, harden media downloads

This commit is contained in:
hh committed 2026-09-01 21:38:22 +02:00
1 parent 9c12628304
commit 1fa4f18a9b
35 files changed
+473 -138

No files matched your search

@@ -30,6 +30,7 @@
const SCROLL_THRESHOLD = 160;
const STICK_OFFSET = 9;
const IDLE_DELAY = 1500;
const MAX_MESSAGES = 360;
let messages = $state<MessageView[]>([]);
let loading = $state(true);
@@ -176,6 +177,27 @@
}
}
function trimTail() {
if (messages.length <= MAX_MESSAGES) {
return;
}
messages = messages.slice(0, MAX_MESSAGES);
hasNewer = true;
}
async function trimHead() {
if (messages.length <= MAX_MESSAGES || container === null) {
return;
}
const el = container;
const prevHeight = el.scrollHeight;
const prevTop = el.scrollTop;
messages = messages.slice(messages.length - MAX_MESSAGES);
hasMore = true;
await tick();
el.scrollTop = prevTop + (el.scrollHeight - prevHeight);
}
async function loadOlder() {
if (
loadingOlder ||
@@ -204,6 +226,7 @@
ensurePeers(fresh);
await tick();
el.scrollTop = prevTop + (el.scrollHeight - prevHeight);
trimTail();
}
} finally {
loadingOlder = false;
@@ -230,6 +253,7 @@
messages = [...messages, ...fresh];
ensurePeers(fresh);
await tick();
await trimHead();
}
} finally {
loadingNewer = false;
@@ -321,6 +345,7 @@
const stick = isNearBottom();
messages = [...messages, message];
ensurePeers([message]);
await trimHead();
if (stick) {
await tick();
scrollToBottom();
@@ -391,6 +416,7 @@
...appended,
];
ensurePeers(fresh);
await trimHead();
if (appended.length > 0 && stick) {
await tick();
scrollToBottom();
@@ -92,11 +92,11 @@
});
$effect(() => {
for (const policy of chatPolicies) {
if (policy.scope_id !== null) {
chats.ensure(policy.scope_id);
}
}
chats.ensureMany(
chatPolicies
.map((policy) => policy.scope_id)
.filter((id): id is number => id !== null)
);
});
function folderTitle(id: number | null): string {
@@ -2,7 +2,6 @@
import { goto } from "$app/navigation";
import { getChatCalendar, getMessageAt } from "$lib/api/endpoints";
import type { DayCount } from "$lib/api/types";
import EmptyState from "$lib/components/ui/EmptyState.svelte";
import Icon from "$lib/components/ui/Icon.svelte";
import Spinner from "$lib/components/ui/Spinner.svelte";
import { accounts } from "$lib/stores/accounts.svelte";
@@ -46,6 +45,13 @@
let days = $state<DayCount[]>([]);
let loading = $state(false);
let cursor = $state<YearMonth | null>(null);
const cache = new Map<string, DayCount[]>();
const now = new Date();
const currentMonth: YearMonth = {
year: now.getUTCFullYear(),
month: now.getUTCMonth(),
};
const byKey = $derived(
new Map(days.map((day) => [day.day.slice(0, 10), day.count]))
@@ -54,33 +60,27 @@
days.reduce((max, day) => Math.max(max, day.count), 0)
);
const minMonth = $derived.by<YearMonth | null>(() => {
if (days.length === 0) {
return null;
}
const date = new Date(days[0].day);
return { year: date.getUTCFullYear(), month: date.getUTCMonth() };
});
const maxMonth = $derived.by<YearMonth | null>(() => {
if (days.length === 0) {
return null;
}
const date = new Date(days.at(-1)?.day ?? days[0].day);
return { year: date.getUTCFullYear(), month: date.getUTCMonth() };
});
const view = $derived(cursor ?? maxMonth);
const view = $derived(cursor ?? currentMonth);
function index(value: YearMonth): number {
return value.year * 12 + value.month;
}
const canPrev = $derived(
Boolean(view && minMonth && index(view) > index(minMonth))
);
const canNext = $derived(
Boolean(view && maxMonth && index(view) < index(maxMonth))
);
const canNext = $derived(index(view) < index(currentMonth));
function monthKey(value: YearMonth): string {
return `${value.year}-${value.month}`;
}
function monthRange(value: YearMonth): {
date_from: string;
date_to: string;
} {
return {
date_from: new Date(Date.UTC(value.year, value.month, 1)).toISOString(),
date_to: new Date(Date.UTC(value.year, value.month + 1, 1)).toISOString(),
};
}
function level(count: number): number {
if (count <= 0 || maxCount <= 0) {
@@ -92,35 +92,17 @@
);
}
function clamp(value: YearMonth): YearMonth {
if (minMonth && index(value) < index(minMonth)) {
return minMonth;
}
if (maxMonth && index(value) > index(maxMonth)) {
return maxMonth;
}
return value;
}
function shift(months: number) {
if (!view) {
return;
}
const total = index(view) + months;
cursor = clamp({ year: Math.floor(total / 12), month: total % 12 });
const total = Math.min(index(view) + months, index(currentMonth));
cursor = { year: Math.floor(total / 12), month: total % 12 };
}
const lead = $derived(
view
? (new Date(Date.UTC(view.year, view.month, 1)).getUTCDay() + 6) % 7
: 0
(new Date(Date.UTC(view.year, view.month, 1)).getUTCDay() + 6) % 7
);
const blanks = $derived(Array.from({ length: lead }, (_, i) => i));
const cells = $derived.by<DayCell[]>(() => {
if (!view) {
return [];
}
const count = new Date(Date.UTC(view.year, view.month + 1, 0)).getUTCDate();
const result: DayCell[] = [];
for (let day = 1; day <= count; day++) {
@@ -133,17 +115,30 @@
return result;
});
$effect(() => {
const _id = chatId;
const _account = accounts.selectedId;
cache.clear();
cursor = null;
days = [];
});
$effect(() => {
const _id = chatId;
if (accounts.selectedId === null) {
return;
}
const target = view;
const cached = cache.get(monthKey(target));
if (cached !== undefined) {
days = cached;
return;
}
let active = true;
loading = true;
days = [];
cursor = null;
getChatCalendar(chatId)
getChatCalendar(chatId, monthRange(target))
.then((result) => {
cache.set(monthKey(target), result);
if (active) {
days = result;
}
@@ -175,8 +170,6 @@
{#if loading && days.length === 0}
<div class="center"><Spinner /></div>
{:else if !view}
<EmptyState title="Нет сообщений" />
{:else}
<div class="calendar">
<header class="nav">
@@ -184,7 +177,6 @@
type="button"
class="step"
onclick={() => shift(-12)}
disabled={!canPrev}
aria-label="Предыдущий год"
>
«
@@ -193,7 +185,6 @@
type="button"
class="step"
onclick={() => shift(-1)}
disabled={!canPrev}
aria-label="Предыдущий месяц"
>
<Icon name="arrow-left" />
@@ -35,6 +35,7 @@
let items = $state<FileShare[]>([]);
let loading = $state(false);
let filter = $state<Filter>("active");
let fetchedAll = $state(false);
let expanded = $state<number | null>(null);
let token = 0;
@@ -44,16 +45,20 @@
: items
);
const activeCount = $derived(
items.filter((item) => shareState(item) === "active").length
fetchedAll
? items.filter((item) => shareState(item) === "active").length
: items.length
);
async function load() {
const current = ++token;
const activeOnly = filter === "active";
loading = true;
try {
const rows = await listShares();
const rows = await listShares(activeOnly);
if (current === token) {
items = rows;
fetchedAll = !activeOnly;
}
} catch {
if (current === token) {
@@ -118,7 +123,7 @@
let loadedKey = "";
$effect(() => {
const key = `${accounts.selectedId}:${shareUi.revision}`;
const key = `${accounts.selectedId}:${shareUi.revision}:${filter}`;
if (accounts.selectedId !== null && key !== loadedKey) {
loadedKey = key;
load();
@@ -2,7 +2,7 @@
import { untrack } from "svelte";
import {
enqueueStoriesBackfill,
getChat,
getChatsBatch,
getPeers,
getStories,
} from "$lib/api/endpoints";
@@ -63,17 +63,13 @@
const chatIds = [...byPeer.keys()].filter((id) => id < 0);
const [peers, fetched] = await Promise.all([
getPeers(peerIds),
Promise.all(chatIds.map((id) => getChat(id))),
getChatsBatch(chatIds),
]);
if (current !== token) {
return;
}
const peerById = new Map(peers.map((peer) => [peer.peer_id, peer]));
const chatById = new Map(
fetched
.filter((item) => item !== null)
.map((item) => [item.chat_id, item])
);
const chatById = new Map(fetched.map((item) => [item.chat_id, item]));
groups = [...byPeer.entries()].map(([peerId, stories]) => {
if (peerId > 0) {
const peer = peerById.get(peerId) ?? null;