UNPKG

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