237 lines
6.7 KiB
Plaintext
237 lines
6.7 KiB
Plaintext
import {
|
|
IProduceWithPatches,
|
|
IProduce,
|
|
ImmerState,
|
|
Drafted,
|
|
isDraftable,
|
|
processResult,
|
|
Patch,
|
|
Objectish,
|
|
DRAFT_STATE,
|
|
Draft,
|
|
PatchListener,
|
|
isDraft,
|
|
isMap,
|
|
isSet,
|
|
createProxyProxy,
|
|
getPlugin,
|
|
die,
|
|
enterScope,
|
|
revokeScope,
|
|
leaveScope,
|
|
usePatchesInScope,
|
|
getCurrentScope,
|
|
NOTHING,
|
|
freeze,
|
|
current
|
|
} from "../internal"
|
|
|
|
interface ProducersFns {
|
|
produce: IProduce
|
|
produceWithPatches: IProduceWithPatches
|
|
}
|
|
|
|
export type StrictMode = boolean | "class_only"
|
|
|
|
export class Immer implements ProducersFns {
|
|
autoFreeze_: boolean = true
|
|
useStrictShallowCopy_: StrictMode = false
|
|
useStrictIteration_: boolean = true
|
|
|
|
constructor(config?: {
|
|
autoFreeze?: boolean
|
|
useStrictShallowCopy?: StrictMode
|
|
useStrictIteration?: boolean
|
|
}) {
|
|
if (typeof config?.autoFreeze === "boolean")
|
|
this.setAutoFreeze(config!.autoFreeze)
|
|
if (typeof config?.useStrictShallowCopy === "boolean")
|
|
this.setUseStrictShallowCopy(config!.useStrictShallowCopy)
|
|
if (typeof config?.useStrictIteration === "boolean")
|
|
this.setUseStrictIteration(config!.useStrictIteration)
|
|
}
|
|
|
|
/**
|
|
* The `produce` function takes a value and a "recipe function" (whose
|
|
* return value often depends on the base state). The recipe function is
|
|
* free to mutate its first argument however it wants. All mutations are
|
|
* only ever applied to a __copy__ of the base state.
|
|
*
|
|
* Pass only a function to create a "curried producer" which relieves you
|
|
* from passing the recipe function every time.
|
|
*
|
|
* Only plain objects and arrays are made mutable. All other objects are
|
|
* considered uncopyable.
|
|
*
|
|
* Note: This function is __bound__ to its `Immer` instance.
|
|
*
|
|
* @param {any} base - the initial state
|
|
* @param {Function} recipe - function that receives a proxy of the base state as first argument and which can be freely modified
|
|
* @param {Function} patchListener - optional function that will be called with all the patches produced here
|
|
* @returns {any} a new state, or the initial state if nothing was modified
|
|
*/
|
|
produce: IProduce = (base: any, recipe?: any, patchListener?: any) => {
|
|
// curried invocation
|
|
if (typeof base === "function" && typeof recipe !== "function") {
|
|
const defaultBase = recipe
|
|
recipe = base
|
|
|
|
const self = this
|
|
return function curriedProduce(
|
|
this: any,
|
|
base = defaultBase,
|
|
...args: any[]
|
|
) {
|
|
return self.produce(base, (draft: Drafted) => recipe.call(this, draft, ...args)) // prettier-ignore
|
|
}
|
|
}
|
|
|
|
if (typeof recipe !== "function") die(6)
|
|
if (patchListener !== undefined && typeof patchListener !== "function")
|
|
die(7)
|
|
|
|
let result
|
|
|
|
// Only plain objects, arrays, and "immerable classes" are drafted.
|
|
if (isDraftable(base)) {
|
|
const scope = enterScope(this)
|
|
const proxy = createProxy(base, undefined)
|
|
let hasError = true
|
|
try {
|
|
result = recipe(proxy)
|
|
hasError = false
|
|
} finally {
|
|
// finally instead of catch + rethrow better preserves original stack
|
|
if (hasError) revokeScope(scope)
|
|
else leaveScope(scope)
|
|
}
|
|
usePatchesInScope(scope, patchListener)
|
|
return processResult(result, scope)
|
|
} else if (!base || typeof base !== "object") {
|
|
result = recipe(base)
|
|
if (result === undefined) result = base
|
|
if (result === NOTHING) result = undefined
|
|
if (this.autoFreeze_) freeze(result, true)
|
|
if (patchListener) {
|
|
const p: Patch[] = []
|
|
const ip: Patch[] = []
|
|
getPlugin("Patches").generateReplacementPatches_(base, result, p, ip)
|
|
patchListener(p, ip)
|
|
}
|
|
return result
|
|
} else die(1, base)
|
|
}
|
|
|
|
produceWithPatches: IProduceWithPatches = (base: any, recipe?: any): any => {
|
|
// curried invocation
|
|
if (typeof base === "function") {
|
|
return (state: any, ...args: any[]) =>
|
|
this.produceWithPatches(state, (draft: any) => base(draft, ...args))
|
|
}
|
|
|
|
let patches: Patch[], inversePatches: Patch[]
|
|
const result = this.produce(base, recipe, (p: Patch[], ip: Patch[]) => {
|
|
patches = p
|
|
inversePatches = ip
|
|
})
|
|
return [result, patches!, inversePatches!]
|
|
}
|
|
|
|
createDraft<T extends Objectish>(base: T): Draft<T> {
|
|
if (!isDraftable(base)) die(8)
|
|
if (isDraft(base)) base = current(base)
|
|
const scope = enterScope(this)
|
|
const proxy = createProxy(base, undefined)
|
|
proxy[DRAFT_STATE].isManual_ = true
|
|
leaveScope(scope)
|
|
return proxy as any
|
|
}
|
|
|
|
finishDraft<D extends Draft<any>>(
|
|
draft: D,
|
|
patchListener?: PatchListener
|
|
): D extends Draft<infer T> ? T : never {
|
|
const state: ImmerState = draft && (draft as any)[DRAFT_STATE]
|
|
if (!state || !state.isManual_) die(9)
|
|
const {scope_: scope} = state
|
|
usePatchesInScope(scope, patchListener)
|
|
return processResult(undefined, scope)
|
|
}
|
|
|
|
/**
|
|
* Pass true to automatically freeze all copies created by Immer.
|
|
*
|
|
* By default, auto-freezing is enabled.
|
|
*/
|
|
setAutoFreeze(value: boolean) {
|
|
this.autoFreeze_ = value
|
|
}
|
|
|
|
/**
|
|
* Pass true to enable strict shallow copy.
|
|
*
|
|
* By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
|
|
*/
|
|
setUseStrictShallowCopy(value: StrictMode) {
|
|
this.useStrictShallowCopy_ = value
|
|
}
|
|
|
|
/**
|
|
* Pass false to use faster iteration that skips non-enumerable properties
|
|
* but still handles symbols for compatibility.
|
|
*
|
|
* By default, strict iteration is enabled (includes all own properties).
|
|
*/
|
|
setUseStrictIteration(value: boolean) {
|
|
this.useStrictIteration_ = value
|
|
}
|
|
|
|
shouldUseStrictIteration(): boolean {
|
|
return this.useStrictIteration_
|
|
}
|
|
|
|
applyPatches<T extends Objectish>(base: T, patches: readonly Patch[]): T {
|
|
// If a patch replaces the entire state, take that replacement as base
|
|
// before applying patches
|
|
let i: number
|
|
for (i = patches.length - 1; i >= 0; i--) {
|
|
const patch = patches[i]
|
|
if (patch.path.length === 0 && patch.op === "replace") {
|
|
base = patch.value
|
|
break
|
|
}
|
|
}
|
|
// If there was a patch that replaced the entire state, start from the
|
|
// patch after that.
|
|
if (i > -1) {
|
|
patches = patches.slice(i + 1)
|
|
}
|
|
|
|
const applyPatchesImpl = getPlugin("Patches").applyPatches_
|
|
if (isDraft(base)) {
|
|
// N.B: never hits if some patch a replacement, patches are never drafts
|
|
return applyPatchesImpl(base, patches)
|
|
}
|
|
// Otherwise, produce a copy of the base state.
|
|
return this.produce(base, (draft: Drafted) =>
|
|
applyPatchesImpl(draft, patches)
|
|
)
|
|
}
|
|
}
|
|
|
|
export function createProxy<T extends Objectish>(
|
|
value: T,
|
|
parent?: ImmerState
|
|
): Drafted<T, ImmerState> {
|
|
// precondition: createProxy should be guarded by isDraftable, so we know we can safely draft
|
|
const draft: Drafted = isMap(value)
|
|
? getPlugin("MapSet").proxyMap_(value, parent)
|
|
: isSet(value)
|
|
? getPlugin("MapSet").proxySet_(value, parent)
|
|
: createProxyProxy(value, parent)
|
|
|
|
const scope = parent ? parent.scope_ : getCurrentScope()
|
|
scope.drafts_.push(draft)
|
|
return draft
|
|
}
|