Skip to content

Commit 7d7ded1

Browse files
committed
refactor: Replace rfdc with klona
1 parent 56f5a8d commit 7d7ded1

File tree

5 files changed

+61
-11
lines changed

5 files changed

+61
-11
lines changed

packages/json-rpc-engine/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"@metamask/utils": "^11.8.1",
7373
"@types/deep-freeze-strict": "^1.1.0",
7474
"deep-freeze-strict": "^1.1.1",
75-
"rfdc": "^1.4.1"
75+
"klona": "^2.0.6"
7676
},
7777
"devDependencies": {
7878
"@lavamoat/allow-scripts": "^3.0.4",

packages/json-rpc-engine/src/v2/asLegacyMiddleware.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export function asLegacyMiddleware<
4343
propagateToRequest(req, context);
4444

4545
if (result !== undefined) {
46-
res.result = deepClone(result) as ResultConstraint<Request>;
46+
// Unclear why the `as unknown` is needed here, but the cast is safe.
47+
res.result = deepClone(result) as unknown as ResultConstraint<Request>;
4748
return undefined;
4849
}
4950
return next();

packages/json-rpc-engine/src/v2/compatibility-utils.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,43 @@ describe('compatibility-utils', () => {
2828
expect(clonedRequest).toStrictEqual(request);
2929
expect(clonedRequest).not.toBe(request);
3030
});
31+
32+
it('produces a mutable clone of a frozen object', () => {
33+
const request = Object.freeze({
34+
jsonrpc,
35+
method: 'test_method' as string,
36+
params: Object.freeze([1, 2, 3]),
37+
id: 1,
38+
});
39+
40+
const clonedRequest = deepClone(request);
41+
42+
expect(clonedRequest).toStrictEqual(request);
43+
expect(clonedRequest).not.toBe(request);
44+
expect(Object.isFrozen(clonedRequest)).toBe(false);
45+
expect(Object.isFrozen(clonedRequest.params)).toBe(false);
46+
47+
clonedRequest.method = 'modified_method';
48+
clonedRequest.params[1] = 42;
49+
50+
expect(request.method).toBe('test_method');
51+
expect(clonedRequest.params[1]).toBe(42);
52+
});
53+
54+
it('ignores symbol properties', () => {
55+
const symbolProp = Symbol('test');
56+
const request = {
57+
jsonrpc,
58+
method: 'test_method' as string,
59+
params: [1, 2, 3],
60+
id: 1,
61+
[symbolProp]: 'value',
62+
};
63+
64+
const clonedRequest = deepClone(request);
65+
// @ts-expect-error - Symbol properties are omitted
66+
expect(clonedRequest[symbolProp]).toBeUndefined();
67+
});
3168
});
3269

3370
describe('fromLegacyRequest', () => {

packages/json-rpc-engine/src/v2/compatibility-utils.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
11
import { getMessageFromCode, JsonRpcError } from '@metamask/rpc-errors';
22
import type { Json } from '@metamask/utils';
33
import { hasProperty, isObject } from '@metamask/utils';
4-
import rfdc from 'rfdc';
4+
// ATTN: We must NOT use 'klona/full' here because it freezes properties on the clone.
5+
import { klona } from 'klona';
56

67
import { MiddlewareContext } from './MiddlewareContext';
78
import { stringify, type JsonRpcRequest } from './utils';
89

910
// Legacy engine compatibility utils
1011

1112
/**
12-
* Create a deep clone of a value. Assumes acyclical objects. Ignores the
13-
* prototype chain.
13+
* Create a deep clone of a value as follows:
14+
* - Assumes acyclical objects
15+
* - Does not copy property descriptors (i.e. uses mutable defaults)
16+
* - Ignores non-enumerable properties
17+
* - Ignores getters and setters
1418
*
19+
* @throws If the value is an object with a circular reference.
1520
* @param value - The value to clone.
1621
* @returns The cloned value.
1722
*/
18-
export const deepClone = rfdc({
19-
circles: false,
20-
proto: false,
21-
});
23+
export const deepClone = <T>(value: T): DeepCloned<T> =>
24+
klona(value) as DeepCloned<T>;
25+
26+
// Matching the default implementation of klona, this type:
27+
// - Removes readonly modifiers
28+
// - Excludes non-enumerable / symbol properties
29+
type DeepCloned<T> = T extends readonly (infer U)[]
30+
? DeepCloned<U>[]
31+
: T extends object
32+
? { -readonly [K in keyof T & (string | number)]: DeepCloned<T[K]> }
33+
: T;
2234

2335
/**
2436
* Standard JSON-RPC request properties.

yarn.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3833,7 +3833,7 @@ __metadata:
38333833
deepmerge: "npm:^4.2.2"
38343834
jest: "npm:^27.5.1"
38353835
jest-it-up: "npm:^2.0.2"
3836-
rfdc: "npm:^1.4.1"
3836+
klona: "npm:^2.0.6"
38373837
ts-jest: "npm:^27.1.4"
38383838
typedoc: "npm:^0.24.8"
38393839
typescript: "npm:~5.2.2"
@@ -13794,7 +13794,7 @@ __metadata:
1379413794
languageName: node
1379513795
linkType: hard
1379613796

13797-
"rfdc@npm:^1.3.0, rfdc@npm:^1.4.1":
13797+
"rfdc@npm:^1.3.0":
1379813798
version: 1.4.1
1379913799
resolution: "rfdc@npm:1.4.1"
1380013800
checksum: 10/2f3d11d3d8929b4bfeefc9acb03aae90f971401de0add5ae6c5e38fec14f0405e6a4aad8fdb76344bfdd20c5193110e3750cbbd28ba86d73729d222b6cf4a729

0 commit comments

Comments
 (0)