| 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 | getPrototypeOf,
|
|---|
| 16 | DRAFT_STATE,
|
|---|
| 17 | die,
|
|---|
| 18 | createProxy,
|
|---|
| 19 | ArchType,
|
|---|
| 20 | ImmerScope
|
|---|
| 21 | } from "../internal"
|
|---|
| 22 |
|
|---|
| 23 | interface ProxyBaseState extends ImmerBaseState {
|
|---|
| 24 | assigned_: {
|
|---|
| 25 | [property: string]: boolean
|
|---|
| 26 | }
|
|---|
| 27 | parent_?: ImmerState
|
|---|
| 28 | revoke_(): void
|
|---|
| 29 | }
|
|---|
| 30 |
|
|---|
| 31 | export interface ProxyObjectState extends ProxyBaseState {
|
|---|
| 32 | type_: ArchType.Object
|
|---|
| 33 | base_: any
|
|---|
| 34 | copy_: any
|
|---|
| 35 | draft_: Drafted<AnyObject, ProxyObjectState>
|
|---|
| 36 | }
|
|---|
| 37 |
|
|---|
| 38 | export interface ProxyArrayState extends ProxyBaseState {
|
|---|
| 39 | type_: ArchType.Array
|
|---|
| 40 | base_: AnyArray
|
|---|
| 41 | copy_: AnyArray | null
|
|---|
| 42 | draft_: Drafted<AnyArray, ProxyArrayState>
|
|---|
| 43 | }
|
|---|
| 44 |
|
|---|
| 45 | type 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 | */
|
|---|
| 52 | export 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 | */
|
|---|
| 102 | export 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 |
|
|---|
| 215 | const arrayTraps: ProxyHandler<[ProxyArrayState]> = {}
|
|---|
| 216 | each(objectTraps, (key, fn) => {
|
|---|
| 217 | // @ts-ignore
|
|---|
| 218 | arrayTraps[key] = function() {
|
|---|
| 219 | arguments[0] = arguments[0][0]
|
|---|
| 220 | return fn.apply(this, arguments)
|
|---|
| 221 | }
|
|---|
| 222 | })
|
|---|
| 223 | arrayTraps.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 | }
|
|---|
| 229 | arrayTraps.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.
|
|---|
| 240 | function peek(draft: Drafted, prop: PropertyKey) {
|
|---|
| 241 | const state = draft[DRAFT_STATE]
|
|---|
| 242 | const source = state ? latest(state) : draft
|
|---|
| 243 | return source[prop]
|
|---|
| 244 | }
|
|---|
| 245 |
|
|---|
| 246 | function 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 |
|
|---|
| 257 | function 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 |
|
|---|
| 272 | export function markChanged(state: ImmerState) {
|
|---|
| 273 | if (!state.modified_) {
|
|---|
| 274 | state.modified_ = true
|
|---|
| 275 | if (state.parent_) {
|
|---|
| 276 | markChanged(state.parent_)
|
|---|
| 277 | }
|
|---|
| 278 | }
|
|---|
| 279 | }
|
|---|
| 280 |
|
|---|
| 281 | export 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 | }
|
|---|