mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
perf: fetch the server plugin list once per page load
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({ apiFetch: vi.fn() }));
|
||||
vi.mock('@/lib/browser-navigation', async (orig) => ({
|
||||
...(await orig<typeof import('@/lib/browser-navigation')>()),
|
||||
apiFetch: (...args: unknown[]) => mocks.apiFetch(...args),
|
||||
}));
|
||||
vi.mock('@/lib/plugin-loader', () => ({
|
||||
loadPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
deactivatePlugin: vi.fn(),
|
||||
setPluginStoreAccessor: vi.fn(),
|
||||
setupAutoDisable: vi.fn(),
|
||||
setSandboxLocale: vi.fn(),
|
||||
}));
|
||||
|
||||
import { fetchServerPluginList } from '@/lib/server-plugin-list';
|
||||
import { usePluginStore } from '@/stores/plugin-store';
|
||||
import { useThemeStore } from '@/stores/theme-store';
|
||||
|
||||
const pluginListCalls = () => mocks.apiFetch.mock.calls.filter(([url]) => url === '/api/plugins');
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.apiFetch.mockReset();
|
||||
mocks.apiFetch.mockImplementation(async () => new Response(JSON.stringify({ plugins: [], themes: [] })));
|
||||
});
|
||||
|
||||
describe('the server plugin list', () => {
|
||||
it('is fetched once when the plugin and theme stores sync at the same time', async () => {
|
||||
usePluginStore.setState({ plugins: [], initialized: false });
|
||||
|
||||
// What the mail view does on mount.
|
||||
await Promise.all([
|
||||
usePluginStore.getState().initializePlugins(),
|
||||
useThemeStore.getState().syncServerThemes(),
|
||||
]);
|
||||
|
||||
expect(pluginListCalls()).toHaveLength(1);
|
||||
expect(usePluginStore.getState().initialized).toBe(true);
|
||||
});
|
||||
|
||||
it('is fetched again by a later call', async () => {
|
||||
const first = await fetchServerPluginList();
|
||||
const second = await fetchServerPluginList();
|
||||
|
||||
expect(pluginListCalls()).toHaveLength(2);
|
||||
expect(first.data).toEqual({ plugins: [], themes: [] });
|
||||
expect(second.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('passes a refused request on without a body', async () => {
|
||||
mocks.apiFetch.mockImplementation(async () => new Response('{"error":"Not authenticated"}', { status: 401 }));
|
||||
|
||||
await expect(fetchServerPluginList()).resolves.toEqual({ ok: false, status: 401, data: null });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// GET /api/plugins, shared by the plugin store and the theme store.
|
||||
//
|
||||
// Both sync from the same list when the app starts, and each used to fetch
|
||||
// it: two copies of a response that carries every plugin's translations
|
||||
// (hundreds of kB compressed) on every page load. Calls that overlap share
|
||||
// one request; a later call (after signing in, say) fetches again.
|
||||
|
||||
import { apiFetch } from '@/lib/browser-navigation';
|
||||
|
||||
export interface ServerPluginListResult {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
/** The parsed body when `ok`, otherwise null. */
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
let inFlight: Promise<ServerPluginListResult> | null = null;
|
||||
|
||||
export function fetchServerPluginList(): Promise<ServerPluginListResult> {
|
||||
if (!inFlight) {
|
||||
inFlight = (async () => {
|
||||
const res = await apiFetch('/api/plugins');
|
||||
return { ok: res.ok, status: res.status, data: res.ok ? await res.json() : null };
|
||||
})().finally(() => { inFlight = null; });
|
||||
}
|
||||
return inFlight;
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { get as getActivePlugin } from '@/lib/plugin-sandbox/registry';
|
||||
import { coerceSetting, effectiveSettings } from '@/lib/plugin-settings';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
import { apiFetch } from '@/lib/browser-navigation';
|
||||
import { fetchServerPluginList } from '@/lib/server-plugin-list';
|
||||
import { IMPLICIT_PERMISSIONS } from '@/lib/plugin-types';
|
||||
import type { Permission } from '@/lib/plugin-types';
|
||||
import { IS_LITE } from '@/lib/lite';
|
||||
@@ -569,12 +570,12 @@ async function syncServerPlugins(
|
||||
set: (partial: Partial<PluginStoreState> | ((state: PluginStoreState) => Partial<PluginStoreState>)) => void,
|
||||
): Promise<'signed-out' | void> {
|
||||
try {
|
||||
const res = await apiFetch('/api/plugins');
|
||||
const res = await fetchServerPluginList();
|
||||
if (res.status === 401) return 'signed-out';
|
||||
if (!res.ok) return;
|
||||
|
||||
const data: { plugins: ServerPluginInfo[] } = await res.json();
|
||||
if (!data.plugins || !Array.isArray(data.plugins)) return;
|
||||
const data = res.data as { plugins: ServerPluginInfo[] };
|
||||
if (!data?.plugins || !Array.isArray(data.plugins)) return;
|
||||
|
||||
const serverPlugins = data.plugins;
|
||||
const serverPluginIds = new Set(serverPlugins.map(p => p.id));
|
||||
|
||||
@@ -14,6 +14,7 @@ import { extractTheme } from '@/lib/plugin-validator';
|
||||
import { BUILTIN_THEMES } from '@/lib/builtin-themes';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
import { apiFetch } from '@/lib/browser-navigation';
|
||||
import { fetchServerPluginList } from '@/lib/server-plugin-list';
|
||||
import { themeHooks } from '@/lib/plugin-hooks';
|
||||
import { syncThemeColorMeta } from '@/lib/theme-color-meta';
|
||||
import { IS_LITE } from '@/lib/lite';
|
||||
@@ -368,11 +369,11 @@ export const useThemeStore = create<ThemeState>()(
|
||||
|
||||
themeSyncPromise = (async () => {
|
||||
try {
|
||||
const res = await apiFetch('/api/plugins');
|
||||
const res = await fetchServerPluginList();
|
||||
if (!res.ok) return;
|
||||
|
||||
const data: { themes: ServerThemeInfo[] } = await res.json();
|
||||
if (!data.themes || !Array.isArray(data.themes)) return;
|
||||
const data = res.data as { themes: ServerThemeInfo[] };
|
||||
if (!data?.themes || !Array.isArray(data.themes)) return;
|
||||
|
||||
const serverThemes = data.themes;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user