Skip to content

Commit 38ec252

Browse files
mshanemcclaude
andcommitted
refactor: dedupe listMetadata via Arr.dedupeAdjacentWith + Equivalence
Extract the fullName dedup into a named Effect Equivalence colocated with FilePropertiesSchema, and use Arr.dedupeAdjacentWith instead of an inline index filter. Decode now runs before sort/dedup so the Equivalence operates on decoded values. Adds unit tests covering sort, single-object coercion, dedup, and combined cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4492622 commit 38ec252

3 files changed

Lines changed: 110 additions & 3 deletions

File tree

packages/salesforcedx-vscode-services/src/core/metadataDescribeService.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import type { Connection } from '@salesforce/core';
9+
import * as Arr from 'effect/Array';
910
import * as Cache from 'effect/Cache';
1011
import * as Chunk from 'effect/Chunk';
1112
import * as Duration from 'effect/Duration';
@@ -20,7 +21,7 @@ import { ExtensionContextService } from '../vscode/extensionContextService';
2021
import { SettingsService } from '../vscode/settingsService';
2122
import { ConnectionService } from './connectionService';
2223
import { getDefaultOrgRef } from './defaultOrgRef';
23-
import { FilePropertiesSchema } from './schemas/fileProperties';
24+
import { FilePropertiesByFullName, FilePropertiesSchema } from './schemas/fileProperties';
2425
import { unknownToErrorCause } from './shared';
2526

2627
const NON_SUPPORTED_TYPES = new Set(['InstalledPackage', 'Profile', 'Scontrol']);
@@ -176,9 +177,9 @@ export class MetadataDescribeService extends Effect.Service<MetadataDescribeServ
176177
Effect.tap(result => Effect.annotateCurrentSpan({ result })),
177178
Effect.withSpan('listMetadata (API call)'),
178179
Effect.map(ensureArray),
179-
Effect.map(arr => arr.toSorted((a, b) => a.fullName.localeCompare(b.fullName))),
180-
Effect.map(arr => arr.filter((item, i, a) => i === 0 || item.fullName !== a[i - 1].fullName)),
181180
Effect.flatMap(arr => S.decodeUnknown(S.Array(FilePropertiesSchema))(arr)),
181+
Effect.map(arr => arr.toSorted((a, b) => a.fullName.localeCompare(b.fullName))),
182+
Effect.map(Arr.dedupeAdjacentWith(FilePropertiesByFullName)),
182183
Effect.mapError(e => {
183184
const { cause } = unknownToErrorCause(e);
184185
return new ListMetadataError({

packages/salesforcedx-vscode-services/src/core/schemas/fileProperties.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7+
import * as Equivalence from 'effect/Equivalence';
78
import * as S from 'effect/Schema';
89

910
/** Schema for FileProperties (metadata.list result) */
@@ -21,3 +22,9 @@ export const FilePropertiesSchema = S.Struct({
2122
manageableState: S.optional(S.String),
2223
namespacePrefix: S.optional(S.String)
2324
});
25+
26+
/** Equivalence for FileProperties keyed on fullName — for deduping listMetadata results. */
27+
export const FilePropertiesByFullName = Equivalence.mapInput(
28+
Equivalence.string,
29+
(fp: S.Schema.Type<typeof FilePropertiesSchema>) => fp.fullName
30+
);
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright (c) 2026, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import type { Connection } from '@salesforce/core';
9+
import * as Effect from 'effect/Effect';
10+
import * as Layer from 'effect/Layer';
11+
import * as SubscriptionRef from 'effect/SubscriptionRef';
12+
import { ChannelService } from '../../../src/vscode/channelService';
13+
import { ConnectionService } from '../../../src/core/connectionService';
14+
import { getDefaultOrgRef } from '../../../src/core/defaultOrgRef';
15+
import { MetadataDescribeService } from '../../../src/core/metadataDescribeService';
16+
17+
type ListItem = {
18+
fullName: string;
19+
type: string;
20+
id?: string;
21+
lastModifiedDate?: string;
22+
};
23+
24+
const createMockConnectionService = (listResult: ListItem | ListItem[]): Layer.Layer<ConnectionService> =>
25+
Layer.succeed(
26+
ConnectionService,
27+
ConnectionService.make({
28+
getConnection: () =>
29+
Effect.succeed({
30+
version: '60.0',
31+
metadata: {
32+
list: jest.fn().mockResolvedValue(listResult)
33+
}
34+
} as unknown as Connection),
35+
invalidateCachedConnections: () => Effect.void
36+
})
37+
);
38+
39+
const seedDefaultOrg = Effect.gen(function* () {
40+
const ref = yield* getDefaultOrgRef();
41+
yield* SubscriptionRef.update(ref, () => ({ orgId: 'test-org' }));
42+
});
43+
44+
const runListMetadata = (listResult: ListItem | ListItem[], type = 'ApexClass') =>
45+
Effect.runPromise(
46+
Effect.gen(function* () {
47+
yield* seedDefaultOrg;
48+
const service = yield* MetadataDescribeService;
49+
return yield* service.listMetadata(type);
50+
}).pipe(
51+
Effect.provide(
52+
Layer.provide(
53+
MetadataDescribeService.DefaultWithoutDependencies,
54+
Layer.mergeAll(createMockConnectionService(listResult), ChannelService.Default)
55+
)
56+
)
57+
)
58+
);
59+
60+
describe('MetadataDescribeService.listMetadata', () => {
61+
it('sorts an out-of-order array by fullName', async () => {
62+
const result = await runListMetadata([
63+
{ fullName: 'Charlie', type: 'ApexClass' },
64+
{ fullName: 'Alpha', type: 'ApexClass' },
65+
{ fullName: 'Bravo', type: 'ApexClass' }
66+
]);
67+
68+
expect(result.map(r => r.fullName)).toEqual(['Alpha', 'Bravo', 'Charlie']);
69+
});
70+
71+
it('wraps a non-array (single object) response into an array of one', async () => {
72+
const result = await runListMetadata({ fullName: 'Solo', type: 'ApexClass' });
73+
74+
expect(result).toHaveLength(1);
75+
expect(result[0].fullName).toBe('Solo');
76+
});
77+
78+
it('deduplicates entries with the same fullName', async () => {
79+
const result = await runListMetadata([
80+
{ fullName: 'Dup', type: 'ApexClass', id: '1' },
81+
{ fullName: 'Dup', type: 'ApexClass', id: '2' },
82+
{ fullName: 'Unique', type: 'ApexClass' }
83+
]);
84+
85+
expect(result.map(r => r.fullName)).toEqual(['Dup', 'Unique']);
86+
});
87+
88+
it('handles out-of-order, duplicate, and single-fullName entries together', async () => {
89+
const result = await runListMetadata([
90+
{ fullName: 'Charlie', type: 'ApexClass', id: 'c1' },
91+
{ fullName: 'Alpha', type: 'ApexClass', id: 'a1' },
92+
{ fullName: 'Charlie', type: 'ApexClass', id: 'c2' },
93+
{ fullName: 'Bravo', type: 'ApexClass' },
94+
{ fullName: 'Alpha', type: 'ApexClass', id: 'a2' }
95+
]);
96+
97+
expect(result.map(r => r.fullName)).toEqual(['Alpha', 'Bravo', 'Charlie']);
98+
});
99+
});

0 commit comments

Comments
 (0)