mirror of
https://github.com/Kong/insomnia.git
synced 2026-07-31 01:37:32 -04:00
* perf: rewrite command palette search to eliminate UI freezes and stale results
Problems fixed:
- clientLoader blocked navigation on every palette open, freezing the UI during
full sequential DB scan
- no AbortController: stale searches could overwrite newer results mid-type
- no debounce: every keystroke triggered a full DB traversal immediately
- React Aria's default contains filter re-filtered server-side fuzzy results,
silently dropping valid matches and making fuzzy work redundant
- no warm baseline: palette started blank until the entire load completed
Changes:
- move fuzzyMatch/fuzzyMatchAll → insomnia-data/common-src/search.ts (shared,
testable in node)
- move search logic → insomnia-data/node-src/services/helpers/command-search.ts
- per-request AbortController cancellation via abort(requestId)
- recursion depth guard (max 20) on request group traversal
- safeParent() replaces non-null assertions, safe against orphaned documents
- add CommandSearchResult type in insomnia-data/src/command-search-types.ts
- add useCommandSearch hook: 250ms debounce + abort on filter change + warm
baseline on mount
- add defaultFilter={() => true} to ComboBox, delegating all filtering to the
service
- delete routes/commands.tsx
* fix
* fix
447 lines
15 KiB
TypeScript
447 lines
15 KiB
TypeScript
import { services } from 'insomnia-data';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import { abortCommandSearch, commandSearch } from './command-search';
|
|
|
|
let seq = 0;
|
|
const uid = (prefix: string) => `${prefix}_cs_${++seq}`;
|
|
|
|
function makeOrg() {
|
|
const id = uid('org');
|
|
return { organizationId: id, allOrganizations: [{ id, name: `Org ${id}` }] };
|
|
}
|
|
|
|
describe('search()', () => {
|
|
it('returns empty results when the org has no projects', async () => {
|
|
const { organizationId, allOrganizations } = makeOrg();
|
|
const result = await commandSearch({
|
|
allOrganizations,
|
|
organizationId,
|
|
projectId: uid('proj'),
|
|
workspaceId: null,
|
|
filter: null,
|
|
requestId: uid('rid'),
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
requestId: expect.any(String),
|
|
current: { requests: [], files: [], environments: [] },
|
|
other: { requests: [], files: [] },
|
|
});
|
|
});
|
|
|
|
it('partitions requests: current workspace vs other workspaces', async () => {
|
|
const { organizationId, allOrganizations } = makeOrg();
|
|
const projId = uid('proj');
|
|
await services.project.create({ _id: projId, name: 'P', parentId: organizationId });
|
|
const ws1Id = uid('wrk');
|
|
const ws2Id = uid('wrk');
|
|
await services.workspace.create({ _id: ws1Id, name: 'WS1', parentId: projId, scope: 'collection' });
|
|
await services.workspace.create({ _id: ws2Id, name: 'WS2', parentId: projId, scope: 'collection' });
|
|
const r1 = await services.request.create({ _id: uid('r'), name: 'Req1', parentId: ws1Id, url: 'http://a.com' });
|
|
const r2 = await services.request.create({ _id: uid('r'), name: 'Req2', parentId: ws2Id, url: 'http://b.com' });
|
|
|
|
const result = await commandSearch({
|
|
allOrganizations,
|
|
organizationId,
|
|
projectId: projId,
|
|
workspaceId: ws1Id,
|
|
filter: null,
|
|
requestId: uid('rid'),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
current: { requests: [{ id: r1._id }] },
|
|
other: { requests: [{ id: r2._id }] },
|
|
});
|
|
});
|
|
|
|
it('partitions files: current project vs other projects', async () => {
|
|
const { organizationId, allOrganizations } = makeOrg();
|
|
const proj1Id = uid('proj');
|
|
const proj2Id = uid('proj');
|
|
await services.project.create({ _id: proj1Id, name: 'P1', parentId: organizationId });
|
|
await services.project.create({ _id: proj2Id, name: 'P2', parentId: organizationId });
|
|
const ws1 = await services.workspace.create({
|
|
_id: uid('wrk'),
|
|
name: 'File1',
|
|
parentId: proj1Id,
|
|
scope: 'collection',
|
|
});
|
|
const ws2 = await services.workspace.create({
|
|
_id: uid('wrk'),
|
|
name: 'File2',
|
|
parentId: proj2Id,
|
|
scope: 'collection',
|
|
});
|
|
|
|
const result = await commandSearch({
|
|
allOrganizations,
|
|
organizationId,
|
|
projectId: proj1Id,
|
|
workspaceId: null,
|
|
filter: null,
|
|
requestId: uid('rid'),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
current: { files: [{ id: ws1._id }] },
|
|
other: { files: [{ id: ws2._id }] },
|
|
});
|
|
});
|
|
|
|
it('filters requests by fuzzy match on name/url', async () => {
|
|
const { organizationId, allOrganizations } = makeOrg();
|
|
const projId = uid('proj');
|
|
await services.project.create({ _id: projId, name: 'P', parentId: organizationId });
|
|
const wsId = uid('wrk');
|
|
await services.workspace.create({ _id: wsId, name: 'WS', parentId: projId, scope: 'collection' });
|
|
const match = await services.request.create({
|
|
_id: uid('r'),
|
|
name: 'Get Users',
|
|
parentId: wsId,
|
|
url: 'http://api.com/users',
|
|
});
|
|
await services.request.create({ _id: uid('r'), name: 'Totally Different', parentId: wsId, url: 'http://zzz.com' });
|
|
|
|
const result = await commandSearch({
|
|
allOrganizations,
|
|
organizationId,
|
|
projectId: projId,
|
|
workspaceId: wsId,
|
|
filter: 'users',
|
|
requestId: uid('rid'),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
current: { requests: [{ id: match._id, name: 'Get Users' }] },
|
|
});
|
|
});
|
|
|
|
it('filters files (workspaces) by fuzzy match on name', async () => {
|
|
const { organizationId, allOrganizations } = makeOrg();
|
|
const projId = uid('proj');
|
|
await services.project.create({ _id: projId, name: 'P', parentId: organizationId });
|
|
const matchWs = await services.workspace.create({
|
|
_id: uid('wrk'),
|
|
name: 'Payments API',
|
|
parentId: projId,
|
|
scope: 'collection',
|
|
});
|
|
await services.workspace.create({
|
|
_id: uid('wrk'),
|
|
name: 'Completely Unrelated',
|
|
parentId: projId,
|
|
scope: 'collection',
|
|
});
|
|
|
|
const result = await commandSearch({
|
|
allOrganizations,
|
|
organizationId,
|
|
projectId: projId,
|
|
workspaceId: null,
|
|
filter: 'payments',
|
|
requestId: uid('rid'),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
current: { files: [{ id: matchWs._id, name: 'Payments API' }] },
|
|
});
|
|
});
|
|
|
|
it('includes base env + sub-environments when workspaceId is set', async () => {
|
|
const { organizationId, allOrganizations } = makeOrg();
|
|
const projId = uid('proj');
|
|
await services.project.create({ _id: projId, name: 'P', parentId: organizationId });
|
|
const wsId = uid('wrk');
|
|
await services.workspace.create({ _id: wsId, name: 'WS', parentId: projId, scope: 'collection' });
|
|
const baseEnv = await services.environment.create({ _id: uid('env'), name: 'Base Environment', parentId: wsId });
|
|
const prodEnv = await services.environment.create({ _id: uid('env'), name: 'Production', parentId: baseEnv._id });
|
|
|
|
const result = await commandSearch({
|
|
allOrganizations,
|
|
organizationId,
|
|
projectId: projId,
|
|
workspaceId: wsId,
|
|
filter: null,
|
|
requestId: uid('rid'),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
current: {
|
|
environments: expect.arrayContaining([
|
|
expect.objectContaining({ _id: baseEnv._id }),
|
|
expect.objectContaining({ _id: prodEnv._id }),
|
|
]),
|
|
},
|
|
});
|
|
});
|
|
|
|
it('returns no environments when workspaceId is null', async () => {
|
|
const { organizationId, allOrganizations } = makeOrg();
|
|
const projId = uid('proj');
|
|
await services.project.create({ _id: projId, name: 'P', parentId: organizationId });
|
|
const wsId = uid('wrk');
|
|
await services.workspace.create({ _id: wsId, name: 'WS', parentId: projId, scope: 'collection' });
|
|
await services.environment.create({ _id: uid('env'), name: 'Base Environment', parentId: wsId });
|
|
|
|
const result = await commandSearch({
|
|
allOrganizations,
|
|
organizationId,
|
|
projectId: projId,
|
|
workspaceId: null,
|
|
filter: null,
|
|
requestId: uid('rid'),
|
|
});
|
|
|
|
expect(result).toMatchObject({ current: { environments: [] } });
|
|
});
|
|
|
|
it('filters environments by fuzzy match on name', async () => {
|
|
const { organizationId, allOrganizations } = makeOrg();
|
|
const projId = uid('proj');
|
|
await services.project.create({ _id: projId, name: 'P', parentId: organizationId });
|
|
const wsId = uid('wrk');
|
|
await services.workspace.create({ _id: wsId, name: 'WS', parentId: projId, scope: 'collection' });
|
|
const baseEnv = await services.environment.create({ _id: uid('env'), name: 'Base Environment', parentId: wsId });
|
|
const matchEnv = await services.environment.create({ _id: uid('env'), name: 'Production', parentId: baseEnv._id });
|
|
await services.environment.create({ _id: uid('env'), name: 'Development', parentId: baseEnv._id });
|
|
|
|
const result = await commandSearch({
|
|
allOrganizations,
|
|
organizationId,
|
|
projectId: projId,
|
|
workspaceId: wsId,
|
|
filter: 'production',
|
|
requestId: uid('rid'),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
current: { environments: [expect.objectContaining({ _id: matchEnv._id, name: 'Production' })] },
|
|
});
|
|
});
|
|
|
|
it('finds requests nested inside request groups (2 levels deep)', async () => {
|
|
const { organizationId, allOrganizations } = makeOrg();
|
|
const projId = uid('proj');
|
|
await services.project.create({ _id: projId, name: 'P', parentId: organizationId });
|
|
const wsId = uid('wrk');
|
|
await services.workspace.create({ _id: wsId, name: 'WS', parentId: projId, scope: 'collection' });
|
|
const folder = await services.requestGroup.create({ _id: uid('rg'), name: 'Auth', parentId: wsId });
|
|
const nested = await services.requestGroup.create({ _id: uid('rg'), name: 'OAuth', parentId: folder._id });
|
|
const req = await services.request.create({
|
|
_id: uid('r'),
|
|
name: 'Token',
|
|
parentId: nested._id,
|
|
url: 'http://auth.com',
|
|
});
|
|
|
|
const result = await commandSearch({
|
|
allOrganizations,
|
|
organizationId,
|
|
projectId: projId,
|
|
workspaceId: wsId,
|
|
filter: null,
|
|
requestId: uid('rid'),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
current: { requests: [{ id: req._id, name: 'Token' }] },
|
|
});
|
|
});
|
|
|
|
it('includes grpcRequests and webSocketRequests', async () => {
|
|
const { organizationId, allOrganizations } = makeOrg();
|
|
const projId = uid('proj');
|
|
await services.project.create({ _id: projId, name: 'P', parentId: organizationId });
|
|
const wsId = uid('wrk');
|
|
await services.workspace.create({ _id: wsId, name: 'WS', parentId: projId, scope: 'collection' });
|
|
const grpc = await services.grpcRequest.create({
|
|
_id: uid('gr'),
|
|
name: 'GrpcCall',
|
|
parentId: wsId,
|
|
url: 'grpc://svc',
|
|
});
|
|
const wsr = await services.webSocketRequest.create({
|
|
_id: uid('ws'),
|
|
name: 'WsEvent',
|
|
parentId: wsId,
|
|
url: 'ws://svc',
|
|
});
|
|
|
|
const result = await commandSearch({
|
|
allOrganizations,
|
|
organizationId,
|
|
projectId: projId,
|
|
workspaceId: wsId,
|
|
filter: null,
|
|
requestId: uid('rid'),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
current: {
|
|
requests: expect.arrayContaining([
|
|
expect.objectContaining({ id: grpc._id, name: 'GrpcCall' }),
|
|
expect.objectContaining({ id: wsr._id, name: 'WsEvent' }),
|
|
]),
|
|
},
|
|
});
|
|
});
|
|
|
|
it('builds the correct request URL and metadata fields', async () => {
|
|
const orgId = uid('org');
|
|
const allOrganizations = [{ id: orgId, name: 'My Org' }];
|
|
const projId = uid('proj');
|
|
await services.project.create({ _id: projId, name: 'My Project', parentId: orgId });
|
|
const wsId = uid('wrk');
|
|
await services.workspace.create({ _id: wsId, name: 'My WS', parentId: projId, scope: 'collection' });
|
|
const req = await services.request.create({
|
|
_id: uid('r'),
|
|
name: 'Get Items',
|
|
parentId: wsId,
|
|
url: 'http://example.com',
|
|
});
|
|
|
|
const result = await commandSearch({
|
|
allOrganizations,
|
|
organizationId: orgId,
|
|
projectId: projId,
|
|
workspaceId: wsId,
|
|
filter: null,
|
|
requestId: uid('rid'),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
current: {
|
|
requests: [
|
|
{
|
|
id: req._id,
|
|
name: 'Get Items',
|
|
url: `/organization/${orgId}/project/${projId}/workspace/${wsId}/debug/request/${req._id}`,
|
|
organizationId: orgId,
|
|
projectId: projId,
|
|
workspaceId: wsId,
|
|
organizationName: 'My Org',
|
|
projectName: 'My Project',
|
|
workspaceName: 'My WS',
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it('aggregates requests across multiple organizations', async () => {
|
|
const orgAId = uid('org');
|
|
const orgBId = uid('org');
|
|
const allOrganizations = [
|
|
{ id: orgAId, name: 'Org A' },
|
|
{ id: orgBId, name: 'Org B' },
|
|
];
|
|
const projA = await services.project.create({ _id: uid('proj'), name: 'ProjA', parentId: orgAId });
|
|
const projB = await services.project.create({ _id: uid('proj'), name: 'ProjB', parentId: orgBId });
|
|
const wsA = await services.workspace.create({
|
|
_id: uid('wrk'),
|
|
name: 'WsA',
|
|
parentId: projA._id,
|
|
scope: 'collection',
|
|
});
|
|
const wsB = await services.workspace.create({
|
|
_id: uid('wrk'),
|
|
name: 'WsB',
|
|
parentId: projB._id,
|
|
scope: 'collection',
|
|
});
|
|
const reqA = await services.request.create({ _id: uid('r'), name: 'ReqA', parentId: wsA._id, url: 'http://a.com' });
|
|
const reqB = await services.request.create({ _id: uid('r'), name: 'ReqB', parentId: wsB._id, url: 'http://b.com' });
|
|
|
|
const result = await commandSearch({
|
|
allOrganizations,
|
|
organizationId: orgAId,
|
|
projectId: projA._id,
|
|
workspaceId: wsA._id,
|
|
filter: null,
|
|
requestId: uid('rid'),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
current: { requests: [{ id: reqA._id, organizationId: orgAId }] },
|
|
other: { requests: [{ id: reqB._id, organizationId: orgBId }] },
|
|
});
|
|
});
|
|
|
|
it('scratchpad org scopes only to its own projects, ignoring allOrganizations', async () => {
|
|
const scratchpadOrgId = 'org_scratchpad';
|
|
const otherOrgId = uid('org');
|
|
const allOrganizations = [
|
|
{ id: scratchpadOrgId, name: 'Scratchpad' },
|
|
{ id: otherOrgId, name: 'Other Org' },
|
|
];
|
|
const scratchProj = await services.project.create({
|
|
_id: uid('proj'),
|
|
name: 'ScratchProj',
|
|
parentId: scratchpadOrgId,
|
|
});
|
|
const otherProj = await services.project.create({ _id: uid('proj'), name: 'OtherProj', parentId: otherOrgId });
|
|
const scratchWs = await services.workspace.create({
|
|
_id: uid('wrk'),
|
|
name: 'ScratchWs',
|
|
parentId: scratchProj._id,
|
|
scope: 'collection',
|
|
});
|
|
const otherWs = await services.workspace.create({
|
|
_id: uid('wrk'),
|
|
name: 'OtherWs',
|
|
parentId: otherProj._id,
|
|
scope: 'collection',
|
|
});
|
|
const scratchReq = await services.request.create({
|
|
_id: uid('r'),
|
|
name: 'ScratchReq',
|
|
parentId: scratchWs._id,
|
|
url: 'http://scratch.com',
|
|
});
|
|
await services.request.create({ _id: uid('r'), name: 'OtherReq', parentId: otherWs._id, url: 'http://other.com' });
|
|
|
|
const result = await commandSearch({
|
|
allOrganizations,
|
|
organizationId: scratchpadOrgId,
|
|
projectId: scratchProj._id,
|
|
workspaceId: scratchWs._id,
|
|
filter: null,
|
|
requestId: uid('rid'),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
current: { requests: [{ id: scratchReq._id }] },
|
|
other: { requests: [] },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('abort()', () => {
|
|
it('does not throw when aborting an unknown requestId', async () => {
|
|
await expect(abortCommandSearch('nonexistent-request-id')).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('rejects with AbortError when aborted synchronously before first DB await settles', async () => {
|
|
const { organizationId, allOrganizations } = makeOrg();
|
|
const projId = uid('proj');
|
|
await services.project.create({ _id: projId, name: 'P', parentId: organizationId });
|
|
|
|
const requestId = uid('rid');
|
|
const searchPromise = commandSearch({
|
|
allOrganizations,
|
|
organizationId,
|
|
projectId: projId,
|
|
workspaceId: null,
|
|
filter: null,
|
|
requestId,
|
|
});
|
|
// controller.abort() inside abort() is synchronous — fires the abort event
|
|
// before any NeDB microtask can resolve, guaranteeing AbortError.
|
|
void abortCommandSearch(requestId);
|
|
|
|
await expect(searchPromise).rejects.toMatchObject({ name: 'AbortError' });
|
|
});
|
|
});
|