198 lines
7.6 KiB
TypeScript
198 lines
7.6 KiB
TypeScript
import type { TreeNode, TreeNodeType } from "@/types/database";
|
|
|
|
export const SIDEBAR_TREE_ROW_HEIGHT = 28;
|
|
export const SIDEBAR_TREE_SCROLL_BUFFER = 600;
|
|
export const SIDEBAR_TREE_PRERENDER_COUNT = 48;
|
|
|
|
/**
|
|
* Sidebar trees at or above this many rows use the virtualized renderer.
|
|
* Smaller trees render plainly (all rows in the DOM). Virtualized rendering
|
|
* has a failure mode where the view pool stops matching the viewport when the
|
|
* tree mutates while scrolled (observed with the Dameng connection, whose
|
|
* subtree keeps streaming in new nodes) — a blank band appears below the last
|
|
* materialized row. Plain rendering cannot exhibit that failure, so keep
|
|
* small/medium trees off the virtual path.
|
|
*/
|
|
export const SIDEBAR_TREE_VIRTUALIZE_THRESHOLD = 500;
|
|
|
|
/**
|
|
* Once a tree is virtualized it stays virtualized until the row count drops
|
|
* below THRESHOLD - HYSTERESIS. Switching renderers remounts the whole tree,
|
|
* so the hysteresis prevents search/filter oscillation around the threshold
|
|
* from thrashing between the two branches.
|
|
*/
|
|
export const SIDEBAR_TREE_VIRTUALIZE_HYSTERESIS = 50;
|
|
|
|
export interface FlatTreeNode {
|
|
node: TreeNode;
|
|
depth: number;
|
|
id: string;
|
|
renderKey: string;
|
|
type: TreeNodeType;
|
|
poolType: string;
|
|
}
|
|
|
|
export interface FlatTreeIndex {
|
|
visibleNodes: TreeNode[];
|
|
selectableVisibleNodes: TreeNode[];
|
|
selectableVisibleNodeIndexById: Map<string, number>;
|
|
flatNodeIndexById: Map<string, number>;
|
|
nodeById: Map<string, TreeNode>;
|
|
expandableNodeIds: string[];
|
|
stickyContainerIndexByIndex: Int32Array;
|
|
nextDatabaseContainerIndexByIndex: Int32Array;
|
|
nextSchemaContainerIndexByIndex: Int32Array;
|
|
}
|
|
|
|
interface FlatTreeIndexOptions {
|
|
isSelectable: (node: TreeNode) => boolean;
|
|
isBoundary: (type: TreeNodeType) => boolean;
|
|
isDatabaseContainer: (type: TreeNodeType) => boolean;
|
|
isSchemaContainer: (type: TreeNodeType) => boolean;
|
|
}
|
|
|
|
export function appendFlatTreeRenderKey(parentRenderKey: string, node: Pick<TreeNode, "id" | "type">): string {
|
|
return `${parentRenderKey}${node.type.length}:${node.type}${node.id.length}:${node.id}`;
|
|
}
|
|
|
|
function walk(children: TreeNode[], depth: number, result: FlatTreeNode[], parentRenderKey: string) {
|
|
for (const node of children) {
|
|
// Business ids are delimiter-composed and can collide across different
|
|
// database/schema paths, while the recycler requires a unique row key.
|
|
const renderKey = appendFlatTreeRenderKey(parentRenderKey, node);
|
|
result.push({
|
|
node,
|
|
depth,
|
|
id: node.id,
|
|
renderKey,
|
|
type: node.type,
|
|
poolType: node.type === "connection-group" ? `${node.type}:${node.id}` : node.type,
|
|
});
|
|
if (node.isExpanded && node.children) {
|
|
walk(node.children, depth + 1, result, renderKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
function flatTreeNode(node: TreeNode, depth: number, renderKey: string): FlatTreeNode {
|
|
return {
|
|
node,
|
|
depth,
|
|
id: node.id,
|
|
renderKey,
|
|
type: node.type,
|
|
poolType: node.type === "connection-group" ? `${node.type}:${node.id}` : node.type,
|
|
};
|
|
}
|
|
|
|
function visibleDescendantEnd(nodes: readonly FlatTreeNode[], parentIndex: number): number {
|
|
const parentDepth = nodes[parentIndex]?.depth;
|
|
if (parentDepth == null) return parentIndex;
|
|
let end = parentIndex + 1;
|
|
while (end < nodes.length && nodes[end].depth > parentDepth) end += 1;
|
|
return end;
|
|
}
|
|
|
|
export function flattenTree(nodes: TreeNode[]): FlatTreeNode[] {
|
|
const result: FlatTreeNode[] = [];
|
|
walk(nodes, 0, result, "");
|
|
return result;
|
|
}
|
|
|
|
export function replaceFlatTreeChildren(nodes: readonly FlatTreeNode[], parentIndex: number, parent: TreeNode): FlatTreeNode[] {
|
|
if (parentIndex < 0 || parentIndex >= nodes.length || nodes[parentIndex].id !== parent.id) return [...nodes];
|
|
|
|
const parentItem = nodes[parentIndex];
|
|
const end = visibleDescendantEnd(nodes, parentIndex);
|
|
const replacement: FlatTreeNode[] = [];
|
|
if (parent.isExpanded && parent.children) walk(parent.children, parentItem.depth + 1, replacement, parentItem.renderKey);
|
|
|
|
// One splice-shaped replacement keeps recycled-list consumers from observing
|
|
// an intermediate state where old and new child ranges coexist.
|
|
return [...nodes.slice(0, parentIndex), flatTreeNode(parent, parentItem.depth, parentItem.renderKey), ...replacement, ...nodes.slice(end)];
|
|
}
|
|
|
|
export function mutateFlatTreeExpansion(nodes: readonly FlatTreeNode[], parentIndex: number, parent: TreeNode, expanded: boolean): FlatTreeNode[] {
|
|
if (parent.isExpanded !== expanded) parent.isExpanded = expanded;
|
|
return replaceFlatTreeChildren(nodes, parentIndex, parent);
|
|
}
|
|
|
|
/** Whether the visible row identities changed enough to refresh recycled views. */
|
|
export function flatTreeRowsChanged(nodes: readonly FlatTreeNode[], previousNodes: readonly FlatTreeNode[] | undefined): boolean {
|
|
if (!previousNodes || nodes.length !== previousNodes.length) return true;
|
|
return nodes.some((item, index) => {
|
|
const previous = previousNodes[index];
|
|
return !previous || item.renderKey !== previous.renderKey || item.poolType !== previous.poolType || item.depth !== previous.depth;
|
|
});
|
|
}
|
|
|
|
export function shouldVirtualizeFlatTree(count: number): boolean {
|
|
return count >= SIDEBAR_TREE_VIRTUALIZE_THRESHOLD;
|
|
}
|
|
|
|
export function createFlatTreeIndex(nodes: readonly FlatTreeNode[], options: FlatTreeIndexOptions): FlatTreeIndex {
|
|
const visibleNodes: TreeNode[] = [];
|
|
const selectableVisibleNodes: TreeNode[] = [];
|
|
const selectableVisibleNodeIndexById = new Map<string, number>();
|
|
const flatNodeIndexById = new Map<string, number>();
|
|
const nodeById = new Map<string, TreeNode>();
|
|
const expandableNodeIds: string[] = [];
|
|
const stickyContainerIndexByIndex = new Int32Array(nodes.length);
|
|
const nextDatabaseContainerIndexByIndex = new Int32Array(nodes.length);
|
|
const nextSchemaContainerIndexByIndex = new Int32Array(nodes.length);
|
|
stickyContainerIndexByIndex.fill(-1);
|
|
nextDatabaseContainerIndexByIndex.fill(-1);
|
|
nextSchemaContainerIndexByIndex.fill(-1);
|
|
|
|
let databaseContainerIndex = -1;
|
|
let schemaContainerIndex = -1;
|
|
for (let index = 0; index < nodes.length; index += 1) {
|
|
const item = nodes[index];
|
|
const node = item.node;
|
|
visibleNodes.push(node);
|
|
flatNodeIndexById.set(item.id, index);
|
|
nodeById.set(item.id, node);
|
|
if (node.children?.length) expandableNodeIds.push(item.id);
|
|
if (options.isSelectable(node)) {
|
|
selectableVisibleNodeIndexById.set(item.id, selectableVisibleNodes.length);
|
|
selectableVisibleNodes.push(node);
|
|
}
|
|
|
|
if (options.isBoundary(item.type)) {
|
|
databaseContainerIndex = -1;
|
|
schemaContainerIndex = -1;
|
|
continue;
|
|
}
|
|
if (options.isDatabaseContainer(item.type)) databaseContainerIndex = index;
|
|
if (options.isSchemaContainer(item.type)) schemaContainerIndex = index;
|
|
stickyContainerIndexByIndex[index] = databaseContainerIndex >= 0 ? databaseContainerIndex : schemaContainerIndex;
|
|
}
|
|
|
|
let nextDatabaseContainerIndex = -1;
|
|
let nextSchemaContainerIndex = -1;
|
|
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
const item = nodes[index];
|
|
if (options.isBoundary(item.type)) {
|
|
nextDatabaseContainerIndex = -1;
|
|
nextSchemaContainerIndex = -1;
|
|
continue;
|
|
}
|
|
nextDatabaseContainerIndexByIndex[index] = nextDatabaseContainerIndex;
|
|
nextSchemaContainerIndexByIndex[index] = nextSchemaContainerIndex;
|
|
if (options.isDatabaseContainer(item.type)) nextDatabaseContainerIndex = index;
|
|
if (options.isSchemaContainer(item.type)) nextSchemaContainerIndex = index;
|
|
}
|
|
|
|
return {
|
|
visibleNodes,
|
|
selectableVisibleNodes,
|
|
selectableVisibleNodeIndexById,
|
|
flatNodeIndexById,
|
|
nodeById,
|
|
expandableNodeIds,
|
|
stickyContainerIndexByIndex,
|
|
nextDatabaseContainerIndexByIndex,
|
|
nextSchemaContainerIndexByIndex,
|
|
};
|
|
}
|