Skip to content

Commit 7af67b9

Browse files
committed
refactor: Use JSON.stringify replacer
1 parent b784961 commit 7af67b9

2 files changed

Lines changed: 90 additions & 2 deletions

File tree

packages/connectivity/src/scp-cf/cache.spec.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Cache } from './cache';
1+
import { Cache, hashCacheKey } from './cache';
22
import { clientCredentialsTokenCache } from './client-credentials-token-cache';
33
import { destinationCache } from './destination';
44
import type { AuthenticationType, Destination } from './destination';
@@ -152,6 +152,54 @@ describe('Cache', () => {
152152
});
153153
});
154154

155+
describe('hashCacheKey', () => {
156+
it('produces the same hash for plain objects with different key insertion order', () => {
157+
expect(hashCacheKey({ a: 1, b: 2 })).toEqual(
158+
hashCacheKey({ b: 2, a: 1 })
159+
);
160+
});
161+
162+
it('produces different hashes for plain objects with different values', () => {
163+
expect(hashCacheKey({ a: 1 })).not.toEqual(hashCacheKey({ a: 2 }));
164+
});
165+
166+
it('produces the same hash for Maps with different insertion order', () => {
167+
const m1 = new Map([
168+
['x', 1],
169+
['y', 2]
170+
]);
171+
const m2 = new Map([
172+
['y', 2],
173+
['x', 1]
174+
]);
175+
expect(hashCacheKey({ m: m1 })).toEqual(hashCacheKey({ m: m2 }));
176+
});
177+
178+
it('produces the same hash for Sets with different insertion order', () => {
179+
const s1 = new Set([1, 2, 3]);
180+
const s2 = new Set([3, 1, 2]);
181+
expect(hashCacheKey({ s: s1 })).toEqual(hashCacheKey({ s: s2 }));
182+
});
183+
184+
it('produces the same hash for class instances regardless of property insertion order', () => {
185+
class Point {
186+
[key: string]: number;
187+
}
188+
const p1 = new Point();
189+
p1.y = 2;
190+
p1.x = 1;
191+
const p2 = new Point();
192+
p2.x = 1;
193+
p2.y = 2;
194+
expect(hashCacheKey({ p: p1 })).toEqual(hashCacheKey({ p: p2 }));
195+
});
196+
it('preserves arrays as arrays (does not coerce to object)', () => {
197+
expect(hashCacheKey({ arr: [1, 2, 3] })).not.toEqual(
198+
hashCacheKey({ arr: { 0: 1, 1: 2, 2: 3 } })
199+
);
200+
});
201+
});
202+
155203
describe('getOrInsertComputed', () => {
156204
it('calls computeFn on cache miss and stores result', () => {
157205
const compute = jest.fn().mockReturnValue({ entry: destinationOne });

packages/connectivity/src/scp-cf/cache.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,14 +149,54 @@ export class Cache<T> implements CacheInterface<T> {
149149
}
150150
}
151151

152+
/**
153+
* Stringifies a given object in a deterministic way, so that the same objects always yield the same string representation.
154+
* @param toSerialize - The object to stringify.
155+
* @returns A string representation of the given object.
156+
*/
157+
function stringifyJsonSafe(toSerialize: Record<string, unknown>): string {
158+
return JSON.stringify(toSerialize, (_key, value) => {
159+
if (value instanceof Map) {
160+
return [
161+
['dataType', 'Map'],
162+
['value', Array.from(value.entries()).sort()]
163+
];
164+
}
165+
if (value instanceof Set) {
166+
return [
167+
['dataType', 'Set'],
168+
['value', Array.from(value.values()).sort()]
169+
];
170+
}
171+
if (typeof value === 'function') {
172+
return [
173+
['dataType', 'Function'],
174+
['value', value.toString()]
175+
];
176+
}
177+
if (value instanceof Object && !Array.isArray(value)) {
178+
return [
179+
['dataType', 'Object'],
180+
[
181+
'value',
182+
Object.entries(value).sort(([keyA], [keyB]) =>
183+
keyA.localeCompare(keyB)
184+
)
185+
]
186+
];
187+
}
188+
return value;
189+
});
190+
}
191+
152192
/**
153193
* Hashes the given value to create a cache key.
154194
* @internal
155195
* @param value - The value to hash.
156196
* @returns A hash of the given value using a cryptographic hash function.
157197
*/
158198
export function hashCacheKey(value: Record<string, unknown>): string {
159-
const serialized = JSON.stringify(value);
199+
const serialized = stringifyJsonSafe(value);
160200
return createHash('blake2s256').update(serialized).digest('hex');
161201
}
162202

0 commit comments

Comments
 (0)