1 parent
c228078e25
commit
2faa755bfe
2 files changed
+145
-11
No files matched your search
@@ -2960,6 +2960,106 @@ describe("connectionStore metadata loading", () => {
|
||||
expect(tablesGroup.children?.some((child) => child.label?.startsWith("fresh_"))).toBe(true);
|
||||
});
|
||||
|
||||
it("restores and drains unfiltered pages when expand-all races a search clear reload", async () => {
|
||||
const tables = Array.from({ length: 5 }, (_, index) => ({
|
||||
name: `t_${String(index + 1).padStart(4, "0")}`,
|
||||
table_type: "TABLE" as const,
|
||||
comment: null,
|
||||
}));
|
||||
let resolveSearchClearReload!: (tables: TableInfo[]) => void;
|
||||
let unfilteredFirstPageCalls = 0;
|
||||
const listTables = vi.fn(
|
||||
(_connectionId: string, _database: string, _schema: string, searchFilter?: string, limit?: number, offset?: number) => {
|
||||
if (searchFilter) return Promise.resolve(tables.filter((table) => table.name.includes(searchFilter)));
|
||||
if ((offset ?? 0) === 0 && ++unfilteredFirstPageCalls === 1) {
|
||||
return new Promise<TableInfo[]>((resolve) => {
|
||||
resolveSearchClearReload = resolve;
|
||||
});
|
||||
}
|
||||
return Promise.resolve(tables.slice(offset ?? 0, (offset ?? 0) + (limit ?? tables.length)));
|
||||
},
|
||||
);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listTables,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useConnectionStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
settingsStore.editorSettings.sidebarObjectDisplay = "grouped";
|
||||
settingsStore.desktopSettings.sidebar_table_page_size = 2;
|
||||
|
||||
const connection = mysqlConnection();
|
||||
const tablesGroup: TreeNode = {
|
||||
id: "mysql-1:app:__tables",
|
||||
label: "tree.tables",
|
||||
type: "group-tables",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
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: "mysql-1:app",
|
||||
label: "app",
|
||||
type: "database",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
isExpanded: true,
|
||||
children: [tablesGroup],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
store.sidebarSearchQuery = "0003";
|
||||
await store.loadObjectGroupChildren(tablesGroup, { force: true });
|
||||
expect(tablesGroup.children?.map((child) => child.label)).toEqual(["t_0003"]);
|
||||
expect(store.isTreeNodeChildrenLoaded(tablesGroup.id)).toBe(true);
|
||||
|
||||
store.sidebarSearchQuery = "";
|
||||
const searchClearReload = store.loadObjectGroupChildren(tablesGroup, { force: true });
|
||||
await vi.waitFor(() => expect(listTables).toHaveBeenCalledTimes(2));
|
||||
|
||||
await store.loadAllObjectGroupChildren(tablesGroup);
|
||||
resolveSearchClearReload(tables.slice(0, 3));
|
||||
await searchClearReload;
|
||||
|
||||
expect(tablesGroup.children?.map((child) => child.label)).toEqual(tables.map((table) => table.name));
|
||||
expect(tablesGroup.children?.some((child) => child.type === "load-more")).toBe(false);
|
||||
expect(tablesGroup.objectCount).toBe(5);
|
||||
expect(tablesGroup.isLoading).toBe(false);
|
||||
expect(listTables.mock.calls.map((call) => [call[3], call[4], call[5]])).toEqual([
|
||||
["0003", 2, undefined],
|
||||
[undefined, 3, 0],
|
||||
[undefined, 3, 0],
|
||||
[undefined, 3, 2],
|
||||
[undefined, 3, 4],
|
||||
]);
|
||||
|
||||
await store.loadAllObjectGroupChildren(tablesGroup);
|
||||
expect(listTables).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it("merges load-more table pages onto the live group node after an in-tree replacement", async () => {
|
||||
const firstPage = Array.from({ length: 201 }, (_, index) => ({
|
||||
name: `t_${String(index + 1).padStart(4, "0")}`,
|
||||
|
||||
@@ -294,6 +294,10 @@ interface LoadTreeOptions {
|
||||
connectedOnly?: boolean;
|
||||
expectedSidebarSearchQuery?: string;
|
||||
searchFilter?: string;
|
||||
// Explicit actions can load the unfiltered backing group while the global search
|
||||
// continues to control presentation; normal watcher refreshes still reject mismatches.
|
||||
allowGlobalSearchMismatch?: boolean;
|
||||
onChildrenApplied?: (node: TreeNode) => void;
|
||||
sidebarTableSearchParentId?: string;
|
||||
expectedSidebarTableSearchQuery?: string;
|
||||
tableNameFilterScopeKey?: string;
|
||||
@@ -475,6 +479,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const connectionStateRevisions = new Map<string, number>();
|
||||
const connectionErrorRevisions = new Map<string, number>();
|
||||
const treeNodeLoads = new TreeNodeLoadRegistry();
|
||||
const filteredObjectGroupChildrenIds = new Set<string>();
|
||||
const primaryVisibleObjectRefreshInFlight = new Set<string>();
|
||||
let nextLocalConnectionAttempt = 0;
|
||||
let beforeConnectHandler: BeforeConnectHandler | null = null;
|
||||
@@ -1237,6 +1242,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
/** Drop loaded/confirmed-empty markers, metadata caches, and generations for a discarded shell. */
|
||||
function forgetTreeNodeLoadState(nodeId: string) {
|
||||
filteredObjectGroupChildrenIds.delete(nodeId);
|
||||
clearLoadedChildrenCache(nodeId);
|
||||
treeNodeLoads.invalidatePrefix(nodeId);
|
||||
}
|
||||
@@ -2170,6 +2176,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
function isTreeLoadSearchChanged(searchFilter: string, options?: LoadTreeOptions): boolean {
|
||||
if (options?.sidebarTableSearchParentId) return isSidebarTableSearchQueryChanged(options);
|
||||
if (options?.allowGlobalSearchMismatch) return false;
|
||||
return (sidebarSearchQuery.value || "") !== searchFilter || isSidebarSearchQueryChanged(options);
|
||||
}
|
||||
|
||||
@@ -2262,6 +2269,11 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
confirmedEmptyTreeNodeIds.value.delete(id);
|
||||
}
|
||||
}
|
||||
for (const id of filteredObjectGroupChildrenIds) {
|
||||
if (id === prefix || id.startsWith(`${prefix}:`)) {
|
||||
filteredObjectGroupChildrenIds.delete(id);
|
||||
}
|
||||
}
|
||||
invalidateMetadataCachesByTreePrefix(prefix);
|
||||
if (options?.deletePersisted === false) return;
|
||||
const rawPrefix = `${prefix}:`;
|
||||
@@ -4534,6 +4546,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
});
|
||||
if (!options?.force && !searchFilter && !options?.sidebarTableSearchParentId && !tableNameFilterForScope) {
|
||||
if (await hydrateTreeNodeFromCache(node, objectGroupCacheKey(node))) {
|
||||
filteredObjectGroupChildrenIds.delete(node.id);
|
||||
void loadObjectGroupChildren(node, { ...options, force: true }).catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
@@ -4559,7 +4572,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
try {
|
||||
await ensureConnected(node.connectionId);
|
||||
load = reclaimTreeNodeLoad(load, node);
|
||||
if (useCachedChildren(node, options, load)) return;
|
||||
if (useCachedChildren(node, options, load)) {
|
||||
filteredObjectGroupChildrenIds.delete(node.id);
|
||||
return;
|
||||
}
|
||||
const objectTypes = objectTypesForGroupNode(node.type);
|
||||
const parentNodeId = objectGroupRefreshParentId(node);
|
||||
if (!objectTypes || !parentNodeId) return;
|
||||
@@ -4580,6 +4596,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
if (!options?.force && !searchFilter && !tableNameFilter) {
|
||||
const cached = await loadPersistedTreeChildren(node, cacheKey, load);
|
||||
if (cached.hit) {
|
||||
filteredObjectGroupChildrenIds.delete(node.id);
|
||||
if (cached.isStale) refreshStaleTreeNode(node);
|
||||
return;
|
||||
}
|
||||
@@ -4624,6 +4641,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
if (!targetNode) return;
|
||||
targetNode.objectCount = nextObjectCount;
|
||||
setChildren(targetNode, children);
|
||||
if (searchFilter) filteredObjectGroupChildrenIds.add(targetNode.id);
|
||||
else filteredObjectGroupChildrenIds.delete(targetNode.id);
|
||||
options?.onChildrenApplied?.(targetNode);
|
||||
if (!searchFilter && !isSidebarTableSearch && !tableNameFilter) {
|
||||
await savePersistedTreeChildren(cacheKey, children);
|
||||
}
|
||||
@@ -4664,7 +4684,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMoreObjectGroupChildren(node: TreeNode) {
|
||||
async function loadMoreObjectGroupChildren(node: TreeNode, options?: { searchFilter?: string }) {
|
||||
if (node.type !== "load-more" || !node.loadMore) return;
|
||||
const loadMore = node.loadMore;
|
||||
const parent = findNode(treeNodes.value, node.loadMore.parentId);
|
||||
@@ -4743,6 +4763,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
objectTypes,
|
||||
offset: loadMore.offset,
|
||||
pageSize: loadMore.pageSize,
|
||||
searchFilter: options?.searchFilter,
|
||||
force: false,
|
||||
});
|
||||
const targetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
@@ -4759,6 +4780,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
objectTypes,
|
||||
offset: loadMore.offset,
|
||||
pageSize: loadMore.pageSize,
|
||||
searchFilter: options?.searchFilter,
|
||||
force: false,
|
||||
});
|
||||
const targetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
@@ -4929,20 +4951,32 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
async function loadAllObjectGroupChildren(parent: TreeNode) {
|
||||
if (!parent.connectionId || !hasTreeNodeDatabaseContext(parent)) return;
|
||||
if (!objectTypesForGroupNode(parent.type)) return;
|
||||
let load = beginTreeNodeLoad(parent);
|
||||
const liveParent = treeNodeInSidebarTree(parent);
|
||||
if (!liveParent) return;
|
||||
if (!isTreeNodeChildrenLoaded(liveParent.id) || filteredObjectGroupChildrenIds.has(liveParent.id)) {
|
||||
let isPreparedPageCurrent: (() => boolean) | undefined;
|
||||
await loadObjectGroupChildren(liveParent, {
|
||||
force: true,
|
||||
searchFilter: "",
|
||||
allowGlobalSearchMismatch: true,
|
||||
onChildrenApplied: (targetNode) => {
|
||||
const epoch = treeNodeLoads.observe(targetNode.id);
|
||||
isPreparedPageCurrent = () => epoch.isCurrent();
|
||||
},
|
||||
});
|
||||
if (!isPreparedPageCurrent?.()) return;
|
||||
}
|
||||
|
||||
const refreshedParent = treeNodeInSidebarTree(parent);
|
||||
if (!refreshedParent) return;
|
||||
const load = beginTreeNodeLoad(refreshedParent);
|
||||
try {
|
||||
await ensureConnected(parent.connectionId);
|
||||
load = reclaimTreeNodeLoad(load, parent);
|
||||
const liveParent = treeNodeLoadTarget(load);
|
||||
if (!liveParent) return;
|
||||
if (!isTreeNodeChildrenLoaded(liveParent.id)) {
|
||||
await loadObjectGroupChildren(liveParent);
|
||||
}
|
||||
if (!load.isCurrent()) return;
|
||||
|
||||
let loadMoreNode = findTreeNodes(liveParent.children ?? [], (child) => child.type === "load-more")[0];
|
||||
while (loadMoreNode?.loadMore) {
|
||||
await loadMoreObjectGroupChildren(loadMoreNode);
|
||||
await loadMoreObjectGroupChildren(loadMoreNode, { searchFilter: "" });
|
||||
if (!load.isCurrent()) return;
|
||||
const currentParent = treeNodeLoadTarget(load);
|
||||
if (!currentParent) return;
|
||||
@@ -4951,7 +4985,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const finishedParent = treeNodeLoadTarget(load);
|
||||
if (finishedParent) finishedParent.isExpanded = true;
|
||||
} catch (e) {
|
||||
recordMetadataLoadError(parent.connectionId, e, load);
|
||||
recordMetadataLoadError(refreshedParent.connectionId!, e, load);
|
||||
throw e;
|
||||
} finally {
|
||||
finishTreeNodeLoad(load);
|
||||
|
||||
Reference in new issue
Block a user