mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-22 08:53:46 +08:00
⚡️ perf(kingbase): 优化数据表首屏加载性能
- 连接池保留 Kingbase 空闲连接,减少重复建连开销 - 首屏数据查询完成后再加载编辑定位元数据 - 合并列、索引和外键元数据请求并保留强制刷新 - 新增连接复用、加载时序和请求缓存回归测试
This commit is contained in:
51
frontend/src/utils/tableMetadataRequestCache.test.ts
Normal file
51
frontend/src/utils/tableMetadataRequestCache.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
requestTableMetadata,
|
||||
resetTableMetadataRequestCacheForTests,
|
||||
} from './tableMetadataRequestCache';
|
||||
|
||||
const requestKey = {
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'ldf_server_dbs',
|
||||
tableName: 'ldf_server.andon_dash_events',
|
||||
kind: 'columns' as const,
|
||||
};
|
||||
|
||||
describe('table metadata request cache', () => {
|
||||
afterEach(() => {
|
||||
resetTableMetadataRequestCacheForTests();
|
||||
});
|
||||
|
||||
it('coalesces concurrent requests and briefly reuses the settled result', async () => {
|
||||
let resolveRequest!: (value: { success: boolean }) => void;
|
||||
const loader = vi.fn(() => new Promise<{ success: boolean }>((resolve) => {
|
||||
resolveRequest = resolve;
|
||||
}));
|
||||
|
||||
const first = requestTableMetadata(requestKey, loader);
|
||||
const concurrent = requestTableMetadata(requestKey, loader);
|
||||
|
||||
expect(loader).toHaveBeenCalledTimes(1);
|
||||
expect(concurrent).toBe(first);
|
||||
|
||||
resolveRequest({ success: true });
|
||||
await expect(first).resolves.toEqual({ success: true });
|
||||
|
||||
const settled = requestTableMetadata(requestKey, loader);
|
||||
expect(loader).toHaveBeenCalledTimes(1);
|
||||
expect(settled).toBe(first);
|
||||
});
|
||||
|
||||
it('supports an explicit refresh and retries after a rejected request', async () => {
|
||||
const loader = vi.fn()
|
||||
.mockRejectedValueOnce(new Error('metadata failed'))
|
||||
.mockResolvedValue({ success: true });
|
||||
|
||||
await expect(requestTableMetadata(requestKey, loader)).rejects.toThrow('metadata failed');
|
||||
await expect(requestTableMetadata(requestKey, loader)).resolves.toEqual({ success: true });
|
||||
await expect(requestTableMetadata(requestKey, loader, { force: true })).resolves.toEqual({ success: true });
|
||||
|
||||
expect(loader).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
89
frontend/src/utils/tableMetadataRequestCache.ts
Normal file
89
frontend/src/utils/tableMetadataRequestCache.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
export type TableMetadataRequestKind = 'columns' | 'indexes' | 'foreignKeys';
|
||||
|
||||
export type TableMetadataRequestKey = {
|
||||
connectionId: string;
|
||||
dbName: string;
|
||||
tableName: string;
|
||||
kind: TableMetadataRequestKind;
|
||||
};
|
||||
|
||||
type TableMetadataRequestOptions = {
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
type TableMetadataRequestEntry<T = unknown> = {
|
||||
promise: Promise<T>;
|
||||
settledAt?: number;
|
||||
};
|
||||
|
||||
const TABLE_METADATA_RESULT_TTL_MS = 5_000;
|
||||
const MAX_TABLE_METADATA_REQUESTS = 128;
|
||||
const tableMetadataRequests = new Map<string, TableMetadataRequestEntry>();
|
||||
|
||||
const serializeTableMetadataRequestKey = (key: TableMetadataRequestKey): string => JSON.stringify([
|
||||
String(key.kind || '').trim(),
|
||||
String(key.connectionId || '').trim(),
|
||||
String(key.dbName || '').trim(),
|
||||
String(key.tableName || '').trim(),
|
||||
]);
|
||||
|
||||
const pruneTableMetadataRequests = (now: number) => {
|
||||
tableMetadataRequests.forEach((entry, key) => {
|
||||
if (entry.settledAt !== undefined && now - entry.settledAt > TABLE_METADATA_RESULT_TTL_MS) {
|
||||
tableMetadataRequests.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
while (tableMetadataRequests.size >= MAX_TABLE_METADATA_REQUESTS) {
|
||||
const oldestKey = tableMetadataRequests.keys().next().value;
|
||||
if (oldestKey === undefined) break;
|
||||
tableMetadataRequests.delete(oldestKey);
|
||||
}
|
||||
};
|
||||
|
||||
export const requestTableMetadata = <T>(
|
||||
key: TableMetadataRequestKey,
|
||||
loader: () => Promise<T>,
|
||||
options: TableMetadataRequestOptions = {},
|
||||
): Promise<T> => {
|
||||
const serializedKey = serializeTableMetadataRequestKey(key);
|
||||
const now = Date.now();
|
||||
const cached = tableMetadataRequests.get(serializedKey) as TableMetadataRequestEntry<T> | undefined;
|
||||
|
||||
if (!options.force && cached) {
|
||||
if (cached.settledAt === undefined || now - cached.settledAt <= TABLE_METADATA_RESULT_TTL_MS) {
|
||||
return cached.promise;
|
||||
}
|
||||
tableMetadataRequests.delete(serializedKey);
|
||||
}
|
||||
|
||||
pruneTableMetadataRequests(now);
|
||||
|
||||
let promise: Promise<T>;
|
||||
try {
|
||||
promise = Promise.resolve(loader());
|
||||
} catch (error) {
|
||||
promise = Promise.reject(error);
|
||||
}
|
||||
|
||||
const entry: TableMetadataRequestEntry<T> = { promise };
|
||||
tableMetadataRequests.set(serializedKey, entry);
|
||||
void promise.then(
|
||||
() => {
|
||||
if (tableMetadataRequests.get(serializedKey) === entry) {
|
||||
entry.settledAt = Date.now();
|
||||
}
|
||||
},
|
||||
() => {
|
||||
if (tableMetadataRequests.get(serializedKey) === entry) {
|
||||
tableMetadataRequests.delete(serializedKey);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
export const resetTableMetadataRequestCacheForTests = () => {
|
||||
tableMetadataRequests.clear();
|
||||
};
|
||||
Reference in New Issue
Block a user