️ perf(kingbase): 优化数据表首屏加载性能

- 连接池保留 Kingbase 空闲连接,减少重复建连开销
- 首屏数据查询完成后再加载编辑定位元数据
- 合并列、索引和外键元数据请求并保留强制刷新
- 新增连接复用、加载时序和请求缓存回归测试
This commit is contained in:
Syngnat
2026-07-30 11:26:50 +08:00
parent c42a8679e9
commit 09a1bceabf
9 changed files with 281 additions and 15 deletions

View 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);
});
});

View 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();
};