fix: guard against infinite loops (#10281)

This commit is contained in:
Ryan Willis
2026-07-21 13:08:37 -07:00
committed by GitHub
parent 0d39a28964
commit 6c2c3b9c24
6 changed files with 95 additions and 20 deletions

View File

@@ -1,9 +1,39 @@
import { services } from 'insomnia-data';
import { services, type Workspace } from 'insomnia-data';
import { beforeEach, describe, expect, it } from 'vitest';
import { database as db } from '~/common/database';
import { checkAllProjectSyncStatus } from './project';
import { checkAllProjectSyncStatus, getUnsyncedRemoteWorkspaces, type InsomniaFile } from './project';
const mkRemoteFile = (id: string): InsomniaFile => ({
id,
name: `${id}-name`,
scope: 'unsynced',
label: 'Unsynced',
created: 0,
lastModifiedTimestamp: 0,
});
const mkWorkspace = (id: string): Workspace => ({ _id: id, scope: 'collection' }) as unknown as Workspace;
describe('getUnsyncedRemoteWorkspaces', () => {
it('excludes a remote file once its workspace exists locally', () => {
const remoteFiles = [mkRemoteFile('wrk_downloaded'), mkRemoteFile('wrk_pending')];
const workspaces = [mkWorkspace('wrk_downloaded')];
const result = getUnsyncedRemoteWorkspaces(remoteFiles, workspaces);
expect(result.map(f => f.id)).toEqual(['wrk_pending']);
});
it('de-duplicates remote files sharing an id', () => {
const remoteFiles = [mkRemoteFile('wrk_dup'), mkRemoteFile('wrk_dup')];
const result = getUnsyncedRemoteWorkspaces(remoteFiles, []);
expect(result.map(f => f.id)).toEqual(['wrk_dup']);
});
});
describe('checkAllProjectSyncStatus', () => {
beforeEach(async () => {

View File

@@ -209,8 +209,18 @@ export async function getAllLocalFiles({ projectId }: { projectId: string }) {
return files;
}
export const getUnsyncedRemoteWorkspaces = (remoteFiles: InsomniaFile[], workspaces: Workspace[]) =>
remoteFiles.filter(remoteFile => !workspaces.find(w => w._id === remoteFile.id));
export const getUnsyncedRemoteWorkspaces = (remoteFiles: InsomniaFile[], workspaces: Workspace[]) => {
const seenIds = new Set<string>();
const uniqueRemoteFiles = remoteFiles.filter(file => {
if (seenIds.has(file.id)) {
return false;
}
seenIds.add(file.id);
return true;
});
return uniqueRemoteFiles.filter(remoteFile => !workspaces.some(w => w._id === remoteFile.id));
};
/**
* Get all projects for an organization with their associated git repositories

View File

@@ -18,6 +18,7 @@ import { fuzzyMatchAll } from 'insomnia-data/common';
import { database } from '~/common/database';
import { sortMethodMap } from '~/common/sorting';
import type { Child } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId';
import { dedupeCollectionItems } from '~/ui/utils/dedupe-collection-items';
export interface SlimRequestDoc extends BaseModel {
type: 'Request' | 'GrpcRequest' | 'WebSocketRequest' | 'SocketIORequest' | 'RequestGroup';
@@ -211,14 +212,7 @@ export function flattenCollectionChildren(
const { isRequestGroup } = models.requestGroup;
const collection: Child[] = [];
const seenIds = new Set<string>();
const uniqueRequests = allRequests.filter(doc => {
if (seenIds.has(doc._id)) {
return false;
}
seenIds.add(doc._id);
return true;
});
const uniqueRequests = dedupeCollectionItems(allRequests, doc => doc._id);
// map of parentId to its direct children requests and request groups
const requestsByParentId = new Map<string, AllRequestDoc[]>();

View File

@@ -57,6 +57,7 @@ import { useLoaderDeferData } from '~/ui/hooks/use-loader-defer-data';
import { useOrganizationPermissions } from '~/ui/hooks/use-organization-features';
import insomniaLogo from '~/ui/images/insomnia-logo.svg';
import { isPrimaryClickModifier } from '~/ui/utils';
import { dedupeCollectionItems } from '~/ui/utils/dedupe-collection-items';
import { getAllRemoteBackendProjectsOfOrg } from '~/ui/utils/remote-projects';
import { Icon } from '../../icon';
@@ -75,6 +76,9 @@ import { useProjectNavigationSidebarNavigation } from './use-project-navigation-
import { useSidebarDragAndDrop } from './use-sidebar-drag-and-drop';
import { WorkspaceNode } from './workspace-node';
const getSidebarGridListItemId = (item: FlatItem): string =>
`${item.kind === 'pinnedRequest' ? 'pinned-request-' : ''}${item.doc._id}`;
interface ProjectNavigationSidebarProps {
storageRules: StorageRules;
activeNodeId?: string;
@@ -354,10 +358,11 @@ const ProjectNavigationSidebarInner = (
for (const file of files) {
const projectId = remoteIdToProjectIdMap.get(file.teamProjectId);
if (projectId) {
if (!filesByProjectId.has(projectId)) {
filesByProjectId.set(projectId, []);
const projectFiles = filesByProjectId.get(projectId) ?? [];
if (projectFiles.some(f => f.id === file.rootDocumentId)) {
continue;
}
filesByProjectId.get(projectId)?.push({
projectFiles.push({
id: file.rootDocumentId,
name: file.name,
scope: 'unsynced',
@@ -366,6 +371,7 @@ const ProjectNavigationSidebarInner = (
created: 0,
lastModifiedTimestamp: 0,
});
filesByProjectId.set(projectId, projectFiles);
}
}
@@ -996,13 +1002,16 @@ const ProjectNavigationSidebarInner = (
const [isShortcutCreateOpen, setIsShortcutCreateOpen] = useState(false);
// The item that the shortcut create dropdown is targeting by keyboard up and down arrow keys
const [shortcutTargetItemId, setShortcutTargetItemId] = useState<string | null>(null);
const visibleFlatItems = useMemo(() => flatItems.filter(i => !i.hidden), [flatItems]);
const visibleFlatItems = useMemo(
() => dedupeCollectionItems(flatItems.filter(i => !i.hidden), getSidebarGridListItemId),
[flatItems],
);
const virtualizer = useVirtualizer({
getScrollElement: () => parentRef.current,
count: visibleFlatItems.length,
estimateSize: useCallback(() => 32, []),
overscan: 30,
getItemKey: index => visibleFlatItems[index].doc._id,
getItemKey: index => getSidebarGridListItemId(visibleFlatItems[index]),
});
const sidebarDragAndDropHooks = useSidebarDragAndDrop({
flatItems,
@@ -1245,9 +1254,8 @@ const ProjectNavigationSidebarInner = (
return (
<GridListItem
// Prefix pinned-request to the key and id to ensure pinned items have a different key and id from non-pinned items with the same doc._id
key={`${item.kind === 'pinnedRequest' ? 'pinned-request-' : ''}${virtualItem.key}`}
id={`${item.kind === 'pinnedRequest' ? 'pinned-request-' : ''}${item.doc._id}`}
key={getSidebarGridListItemId(item)}
id={getSidebarGridListItemId(item)}
textValue={item.doc.name || item.kind}
onAuxClick={e => {
if (e.button === 1 && item.kind === 'collectionChild') {

View File

@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { dedupeCollectionItems } from './dedupe-collection-items';
describe('dedupeCollectionItems', () => {
it('keeps the first occurrence of each id and preserves order', () => {
const items = [{ id: 'a' }, { id: 'b' }, { id: 'a' }, { id: 'c' }];
const result = dedupeCollectionItems(items, item => item.id);
expect(result.map(i => i.id)).toEqual(['a', 'b', 'c']);
expect(result[0]).toBe(items[0]);
});
it('returns the same array reference when there are no duplicates', () => {
const items = [{ id: 'a' }, { id: 'b' }];
expect(dedupeCollectionItems(items, item => item.id)).toBe(items);
});
});

View File

@@ -0,0 +1,13 @@
export function dedupeCollectionItems<T>(items: T[], getId: (item: T) => string): T[] {
const seenIds = new Set<string>();
const uniqueItems = items.filter(item => {
const id = getId(item);
if (seenIds.has(id)) {
return false;
}
seenIds.add(id);
return true;
});
return uniqueItems.length === items.length ? items : uniqueItems;
}