| 1 | import {
|
|---|
| 2 | IProduceWithPatches,
|
|---|
| 3 | IProduce,
|
|---|
| 4 | ImmerState,
|
|---|
| 5 | Drafted,
|
|---|
| 6 | isDraftable,
|
|---|
| 7 | processResult,
|
|---|
| 8 | Patch,
|
|---|
| 9 | Objectish,
|
|---|
| 10 | DRAFT_STATE,
|
|---|
| 11 | Draft,
|
|---|
| 12 | PatchListener,
|
|---|
| 13 | isDraft,
|
|---|
| 14 | isMap,
|
|---|
| 15 | isSet,
|
|---|
| 16 | createProxyProxy,
|
|---|
| 17 | getPlugin,
|
|---|
| 18 | die,
|
|---|
| 19 | enterScope,
|
|---|
| 20 | revokeScope,
|
|---|
| 21 | leaveScope,
|
|---|
| 22 | usePatchesInScope,
|
|---|
| 23 | getCurrentScope,
|
|---|
| 24 | NOTHING,
|
|---|
| 25 | freeze,
|
|---|
| 26 | current
|
|---|
| 27 | } from "../internal"
|
|---|
| 28 |
|
|---|
| 29 | interface ProducersFns {
|
|---|
| 30 | produce: IProduce
|
|---|
| 31 | produceWithPatches: IProduceWithPatches
|
|---|
| 32 | }
|
|---|
| 33 |
|
|---|
| 34 | export type StrictMode = boolean | "class_only"
|
|---|
| 35 |
|
|---|
| 36 | export class Immer implements ProducersFns {
|
|---|
| 37 | autoFreeze_: boolean = true
|
|---|
| 38 | useStrictShallowCopy_: StrictMode = false
|
|---|
| 39 | useStrictIteration_: boolean = true
|
|---|
| 40 |
|
|---|
| 41 | constructor(config?: {
|
|---|
| 42 | autoFreeze?: boolean
|
|---|
| 43 | useStrictShallowCopy?: StrictMode
|
|---|
| 44 | useStrictIteration?: boolean
|
|---|
| 45 | }) {
|
|---|
| 46 | if (typeof config?.autoFreeze === "boolean")
|
|---|
| 47 | this.setAutoFreeze(config!.autoFreeze)
|
|---|
| 48 | if (typeof config?.useStrictShallowCopy === "boolean")
|
|---|
| 49 | this.setUseStrictShallowCopy(config!.useStrictShallowCopy)
|
|---|
| 50 | if (typeof config?.useStrictIteration === "boolean")
|
|---|
| 51 | this.setUseStrictIteration(config!.useStrictIteration)
|
|---|
| 52 | }
|
|---|
| 53 |
|
|---|
| 54 | /**
|
|---|
| 55 | * The `produce` function takes a value and a "recipe function" (whose
|
|---|
| 56 | * return value often depends on the base state). The recipe function is
|
|---|
| 57 | * free to mutate its first argument however it wants. All mutations are
|
|---|
| 58 | * only ever applied to a __copy__ of the base state.
|
|---|
| 59 | *
|
|---|
| 60 | * Pass only a function to create a "curried producer" which relieves you
|
|---|
| 61 | * from passing the recipe function every time.
|
|---|
| 62 | *
|
|---|
| 63 | * Only plain objects and arrays are made mutable. All other objects are
|
|---|
| 64 | * considered uncopyable.
|
|---|
| 65 | *
|
|---|
| 66 | * Note: This function is __bound__ to its `Immer` instance.
|
|---|
| 67 | *
|
|---|
| 68 | * @param {any} base - the initial state
|
|---|
| 69 | * @param {Function} recipe - function that receives a proxy of the base state as first argument and which can be freely modified
|
|---|
| 70 | * @param {Function} patchListener - optional function that will be called with all the patches produced here
|
|---|
| 71 | * @returns {any} a new state, or the initial state if nothing was modified
|
|---|
| 72 | */
|
|---|
| 73 | produce: IProduce = (base: any, recipe?: any, patchListener?: any) => {
|
|---|
| 74 | // curried invocation
|
|---|
| 75 | if (typeof base === "function" && typeof recipe !== "function") {
|
|---|
| 76 | const defaultBase = recipe
|
|---|
| 77 | recipe = base
|
|---|
| 78 |
|
|---|
| 79 | const self = this
|
|---|
| 80 | return function curriedProduce(
|
|---|
| 81 | this: any,
|
|---|
| 82 | base = defaultBase,
|
|---|
| 83 | ...args: any[]
|
|---|
| 84 | ) {
|
|---|
| 85 | return self.produce(base, (draft: Drafted) => recipe.call(this, draft, ...args)) // prettier-ignore
|
|---|
| 86 | }
|
|---|
| 87 | }
|
|---|
| 88 |
|
|---|
| 89 | if (typeof recipe !== "function") die(6)
|
|---|
| 90 | if (patchListener !== undefined && typeof patchListener !== "function")
|
|---|
| 91 | die(7)
|
|---|
| 92 |
|
|---|
| 93 | let result
|
|---|
| 94 |
|
|---|
| 95 | // Only plain objects, arrays, and "immerable classes" are drafted.
|
|---|
| 96 | if (isDraftable(base)) {
|
|---|
| 97 | const scope = enterScope(this)
|
|---|
| 98 | const proxy = createProxy(base, undefined)
|
|---|
| 99 | let hasError = true
|
|---|
| 100 | try {
|
|---|
| 101 | result = recipe(proxy)
|
|---|
| 102 | hasError = false
|
|---|
| 103 | } finally {
|
|---|
| 104 | // finally instead of catch + rethrow better preserves original stack
|
|---|
| 105 | if (hasError) revokeScope(scope)
|
|---|
| 106 | else leaveScope(scope)
|
|---|
| 107 | }
|
|---|
| 108 | usePatchesInScope(scope, patchListener)
|
|---|
| 109 | return processResult(result, scope)
|
|---|
| 110 | } else if (!base || typeof base !== "object") {
|
|---|
| 111 | result = recipe(base)
|
|---|
| 112 | if (result === undefined) result = base
|
|---|
| 113 | if (result === NOTHING) result = undefined
|
|---|
| 114 | if (this.autoFreeze_) freeze(result, true)
|
|---|
| 115 | if (patchListener) {
|
|---|
| 116 | const p: Patch[] = []
|
|---|
| 117 | const ip: Patch[] = []
|
|---|
| 118 | getPlugin("Patches").generateReplacementPatches_(base, result, p, ip)
|
|---|
| 119 | patchListener(p, ip)
|
|---|
| 120 | }
|
|---|
| 121 | return result
|
|---|
| 122 | } else die(1, base)
|
|---|
| 123 | }
|
|---|
| 124 |
|
|---|
| 125 | produceWithPatches: IProduceWithPatches = (base: any, recipe?: any): any => {
|
|---|
| 126 | // curried invocation
|
|---|
| 127 | if (typeof base === "function") {
|
|---|
| 128 | return (state: any, ...args: any[]) =>
|
|---|
| 129 | this.produceWithPatches(state, (draft: any) => base(draft, ...args))
|
|---|
| 130 | }
|
|---|
| 131 |
|
|---|
| 132 | let patches: Patch[], inversePatches: Patch[]
|
|---|
| 133 | const result = this.produce(base, recipe, (p: Patch[], ip: Patch[]) => {
|
|---|
| 134 | patches = p
|
|---|
| 135 | inversePatches = ip
|
|---|
| 136 | })
|
|---|
| 137 | return [result, patches!, inversePatches!]
|
|---|
| 138 | }
|
|---|
| 139 |
|
|---|
| 140 | createDraft<T extends Objectish>(base: T): Draft<T> {
|
|---|
| 141 | if (!isDraftable(base)) die(8)
|
|---|
| 142 | if (isDraft(base)) base = current(base)
|
|---|
| 143 | const scope = enterScope(this)
|
|---|
| 144 | const proxy = createProxy(base, undefined)
|
|---|
| 145 | proxy[DRAFT_STATE].isManual_ = true
|
|---|
| 146 | leaveScope(scope)
|
|---|
| 147 | return proxy as any
|
|---|
| 148 | }
|
|---|
| 149 |
|
|---|
| 150 | finishDraft<D extends Draft<any>>(
|
|---|
| 151 | draft: D,
|
|---|
| 152 | patchListener?: PatchListener
|
|---|
| 153 | ): D extends Draft<infer T> ? T : never {
|
|---|
| 154 | const state: ImmerState = draft && (draft as any)[DRAFT_STATE]
|
|---|
| 155 | if (!state || !state.isManual_) die(9)
|
|---|
| 156 | const {scope_: scope} = state
|
|---|
| 157 | usePatchesInScope(scope, patchListener)
|
|---|
| 158 | return processResult(undefined, scope)
|
|---|
| 159 | }
|
|---|
| 160 |
|
|---|
| 161 | /**
|
|---|
| 162 | * Pass true to automatically freeze all copies created by Immer.
|
|---|
| 163 | *
|
|---|
| 164 | * By default, auto-freezing is enabled.
|
|---|
| 165 | */
|
|---|
| 166 | setAutoFreeze(value: boolean) {
|
|---|
| 167 | this.autoFreeze_ = value
|
|---|
| 168 | }
|
|---|
| 169 |
|
|---|
| 170 | /**
|
|---|
| 171 | * Pass true to enable strict shallow copy.
|
|---|
| 172 | *
|
|---|
| 173 | * By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
|
|---|
| 174 | */
|
|---|
| 175 | setUseStrictShallowCopy(value: StrictMode) {
|
|---|
| 176 | this.useStrictShallowCopy_ = value
|
|---|
| 177 | }
|
|---|
| 178 |
|
|---|
| 179 | /**
|
|---|
| 180 | * Pass false to use faster iteration that skips non-enumerable properties
|
|---|
| 181 | * but still handles symbols for compatibility.
|
|---|
| 182 | *
|
|---|
| 183 | * By default, strict iteration is enabled (includes all own properties).
|
|---|
| 184 | */
|
|---|
| 185 | setUseStrictIteration(value: boolean) {
|
|---|
| 186 | this.useStrictIteration_ = value
|
|---|
| 187 | }
|
|---|
| 188 |
|
|---|
| 189 | shouldUseStrictIteration(): boolean {
|
|---|
| 190 | return this.useStrictIteration_
|
|---|
| 191 | }
|
|---|
| 192 |
|
|---|
| 193 | applyPatches<T extends Objectish>(base: T, patches: readonly Patch[]): T {
|
|---|
| 194 | // If a patch replaces the entire state, take that replacement as base
|
|---|
| 195 | // before applying patches
|
|---|
| 196 | let i: number
|
|---|
| 197 | for (i = patches.length - 1; i >= 0; i--) {
|
|---|
| 198 | const patch = patches[i]
|
|---|
| 199 | if (patch.path.length === 0 && patch.op === "replace") {
|
|---|
| 200 | base = patch.value
|
|---|
| 201 | break
|
|---|
| 202 | }
|
|---|
| 203 | }
|
|---|
| 204 | // If there was a patch that replaced the entire state, start from the
|
|---|
| 205 | // patch after that.
|
|---|
| 206 | if (i > -1) {
|
|---|
| 207 | patches = patches.slice(i + 1)
|
|---|
| 208 | }
|
|---|
| 209 |
|
|---|
| 210 | const applyPatchesImpl = getPlugin("Patches").applyPatches_
|
|---|
| 211 | if (isDraft(base)) {
|
|---|
| 212 | // N.B: never hits if some patch a replacement, patches are never drafts
|
|---|
| 213 | return applyPatchesImpl(base, patches)
|
|---|
| 214 | }
|
|---|
| 215 | // Otherwise, produce a copy of the base state.
|
|---|
| 216 | return this.produce(base, (draft: Drafted) =>
|
|---|
| 217 | applyPatchesImpl(draft, patches)
|
|---|
| 218 | )
|
|---|
| 219 | }
|
|---|
| 220 | }
|
|---|
| 221 |
|
|---|
| 222 | export function createProxy<T extends Objectish>(
|
|---|
| 223 | value: T,
|
|---|
| 224 | parent?: ImmerState
|
|---|
| 225 | ): Drafted<T, ImmerState> {
|
|---|
| 226 | // precondition: createProxy should be guarded by isDraftable, so we know we can safely draft
|
|---|
| 227 | const draft: Drafted = isMap(value)
|
|---|
| 228 | ? getPlugin("MapSet").proxyMap_(value, parent)
|
|---|
| 229 | : isSet(value)
|
|---|
| 230 | ? getPlugin("MapSet").proxySet_(value, parent)
|
|---|
| 231 | : createProxyProxy(value, parent)
|
|---|
| 232 |
|
|---|
| 233 | const scope = parent ? parent.scope_ : getCurrentScope()
|
|---|
| 234 | scope.drafts_.push(draft)
|
|---|
| 235 | return draft
|
|---|
| 236 | }
|
|---|