[runtime] Update makeReadOnly runtime
--- (wip, waiting for feedback on workplace post) - remove calls to `isInROMode`, as we want to log all mutations after 'freezing' a value (both within and outside of render cycle) - add `source` parameter -- this is the function name of the parent component / hook
Mofei Zhang committed
Jun 5, 2023 at 14:46 UTC
6d1d05ff91507ea844f332f65f5bc15cc8e10655
2 files changed
+91
-58
compiler/forget/packages/make-read-only-util/src/makeReadOnly.test.ts
+66
-39
@@ -9,11 +9,11 @@ import buildMakeReadOnly from "./makeReadOnly";
9
10
describe("makeReadOnly", () => {
11
let logger: jest.Func;
12
- let makeReadOnly: <T>(value: T) => T;
12
+ let makeReadOnly: <T>(value: T, source: string) => T;
13
14
beforeEach(() => {
15
logger = jest.fn();
16
- makeReadOnly = buildMakeReadOnly(logger, [], () => true);
16
+ makeReadOnly = buildMakeReadOnly(logger, []);
17
});
18
19
describe("Tracking mutations", () => {
@@ -21,45 +21,50 @@ describe("makeReadOnly", () => {
21
const a = 5;
22
const b = true;
23
const c = null;
24
- expect(makeReadOnly(a)).toBe(a);
25
- expect(makeReadOnly(b)).toBe(b);
26
- expect(makeReadOnly(c)).toBe(c);
24
+ expect(makeReadOnly(a, "test1")).toBe(a);
25
+ expect(makeReadOnly(b, "test1")).toBe(b);
26
+ expect(makeReadOnly(c, "test1")).toBe(c);
27
});
28
29
it("retains referential equality", () => {
30
const valA = {};
31
const valB = { a: valA, _: valA };
32
const o = { a: valA, b: valB, c: "c" };
33
- expect(makeReadOnly(o)).toBe(o);
34
- expect(makeReadOnly(o.a)).toBe(valA);
35
- expect(makeReadOnly(o.b)).toBe(valB);
36
- expect(makeReadOnly(o.b.a)).toBe(valA);
37
- expect(makeReadOnly(o.b._)).toBe(valA);
38
- expect(makeReadOnly(o.c)).toBe("c");
33
+ expect(makeReadOnly(o, "test2")).toBe(o);
34
+ expect(makeReadOnly(o.a, "test2")).toBe(valA);
35
+ expect(makeReadOnly(o.b, "test2")).toBe(valB);
36
+ expect(makeReadOnly(o.b.a, "test2")).toBe(valA);
37
+ expect(makeReadOnly(o.b._, "test2")).toBe(valA);
38
+ expect(makeReadOnly(o.c, "test2")).toBe("c");
39
});
40
41
it("deals with cyclic references", () => {
42
const o: any = {};
43
o.self_ref = o;
44
- expect(makeReadOnly(o)).toBe(o);
45
- expect(makeReadOnly(o.self_ref)).toBe(o);
44
+ expect(makeReadOnly(o, "test3")).toBe(o);
45
+ expect(makeReadOnly(o.self_ref, "test3")).toBe(o);
46
});
47
it("logs direct interior mutability", () => {
48
const o = { a: 0 };
49
- makeReadOnly(o);
49
+ makeReadOnly(o, "test4");
50
o.a = 42;
51
- expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "a", 42);
51
+ expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "test4", "a", 42);
52
});
53
54
it("tracks changes to known RO properties", () => {
55
const o: any = { a: {} };
56
- makeReadOnly(o);
56
+ makeReadOnly(o, "test5");
57
o.a = 42;
58
- expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "a", 42);
58
+ expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "test5", "a", 42);
59
expect(o.a).toBe(42);
60
const newVal = { x: 0 };
61
o.a = newVal;
62
- expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "a", newVal);
62
+ expect(logger).toBeCalledWith(
63
+ "FORGET_MUTATE_IMMUT",
64
+ "test5",
65
+ "a",
66
+ newVal
67
+ );
68
expect(o.a).toBe(newVal);
69
});
70
@@ -67,18 +72,23 @@ describe("makeReadOnly", () => {
72
const o: any = { a: { x: 4 } };
73
74
const alias = o;
70
- makeReadOnly(o);
75
+ makeReadOnly(o, "test6");
76
const newVal = {};
77
alias.a = newVal;
73
- expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "a", newVal);
78
+ expect(logger).toBeCalledWith(
79
+ "FORGET_MUTATE_IMMUT",
80
+ "test6",
81
+ "a",
82
+ newVal
83
+ );
84
expect(o.a).toBe(newVal);
85
});
86
87
it("logs transitive interior mutability", () => {
88
const o: any = { a: { x: 0 } };
79
- makeReadOnly(o);
89
+ makeReadOnly(o, "test7");
90
o.a.x = 42;
81
- expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "x", 42);
91
+ expect(logger).toBeCalledWith("FORGET_MUTATE_IMMUT", "test7", "x", 42);
92
});
93
94
describe("todo", () => {
@@ -86,7 +96,7 @@ describe("makeReadOnly", () => {
96
// this is a limitation of the current "proxy" approach,
97
// which overwrites object properties with getters and setters
98
const x: any = { a: {} };
89
- makeReadOnly(x);
99
+ makeReadOnly(x, "test8");
100
101
delete x.a;
102
x.b = 0;
@@ -96,44 +106,61 @@ describe("makeReadOnly", () => {
106
// this could be easily implemented by making caching eager
107
const innerObj = { x: 0 };
108
const o = { a: innerObj };
99
- makeReadOnly(o);
109
+ makeReadOnly(o, "test9");
110
innerObj.x = 42;
111
expect(o.a.x).toBe(42);
112
113
const o1 = { a: { x: 0 } };
114
const innerObj1 = o1.a;
105
- makeReadOnly(o1);
115
+ makeReadOnly(o1, "test9");
116
innerObj1.x = 42;
117
118
expect(o1.a.x).toBe(42);
119
expect(logger).toBeCalledTimes(0);
120
});
121
+
122
+ it("does not track objects with getter/setters", () => {
123
+ let backedX: string | null = null;
124
+ const o = {
125
+ set val(val: string | null) {
126
+ backedX = val;
127
+ },
128
+ get val(): string | null {
129
+ return backedX;
130
+ },
131
+ };
132
+ expect(makeReadOnly(o, "test10")).toBe(o);
133
+ expect(makeReadOnly(o.val, "test10")).toBe(null);
134
+
135
+ o.val = "40";
136
+ expect(logger).toBeCalledTimes(0);
137
+ });
138
});
139
});
140
141
describe("Tracking adding or deleting properties", () => {
142
it("tracks new properties added between calls to makeReadOnly", () => {
143
const o: any = {};
117
- makeReadOnly(o);
144
+ makeReadOnly(o, "test11");
145
o.a = "new value";
119
- makeReadOnly(o);
120
- expect(logger).toBeCalledWith("FORGET_ADD_PROP_IMMUT", "a");
146
+ makeReadOnly(o, "test11");
147
+ expect(logger).toBeCalledWith("FORGET_ADD_PROP_IMMUT", "test11", "a");
148
});
149
it("tracks properties deleted between calls to makeReadOnly", () => {
150
const o: any = { a: 0 };
124
- makeReadOnly(o);
151
+ makeReadOnly(o, "test12");
152
delete o.a;
126
- makeReadOnly(o);
127
- expect(logger).toBeCalledWith("FORGET_DELETE_PROP_IMMUT", "a");
153
+ makeReadOnly(o, "test12");
154
+ expect(logger).toBeCalledWith("FORGET_DELETE_PROP_IMMUT", "test12", "a");
155
});
156
130
- it("tracks properties deleted and re-added between calls to makeReadOnly", () => {
131
- const o: any = { a: 0 };
132
- makeReadOnly(o);
133
- delete o.a;
134
- o.a = {};
135
- makeReadOnly(o);
136
- expect(logger).toBeCalledWith("FORGET_CHANGE_PROP_IMMUT", "a");
137
- });
157
+ // it("tracks properties deleted and re-added between calls to makeReadOnly", () => {
158
+ // const o: any = { a: 0 };
159
+ // makeReadOnly(o);
160
+ // delete o.a;
161
+ // o.a = {};
162
+ // makeReadOnly(o);
163
+ // expect(logger).toBeCalledWith("FORGET_CHANGE_PROP_IMMUT", "a");
164
+ // });
165
});
166
});
compiler/forget/packages/make-read-only-util/src/makeReadOnly.ts
+25
-19
@@ -12,9 +12,9 @@ type ROViolationType =
12
| "FORGET_DELETE_PROP_IMMUT"
13
| "FORGET_CHANGE_PROP_IMMUT"
14
| "FORGET_ADD_PROP_IMMUT";
15
-type ROModeChecker = () => boolean;
15
type ROViolationLogger = (
16
violation: ROViolationType,
17
+ source: string,
18
key: string,
19
value?: any
20
) => void;
@@ -52,15 +52,15 @@ function getOrInsertDefault(
52
53
function buildMakeReadOnly(
54
logger: ROViolationLogger,
55
- skippedClasses: string[],
56
- isInROMode: ROModeChecker
57
-): <T>(val: T) => T {
55
+ skippedClasses: string[]
56
+): <T>(val: T, source: string) => T {
57
// All saved proxys
58
const savedROObjects: SavedROObjects = new WeakMap();
59
60
// Overwrites an object property with its proxy and saves its original value
61
function addProperty(
62
obj: Object,
63
+ source: string,
64
key: string,
65
prop: PropertyDescriptor,
66
savedEntries: Map<string, SavedEntry>
@@ -68,12 +68,10 @@ function buildMakeReadOnly(
68
const proxy: PropertyDescriptor & { get(): unknown } = {
69
get() {
70
// read from backing cache entry
71
- return makeReadOnly(savedEntries.get(key)!.savedVal);
71
+ return makeReadOnly(savedEntries.get(key)!.savedVal, source);
72
},
73
set(newVal: unknown) {
74
- if (isInROMode()) {
75
- logger("FORGET_MUTATE_IMMUT", key, newVal);
76
- }
74
+ logger("FORGET_MUTATE_IMMUT", source, key, newVal);
75
// update backing cache entry
76
savedEntries.get(key)!.savedVal = newVal;
77
},
@@ -90,10 +88,13 @@ function buildMakeReadOnly(
88
}
89
90
// Changes an object to be read-only, returns its input
93
- function makeReadOnly<T>(o: T): T {
91
+ function makeReadOnly<T>(o: T, source: string): T {
92
if (typeof o !== "object" || o == null) {
93
return o;
96
- } else if (skippedClasses.includes(o.constructor.name)) {
94
+ } else if (
95
+ o.constructor?.name != null &&
96
+ skippedClasses.includes(o.constructor.name)
97
+ ) {
98
return o;
99
}
100
@@ -114,13 +115,11 @@ function buildMakeReadOnly(
115
// (meaning that new value is not proxied,
116
// and the current proxied value is stale)
117
cache.delete(k);
117
- if (!currentProp && isInROMode()) {
118
- logger("FORGET_DELETE_PROP_IMMUT", k);
118
+ if (!currentProp) {
119
+ logger("FORGET_DELETE_PROP_IMMUT", source, k);
120
} else if (currentProp) {
120
- if (isInROMode()) {
121
- logger("FORGET_CHANGE_PROP_IMMUT", k);
122
- }
123
- addProperty(o, k, currentProp, cache);
121
+ logger("FORGET_CHANGE_PROP_IMMUT", source, k);
122
+ addProperty(o, source, k, currentProp, cache);
123
}
124
}
125
}
@@ -128,10 +127,17 @@ function buildMakeReadOnly(
127
Object.getOwnPropertyDescriptors(o)
128
)) {
129
if (!cache.has(k) && isWriteable(prop)) {
131
- if (isInROMode() && existed) {
132
- logger("FORGET_ADD_PROP_IMMUT", k);
130
+ if (prop.hasOwnProperty("set") || prop.hasOwnProperty("get") || k === "current") {
131
+ // - we currently don't handle accessor properties
132
+ // - we currently have no other way of checking whether an object
133
+ // is a `ref` (i.e. returned by useRef).
134
+ continue;
135
+ }
136
+
137
+ if (existed) {
138
+ logger("FORGET_ADD_PROP_IMMUT", source, k);
139
}
134
- addProperty(o, k, prop, cache);
140
+ addProperty(o, source, k, prop, cache);
141
}
142
}
143
return o;