source: node_modules/immer/src/core/proxy.ts

Last change on this file was a762898, checked in by istevanoska <ilinastevanoska@…>, 5 months ago

Added visualizations

  • Property mode set to 100644
File size: 7.6 KB
Line 
1import {
2 each,
3 has,
4 is,
5 isDraftable,
6 shallowCopy,
7 latest,
8 ImmerBaseState,
9 ImmerState,
10 Drafted,
11 AnyObject,
12 AnyArray,
13 Objectish,
14 getCurrentScope,
15 getPrototypeOf,
16 DRAFT_STATE,
17 die,
18 createProxy,
19 ArchType,
20 ImmerScope
21} from "../internal"
22
23interface ProxyBaseState extends ImmerBaseState {
24 assigned_: {
25 [property: string]: boolean
26 }
27 parent_?: ImmerState
28 revoke_(): void
29}
30
31export interface ProxyObjectState extends ProxyBaseState {
32 type_: ArchType.Object
33 base_: any
34 copy_: any
35 draft_: Drafted<AnyObject, ProxyObjectState>
36}
37
38export interface ProxyArrayState extends ProxyBaseState {
39 type_: ArchType.Array
40 base_: AnyArray
41 copy_: AnyArray | null
42 draft_: Drafted<AnyArray, ProxyArrayState>
43}
44
45type ProxyState = ProxyObjectState | ProxyArrayState
46
47/**
48 * Returns a new draft of the `base` object.
49 *
50 * The second argument is the parent draft-state (used internally).
51 */
52export function createProxyProxy<T extends Objectish>(
53 base: T,
54 parent?: ImmerState
55): Drafted<T, ProxyState> {
56 const isArray = Array.isArray(base)
57 const state: ProxyState = {
58 type_: isArray ? ArchType.Array : (ArchType.Object as any),
59 // Track which produce call this is associated with.
60 scope_: parent ? parent.scope_ : getCurrentScope()!,
61 // True for both shallow and deep changes.
62 modified_: false,
63 // Used during finalization.
64 finalized_: false,
65 // Track which properties have been assigned (true) or deleted (false).
66 assigned_: {},
67 // The parent draft state.
68 parent_: parent,
69 // The base state.
70 base_: base,
71 // The base proxy.
72 draft_: null as any, // set below
73 // The base copy with any updated values.
74 copy_: null,
75 // Called by the `produce` function.
76 revoke_: null as any,
77 isManual_: false
78 }
79
80 // the traps must target something, a bit like the 'real' base.
81 // but also, we need to be able to determine from the target what the relevant state is
82 // (to avoid creating traps per instance to capture the state in closure,
83 // and to avoid creating weird hidden properties as well)
84 // So the trick is to use 'state' as the actual 'target'! (and make sure we intercept everything)
85 // Note that in the case of an array, we put the state in an array to have better Reflect defaults ootb
86 let target: T = state as any
87 let traps: ProxyHandler<object | Array<any>> = objectTraps
88 if (isArray) {
89 target = [state] as any
90 traps = arrayTraps
91 }
92
93 const {revoke, proxy} = Proxy.revocable(target, traps)
94 state.draft_ = proxy as any
95 state.revoke_ = revoke
96 return proxy as any
97}
98
99/**
100 * Object drafts
101 */
102export const objectTraps: ProxyHandler<ProxyState> = {
103 get(state, prop) {
104 if (prop === DRAFT_STATE) return state
105
106 const source = latest(state)
107 if (!has(source, prop)) {
108 // non-existing or non-own property...
109 return readPropFromProto(state, source, prop)
110 }
111 const value = source[prop]
112 if (state.finalized_ || !isDraftable(value)) {
113 return value
114 }
115 // Check for existing draft in modified state.
116 // Assigned values are never drafted. This catches any drafts we created, too.
117 if (value === peek(state.base_, prop)) {
118 prepareCopy(state)
119 return (state.copy_![prop as any] = createProxy(value, state))
120 }
121 return value
122 },
123 has(state, prop) {
124 return prop in latest(state)
125 },
126 ownKeys(state) {
127 return Reflect.ownKeys(latest(state))
128 },
129 set(
130 state: ProxyObjectState,
131 prop: string /* strictly not, but helps TS */,
132 value
133 ) {
134 const desc = getDescriptorFromProto(latest(state), prop)
135 if (desc?.set) {
136 // special case: if this write is captured by a setter, we have
137 // to trigger it with the correct context
138 desc.set.call(state.draft_, value)
139 return true
140 }
141 if (!state.modified_) {
142 // the last check is because we need to be able to distinguish setting a non-existing to undefined (which is a change)
143 // from setting an existing property with value undefined to undefined (which is not a change)
144 const current = peek(latest(state), prop)
145 // special case, if we assigning the original value to a draft, we can ignore the assignment
146 const currentState: ProxyObjectState = current?.[DRAFT_STATE]
147 if (currentState && currentState.base_ === value) {
148 state.copy_![prop] = value
149 state.assigned_[prop] = false
150 return true
151 }
152 if (is(value, current) && (value !== undefined || has(state.base_, prop)))
153 return true
154 prepareCopy(state)
155 markChanged(state)
156 }
157
158 if (
159 (state.copy_![prop] === value &&
160 // special case: handle new props with value 'undefined'
161 (value !== undefined || prop in state.copy_)) ||
162 // special case: NaN
163 (Number.isNaN(value) && Number.isNaN(state.copy_![prop]))
164 )
165 return true
166
167 // @ts-ignore
168 state.copy_![prop] = value
169 state.assigned_[prop] = true
170 return true
171 },
172 deleteProperty(state, prop: string) {
173 // The `undefined` check is a fast path for pre-existing keys.
174 if (peek(state.base_, prop) !== undefined || prop in state.base_) {
175 state.assigned_[prop] = false
176 prepareCopy(state)
177 markChanged(state)
178 } else {
179 // if an originally not assigned property was deleted
180 delete state.assigned_[prop]
181 }
182 if (state.copy_) {
183 delete state.copy_[prop]
184 }
185 return true
186 },
187 // Note: We never coerce `desc.value` into an Immer draft, because we can't make
188 // the same guarantee in ES5 mode.
189 getOwnPropertyDescriptor(state, prop) {
190 const owner = latest(state)
191 const desc = Reflect.getOwnPropertyDescriptor(owner, prop)
192 if (!desc) return desc
193 return {
194 writable: true,
195 configurable: state.type_ !== ArchType.Array || prop !== "length",
196 enumerable: desc.enumerable,
197 value: owner[prop]
198 }
199 },
200 defineProperty() {
201 die(11)
202 },
203 getPrototypeOf(state) {
204 return getPrototypeOf(state.base_)
205 },
206 setPrototypeOf() {
207 die(12)
208 }
209}
210
211/**
212 * Array drafts
213 */
214
215const arrayTraps: ProxyHandler<[ProxyArrayState]> = {}
216each(objectTraps, (key, fn) => {
217 // @ts-ignore
218 arrayTraps[key] = function() {
219 arguments[0] = arguments[0][0]
220 return fn.apply(this, arguments)
221 }
222})
223arrayTraps.deleteProperty = function(state, prop) {
224 if (process.env.NODE_ENV !== "production" && isNaN(parseInt(prop as any)))
225 die(13)
226 // @ts-ignore
227 return arrayTraps.set!.call(this, state, prop, undefined)
228}
229arrayTraps.set = function(state, prop, value) {
230 if (
231 process.env.NODE_ENV !== "production" &&
232 prop !== "length" &&
233 isNaN(parseInt(prop as any))
234 )
235 die(14)
236 return objectTraps.set!.call(this, state[0], prop, value, state[0])
237}
238
239// Access a property without creating an Immer draft.
240function peek(draft: Drafted, prop: PropertyKey) {
241 const state = draft[DRAFT_STATE]
242 const source = state ? latest(state) : draft
243 return source[prop]
244}
245
246function readPropFromProto(state: ImmerState, source: any, prop: PropertyKey) {
247 const desc = getDescriptorFromProto(source, prop)
248 return desc
249 ? `value` in desc
250 ? desc.value
251 : // This is a very special case, if the prop is a getter defined by the
252 // prototype, we should invoke it with the draft as context!
253 desc.get?.call(state.draft_)
254 : undefined
255}
256
257function getDescriptorFromProto(
258 source: any,
259 prop: PropertyKey
260): PropertyDescriptor | undefined {
261 // 'in' checks proto!
262 if (!(prop in source)) return undefined
263 let proto = getPrototypeOf(source)
264 while (proto) {
265 const desc = Object.getOwnPropertyDescriptor(proto, prop)
266 if (desc) return desc
267 proto = getPrototypeOf(proto)
268 }
269 return undefined
270}
271
272export function markChanged(state: ImmerState) {
273 if (!state.modified_) {
274 state.modified_ = true
275 if (state.parent_) {
276 markChanged(state.parent_)
277 }
278 }
279}
280
281export function prepareCopy(state: {
282 base_: any
283 copy_: any
284 scope_: ImmerScope
285}) {
286 if (!state.copy_) {
287 state.copy_ = shallowCopy(
288 state.base_,
289 state.scope_.immer_.useStrictShallowCopy_
290 )
291 }
292}
Note: See TracBrowser for help on using the repository browser.