1import { 2 ImmerState, 3 Drafted, 4 ES5ArrayState, 5 ES5ObjectState, 6 each, 7 has, 8 isDraft, 9 latest, 10 DRAFT_STATE, 11 is, 12 loadPlugin, 13 ImmerScope, 14 ProxyType, 15 getCurrentScope, 16 die, 17 markChanged, 18 objectTraps, 19 ownKeys, 20 getOwnPropertyDescriptors 21} from "../internal" 22 23type ES5State = ES5ArrayState | ES5ObjectState 24 25export function enableES5() { 26 function willFinalizeES5_( 27 scope: ImmerScope, 28 result: any, 29 isReplaced: boolean 30 ) { 31 if (!isReplaced) { 32 if (scope.patches_) { 33 markChangesRecursively(scope.drafts_![0]) 34 } 35 // This is faster when we don't care about which attributes changed. 36 markChangesSweep(scope.drafts_) 37 } 38 // When a child draft is returned, look for changes. 39 else if ( 40 isDraft(result) && 41 (result[DRAFT_STATE] as ES5State).scope_ === scope 42 ) { 43 markChangesSweep(scope.drafts_) 44 } 45 } 46 47 function createES5Draft(isArray: boolean, base: any) { 48 if (isArray) { 49 const draft = new Array(base.length) 50 for (let i = 0; i < base.length; i++) 51 Object.defineProperty(draft, "" + i, proxyProperty(i, true)) 52 return draft 53 } else { 54 const descriptors = getOwnPropertyDescriptors(base) 55 delete descriptors[DRAFT_STATE as any] 56 const keys = ownKeys(descriptors) 57 for (let i = 0; i < keys.length; i++) { 58 const key: any = keys[i] 59 descriptors[key] = proxyProperty( 60 key, 61 isArray || !!descriptors[key].enumerable 62 ) 63 } 64 return Object.create(Object.getPrototypeOf(base), descriptors) 65 } 66 } 67 68 function createES5Proxy_<T>( 69 base: T, 70 parent?: ImmerState 71 ): Drafted<T, ES5ObjectState | ES5ArrayState> { 72 const isArray = Array.isArray(base) 73 const draft = createES5Draft(isArray, base) 74 75 const state: ES5ObjectState | ES5ArrayState = { 76 type_: isArray ? ProxyType.ES5Array : (ProxyType.ES5Object as any), 77 scope_: parent ? parent.scope_ : getCurrentScope(), 78 modified_: false, 79 finalized_: false, 80 assigned_: {}, 81 parent_: parent, 82 // base is the object we are drafting 83 base_: base, 84 // draft is the draft object itself, that traps all reads and reads from either the base (if unmodified) or copy (if modified) 85 draft_: draft, 86 copy_: null, 87 revoked_: false, 88 isManual_: false 89 } 90 91 Object.defineProperty(draft, DRAFT_STATE, { 92 value: state, 93 // enumerable: false <- the default 94 writable: true 95 }) 96 return draft 97 } 98 99 // property descriptors are recycled to make sure we don't create a get and set closure per property, 100 // but share them all instead 101 const descriptors: {[prop: string]: PropertyDescriptor} = {} 102 103 function proxyProperty( 104 prop: string | number, 105 enumerable: boolean 106 ): PropertyDescriptor { 107 let desc = descriptors[prop] 108 if (desc) { 109 desc.enumerable = enumerable 110 } else { 111 descriptors[prop] = desc = { 112 configurable: true, 113 enumerable, 114 get(this: any) { 115 const state = this[DRAFT_STATE] 116 if (__DEV__) assertUnrevoked(state) 117 // @ts-ignore 118 return objectTraps.get(state, prop) 119 }, 120 set(this: any, value) { 121 const state = this[DRAFT_STATE] 122 if (__DEV__) assertUnrevoked(state) 123 // @ts-ignore 124 objectTraps.set(state, prop, value) 125 } 126 } 127 } 128 return desc 129 } 130 131 // This looks expensive, but only proxies are visited, and only objects without known changes are scanned. 132 function markChangesSweep(drafts: Drafted<any, ImmerState>[]) { 133 // The natural order of drafts in the `scope` array is based on when they 134 // were accessed. By processing drafts in reverse natural order, we have a 135 // better chance of processing leaf nodes first. When a leaf node is known to 136 // have changed, we can avoid any traversal of its ancestor nodes. 137 for (let i = drafts.length - 1; i >= 0; i--) { 138 const state: ES5State = drafts[i][DRAFT_STATE] 139 if (!state.modified_) { 140 switch (state.type_) { 141 case ProxyType.ES5Array: 142 if (hasArrayChanges(state)) markChanged(state) 143 break 144 case ProxyType.ES5Object: 145 if (hasObjectChanges(state)) markChanged(state) 146 break 147 } 148 } 149 } 150 } 151 152 function markChangesRecursively(object: any) { 153 if (!object || typeof object !== "object") return 154 const state: ES5State | undefined = object[DRAFT_STATE] 155 if (!state) return 156 const {base_, draft_, assigned_, type_} = state 157 if (type_ === ProxyType.ES5Object) { 158 // Look for added keys. 159 // probably there is a faster way to detect changes, as sweep + recurse seems to do some 160 // unnecessary work. 161 // also: probably we can store the information we detect here, to speed up tree finalization! 162 each(draft_, key => { 163 if ((key as any) === DRAFT_STATE) return 164 // The `undefined` check is a fast path for pre-existing keys. 165 if ((base_ as any)[key] === undefined && !has(base_, key)) { 166 assigned_[key] = true 167 markChanged(state) 168 } else if (!assigned_[key]) { 169 // Only untouched properties trigger recursion. 170 markChangesRecursively(draft_[key]) 171 } 172 }) 173 // Look for removed keys. 174 each(base_, key => { 175 // The `undefined` check is a fast path for pre-existing keys. 176 if (draft_[key] === undefined && !has(draft_, key)) { 177 assigned_[key] = false 178 markChanged(state) 179 } 180 }) 181 } else if (type_ === ProxyType.ES5Array) { 182 if (hasArrayChanges(state as ES5ArrayState)) { 183 markChanged(state) 184 assigned_.length = true 185 } 186 187 if (draft_.length < base_.length) { 188 for (let i = draft_.length; i < base_.length; i++) assigned_[i] = false 189 } else { 190 for (let i = base_.length; i < draft_.length; i++) assigned_[i] = true 191 } 192 193 // Minimum count is enough, the other parts has been processed. 194 const min = Math.min(draft_.length, base_.length) 195 196 for (let i = 0; i < min; i++) { 197 // Only untouched indices trigger recursion. 198 if (assigned_[i] === undefined) markChangesRecursively(draft_[i]) 199 } 200 } 201 } 202 203 function hasObjectChanges(state: ES5ObjectState) { 204 const {base_, draft_} = state 205 206 // Search for added keys and changed keys. Start at the back, because 207 // non-numeric keys are ordered by time of definition on the object. 208 const keys = ownKeys(draft_) 209 for (let i = keys.length - 1; i >= 0; i--) { 210 const key: any = keys[i] 211 if (key === DRAFT_STATE) continue 212 const baseValue = base_[key] 213 // The `undefined` check is a fast path for pre-existing keys. 214 if (baseValue === undefined && !has(base_, key)) { 215 return true 216 } 217 // Once a base key is deleted, future changes go undetected, because its 218 // descriptor is erased. This branch detects any missed changes. 219 else { 220 const value = draft_[key] 221 const state: ImmerState = value && value[DRAFT_STATE] 222 if (state ? state.base_ !== baseValue : !is(value, baseValue)) { 223 return true 224 } 225 } 226 } 227 228 // At this point, no keys were added or changed. 229 // Compare key count to determine if keys were deleted. 230 const baseIsDraft = !!base_[DRAFT_STATE as any] 231 return keys.length !== ownKeys(base_).length + (baseIsDraft ? 0 : 1) // + 1 to correct for DRAFT_STATE 232 } 233 234 function hasArrayChanges(state: ES5ArrayState) { 235 const {draft_} = state 236 if (draft_.length !== state.base_.length) return true 237 // See #116 238 // If we first shorten the length, our array interceptors will be removed. 239 // If after that new items are added, result in the same original length, 240 // those last items will have no intercepting property. 241 // So if there is no own descriptor on the last position, we know that items were removed and added 242 // N.B.: splice, unshift, etc only shift values around, but not prop descriptors, so we only have to check 243 // the last one 244 const descriptor = Object.getOwnPropertyDescriptor( 245 draft_, 246 draft_.length - 1 247 ) 248 // descriptor can be null, but only for newly created sparse arrays, eg. new Array(10) 249 if (descriptor && !descriptor.get) return true 250 // For all other cases, we don't have to compare, as they would have been picked up by the index setters 251 return false 252 } 253 254 function hasChanges_(state: ES5State) { 255 return state.type_ === ProxyType.ES5Object 256 ? hasObjectChanges(state) 257 : hasArrayChanges(state) 258 } 259 260 function assertUnrevoked(state: any /*ES5State | MapState | SetState*/) { 261 if (state.revoked_) die(3, JSON.stringify(latest(state))) 262 } 263 264 loadPlugin("ES5", { 265 createES5Proxy_, 266 willFinalizeES5_, 267 hasChanges_ 268 }) 269} 270