Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { createFileRoute } from '@tanstack/react-router';
import { BoardRouteWarmup } from 'modules.board/warmup';
import { lazy } from 'react';
import { z } from 'zod';

Expand Down Expand Up @@ -38,6 +39,7 @@ export const Route = createFileRoute('/(app)/_layout/classrooms/$classroomId/')(
function ClassroomPageComponent() {
return (
<>
<BoardRouteWarmup />
<ClassroomPage />
</>
);
Expand Down
8 changes: 7 additions & 1 deletion apps/xi.web/src/pages/(app)/_layout/materials/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { createFileRoute } from '@tanstack/react-router';
import { BoardRouteWarmup } from 'modules.board/warmup';
import { MaterialsPage } from 'pages.materials';
import { z } from 'zod';

Expand All @@ -8,7 +9,12 @@ const searchSchema = z.object({
});

const Materials = () => {
return <MaterialsPage />;
return (
<>
<BoardRouteWarmup />
<MaterialsPage />
</>
);
};

// @ts-ignore
Expand Down
3 changes: 2 additions & 1 deletion packages/modules.board/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"version": "0.0.0",
"type": "module",
"exports": {
".": "./index.ts"
".": "./index.ts",
"./warmup": "./src/warmup/index.ts"
},
"license": "MIT",
"scripts": {
Expand Down
32 changes: 32 additions & 0 deletions packages/modules.board/src/warmup/BoardRouteWarmup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { env } from 'common.env';
import { useEffect, useRef } from 'react';
import { prefetchBoardModule } from './prefetchBoardModule';
import {
ensureResourceHint,
scheduleIdleTask,
shouldSkipBackgroundPrefetch,
toHttpOrigin,
} from './utils';

/** Фоновый прогрев доски на страницах, откуда чаще всего открывают доску (кабинет, материалы). */
export const BoardRouteWarmup = () => {
const startedRef = useRef(false);

useEffect(() => {
if (startedRef.current || shouldSkipBackgroundPrefetch()) return;

ensureResourceHint('preconnect', toHttpOrigin(env.VITE_SERVER_URL_HOCUS), true);
ensureResourceHint('dns-prefetch', env.VITE_SERVER_URL_BACKEND);

const cancel = scheduleIdleTask(() => {
startedRef.current = true;
void prefetchBoardModule().catch(() => {
startedRef.current = false;
});
}, 3000);

return cancel;
}, []);

return null;
};
3 changes: 3 additions & 0 deletions packages/modules.board/src/warmup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { BoardRouteWarmup } from './BoardRouteWarmup';
export { prefetchBoardModule } from './prefetchBoardModule';
export { prefetchBoardStorageItem } from './prefetchBoardStorageItem';
13 changes: 13 additions & 0 deletions packages/modules.board/src/warmup/prefetchBoardModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
let boardModulePrefetch: Promise<unknown> | null = null;

/** Подгружает DrawBoard и зависимости — тот же граф, что при открытии доски. */
export function prefetchBoardModule(): Promise<unknown> {
if (!boardModulePrefetch) {
boardModulePrefetch = import('../ui/DrawBoard').catch((error) => {
boardModulePrefetch = null;
throw error;
});
}

return boardModulePrefetch;
}
43 changes: 43 additions & 0 deletions packages/modules.board/src/warmup/prefetchBoardStorageItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { QueryClient } from '@tanstack/react-query';
import { classroomMaterialsApiConfig, ClassroomMaterialsQueryKey } from 'common.api';
import { getAxiosInstance } from 'common.config';

async function fetchClassroomStorageItem(
classroomId: string,
boardId: string,
isTutor: boolean,
): Promise<unknown> {
const queryKey = isTutor
? ClassroomMaterialsQueryKey.ClassroomStorageItem
: ClassroomMaterialsQueryKey.ClassroomStorageItemStudent;
const apiConfig = classroomMaterialsApiConfig[queryKey];
const axiosInstance = await getAxiosInstance();

const response = await axiosInstance({
method: apiConfig.method,
url: apiConfig.getUrl(classroomId, boardId),
headers: {
'Content-Type': 'application/json',
},
});

return response.data;
}

export function prefetchBoardStorageItem(
queryClient: QueryClient,
classroomId: string,
boardId: string,
isTutor: boolean,
): Promise<void> {
const queryKey = isTutor
? [ClassroomMaterialsQueryKey.ClassroomStorageItem, classroomId, boardId]
: [ClassroomMaterialsQueryKey.ClassroomStorageItemStudent, classroomId, boardId];

return queryClient
.prefetchQuery({
queryKey,
queryFn: () => fetchClassroomStorageItem(classroomId, boardId, isTutor),
})
.then(() => undefined);
}
42 changes: 42 additions & 0 deletions packages/modules.board/src/warmup/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
type NetworkInformationLike = {
saveData?: boolean;
effectiveType?: string;
};

export function shouldSkipBackgroundPrefetch(): boolean {
const conn = (navigator as Navigator & { connection?: NetworkInformationLike }).connection;
if (!conn) return false;
if (conn.saveData) return true;
return conn.effectiveType === '2g' || conn.effectiveType === 'slow-2g';
}

export function scheduleIdleTask(task: () => void, timeoutMs = 3000): () => void {
if (typeof requestIdleCallback !== 'undefined') {
const id = requestIdleCallback(() => task(), { timeout: timeoutMs });
return () => cancelIdleCallback(id);
}

const id = window.setTimeout(task, 1000);
return () => clearTimeout(id);
}

export function toHttpOrigin(url: string): string {
return url.replace(/^wss:\/\//, 'https://').replace(/^ws:\/\//, 'http://');
}

export function ensureResourceHint(
rel: 'preconnect' | 'dns-prefetch',
href: string,
crossOrigin = false,
): void {
const selector = `link[rel="${rel}"][href="${href}"]`;
if (document.head.querySelector(selector)) return;

const link = document.createElement('link');
link.rel = rel;
link.href = href;
if (crossOrigin) {
link.crossOrigin = 'anonymous';
}
document.head.appendChild(link);
}
1 change: 1 addition & 0 deletions packages/modules.calls/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"common.env": "workspace:*",
"common.services": "workspace:*",
"common.utils": "workspace:*",
"modules.board": "workspace:*",
"@xipkg/calls": "^0.1.4",
"@xipkg/calls-chat": "^0.1.4",
"@xipkg/calls-compactview": "^0.1.4",
Expand Down
2 changes: 2 additions & 0 deletions packages/modules.calls/src/CallsShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { callsSessionPort } from './callsSession';
import { createCallsRuntimeConfig } from './createCallsRuntimeConfig';
import { useCallsDeps } from './useCallsDeps';
import { ProductCallAnalyticsTracker } from './productAnalytics/ProductCallAnalyticsTracker';
import { BoardCallStorageWarmup } from './boardWarmup/BoardCallStorageWarmup';

import '@xipkg/calls-ui/video-security.css';
import '@xipkg/calls-ui/driver.css';
Expand Down Expand Up @@ -62,6 +63,7 @@ const CallsShellProviders = ({ children }: CallsShellPropsT) => {
<LiveKitProvider>
<ModeSyncProvider>
<ProductCallAnalyticsTracker />
<BoardCallStorageWarmup />
<CallsShellInit />
{children}
</ModeSyncProvider>
Expand Down
25 changes: 25 additions & 0 deletions packages/modules.calls/src/boardWarmup/BoardCallStorageWarmup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useQueryClient } from '@tanstack/react-query';
import { useCallStore } from '@xipkg/calls-store';
import { useCurrentUser } from 'common.services';
import { prefetchBoardStorageItem } from 'modules.board/warmup';
import { useEffect } from 'react';

/** Prefetch storage-item при смене activeBoardId в звонке (до редиректа на доску). */
export const BoardCallStorageWarmup = () => {
const queryClient = useQueryClient();
const { data: user } = useCurrentUser();
const activeBoardId = useCallStore((state) => state.activeBoardId);
const activeClassroom = useCallStore((state) => state.activeClassroom);

const isTutor = user?.default_layout === 'tutor';

useEffect(() => {
if (!activeBoardId || !activeClassroom) return;

void prefetchBoardStorageItem(queryClient, activeClassroom, activeBoardId, isTutor).catch(
() => undefined,
);
}, [activeBoardId, activeClassroom, isTutor, queryClient]);

return null;
};
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading