immer
Version:
Create your next immutable state by mutating the current one
259 lines (239 loc) • 6.74 kB
text/typescript
import {
each,
has,
is,
isDraftable,
shallowCopy,
latest,
ImmerBaseState,
ImmerState,
Drafted,
AnyObject,
AnyArray,
Objectish,
getCurrentScope,
DRAFT_STATE,
die,
createProxy,
ProxyTypeProxyObject,
ProxyTypeProxyArray
} from "../internal"
interface ProxyBaseState extends ImmerBaseState {
assigned_: {
[property: string]: boolean
}
parent_?: ImmerState
drafts_?: {
[property: string]: Drafted<any, any>
}
revoke_(): void
}
export interface ProxyObjectState extends ProxyBaseState {
type_: typeof ProxyTypeProxyObject
base_: AnyObject
copy_: AnyObject | null
draft_: Drafted<AnyObject, ProxyObjectState>
}
export interface ProxyArrayState extends ProxyBaseState {
type_: typeof ProxyTypeProxyArray
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 ? ProxyTypeProxyArray : (ProxyTypeProxyObject 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
// Any property proxies.
drafts_: {},
// 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
*/
const objectTraps: ProxyHandler<ProxyState> = {
get(state, prop) {
if (prop === DRAFT_STATE) return state
let {drafts_: drafts} = state
// Check for existing draft in unmodified state.
if (!state.modified_ && has(drafts, prop)) {
return drafts![prop as any]
}
const value = latest(state)[prop]
if (state.finalized_ || !isDraftable(value)) {
return value
}
// Check for existing draft in modified state.
if (state.modified_) {
// Assigned values are never drafted. This catches any drafts we created, too.
if (value !== peek(state.base_, prop)) return value
// Store drafts on the copy (when one exists).
// @ts-ignore
drafts = state.copy_
}
return (drafts![prop as any] = createProxy(
state.scope_.immer_,
value,
state
))
},
has(state, prop) {
return prop in latest(state)
},
ownKeys(state) {
return Reflect.ownKeys(latest(state))
},
set(state, prop: string /* strictly not, but helps TS */, value) {
if (!state.modified_) {
const baseValue = peek(state.base_, prop)
// Optimize based on value's truthiness. Truthy values are guaranteed to
// never be undefined, so we can avoid the `in` operator. Lastly, truthy
// values may be drafts, but falsy values are never drafts.
const isUnchanged = value
? is(baseValue, value) || value === state.drafts_![prop]
: is(baseValue, value) && prop in state.base_
if (isUnchanged) return true
prepareCopy(state)
markChangedProxy(state)
}
state.assigned_[prop] = true
// @ts-ignore
state.copy_![prop] = value
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)
markChangedProxy(state)
} else if (state.assigned_[prop]) {
// 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) {
desc.writable = true
desc.configurable =
state.type_ !== ProxyTypeProxyArray || prop !== "length"
}
return desc
},
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)
return objectTraps.deleteProperty!.call(this, state[0], prop)
}
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])
}
/**
* Map drafts
*/
// Access a property without creating an Immer draft.
function peek(draft: Drafted, prop: PropertyKey): any {
const state = draft[DRAFT_STATE]
const desc = Reflect.getOwnPropertyDescriptor(
state ? latest(state) : draft,
prop
)
return desc && desc.value
}
export function markChangedProxy(state: ImmerState) {
if (!state.modified_) {
state.modified_ = true
if (
state.type_ === ProxyTypeProxyObject ||
state.type_ === ProxyTypeProxyArray
) {
const copy = (state.copy_ = shallowCopy(state.base_))
each(state.drafts_!, (key, value) => {
// @ts-ignore
copy[key] = value
})
state.drafts_ = undefined
}
if (state.parent_) {
markChangedProxy(state.parent_)
}
}
}
function prepareCopy(state: ProxyState) {
if (!state.copy_) {
state.copy_ = shallowCopy(state.base_)
}
}