fix(connection): clear warnings after metadata recovery
This commit is contained in:
10 files changed
+226
-9
No files matched your search
@@ -212,6 +212,187 @@ describe("connectionStore metadata loading", () => {
|
||||
expect(schemaNode.children?.map((node) => node.label)).toEqual(["users"]);
|
||||
});
|
||||
|
||||
it("clears a stale connection error after a schema metadata retry succeeds", async () => {
|
||||
const listSchemaInfos = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("connection slots exhausted"))
|
||||
.mockResolvedValueOnce([{ name: "public", comment: null }]);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listInstalledAgents: vi.fn().mockResolvedValue([]),
|
||||
listSchemaInfos,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = postgresConnection();
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: `${connection.id}:app`,
|
||||
label: "app",
|
||||
type: "database",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
await expect(store.loadSchemas(connection.id, "app", { force: true })).rejects.toThrow("connection slots exhausted");
|
||||
expect(store.connectionErrors[connection.id]).toBe("connection slots exhausted");
|
||||
|
||||
await store.loadSchemas(connection.id, "app", { force: true });
|
||||
|
||||
expect(store.connectionErrors[connection.id]).toBeUndefined();
|
||||
expect(store.treeNodes[0]?.children?.[0]?.children?.map((node) => node.label)).toEqual(["public"]);
|
||||
});
|
||||
|
||||
it("clears a failed metadata warning when the driver hint finishes during retry", async () => {
|
||||
let resolveAgents!: (drivers: Array<{ db_type: string; installed: boolean; update_available: boolean }>) => void;
|
||||
let resolveSchemas!: (schemas: Array<{ name: string; comment: null }>) => void;
|
||||
const listInstalledAgents = vi.fn(
|
||||
() =>
|
||||
new Promise<Array<{ db_type: string; installed: boolean; update_available: boolean }>>((resolve) => {
|
||||
resolveAgents = resolve;
|
||||
}),
|
||||
);
|
||||
const listSchemaInfos = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("connection slots exhausted"))
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<Array<{ name: string; comment: null }>>((resolve) => {
|
||||
resolveSchemas = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listInstalledAgents,
|
||||
listSchemaInfos,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = { ...postgresConnection(), db_type: "oracle" } as ConnectionConfig;
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: `${connection.id}:app`,
|
||||
label: "app",
|
||||
type: "database",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
await expect(store.loadSchemas(connection.id, "app", { force: true })).rejects.toThrow("connection slots exhausted");
|
||||
expect(store.connectionErrors[connection.id]).toBe("connection slots exhausted");
|
||||
|
||||
const retry = store.loadSchemas(connection.id, "app", { force: true });
|
||||
await vi.waitFor(() => expect(listSchemaInfos).toHaveBeenCalledTimes(2));
|
||||
|
||||
resolveAgents([{ db_type: "oracle", installed: true, update_available: true }]);
|
||||
await vi.waitFor(() => expect(store.connectionErrors[connection.id]).toContain("built-in driver update"));
|
||||
|
||||
resolveSchemas([{ name: "public", comment: null }]);
|
||||
await retry;
|
||||
|
||||
expect(store.connectionErrors[connection.id]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not clear a newer error when an older metadata request succeeds", async () => {
|
||||
let resolveSchemas!: (schemas: Array<{ name: string; comment: null }>) => void;
|
||||
const listSchemaInfos = vi.fn(
|
||||
() =>
|
||||
new Promise<Array<{ name: string; comment: null }>>((resolve) => {
|
||||
resolveSchemas = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listSchemaInfos,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = postgresConnection();
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: `${connection.id}:app`,
|
||||
label: "app",
|
||||
type: "database",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
store.setConnectionError(connection.id, "old error");
|
||||
|
||||
const load = store.loadSchemas(connection.id, "app", { force: true });
|
||||
await vi.waitFor(() => expect(listSchemaInfos).toHaveBeenCalledOnce());
|
||||
store.setConnectionError(connection.id, "newer error");
|
||||
resolveSchemas([{ name: "public", comment: null }]);
|
||||
await load;
|
||||
|
||||
expect(store.connectionErrors[connection.id]).toBe("newer error");
|
||||
});
|
||||
|
||||
it("keeps an expanding schema attached while its parent refreshes", async () => {
|
||||
const listSchemaInfos = vi.fn().mockResolvedValue([{ name: "core", comment: null }]);
|
||||
|
||||
|
||||
@@ -352,6 +352,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const cancelledLocalConnectionAttempts = new Map<string, Set<number>>();
|
||||
const successfulLocalConnectionAttempts = new Map<string, number>();
|
||||
const connectionStateRevisions = new Map<string, number>();
|
||||
const connectionErrorRevisions = new Map<string, number>();
|
||||
let nextLocalConnectionAttempt = 0;
|
||||
let beforeConnectHandler: BeforeConnectHandler | null = null;
|
||||
let initFromDiskPromise: Promise<void> | null = null;
|
||||
@@ -375,7 +376,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
|
||||
logMetadataLoadTrace(metadataTraceLogger, trace, "cache-miss", { cacheStatus: options?.force ? "refresh" : "miss", force: options?.force === true });
|
||||
const errorRevision = connectionErrorRevision(scope.connectionId);
|
||||
const result = await load();
|
||||
clearConnectionErrorIfUnchanged(scope.connectionId, errorRevision);
|
||||
metadataListPageCache.set(scope, result);
|
||||
logMetadataLoadTrace(metadataTraceLogger, trace, "done", {
|
||||
cacheStatus: options?.force ? "refresh" : "miss",
|
||||
@@ -610,6 +613,22 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
function setConnectionError(connectionId: string, message: string) {
|
||||
connectionErrors.value[connectionId] = message;
|
||||
connectionErrorRevisions.set(connectionId, connectionErrorRevision(connectionId) + 1);
|
||||
}
|
||||
|
||||
function updateConnectionErrorPresentation(connectionId: string, expectedMessage: string, message: string) {
|
||||
// Hints decorate the current error event and must not invalidate a retry's recovery revision.
|
||||
if (connectionErrors.value[connectionId] !== expectedMessage) return;
|
||||
connectionErrors.value[connectionId] = message;
|
||||
}
|
||||
|
||||
function connectionErrorRevision(connectionId?: string | null): number {
|
||||
return connectionId ? (connectionErrorRevisions.get(connectionId) ?? 0) : 0;
|
||||
}
|
||||
|
||||
function clearConnectionErrorIfUnchanged(connectionId: string | null | undefined, revision: number) {
|
||||
if (!connectionId || connectionErrorRevision(connectionId) !== revision) return;
|
||||
clearConnectionError(connectionId);
|
||||
}
|
||||
|
||||
function agentDriverUpdateHint(): string {
|
||||
@@ -641,19 +660,20 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const config = getConfig(connectionId);
|
||||
const message = connectionErrorWithDriverUpdateHint(config, baseMessage);
|
||||
if (message !== baseMessage) {
|
||||
setConnectionError(connectionId, message);
|
||||
updateConnectionErrorPresentation(connectionId, baseMessage, message);
|
||||
return;
|
||||
}
|
||||
void refreshAgentDriversForErrorHint().then(() => {
|
||||
if (connectionErrors.value[connectionId] !== baseMessage) return;
|
||||
const refreshedMessage = connectionErrorWithDriverUpdateHint(config, baseMessage);
|
||||
if (refreshedMessage !== baseMessage) setConnectionError(connectionId, refreshedMessage);
|
||||
if (refreshedMessage !== baseMessage) updateConnectionErrorPresentation(connectionId, baseMessage, refreshedMessage);
|
||||
});
|
||||
}
|
||||
|
||||
function clearConnectionError(connectionId: string) {
|
||||
if (!connectionErrors.value[connectionId]) return;
|
||||
delete connectionErrors.value[connectionId];
|
||||
connectionErrorRevisions.set(connectionId, connectionErrorRevision(connectionId) + 1);
|
||||
}
|
||||
|
||||
function markConnectionHealthChecked(connectionId: string) {
|
||||
@@ -703,9 +723,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
async function withMetadataLoadTimeout<T>(connectionId: string, promise: Promise<T>, label: string): Promise<T> {
|
||||
const timeoutMs = metadataLoadTimeoutMs(getConfig(connectionId));
|
||||
const errorRevision = connectionErrorRevision(connectionId);
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
return await Promise.race([
|
||||
const result = await Promise.race([
|
||||
promise,
|
||||
new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
@@ -713,6 +734,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}, timeoutMs);
|
||||
}),
|
||||
]);
|
||||
clearConnectionErrorIfUnchanged(connectionId, errorRevision);
|
||||
return result;
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
@@ -1753,6 +1776,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
persistPinnedTreeNodeIds();
|
||||
for (const id of removedIds) {
|
||||
clearConnectionError(id);
|
||||
connectionErrorRevisions.delete(id);
|
||||
connectedIds.value.delete(id);
|
||||
clearConnectionIdentifierQuote(id);
|
||||
clearConnectionHealthCheck(id);
|
||||
@@ -2821,7 +2845,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
}
|
||||
|
||||
const schemas = sortSidebarSchemaInfos(await api.listSchemaInfos(connectionId, database));
|
||||
const schemas = sortSidebarSchemaInfos(await withMetadataLoadTimeout(connectionId, api.listSchemaInfos(connectionId, database), "schemas"));
|
||||
const visibleSchemaNames = new Set(
|
||||
filterSchemaNamesForConnection(
|
||||
schemas.map((schema) => schema.name),
|
||||
|
||||
Reference in new issue
Block a user