UNPKG

7.78 kBPlain TextView Raw
1import {immerable} from "../immer"
2import {
3 ImmerState,
4 Patch,
5 SetState,
6 ProxyArrayState,
7 MapState,
8 ProxyObjectState,
9 PatchPath,
10 get,
11 each,
12 has,
13 getArchtype,
14 getPrototypeOf,
15 isSet,
16 isMap,
17 loadPlugin,
18 ArchType,
19 die,
20 isDraft,
21 isDraftable,
22 NOTHING,
23 errors
24} from "../internal"
25
26export function enablePatches() {
27 const errorOffset = 16
28 if (process.env.NODE_ENV !== "production") {
29 errors.push(
30 'Sets cannot have "replace" patches.',
31 function(op: string) {
32 return "Unsupported patch operation: " + op
33 },
34 function(path: string) {
35 return "Cannot apply patch, path doesn't resolve: " + path
36 },
37 "Patching reserved attributes like __proto__, prototype and constructor is not allowed"
38 )
39 }
40
41 const REPLACE = "replace"
42 const ADD = "add"
43 const REMOVE = "remove"
44
45 function generatePatches_(
46 state: ImmerState,
47 basePath: PatchPath,
48 patches: Patch[],
49 inversePatches: Patch[]
50 ): void {
51 switch (state.type_) {
52 case ArchType.Object:
53 case ArchType.Map:
54 return generatePatchesFromAssigned(
55 state,
56 basePath,
57 patches,
58 inversePatches
59 )
60 case ArchType.Array:
61 return generateArrayPatches(state, basePath, patches, inversePatches)
62 case ArchType.Set:
63 return generateSetPatches(
64 (state as any) as SetState,
65 basePath,
66 patches,
67 inversePatches
68 )
69 }
70 }
71
72 function generateArrayPatches(
73 state: ProxyArrayState,
74 basePath: PatchPath,
75 patches: Patch[],
76 inversePatches: Patch[]
77 ) {
78 let {base_, assigned_} = state
79 let copy_ = state.copy_!
80
81 // Reduce complexity by ensuring `base` is never longer.
82 if (copy_.length < base_.length) {
83 // @ts-ignore
84 ;[base_, copy_] = [copy_, base_]
85 ;[patches, inversePatches] = [inversePatches, patches]
86 }
87
88 // Process replaced indices.
89 for (let i = 0; i < base_.length; i++) {
90 if (assigned_[i] && copy_[i] !== base_[i]) {
91 const path = basePath.concat([i])
92 patches.push({
93 op: REPLACE,
94 path,
95 // Need to maybe clone it, as it can in fact be the original value
96 // due to the base/copy inversion at the start of this function
97 value: clonePatchValueIfNeeded(copy_[i])
98 })
99 inversePatches.push({
100 op: REPLACE,
101 path,
102 value: clonePatchValueIfNeeded(base_[i])
103 })
104 }
105 }
106
107 // Process added indices.
108 for (let i = base_.length; i < copy_.length; i++) {
109 const path = basePath.concat([i])
110 patches.push({
111 op: ADD,
112 path,
113 // Need to maybe clone it, as it can in fact be the original value
114 // due to the base/copy inversion at the start of this function
115 value: clonePatchValueIfNeeded(copy_[i])
116 })
117 }
118 for (let i = copy_.length - 1; base_.length <= i; --i) {
119 const path = basePath.concat([i])
120 inversePatches.push({
121 op: REMOVE,
122 path
123 })
124 }
125 }
126
127 // This is used for both Map objects and normal objects.
128 function generatePatchesFromAssigned(
129 state: MapState | ProxyObjectState,
130 basePath: PatchPath,
131 patches: Patch[],
132 inversePatches: Patch[]
133 ) {
134 const {base_, copy_} = state
135 each(state.assigned_!, (key, assignedValue) => {
136 const origValue = get(base_, key)
137 const value = get(copy_!, key)
138 const op = !assignedValue ? REMOVE : has(base_, key) ? REPLACE : ADD
139 if (origValue === value && op === REPLACE) return
140 const path = basePath.concat(key as any)
141 patches.push(op === REMOVE ? {op, path} : {op, path, value})
142 inversePatches.push(
143 op === ADD
144 ? {op: REMOVE, path}
145 : op === REMOVE
146 ? {op: ADD, path, value: clonePatchValueIfNeeded(origValue)}
147 : {op: REPLACE, path, value: clonePatchValueIfNeeded(origValue)}
148 )
149 })
150 }
151
152 function generateSetPatches(
153 state: SetState,
154 basePath: PatchPath,
155 patches: Patch[],
156 inversePatches: Patch[]
157 ) {
158 let {base_, copy_} = state
159
160 let i = 0
161 base_.forEach((value: any) => {
162 if (!copy_!.has(value)) {
163 const path = basePath.concat([i])
164 patches.push({
165 op: REMOVE,
166 path,
167 value
168 })
169 inversePatches.unshift({
170 op: ADD,
171 path,
172 value
173 })
174 }
175 i++
176 })
177 i = 0
178 copy_!.forEach((value: any) => {
179 if (!base_.has(value)) {
180 const path = basePath.concat([i])
181 patches.push({
182 op: ADD,
183 path,
184 value
185 })
186 inversePatches.unshift({
187 op: REMOVE,
188 path,
189 value
190 })
191 }
192 i++
193 })
194 }
195
196 function generateReplacementPatches_(
197 baseValue: any,
198 replacement: any,
199 patches: Patch[],
200 inversePatches: Patch[]
201 ): void {
202 patches.push({
203 op: REPLACE,
204 path: [],
205 value: replacement === NOTHING ? undefined : replacement
206 })
207 inversePatches.push({
208 op: REPLACE,
209 path: [],
210 value: baseValue
211 })
212 }
213
214 function applyPatches_<T>(draft: T, patches: readonly Patch[]): T {
215 patches.forEach(patch => {
216 const {path, op} = patch
217
218 let base: any = draft
219 for (let i = 0; i < path.length - 1; i++) {
220 const parentType = getArchtype(base)
221 let p = path[i]
222 if (typeof p !== "string" && typeof p !== "number") {
223 p = "" + p
224 }
225
226 // See #738, avoid prototype pollution
227 if (
228 (parentType === ArchType.Object || parentType === ArchType.Array) &&
229 (p === "__proto__" || p === "constructor")
230 )
231 die(errorOffset + 3)
232 if (typeof base === "function" && p === "prototype")
233 die(errorOffset + 3)
234 base = get(base, p)
235 if (typeof base !== "object") die(errorOffset + 2, path.join("/"))
236 }
237
238 const type = getArchtype(base)
239 const value = deepClonePatchValue(patch.value) // used to clone patch to ensure original patch is not modified, see #411
240 const key = path[path.length - 1]
241 switch (op) {
242 case REPLACE:
243 switch (type) {
244 case ArchType.Map:
245 return base.set(key, value)
246 /* istanbul ignore next */
247 case ArchType.Set:
248 die(errorOffset)
249 default:
250 // if value is an object, then it's assigned by reference
251 // in the following add or remove ops, the value field inside the patch will also be modifyed
252 // so we use value from the cloned patch
253 // @ts-ignore
254 return (base[key] = value)
255 }
256 case ADD:
257 switch (type) {
258 case ArchType.Array:
259 return key === "-"
260 ? base.push(value)
261 : base.splice(key as any, 0, value)
262 case ArchType.Map:
263 return base.set(key, value)
264 case ArchType.Set:
265 return base.add(value)
266 default:
267 return (base[key] = value)
268 }
269 case REMOVE:
270 switch (type) {
271 case ArchType.Array:
272 return base.splice(key as any, 1)
273 case ArchType.Map:
274 return base.delete(key)
275 case ArchType.Set:
276 return base.delete(patch.value)
277 default:
278 return delete base[key]
279 }
280 default:
281 die(errorOffset + 1, op)
282 }
283 })
284
285 return draft
286 }
287
288 // optimize: this is quite a performance hit, can we detect intelligently when it is needed?
289 // E.g. auto-draft when new objects from outside are assigned and modified?
290 // (See failing test when deepClone just returns obj)
291 function deepClonePatchValue<T>(obj: T): T
292 function deepClonePatchValue(obj: any) {
293 if (!isDraftable(obj)) return obj
294 if (Array.isArray(obj)) return obj.map(deepClonePatchValue)
295 if (isMap(obj))
296 return new Map(
297 Array.from(obj.entries()).map(([k, v]) => [k, deepClonePatchValue(v)])
298 )
299 if (isSet(obj)) return new Set(Array.from(obj).map(deepClonePatchValue))
300 const cloned = Object.create(getPrototypeOf(obj))
301 for (const key in obj) cloned[key] = deepClonePatchValue(obj[key])
302 if (has(obj, immerable)) cloned[immerable] = obj[immerable]
303 return cloned
304 }
305
306 function clonePatchValueIfNeeded<T>(obj: T): T {
307 if (isDraft(obj)) {
308 return deepClonePatchValue(obj)
309 } else return obj
310 }
311
312 loadPlugin("Patches", {
313 applyPatches_,
314 generatePatches_,
315 generateReplacementPatches_
316 })
317}