Skip to content

Commit 39c48de

Browse files
committed
paginate resource and stack list
1 parent da1685d commit 39c48de

20 files changed

+526
-272
lines changed

src/handlers/ResourceHandler.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
ResourceStateResult,
1414
ResourceSummary,
1515
ResourceIdentifier,
16+
SearchResourceParams,
17+
SearchResourceResult,
1618
} from '../resourceState/ResourceStateTypes';
1719
import { ResourceStackManagementResult } from '../resourceState/StackManagementInfoProvider';
1820
import { ServerComponents } from '../server/ServerComponents';
@@ -38,22 +40,26 @@ export function getResourceTypesHandler(
3840

3941
export function listResourcesHandler(
4042
components: ServerComponents,
41-
): ServerRequestHandler<ListResourcesParams, ListResourcesResult, never, void> {
43+
): RequestHandler<ListResourcesParams, ListResourcesResult, void> {
4244
return async (params: ListResourcesParams): Promise<ListResourcesResult> => {
4345
try {
44-
const resourceTypes = params.resourceTypes;
45-
if (!resourceTypes || resourceTypes.length === 0) {
46+
const resourceRequests = params.resources;
47+
if (!resourceRequests || resourceRequests.length === 0) {
4648
return { resources: [] };
4749
}
4850

4951
const resources: ResourceSummary[] = [];
5052

51-
for (const typeName of resourceTypes) {
52-
const resourceList = await components.resourceStateManager.listResources(typeName);
53+
for (const request of resourceRequests) {
54+
const resourceList = await components.resourceStateManager.listResources(
55+
request.resourceType,
56+
request.nextToken,
57+
);
5358
if (resourceList) {
5459
resources.push({
5560
typeName: resourceList.typeName,
5661
resourceIdentifiers: resourceList.resourceIdentifiers,
62+
nextToken: resourceList.nextToken,
5763
});
5864
}
5965
}
@@ -83,17 +89,41 @@ export function refreshResourceListHandler(
8389
setTimeout(() => reject(new Error('Resource list refresh timed out')), 30_000),
8490
);
8591

86-
return await Promise.race([
87-
components.resourceStateManager.refreshResourceList(params.resourceTypes),
88-
timeout,
89-
]);
92+
const resourceTypes = params.resources.map((r) => r.resourceType);
93+
return await Promise.race([components.resourceStateManager.refreshResourceList(resourceTypes), timeout]);
9094
} catch (error) {
9195
log.error(error, 'Failed to refresh resource list');
9296
throw new Error(`Failed to refresh resource list: ${extractErrorMessage(error)}`);
9397
}
9498
};
9599
}
96100

101+
export function searchResourceHandler(
102+
components: ServerComponents,
103+
): ServerRequestHandler<SearchResourceParams, SearchResourceResult, never, void> {
104+
return async (params: SearchResourceParams): Promise<SearchResourceResult> => {
105+
try {
106+
const result = await components.resourceStateManager.searchResourceByIdentifier(
107+
params.resourceType,
108+
params.identifier,
109+
);
110+
return {
111+
found: result.found,
112+
resource: result.resourceList
113+
? {
114+
typeName: result.resourceList.typeName,
115+
resourceIdentifiers: result.resourceList.resourceIdentifiers,
116+
nextToken: result.resourceList.nextToken,
117+
}
118+
: undefined,
119+
};
120+
} catch (error) {
121+
log.error(error, 'Failed to search resource');
122+
return { found: false };
123+
}
124+
};
125+
}
126+
97127
export function getStackMgmtInfo(
98128
components: ServerComponents,
99129
): ServerRequestHandler<ResourceIdentifier, ResourceStackManagementResult, never, void> {

src/handlers/StackHandler.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ResponseError, ErrorCodes, RequestHandler } from 'vscode-languageserver';
1+
import { ErrorCodes, RequestHandler, ResponseError } from 'vscode-languageserver';
22
import { TopLevelSection } from '../context/ContextType';
33
import { getEntityMap } from '../context/SectionContextBuilder';
44
import { Parameter, Resource } from '../context/semantic/Entity';
@@ -8,15 +8,15 @@ import { ServerComponents } from '../server/ServerComponents';
88
import { analyzeCapabilities } from '../stacks/actions/CapabilityAnalyzer';
99
import { parseStackActionParams, parseTemplateUriParams } from '../stacks/actions/StackActionParser';
1010
import {
11-
GetCapabilitiesResult,
12-
TemplateUri,
13-
GetParametersResult,
1411
CreateStackActionParams,
1512
CreateStackActionResult,
16-
GetStackActionStatusResult,
17-
DescribeValidationStatusResult,
1813
DescribeDeploymentStatusResult,
14+
DescribeValidationStatusResult,
15+
GetCapabilitiesResult,
16+
GetParametersResult,
17+
GetStackActionStatusResult,
1918
GetTemplateResourcesResult,
19+
TemplateUri,
2020
} from '../stacks/actions/StackActionRequestType';
2121
import { ListStacksParams, ListStacksResult } from '../stacks/StackRequestType';
2222
import { LoggerFactory } from '../telemetry/LoggerFactory';
@@ -239,10 +239,14 @@ export function listStacksHandler(
239239
if (params.statusToInclude?.length && params.statusToExclude?.length) {
240240
throw new Error('Cannot specify both statusToInclude and statusToExclude');
241241
}
242-
return { stacks: await components.cfnService.listStacks(params.statusToInclude, params.statusToExclude) };
242+
return await components.stackManager.listStacks(
243+
params.statusToInclude,
244+
params.statusToExclude,
245+
params.loadMore,
246+
);
243247
} catch (error) {
244248
log.error({ error: extractErrorMessage(error) }, 'Error listing stacks');
245-
return { stacks: [] };
249+
return { stacks: [], nextToken: undefined };
246250
}
247251
};
248252
}

src/protocol/LspResourceHandlers.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Connection, ServerRequestHandler } from 'vscode-languageserver';
2+
import { RequestHandler } from 'vscode-languageserver/node';
23
import {
34
ResourceTypesResult,
45
ResourceTypesRequest,
@@ -13,13 +14,16 @@ import {
1314
RefreshResourcesResult,
1415
StackMgmtInfoRequest,
1516
ResourceIdentifier,
17+
SearchResourceRequest,
18+
SearchResourceParams,
19+
SearchResourceResult,
1620
} from '../resourceState/ResourceStateTypes';
1721
import { ResourceStackManagementResult } from '../resourceState/StackManagementInfoProvider';
1822

1923
export class LspResourceHandlers {
2024
constructor(private readonly connection: Connection) {}
2125

22-
onListResources(handler: ServerRequestHandler<ListResourcesParams, ListResourcesResult, never, void>) {
26+
onListResources(handler: RequestHandler<ListResourcesParams, ListResourcesResult, void>) {
2327
this.connection.onRequest(ListResourcesRequest.method, handler);
2428
}
2529

@@ -38,4 +42,8 @@ export class LspResourceHandlers {
3842
onStackMgmtInfo(handler: ServerRequestHandler<ResourceIdentifier, ResourceStackManagementResult, never, void>) {
3943
this.connection.onRequest(StackMgmtInfoRequest.method, handler);
4044
}
45+
46+
onSearchResource(handler: ServerRequestHandler<SearchResourceParams, SearchResourceResult, never, void>) {
47+
this.connection.onRequest(SearchResourceRequest.method, handler);
48+
}
4149
}

src/resourceState/ResourceStateManager.ts

Lines changed: 93 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GetResourceCommandOutput, ListResourcesOutput, ResourceNotFoundException } from '@aws-sdk/client-cloudcontrol';
1+
import { GetResourceCommandOutput, ResourceNotFoundException } from '@aws-sdk/client-cloudcontrol';
22
import { DateTime } from 'luxon';
33
import { SchemaRetriever } from '../schema/SchemaRetriever';
44
import { CfnExternal } from '../server/CfnExternal';
@@ -21,6 +21,7 @@ export type ResourceState = {
2121
type ResourceList = {
2222
typeName: string;
2323
resourceIdentifiers: string[];
24+
nextToken?: string;
2425
createdTimestamp: DateTime;
2526
lastUpdatedTimestamp: DateTime;
2627
};
@@ -79,19 +80,65 @@ export class ResourceStateManager implements SettingsConfigurable, Closeable {
7980
return value;
8081
}
8182

82-
public async listResources(typeName: string, updateFromLive?: boolean): Promise<ResourceList | undefined> {
83-
const cachedResourceList = this.resourceListMap.get(typeName);
84-
if (cachedResourceList && !updateFromLive) {
85-
return cachedResourceList;
86-
}
87-
const resourceList = await this.retrieveResourceList(typeName);
88-
if (!resourceList) {
83+
public async listResources(typeName: string, nextToken?: string): Promise<ResourceList | undefined> {
84+
const cached = this.resourceListMap.get(typeName);
85+
86+
if (!nextToken) {
87+
// Initial request - fetch first page and cache it
88+
const resourceList = await this.retrieveResourceList(typeName);
89+
if (resourceList) {
90+
this.resourceListMap.set(typeName, resourceList);
91+
return resourceList;
92+
}
8993
return;
9094
}
9195

92-
this.resourceListMap.set(typeName, resourceList);
96+
// Pagination request - fetch next page and append to cache
97+
const resourceListNextPage = await this.retrieveResourceList(typeName, nextToken);
98+
if (resourceListNextPage && cached) {
99+
// Deduplicate efficiently using Set for O(1) lookup
100+
const cachedSet = new Set(cached.resourceIdentifiers);
101+
const newIdentifiers = resourceListNextPage.resourceIdentifiers.filter((id) => !cachedSet.has(id));
102+
cached.resourceIdentifiers.push(...newIdentifiers);
103+
cached.nextToken = resourceListNextPage.nextToken;
104+
cached.lastUpdatedTimestamp = DateTime.now();
105+
return cached;
106+
}
93107

94-
return resourceList;
108+
return resourceListNextPage;
109+
}
110+
111+
public async searchResourceByIdentifier(
112+
typeName: string,
113+
identifier: string,
114+
): Promise<{ found: boolean; resourceList?: ResourceList }> {
115+
const resource = await this.getResource(typeName, identifier);
116+
if (!resource) {
117+
return { found: false };
118+
}
119+
120+
// Add to cache
121+
const cached = this.resourceListMap.get(typeName);
122+
if (cached && !cached.resourceIdentifiers.includes(identifier)) {
123+
cached.resourceIdentifiers.push(identifier);
124+
cached.lastUpdatedTimestamp = DateTime.now();
125+
return { found: true, resourceList: cached };
126+
}
127+
128+
// Create new cache entry if doesn't exist
129+
if (!cached) {
130+
const newList: ResourceList = {
131+
typeName,
132+
resourceIdentifiers: [identifier],
133+
nextToken: undefined,
134+
createdTimestamp: DateTime.now(),
135+
lastUpdatedTimestamp: DateTime.now(),
136+
};
137+
this.resourceListMap.set(typeName, newList);
138+
return { found: true, resourceList: newList };
139+
}
140+
141+
return { found: true, resourceList: cached };
95142
}
96143

97144
public getResourceTypes(): string[] {
@@ -113,40 +160,42 @@ export class ResourceStateManager implements SettingsConfigurable, Closeable {
113160
return resourceIdToStateMap?.get(identifier);
114161
}
115162

116-
private async retrieveResourceList(typeName: string): Promise<ResourceList | undefined> {
117-
let output: ListResourcesOutput | undefined = undefined;
118-
163+
private async retrieveResourceList(typeName: string, nextToken?: string): Promise<ResourceList | undefined> {
119164
try {
120-
output = await this.ccapiService.listResources(typeName);
165+
const output = await this.ccapiService.listResources(typeName, { nextToken });
166+
167+
const identifiers =
168+
output.ResourceDescriptions?.map((desc) => desc.Identifier).filter(
169+
(id): id is string => id !== undefined,
170+
) ?? [];
171+
172+
const now = DateTime.now();
173+
174+
return {
175+
typeName: typeName,
176+
resourceIdentifiers: identifiers,
177+
createdTimestamp: now,
178+
lastUpdatedTimestamp: now,
179+
nextToken: output.NextToken,
180+
};
121181
} catch (error) {
122182
log.error(error, `CCAPI ListResource failed for type ${typeName}`);
123183
return;
124184
}
125-
126-
if (!output?.ResourceDescriptions) {
127-
return;
128-
}
129-
130-
const now = DateTime.now();
131-
132-
return {
133-
typeName: typeName,
134-
resourceIdentifiers: output.ResourceDescriptions.map((desc) => desc.Identifier).filter(
135-
(id) => id !== undefined,
136-
),
137-
createdTimestamp: now,
138-
lastUpdatedTimestamp: now,
139-
};
140185
}
141186

142187
public async refreshResourceList(resourceTypes: string[]): Promise<RefreshResourcesResult> {
143188
if (this.isRefreshing) {
144189
// return cached resource list
145190
return {
146-
resources: resourceTypes.map((resourceType) => ({
147-
typeName: resourceType,
148-
resourceIdentifiers: this.resourceListMap.get(resourceType)?.resourceIdentifiers ?? [],
149-
})),
191+
resources: resourceTypes.map((resourceType) => {
192+
const cached = this.resourceListMap.get(resourceType);
193+
return {
194+
typeName: resourceType,
195+
resourceIdentifiers: cached?.resourceIdentifiers ?? [],
196+
nextToken: cached?.nextToken,
197+
};
198+
}),
150199
refreshFailed: false,
151200
};
152201
}
@@ -158,32 +207,30 @@ export class ResourceStateManager implements SettingsConfigurable, Closeable {
158207
try {
159208
this.isRefreshing = true;
160209
const result: ListResourcesResult = { resources: [] };
161-
const now = DateTime.now();
162210
let anyRefreshFailed = false;
163211

164212
for (const resourceType of resourceTypes) {
165-
const storedResourceList = this.resourceListMap.get(resourceType);
213+
// Clear cache and fetch first page only
214+
this.resourceListMap.delete(resourceType);
166215

167-
const newResourceList = await this.retrieveResourceList(resourceType);
168-
if (!newResourceList) {
169-
// Failed to update this resource type
216+
const response = await this.retrieveResourceList(resourceType);
217+
if (!response) {
218+
anyRefreshFailed = true;
170219
result.resources.push({
171220
typeName: resourceType,
172-
resourceIdentifiers: storedResourceList?.resourceIdentifiers ?? [],
221+
resourceIdentifiers: [],
222+
nextToken: undefined,
173223
});
174-
anyRefreshFailed = true;
175224
continue;
176225
}
177226

178-
this.resourceListMap.set(resourceType, {
179-
...newResourceList,
180-
createdTimestamp: storedResourceList?.createdTimestamp ?? now,
181-
lastUpdatedTimestamp: now,
182-
});
227+
// Cache the first page
228+
this.resourceListMap.set(resourceType, response);
183229

184230
result.resources.push({
185231
typeName: resourceType,
186-
resourceIdentifiers: newResourceList.resourceIdentifiers,
232+
resourceIdentifiers: response.resourceIdentifiers,
233+
nextToken: response.nextToken,
187234
});
188235
}
189236
return { ...result, refreshFailed: anyRefreshFailed };

0 commit comments

Comments
 (0)