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