Skip to content

Commit aa0774a

Browse files
authored
Issue 53137: Resolve storage details race condition (#1795)
1 parent 98253ea commit aa0774a

6 files changed

Lines changed: 68 additions & 3 deletions

File tree

packages/components/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@labkey/components",
3-
"version": "6.43.2",
3+
"version": "6.43.3",
44
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
55
"sideEffects": false,
66
"files": [

packages/components/releaseNotes/components.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version 6.43.3
5+
*Released*: 26 May 2025
6+
- Migrate `isSetEqual` to `@labkey/components`. Extend with additional support for deep comparison.
7+
48
### version 6.43.2
59
*Released*: 23 May 2025
610
- Add detailed auditing of domain changes & comment ability

packages/components/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import {
6565
isIntegerInRange,
6666
isNonNegativeFloat,
6767
isNonNegativeInteger,
68+
isSetEqual,
6869
parseCsvString,
6970
parseScientificInt,
7071
quoteValueWithDelimiters,
@@ -1666,6 +1667,7 @@ export {
16661667
isNonNegativeFloat,
16671668
isNonNegativeInteger,
16681669
isLoading,
1670+
isSetEqual,
16691671
naturalSort,
16701672
naturalSortByProperty,
16711673
generateId,

packages/components/src/internal/util/utils.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { QueryInfo } from '../../public/QueryInfo';
1919
import { ExtendedMap } from '../../public/ExtendedMap';
2020
import { QueryColumn } from '../../public/QueryColumn';
2121

22+
import getDomainDetailsJSON from '../../test/data/dataclass-getDomainDetails.json';
23+
2224
import {
2325
arrayEquals,
2426
camelCaseToTitleCase,
@@ -39,6 +41,7 @@ import {
3941
isNonNegativeFloat,
4042
isNonNegativeInteger,
4143
isQuotedWithDelimiters,
44+
isSetEqual,
4245
makeCommaSeparatedString,
4346
parseCsvString,
4447
parseScientificInt,
@@ -1490,3 +1493,22 @@ describe('styleStringToObj', () => {
14901493
});
14911494
});
14921495
});
1496+
1497+
describe('isSetEqual', () => {
1498+
test('equivalency', () => {
1499+
expect(isSetEqual([], new Set())).toBe(true);
1500+
expect(isSetEqual([1], new Set([1, 1]))).toBe(true);
1501+
expect(isSetEqual([1], [1, 1])).toBe(true);
1502+
expect(isSetEqual([1, 3, 2], [2, 1, 1, 3, 2])).toBe(true);
1503+
expect(isSetEqual([{ x: 'a', y: 'b' }], [{ x: 'a', y: 'b' }])).toBe(true);
1504+
expect(isSetEqual([{ x: 'a', y: 'b' }], [{ y: 'b', x: 'a' }])).toBe(true);
1505+
expect(isSetEqual([undefined, null], [null, undefined])).toBe(true);
1506+
1507+
// Compare more complex objects to check for deep equivalency
1508+
expect(isSetEqual([{ x: 'a', y: 'b', z: { a: 'x', 1: -1 } }], [{ z: { 1: -1, a: 'x' }, y: 'b', x: 'a' }])).toBe(
1509+
true
1510+
);
1511+
expect(isSetEqual([getDomainDetailsJSON, 1], [getDomainDetailsJSON, 1, 1])).toBe(true);
1512+
expect(isSetEqual([getDomainDetailsJSON, 1], [getDomainDetailsJSON, 1, 2])).toBe(false);
1513+
});
1514+
});

packages/components/src/internal/util/utils.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,43 @@ export function styleStringToObj(styleString: string): CSSProperties {
753753
}, {});
754754
}
755755

756+
type Collection<C> = C[] | Set<C>;
757+
758+
/**
759+
* Replacer function for JSON.stringify() to support having object keys sorted in output. Supports deeply nested objects.
760+
*
761+
* https://stackoverflow.com/a/43636793
762+
*/
763+
const stringifyReplacer = (_, value): any => {
764+
if (value instanceof Object && !(value instanceof Array) && Object.keys(value).length > 0) {
765+
return Object.keys(value)
766+
.sort()
767+
.reduce((sorted, key) => {
768+
sorted[key] = value[key];
769+
return sorted;
770+
}, {});
771+
}
772+
773+
return value;
774+
};
775+
776+
/**
777+
* Serializes a Set/Array into a JSON string with sorted unique members.
778+
* Useful for determining deep equivalency.
779+
*
780+
* https://stackoverflow.com/a/43858768
781+
*/
782+
const toJsonSet = (s): string => JSON.stringify([...new Set(s)].sort(), stringifyReplacer);
783+
784+
/**
785+
* Compare any combination of two Set(s)/Array(s) to determine if they're equivalent.
786+
* NOTE: This does not do deeply nested equivalency in all cases. Specifically, when objects are
787+
* compared the order of their properties is determined by the symbol/type of the property.
788+
*/
789+
export function isSetEqual<T = any>(a: Collection<T>, b: Collection<T>): boolean {
790+
return toJsonSet(a) === toJsonSet(b);
791+
}
792+
756793
/**
757794
* When this package is exported this environment variable reference is inline rewritten as
758795
* `const IS_NODE_TEST_ENV = "production" === 'test';`

0 commit comments

Comments
 (0)