forked from immerjs/immer
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Every instance of the `Immer` class provides a bound `produce` function. As explained in immerjs#254, this class makes room for advanced configuration (if we need it) and allows for interop between Immer-using libraries that have different needs. I took the time to refactor while I was implementing this, so the diff may look more overwhelming than it is. :) Summary: 1. Renamed `immer.js` to `index.js` so the `Immer` class can have `immer.js` 2. Use the word "draft" in place of "proxy" in most places, because "draft" is a higher level of abstraction. It also makes sense to use the same semantics both internally and externally. 3. Moved `finalize` logic to the `Immer` class 4. Inlined the `freeze` function as only one callsite existed 5. Inlined the `createState` functions in both `proxy.js` and `es5.js` 6. Extract repeated code from `produceEs5` and `produceProxy` (which have since been removed) into the `Immer` class 7. The `es5.js` and `proxy.js` modules now have different exports: - `scopes`: an array of nested `produce` calls, where each value is an array of unrevoked proxies - `currentScope()`: shortcut for getting the last value of the `scopes` array - `createDraft()`: a renamed `createProxy` with the arguments swapped - `willFinalize()`: called right after the recipe function returns (only used by ES5 proxies) 8. Changed some "draft state" properties: - removed `hasCopy` in ES5 state (checking for truthiness of `copy` does the job) - renamed `proxy` to `draft` in ES5 state - renamed `finished` to `revoked` in ES5 state - renamed `proxies` to `drafts` in Proxy state - added `revoke` method (called by the `Immer` class) - added `draft` property to Proxy state - use null literals instead of undefined 9. Delay creation of `patches` and `inversePatches` arrays until the recipe function returns. This avoids array allocations when a rollback is performed by throwing. 10. Simplified `generatePatches` by removing the last two arguments, since they can be obtained from the `state` argument.
1 parent
d157d7a
commit 622438a
Showing
8 changed files
with
396 additions
and
454 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,77 +1,171 @@ | ||
export { | ||
setAutoFreeze, | ||
setUseProxies, | ||
original, | ||
isProxy as isDraft | ||
import * as legacyImpl from "./es5" | ||
import * as modernImpl from "./proxy" | ||
import {generatePatches} from "./patches" | ||
import { | ||
assign, | ||
each, | ||
has, | ||
isDraft, | ||
isDraftable, | ||
shallowCopy, | ||
DRAFT_STATE, | ||
NOTHING | ||
} from "./common" | ||
|
||
import {applyPatches as applyPatchesImpl} from "./patches" | ||
import {isProxy, isProxyable, getUseProxies, NOTHING} from "./common" | ||
import {produceProxy} from "./proxy" | ||
import {produceEs5} from "./es5" | ||
function verifyMinified() {} | ||
|
||
/** | ||
* produce takes a state, and runs a function against it. | ||
* That function can freely mutate the state, as it will create copies-on-write. | ||
* This means that the original state will stay unchanged, and once the function finishes, the modified state is returned | ||
* | ||
* @export | ||
* @param {any} baseState - the state to start with | ||
* @param {Function} producer - 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 base state if nothing was modified | ||
*/ | ||
export function produce(baseState, producer, patchListener) { | ||
// prettier-ignore | ||
if (arguments.length < 1 || arguments.length > 3) throw new Error("produce expects 1 to 3 arguments, got " + arguments.length) | ||
const configDefaults = { | ||
useProxies: typeof Proxy !== "undefined" && typeof Reflect !== "undefined", | ||
autoFreeze: | ||
typeof process !== "undefined" | ||
? process.env.NODE_ENV !== "production" | ||
: verifyMinified.name === "verifyMinified" | ||
} | ||
|
||
// curried invocation | ||
if (typeof baseState === "function" && typeof producer !== "function") { | ||
const initialState = producer | ||
const recipe = baseState | ||
export class Immer { | ||
constructor(config) { | ||
assign(this, configDefaults, config) | ||
this.setUseProxies(this.useProxies) | ||
|
||
return function(currentState = initialState, ...args) { | ||
return produce(currentState, draft => | ||
recipe.call(draft, draft, ...args) | ||
) | ||
} | ||
} | ||
|
||
// prettier-ignore | ||
{ | ||
if (typeof producer !== "function") throw new Error("if first argument is not a function, the second argument to produce should be a function") | ||
if (patchListener !== undefined && typeof patchListener !== "function") throw new Error("the third argument of a producer should not be set or a function") | ||
} | ||
this.produce = (base, recipe, patchListener) => { | ||
// curried invocation | ||
if (typeof base === "function" && typeof recipe !== "function") { | ||
const defaultBase = recipe | ||
recipe = base | ||
|
||
// avoid proxying anything except plain objects and arrays | ||
if (!isProxyable(baseState)) { | ||
const returnValue = producer(baseState) | ||
return returnValue === undefined | ||
? baseState | ||
: normalizeResult(returnValue) | ||
} | ||
// prettier-ignore | ||
return (base = defaultBase, ...args) => | ||
this.produce(base, draft => recipe.call(draft, draft, ...args)) | ||
} | ||
|
||
// See #100, don't nest producers | ||
if (isProxy(baseState)) { | ||
const returnValue = producer.call(baseState, baseState) | ||
return returnValue === undefined | ||
? baseState | ||
: normalizeResult(returnValue) | ||
} | ||
// prettier-ignore | ||
{ | ||
if (typeof recipe !== "function") throw new Error("if first argument is not a function, the second argument to produce should be a function") | ||
if (patchListener !== undefined && typeof patchListener !== "function") throw new Error("the third argument of a producer should not be set or a function") | ||
} | ||
|
||
return normalizeResult( | ||
getUseProxies() | ||
? produceProxy(baseState, producer, patchListener) | ||
: produceEs5(baseState, producer, patchListener) | ||
) | ||
} | ||
let result | ||
// Only create proxies for plain objects/arrays. | ||
if (!isDraftable(base)) { | ||
result = recipe(base) | ||
if (result === undefined) return base | ||
} | ||
// See #100, don't nest producers | ||
else if (isDraft(base)) { | ||
result = recipe.call(base, base) | ||
if (result === undefined) return base | ||
} | ||
// The given value must be proxied. | ||
else { | ||
this.scopes.push([]) | ||
const baseDraft = this.createDraft(base) | ||
try { | ||
result = recipe.call(baseDraft, baseDraft) | ||
this.willFinalize(result, baseDraft, !!patchListener) | ||
|
||
function normalizeResult(result) { | ||
return result === NOTHING ? undefined : result | ||
} | ||
// Never generate patches when no listener exists. | ||
var patches = patchListener && [], | ||
inversePatches = patchListener && [] | ||
|
||
export default produce | ||
// Finalize the modified draft... | ||
if (result === undefined || result === baseDraft) { | ||
result = this.finalize( | ||
baseDraft, | ||
[], | ||
patches, | ||
inversePatches | ||
) | ||
} | ||
// ...or use a replacement value. | ||
else { | ||
// Users must never modify the draft _and_ return something else. | ||
if (baseDraft[DRAFT_STATE].modified) | ||
throw new Error("An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.") // prettier-ignore | ||
|
||
export const applyPatches = produce(applyPatchesImpl) | ||
// Finalize the replacement in case it contains (or is) a subset of the draft. | ||
result = this.finalize(result) | ||
|
||
export const nothing = NOTHING | ||
if (patchListener) { | ||
patches.push({ | ||
op: "replace", | ||
path: [], | ||
value: result | ||
}) | ||
inversePatches.push({ | ||
op: "replace", | ||
path: [], | ||
value: base | ||
}) | ||
} | ||
} | ||
} finally { | ||
this.currentScope().forEach(state => state.revoke()) | ||
this.scopes.pop() | ||
} | ||
patchListener && patchListener(patches, inversePatches) | ||
} | ||
// Normalize the result. | ||
return result === NOTHING ? undefined : result | ||
} | ||
} | ||
setAutoFreeze(value) { | ||
this.autoFreeze = value | ||
} | ||
setUseProxies(value) { | ||
this.useProxies = value | ||
assign(this, value ? modernImpl : legacyImpl) | ||
} | ||
/** | ||
* @internal | ||
* Finalize a draft, returning either the unmodified base state or a modified | ||
* copy of the base state. | ||
* | ||
* For undrafted objects, only the first argument is required. Otherwise, | ||
* the first two arguments are required. | ||
*/ | ||
finalize(value, path, patches, inversePatches) { | ||
if (!isDraftable(value)) return value | ||
const state = value[DRAFT_STATE] | ||
if (!state) { | ||
if (Object.isFrozen(value)) return value | ||
return this.finalizeTree(value) | ||
} | ||
if (!state.modified) return state.base | ||
if (!state.finalized) { | ||
state.finalized = true | ||
this.finalizeTree(state.draft, path, patches, inversePatches) | ||
if (this.autoFreeze) Object.freeze(state.copy) | ||
if (patches) generatePatches(state, path, patches, inversePatches) | ||
} | ||
return state.copy | ||
} | ||
/** | ||
* @internal | ||
* Finalize all drafts in the given state tree. | ||
*/ | ||
finalizeTree(root, path, patches, inversePatches) { | ||
const state = root[DRAFT_STATE] | ||
if (state) { | ||
root = this.useProxies | ||
? state.copy | ||
: (state.copy = shallowCopy(state.draft)) | ||
} | ||
const finalizeProperty = (prop, value, parent) => { | ||
// Skip unchanged properties in draft objects. | ||
if (state && parent === root && value === state.base[prop]) return | ||
if (!isDraftable(value)) return | ||
if (!isDraft(value)) { | ||
// Frozen values are already finalized. | ||
return Object.isFrozen(value) || each(value, finalizeProperty) | ||
} | ||
// prettier-ignore | ||
parent[prop] = | ||
// Patches are never generated for assigned properties. | ||
patches && parent === root && !(state && has(state.assigned, prop)) | ||
? this.finalize(value, path.concat(prop), patches, inversePatches) | ||
: this.finalize(value) | ||
} | ||
each(root, finalizeProperty) | ||
return root | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import {applyPatches as applyPatchesImpl} from "./patches" | ||
import {Immer} from "./immer" | ||
|
||
const immer = new Immer() | ||
|
||
/** | ||
* The `produce` function takes a value and a producer function whose return | ||
* value typically depends on the input value. When the given value is | ||
* proxyable, the producer function can perform mutations, even when the given | ||
* value is immutable. When mutations are made, a fresh copy is created and returned. | ||
* The input value is never _actually_ mutated. | ||
* | ||
* @param {any} baseState - the state to start with | ||
* @param {Function} producer - 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 base state if nothing was modified | ||
*/ | ||
export const produce = immer.produce | ||
export default produce | ||
|
||
/** | ||
* Pass true to automatically freeze all copies created by Immer. | ||
* | ||
* By default, auto-freezing is disabled in production. | ||
*/ | ||
export const setAutoFreeze = value => immer.setAutoFreeze(value) | ||
|
||
/** | ||
* Pass true to use the ES2015 `Proxy` class when creating drafts, which is | ||
* always faster than using ES5 proxies. | ||
* | ||
* By default, feature detection is used, so calling this is rarely necessary. | ||
*/ | ||
export const setUseProxies = value => immer.setUseProxies(value) | ||
|
||
/** | ||
* Apply an array of Immer patches to the first argument. | ||
* | ||
* This function is a producer, which means copy-on-write is in effect. | ||
*/ | ||
export const applyPatches = produce(applyPatchesImpl) | ||
|
||
export {original, isDraft, NOTHING as nothing} from "./common" | ||
|
||
export {Immer} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters