-
-
Notifications
You must be signed in to change notification settings - Fork 842
/
es5.ts
303 lines (279 loc) · 8.54 KB
/
es5.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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
"use strict"
import {
each,
has,
is,
isDraft,
isDraftable,
isEnumerable,
shallowCopy,
latest,
createHiddenProperty,
ImmerScope,
ImmerState,
Drafted,
AnyObject,
Objectish,
ImmerBaseState,
AnyArray,
ProxyType,
MapState,
SetState,
DRAFT_STATE
} from "./internal"
interface ES5BaseState extends ImmerBaseState {
finalizing: boolean
assigned: {[key: string]: any}
parent?: ImmerState
revoked: boolean
}
export interface ES5ObjectState extends ES5BaseState {
type: ProxyType.ES5Object
draft: Drafted<AnyObject, ES5ObjectState>
base: AnyObject
copy: AnyObject | null
}
export interface ES5ArrayState extends ES5BaseState {
type: ProxyType.ES5Array
draft: Drafted<AnyObject, ES5ArrayState>
base: AnyArray
copy: AnyArray | null
}
type ES5State = ES5ArrayState | ES5ObjectState
export function willFinalizeES5(
scope: ImmerScope,
result: any,
isReplaced: boolean
) {
scope.drafts!.forEach(draft => {
draft[DRAFT_STATE].finalizing = true
})
if (!isReplaced) {
if (scope.patches) {
markChangesRecursively(scope.drafts![0])
}
// This is faster when we don't care about which attributes changed.
markChangesSweep(scope.drafts)
}
// When a child draft is returned, look for changes.
else if (isDraft(result) && result[DRAFT_STATE].scope === scope) {
markChangesSweep(scope.drafts)
}
}
export function createES5Proxy<T>(
base: T,
parent?: ImmerState
): Drafted<T, ES5ObjectState | ES5ArrayState> {
const isArray = Array.isArray(base)
const draft = clonePotentialDraft(base)
each(draft, prop => {
proxyProperty(draft, prop, isArray || isEnumerable(base, prop))
})
const state: ES5ObjectState | ES5ArrayState = {
type: isArray ? ProxyType.ES5Array : (ProxyType.ES5Object as any),
scope: parent ? parent.scope : ImmerScope.current!,
modified: false,
finalizing: false,
finalized: false,
assigned: {},
parent,
base,
draft,
copy: null,
revoked: false,
isManual: false
}
createHiddenProperty(draft, DRAFT_STATE, state)
return draft
}
// Access a property without creating an Immer draft.
function peek(draft: Drafted, prop: PropertyKey) {
const state = draft[DRAFT_STATE]
if (state && !state.finalizing) {
state.finalizing = true
const value = draft[prop]
state.finalizing = false
return value
}
return draft[prop]
}
function get(state: ES5State, prop: string | number) {
assertUnrevoked(state)
const value = peek(latest(state), prop)
if (state.finalizing) return value
// Create a draft if the value is unmodified.
if (value === peek(state.base, prop) && isDraftable(value)) {
prepareCopy(state)
// @ts-ignore
return (state.copy![prop] = state.scope.immer.createProxy(value, state))
}
return value
}
function set(state: ES5State, prop: string | number, value: any) {
assertUnrevoked(state)
state.assigned[prop] = true
if (!state.modified) {
if (is(value, peek(latest(state), prop))) return
markChangedES5(state)
prepareCopy(state)
}
// @ts-ignore
state.copy![prop] = value
}
export function markChangedES5(state: ImmerState) {
if (!state.modified) {
state.modified = true
if (state.parent) markChangedES5(state.parent)
}
}
function prepareCopy(state: ES5State) {
if (!state.copy) state.copy = clonePotentialDraft(state.base)
}
function clonePotentialDraft(base: Objectish) {
const state = base && (base as any)[DRAFT_STATE]
if (state) {
state.finalizing = true
const draft = shallowCopy(state.draft, true)
state.finalizing = false
return draft
}
return shallowCopy(base)
}
// property descriptors are recycled to make sure we don't create a get and set closure per property,
// but share them all instead
const descriptors: {[prop: string]: PropertyDescriptor} = {}
function proxyProperty(
draft: Drafted<any, ES5State>,
prop: string | number,
enumerable: boolean
) {
let desc = descriptors[prop]
if (desc) {
desc.enumerable = enumerable
} else {
descriptors[prop] = desc = {
configurable: true,
enumerable,
get(this: any) {
return get(this[DRAFT_STATE], prop)
},
set(this: any, value) {
set(this[DRAFT_STATE], prop, value)
}
}
}
Object.defineProperty(draft, prop, desc)
}
export function assertUnrevoked(state: ES5State | MapState | SetState) {
if (state.revoked === true)
throw new Error(
"Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? " +
JSON.stringify(latest(state))
)
}
// This looks expensive, but only proxies are visited, and only objects without known changes are scanned.
function markChangesSweep(drafts: Drafted<any, ImmerState>[]) {
// The natural order of drafts in the `scope` array is based on when they
// were accessed. By processing drafts in reverse natural order, we have a
// better chance of processing leaf nodes first. When a leaf node is known to
// have changed, we can avoid any traversal of its ancestor nodes.
for (let i = drafts.length - 1; i >= 0; i--) {
const state = drafts[i][DRAFT_STATE]
if (!state.modified) {
switch (state.type) {
case ProxyType.ES5Array:
if (hasArrayChanges(state)) markChangedES5(state)
break
case ProxyType.ES5Object:
if (hasObjectChanges(state)) markChangedES5(state)
break
}
}
}
}
function markChangesRecursively(object: any) {
if (!object || typeof object !== "object") return
const state = object[DRAFT_STATE]
if (!state) return
const {base, draft, assigned, type} = state
if (type === ProxyType.ES5Object) {
// Look for added keys.
// TODO: looks quite duplicate to hasObjectChanges,
// probably there is a faster way to detect changes, as sweep + recurse seems to do some
// unnecessary work.
// also: probably we can store the information we detect here, to speed up tree finalization!
each(draft, key => {
if ((key as any) === DRAFT_STATE) return
// The `undefined` check is a fast path for pre-existing keys.
if (base[key] === undefined && !has(base, key)) {
assigned[key] = true
markChangedES5(state)
} else if (!assigned[key]) {
// Only untouched properties trigger recursion.
markChangesRecursively(draft[key])
}
})
// Look for removed keys.
each(base, key => {
// The `undefined` check is a fast path for pre-existing keys.
if (draft[key] === undefined && !has(draft, key)) {
assigned[key] = false
markChangedES5(state)
}
})
} else if (type === ProxyType.ES5Array && hasArrayChanges(state)) {
markChangedES5(state)
assigned.length = true
if (draft.length < base.length) {
for (let i = draft.length; i < base.length; i++) assigned[i] = false
} else {
for (let i = base.length; i < draft.length; i++) assigned[i] = true
}
for (let i = 0; i < draft.length; i++) {
// Only untouched indices trigger recursion.
if (assigned[i] === undefined) markChangesRecursively(draft[i])
}
}
}
function hasObjectChanges(state: ES5ObjectState) {
const {base, draft} = state
// Search for added keys and changed keys. Start at the back, because
// non-numeric keys are ordered by time of definition on the object.
const keys = Object.keys(draft)
for (let i = keys.length - 1; i >= 0; i--) {
const key = keys[i]
const baseValue = base[key]
// The `undefined` check is a fast path for pre-existing keys.
if (baseValue === undefined && !has(base, key)) {
return true
}
// Once a base key is deleted, future changes go undetected, because its
// descriptor is erased. This branch detects any missed changes.
else {
const value = draft[key]
const state = value && value[DRAFT_STATE]
if (state ? state.base !== baseValue : !is(value, baseValue)) {
return true
}
}
}
// At this point, no keys were added or changed.
// Compare key count to determine if keys were deleted.
return keys.length !== Object.keys(base).length
}
function hasArrayChanges(state: ES5ArrayState) {
const {draft} = state
if (draft.length !== state.base.length) return true
// See #116
// If we first shorten the length, our array interceptors will be removed.
// If after that new items are added, result in the same original length,
// those last items will have no intercepting property.
// So if there is no own descriptor on the last position, we know that items were removed and added
// N.B.: splice, unshift, etc only shift values around, but not prop descriptors, so we only have to check
// the last one
const descriptor = Object.getOwnPropertyDescriptor(draft, draft.length - 1)
// descriptor can be null, but only for newly created sparse arrays, eg. new Array(10)
if (descriptor && !descriptor.get) return true
// For all other cases, we don't have to compare, as they would have been picked up by the index setters
return false
}