You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
281 lines
7.5 KiB
281 lines
7.5 KiB
|
2 years ago
|
import {
|
||
|
|
each,
|
||
|
|
has,
|
||
|
|
is,
|
||
|
|
isDraftable,
|
||
|
|
shallowCopy,
|
||
|
|
latest,
|
||
|
|
ImmerBaseState,
|
||
|
|
ImmerState,
|
||
|
|
Drafted,
|
||
|
|
AnyObject,
|
||
|
|
AnyArray,
|
||
|
|
Objectish,
|
||
|
|
getCurrentScope,
|
||
|
|
DRAFT_STATE,
|
||
|
|
die,
|
||
|
|
createProxy,
|
||
|
|
ProxyType
|
||
|
|
} from "../internal"
|
||
|
|
|
||
|
|
interface ProxyBaseState extends ImmerBaseState {
|
||
|
|
assigned_: {
|
||
|
|
[property: string]: boolean
|
||
|
|
}
|
||
|
|
parent_?: ImmerState
|
||
|
|
revoke_(): void
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ProxyObjectState extends ProxyBaseState {
|
||
|
|
type_: ProxyType.ProxyObject
|
||
|
|
base_: any
|
||
|
|
copy_: any
|
||
|
|
draft_: Drafted<AnyObject, ProxyObjectState>
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ProxyArrayState extends ProxyBaseState {
|
||
|
|
type_: ProxyType.ProxyArray
|
||
|
|
base_: AnyArray
|
||
|
|
copy_: AnyArray | null
|
||
|
|
draft_: Drafted<AnyArray, ProxyArrayState>
|
||
|
|
}
|
||
|
|
|
||
|
|
type ProxyState = ProxyObjectState | ProxyArrayState
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns a new draft of the `base` object.
|
||
|
|
*
|
||
|
|
* The second argument is the parent draft-state (used internally).
|
||
|
|
*/
|
||
|
|
export function createProxyProxy<T extends Objectish>(
|
||
|
|
base: T,
|
||
|
|
parent?: ImmerState
|
||
|
|
): Drafted<T, ProxyState> {
|
||
|
|
const isArray = Array.isArray(base)
|
||
|
|
const state: ProxyState = {
|
||
|
|
type_: isArray ? ProxyType.ProxyArray : (ProxyType.ProxyObject as any),
|
||
|
|
// Track which produce call this is associated with.
|
||
|
|
scope_: parent ? parent.scope_ : getCurrentScope()!,
|
||
|
|
// True for both shallow and deep changes.
|
||
|
|
modified_: false,
|
||
|
|
// Used during finalization.
|
||
|
|
finalized_: false,
|
||
|
|
// Track which properties have been assigned (true) or deleted (false).
|
||
|
|
assigned_: {},
|
||
|
|
// The parent draft state.
|
||
|
|
parent_: parent,
|
||
|
|
// The base state.
|
||
|
|
base_: base,
|
||
|
|
// The base proxy.
|
||
|
|
draft_: null as any, // set below
|
||
|
|
// The base copy with any updated values.
|
||
|
|
copy_: null,
|
||
|
|
// Called by the `produce` function.
|
||
|
|
revoke_: null as any,
|
||
|
|
isManual_: false
|
||
|
|
}
|
||
|
|
|
||
|
|
// the traps must target something, a bit like the 'real' base.
|
||
|
|
// but also, we need to be able to determine from the target what the relevant state is
|
||
|
|
// (to avoid creating traps per instance to capture the state in closure,
|
||
|
|
// and to avoid creating weird hidden properties as well)
|
||
|
|
// So the trick is to use 'state' as the actual 'target'! (and make sure we intercept everything)
|
||
|
|
// Note that in the case of an array, we put the state in an array to have better Reflect defaults ootb
|
||
|
|
let target: T = state as any
|
||
|
|
let traps: ProxyHandler<object | Array<any>> = objectTraps
|
||
|
|
if (isArray) {
|
||
|
|
target = [state] as any
|
||
|
|
traps = arrayTraps
|
||
|
|
}
|
||
|
|
|
||
|
|
const {revoke, proxy} = Proxy.revocable(target, traps)
|
||
|
|
state.draft_ = proxy as any
|
||
|
|
state.revoke_ = revoke
|
||
|
|
return proxy as any
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Object drafts
|
||
|
|
*/
|
||
|
|
export const objectTraps: ProxyHandler<ProxyState> = {
|
||
|
|
get(state, prop) {
|
||
|
|
if (prop === DRAFT_STATE) return state
|
||
|
|
|
||
|
|
const source = latest(state)
|
||
|
|
if (!has(source, prop)) {
|
||
|
|
// non-existing or non-own property...
|
||
|
|
return readPropFromProto(state, source, prop)
|
||
|
|
}
|
||
|
|
const value = source[prop]
|
||
|
|
if (state.finalized_ || !isDraftable(value)) {
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
// Check for existing draft in modified state.
|
||
|
|
// Assigned values are never drafted. This catches any drafts we created, too.
|
||
|
|
if (value === peek(state.base_, prop)) {
|
||
|
|
prepareCopy(state)
|
||
|
|
return (state.copy_![prop as any] = createProxy(
|
||
|
|
state.scope_.immer_,
|
||
|
|
value,
|
||
|
|
state
|
||
|
|
))
|
||
|
|
}
|
||
|
|
return value
|
||
|
|
},
|
||
|
|
has(state, prop) {
|
||
|
|
return prop in latest(state)
|
||
|
|
},
|
||
|
|
ownKeys(state) {
|
||
|
|
return Reflect.ownKeys(latest(state))
|
||
|
|
},
|
||
|
|
set(
|
||
|
|
state: ProxyObjectState,
|
||
|
|
prop: string /* strictly not, but helps TS */,
|
||
|
|
value
|
||
|
|
) {
|
||
|
|
const desc = getDescriptorFromProto(latest(state), prop)
|
||
|
|
if (desc?.set) {
|
||
|
|
// special case: if this write is captured by a setter, we have
|
||
|
|
// to trigger it with the correct context
|
||
|
|
desc.set.call(state.draft_, value)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
if (!state.modified_) {
|
||
|
|
// the last check is because we need to be able to distinguish setting a non-existing to undefined (which is a change)
|
||
|
|
// from setting an existing property with value undefined to undefined (which is not a change)
|
||
|
|
const current = peek(latest(state), prop)
|
||
|
|
// special case, if we assigning the original value to a draft, we can ignore the assignment
|
||
|
|
const currentState: ProxyObjectState = current?.[DRAFT_STATE]
|
||
|
|
if (currentState && currentState.base_ === value) {
|
||
|
|
state.copy_![prop] = value
|
||
|
|
state.assigned_[prop] = false
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
if (is(value, current) && (value !== undefined || has(state.base_, prop)))
|
||
|
|
return true
|
||
|
|
prepareCopy(state)
|
||
|
|
markChanged(state)
|
||
|
|
}
|
||
|
|
|
||
|
|
if (
|
||
|
|
(state.copy_![prop] === value &&
|
||
|
|
// special case: handle new props with value 'undefined'
|
||
|
|
(value !== undefined || prop in state.copy_)) ||
|
||
|
|
// special case: NaN
|
||
|
|
(Number.isNaN(value) && Number.isNaN(state.copy_![prop]))
|
||
|
|
)
|
||
|
|
return true
|
||
|
|
|
||
|
|
// @ts-ignore
|
||
|
|
state.copy_![prop] = value
|
||
|
|
state.assigned_[prop] = true
|
||
|
|
return true
|
||
|
|
},
|
||
|
|
deleteProperty(state, prop: string) {
|
||
|
|
// The `undefined` check is a fast path for pre-existing keys.
|
||
|
|
if (peek(state.base_, prop) !== undefined || prop in state.base_) {
|
||
|
|
state.assigned_[prop] = false
|
||
|
|
prepareCopy(state)
|
||
|
|
markChanged(state)
|
||
|
|
} else {
|
||
|
|
// if an originally not assigned property was deleted
|
||
|
|
delete state.assigned_[prop]
|
||
|
|
}
|
||
|
|
// @ts-ignore
|
||
|
|
if (state.copy_) delete state.copy_[prop]
|
||
|
|
return true
|
||
|
|
},
|
||
|
|
// Note: We never coerce `desc.value` into an Immer draft, because we can't make
|
||
|
|
// the same guarantee in ES5 mode.
|
||
|
|
getOwnPropertyDescriptor(state, prop) {
|
||
|
|
const owner = latest(state)
|
||
|
|
const desc = Reflect.getOwnPropertyDescriptor(owner, prop)
|
||
|
|
if (!desc) return desc
|
||
|
|
return {
|
||
|
|
writable: true,
|
||
|
|
configurable: state.type_ !== ProxyType.ProxyArray || prop !== "length",
|
||
|
|
enumerable: desc.enumerable,
|
||
|
|
value: owner[prop]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
defineProperty() {
|
||
|
|
die(11)
|
||
|
|
},
|
||
|
|
getPrototypeOf(state) {
|
||
|
|
return Object.getPrototypeOf(state.base_)
|
||
|
|
},
|
||
|
|
setPrototypeOf() {
|
||
|
|
die(12)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Array drafts
|
||
|
|
*/
|
||
|
|
|
||
|
|
const arrayTraps: ProxyHandler<[ProxyArrayState]> = {}
|
||
|
|
each(objectTraps, (key, fn) => {
|
||
|
|
// @ts-ignore
|
||
|
|
arrayTraps[key] = function() {
|
||
|
|
arguments[0] = arguments[0][0]
|
||
|
|
return fn.apply(this, arguments)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
arrayTraps.deleteProperty = function(state, prop) {
|
||
|
|
if (__DEV__ && isNaN(parseInt(prop as any))) die(13)
|
||
|
|
// @ts-ignore
|
||
|
|
return arrayTraps.set!.call(this, state, prop, undefined)
|
||
|
|
}
|
||
|
|
arrayTraps.set = function(state, prop, value) {
|
||
|
|
if (__DEV__ && prop !== "length" && isNaN(parseInt(prop as any))) die(14)
|
||
|
|
return objectTraps.set!.call(this, state[0], prop, value, state[0])
|
||
|
|
}
|
||
|
|
|
||
|
|
// Access a property without creating an Immer draft.
|
||
|
|
function peek(draft: Drafted, prop: PropertyKey) {
|
||
|
|
const state = draft[DRAFT_STATE]
|
||
|
|
const source = state ? latest(state) : draft
|
||
|
|
return source[prop]
|
||
|
|
}
|
||
|
|
|
||
|
|
function readPropFromProto(state: ImmerState, source: any, prop: PropertyKey) {
|
||
|
|
const desc = getDescriptorFromProto(source, prop)
|
||
|
|
return desc
|
||
|
|
? `value` in desc
|
||
|
|
? desc.value
|
||
|
|
: // This is a very special case, if the prop is a getter defined by the
|
||
|
|
// prototype, we should invoke it with the draft as context!
|
||
|
|
desc.get?.call(state.draft_)
|
||
|
|
: undefined
|
||
|
|
}
|
||
|
|
|
||
|
|
function getDescriptorFromProto(
|
||
|
|
source: any,
|
||
|
|
prop: PropertyKey
|
||
|
|
): PropertyDescriptor | undefined {
|
||
|
|
// 'in' checks proto!
|
||
|
|
if (!(prop in source)) return undefined
|
||
|
|
let proto = Object.getPrototypeOf(source)
|
||
|
|
while (proto) {
|
||
|
|
const desc = Object.getOwnPropertyDescriptor(proto, prop)
|
||
|
|
if (desc) return desc
|
||
|
|
proto = Object.getPrototypeOf(proto)
|
||
|
|
}
|
||
|
|
return undefined
|
||
|
|
}
|
||
|
|
|
||
|
|
export function markChanged(state: ImmerState) {
|
||
|
|
if (!state.modified_) {
|
||
|
|
state.modified_ = true
|
||
|
|
if (state.parent_) {
|
||
|
|
markChanged(state.parent_)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function prepareCopy(state: {base_: any; copy_: any}) {
|
||
|
|
if (!state.copy_) {
|
||
|
|
state.copy_ = shallowCopy(state.base_)
|
||
|
|
}
|
||
|
|
}
|