/
finalize.ts
207 lines (194 loc) · 5.08 KB
/
finalize.ts
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
200
201
202
203
204
205
206
207
import {
Immer,
ImmerScope,
DRAFT_STATE,
isDraftable,
NOTHING,
Drafted,
PatchPath,
ProxyType,
each,
has,
freeze,
generatePatches,
shallowCopy,
ImmerState,
isSet,
isDraft,
SetState,
set,
is,
get
} from "./internal"
export function processResult(immer: Immer, result: any, scope: ImmerScope) {
const baseDraft = scope.drafts![0]
const isReplaced = result !== undefined && result !== baseDraft
immer.willFinalize(scope, result, isReplaced)
if (isReplaced) {
if (baseDraft[DRAFT_STATE].modified) {
scope.revoke()
throw new Error("An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.") // prettier-ignore
}
if (isDraftable(result)) {
// Finalize the result in case it contains (or is) a subset of the draft.
result = finalize(immer, result, scope)
maybeFreeze(immer, result)
}
if (scope.patches) {
scope.patches.push({
op: "replace",
path: [],
value: result
})
scope.inversePatches!.push({
op: "replace",
path: [],
value: baseDraft[DRAFT_STATE].base
})
}
} else {
// Finalize the base draft.
result = finalize(immer, baseDraft, scope, [])
}
scope.revoke()
if (scope.patches) {
scope.patchListener!(scope.patches, scope.inversePatches!)
}
return result !== NOTHING ? result : undefined
}
function finalize(
immer: Immer,
draft: Drafted,
scope: ImmerScope,
path?: PatchPath
) {
const state = draft[DRAFT_STATE]
if (!state) {
if (Object.isFrozen(draft)) return draft
return finalizeTree(immer, draft, scope)
}
// Never finalize drafts owned by another scope.
if (state.scope !== scope) {
return draft
}
if (!state.modified) {
maybeFreeze(immer, state.base, true)
return state.base
}
if (!state.finalized) {
state.finalized = true
finalizeTree(immer, state.draft, scope, path)
// We cannot really delete anything inside of a Set. We can only replace the whole Set.
if (immer.onDelete && state.type !== ProxyType.Set) {
// The `assigned` object is unreliable with ES5 drafts.
if (immer.useProxies) {
const {assigned} = state
each(assigned, (prop, exists) => {
if (!exists) immer.onDelete!(state, prop as any)
})
} else {
const {base, copy} = state
each(base, prop => {
if (!has(copy, prop)) immer.onDelete!(state, prop as any)
})
}
}
if (immer.onCopy) {
immer.onCopy(state)
}
// At this point, all descendants of `state.copy` have been finalized,
// so we can be sure that `scope.canAutoFreeze` is accurate.
if (immer.autoFreeze && scope.canAutoFreeze) {
freeze(state.copy, false)
}
if (path && scope.patches) {
generatePatches(state, path, scope.patches, scope.inversePatches!)
}
}
return state.copy
}
function finalizeTree(
immer: Immer,
root: Drafted,
scope: ImmerScope,
rootPath?: PatchPath
) {
const state = root[DRAFT_STATE]
if (state) {
if (
state.type === ProxyType.ES5Object ||
state.type === ProxyType.ES5Array
) {
// Create the final copy, with added keys and without deleted keys.
state.copy = shallowCopy(state.draft, true)
}
root = state.copy
}
each(root, (key, value) =>
finalizeProperty(immer, scope, root, state, root, key, value, rootPath)
)
return root
}
function finalizeProperty(
immer: Immer,
scope: ImmerScope,
root: Drafted,
rootState: ImmerState,
parentValue: Drafted,
prop: string | number,
childValue: any,
rootPath?: PatchPath
) {
if (childValue === parentValue) {
throw Error("Immer forbids circular references")
}
// In the `finalizeTree` method, only the `root` object may be a draft.
const isDraftProp = !!rootState && parentValue === root
const isSetMember = isSet(parentValue)
if (isDraft(childValue)) {
const path =
rootPath &&
isDraftProp &&
!isSetMember && // Set objects are atomic since they have no keys.
!has((rootState as Exclude<ImmerState, SetState>).assigned!, prop) // Skip deep patches for assigned keys.
? rootPath!.concat(prop)
: undefined
// Drafts owned by `scope` are finalized here.
childValue = finalize(immer, childValue, scope, path)
set(parentValue, prop, childValue)
// Drafts from another scope must prevent auto-freezing.
if (isDraft(childValue)) {
scope.canAutoFreeze = false
}
}
// Unchanged draft properties are ignored.
else if (isDraftProp && is(childValue, get(rootState.base, prop))) {
return
}
// Search new objects for unfinalized drafts. Frozen objects should never contain drafts.
// TODO: the recursion over here looks weird, shouldn't non-draft stuff have it's own recursion?
// especially the passing on of root and rootState doesn't make sense...
else if (isDraftable(childValue) && !Object.isFrozen(childValue)) {
each(childValue, (key, grandChild) =>
finalizeProperty(
immer,
scope,
root,
rootState,
childValue,
key,
grandChild,
rootPath
)
)
maybeFreeze(immer, childValue)
}
if (isDraftProp && immer.onAssign && !isSetMember) {
immer.onAssign(rootState, prop, childValue)
}
}
export function maybeFreeze(immer: Immer, value: any, deep = false) {
if (immer.autoFreeze && !isDraft(value)) {
freeze(value, deep)
}
}