1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114 | type JSONValue =
| null
| boolean
| number
| string
| JSONValue[]
| { [key: string]: JSONValue };
type InputObj = Record<string, JSONValue> | Array<JSONValue>;
type RecursiveHandler = {
set: (target: any, prop: string, value: any) => boolean;
get: (target: any, prop: string) => unknown;
};
const isObject = (o: any) => o !== null && typeof o === 'object';
// Access history of properties
class AccessHistory {
value: JSONValue | null = null;
props: Map<string, AccessHistory> = new Map();
}
class ImmutableHelper {
private obj: InputObj;
constructor(obj: InputObj) {
this.obj = obj;
}
produce(mutator: (obj: InputObj) => void): InputObj {
// Creates a proxied object to track property access history.
function createProxiedObj(
obj: InputObj,
accessHistory: AccessHistory
): InputObj {
const handler: RecursiveHandler = {
// 'set' trap intercepts property assignment.
set(_, prop, value) {
if (!accessHistory.props.has(prop)) {
accessHistory.props.set(prop, new AccessHistory());
}
accessHistory.props.get(prop)!.value = value;
return true;
},
// 'get' trap intercepts property access.
get(_, prop) {
if (accessHistory.value !== null) {
return accessHistory.value;
}
if (!accessHistory.props.has(prop)) {
accessHistory.props.set(prop, new AccessHistory());
}
if (accessHistory.props.get(prop)!.value !== null) {
return accessHistory.props.get(prop)!.value;
}
if (isObject(obj[prop])) {
// Recursively create a proxed object for object property.
return createProxiedObj(
obj[prop] as InputObj,
accessHistory.props.get(prop)! as AccessHistory
);
}
return obj[prop];
},
};
return new Proxy(obj, handler);
}
// Returns true if there are mutated properties in the access history;
// otherwise, returns false and deletes the unnecessary properties.
function deleteUnmutatedProps(accessHistory: AccessHistory): boolean {
if (accessHistory.value !== null) {
return true;
}
let hasMutation = false;
for (const [prop, childAccessHistory] of [...accessHistory.props]) {
if (deleteUnmutatedProps(childAccessHistory)) {
hasMutation = true;
} else {
accessHistory.props.delete(prop);
}
}
return hasMutation;
}
// Function to transform the original object based on the access history
function transform(obj: InputObj, accessHistory: AccessHistory): InputObj {
if (accessHistory.value !== null) {
return accessHistory.value as InputObj;
}
if (accessHistory.props.size === 0) {
return obj;
}
if (!isObject(obj)) {
return obj;
}
let clone = Array.isArray(obj) ? [...obj] : { ...obj };
for (const [prop, childAccessHistory] of [...accessHistory.props]) {
clone[prop] = transform(obj[prop] as InputObj, childAccessHistory);
}
return clone;
}
const accessHistory = new AccessHistory();
const proxiedObj = createProxiedObj(this.obj, accessHistory);
// Apply the mutator function on the proxied object. This will also record
// the property access history in `accessHistory`.
mutator(proxiedObj);
// Simplify the access history.
deleteUnmutatedProps(accessHistory);
// Transform the original object based on the simplified access history.
return transform(this.obj, accessHistory);
}
}
|