-
-
Notifications
You must be signed in to change notification settings - Fork 851
/
Copy pathpatches.js
199 lines (182 loc) · 4.5 KB
/
patches.js
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import {get, each, isMap, isSet, has} from "./common"
export function generatePatches(state, basePath, patches, inversePatches) {
const generatePatchesFn = Array.isArray(state.base)
? generateArrayPatches
: isSet(state.base)
? generateSetPatches
: generatePatchesFromAssigned
generatePatchesFn(state, basePath, patches, inversePatches)
}
function generateArrayPatches(state, basePath, patches, inversePatches) {
let {base, copy, assigned} = state
// Reduce complexity by ensuring `base` is never longer.
if (copy.length < base.length) {
;[base, copy] = [copy, base]
;[patches, inversePatches] = [inversePatches, patches]
}
const delta = copy.length - base.length
// Find the first replaced index.
let start = 0
while (base[start] === copy[start] && start < base.length) {
++start
}
// Find the last replaced index. Search from the end to optimize splice patches.
let end = base.length
while (end > start && base[end - 1] === copy[end + delta - 1]) {
--end
}
// Process replaced indices.
for (let i = start; i < end; ++i) {
if (assigned[i] && copy[i] !== base[i]) {
const path = basePath.concat([i])
patches.push({
op: "replace",
path,
value: copy[i]
})
inversePatches.push({
op: "replace",
path,
value: base[i]
})
}
}
const useRemove = end != base.length
const replaceCount = patches.length
// Process added indices.
for (let i = end + delta - 1; i >= end; --i) {
const path = basePath.concat([i])
patches[replaceCount + i - end] = {
op: "add",
path,
value: copy[i]
}
if (useRemove) {
inversePatches.push({
op: "remove",
path
})
}
}
// One "replace" patch reverses all non-splicing "add" patches.
if (!useRemove) {
inversePatches.push({
op: "replace",
path: basePath.concat(["length"]),
value: base.length
})
}
}
// This is used for both Map objects and normal objects.
function generatePatchesFromAssigned(state, basePath, patches, inversePatches) {
const {base, copy} = state
each(state.assigned, (key, assignedValue) => {
const origValue = get(base, key)
const value = get(copy, key)
const op = !assignedValue ? "remove" : has(base, key) ? "replace" : "add"
if (origValue === value && op === "replace") return
const path = basePath.concat(key)
patches.push(op === "remove" ? {op, path} : {op, path, value})
inversePatches.push(
op === "add"
? {op: "remove", path}
: op === "remove"
? {op: "add", path, value: origValue}
: {op: "replace", path, value: origValue}
)
})
}
function generateSetPatches(state, basePath, patches, inversePatches) {
let {base, copy} = state
let i = 0
for (const value of base) {
if (!copy.has(value)) {
const path = basePath.concat([i])
patches.push({
op: "remove",
path,
value
})
inversePatches.unshift({
op: "add",
path,
value
})
}
i++
}
i = 0
for (const value of copy) {
if (!base.has(value)) {
const path = basePath.concat([i])
patches.push({
op: "add",
path,
value
})
inversePatches.unshift({
op: "remove",
path,
value
})
}
i++
}
}
export function applyPatches(draft, patches) {
for (let i = 0; i < patches.length; i++) {
const patch = patches[i]
const {path} = patch
if (path.length === 0 && patch.op === "replace") {
draft = patch.value
} else {
let base = draft
for (let i = 0; i < path.length - 1; i++) {
base = get(base, path[i])
if (!base || typeof base !== "object")
throw new Error("Cannot apply patch, path doesn't resolve: " + path.join("/")) // prettier-ignore
}
const key = path[path.length - 1]
const replace = (key, value) => {
if (isMap(base)) {
base.set(key, value)
return
}
if (isSet(base)) {
throw new Error('Sets cannot have "replace" patches.')
}
base[key] = value
}
const add = (key, value) =>
Array.isArray(base)
? base.splice(key, 0, value)
: isMap(base)
? base.set(key, value)
: isSet(base)
? base.add(value)
: (base[key] = value)
const remove = (key, value) =>
Array.isArray(base)
? base.splice(key, 1)
: isMap(base)
? base.delete(key)
: isSet(base)
? base.delete(value)
: delete base[key]
switch (patch.op) {
case "replace":
replace(key, patch.value)
break
case "add":
add(key, patch.value)
break
case "remove":
remove(key, patch.value)
break
default:
throw new Error("Unsupported patch operation: " + patch.op)
}
}
}
return draft
}