Comparing version 1.4.0 to 1.5.0
# Changelog | ||
### 1.5.0 | ||
* Added support for patches, through [#168](https://github.com/mweststrate/immer/pull/168). See the [patches](https://github.com/mweststrate/immer#patches) section for details | ||
### 1.4.0 | ||
@@ -4,0 +8,0 @@ |
@@ -13,2 +13,10 @@ // Mapped type to remove readonly modifiers from state | ||
export interface Patch { | ||
op: "replace" | "remove" | "add", | ||
path: (string|number)[] | ||
value?: any | ||
} | ||
export type PatchListener = (patches: Patch[], inversePatches: Patch[]) => void | ||
export interface IProduce { | ||
@@ -30,3 +38,4 @@ /** | ||
currentState: S, | ||
recipe: (this: Draft<S>, draftState: Draft<S>) => void | S | ||
recipe: (this: Draft<S>, draftState: Draft<S>) => void | S, | ||
listener?: PatchListener | ||
): S | ||
@@ -104,1 +113,3 @@ | ||
export function setUseProxies(useProxies: boolean): void | ||
export function applyPatches<S>(state: S, patches: Patch[]): S |
@@ -5,2 +5,88 @@ 'use strict'; | ||
function generatePatches(state, basepath, patches, inversePatches, baseValue, resultValue) { | ||
if (patches) if (Array.isArray(baseValue)) generateArrayPatches(state, basepath, patches, inversePatches, baseValue, resultValue);else generateObjectPatches(state, basepath, patches, inversePatches, baseValue, resultValue); | ||
} | ||
function generateArrayPatches(state, basepath, patches, inversePatches, baseValue, resultValue) { | ||
var shared = Math.min(baseValue.length, resultValue.length); | ||
for (var i = 0; i < shared; i++) { | ||
if (state.assigned[i] && baseValue[i] !== resultValue[i]) { | ||
var path = basepath.concat(i); | ||
patches.push({ op: "replace", path: path, value: resultValue[i] }); | ||
inversePatches.push({ op: "replace", path: path, value: baseValue[i] }); | ||
} | ||
} | ||
if (shared < resultValue.length) { | ||
// stuff was added | ||
for (var _i = shared; _i < resultValue.length; _i++) { | ||
var _path = basepath.concat(_i); | ||
patches.push({ op: "add", path: _path, value: resultValue[_i] }); | ||
} | ||
inversePatches.push({ | ||
op: "replace", | ||
path: basepath.concat("length"), | ||
value: baseValue.length | ||
}); | ||
} else if (shared < baseValue.length) { | ||
// stuff was removed | ||
patches.push({ | ||
op: "replace", | ||
path: basepath.concat("length"), | ||
value: resultValue.length | ||
}); | ||
for (var _i2 = shared; _i2 < baseValue.length; _i2++) { | ||
var _path2 = basepath.concat(_i2); | ||
inversePatches.push({ op: "add", path: _path2, value: baseValue[_i2] }); | ||
} | ||
} | ||
} | ||
function generateObjectPatches(state, basepath, patches, inversePatches, baseValue, resultValue) { | ||
each(state.assigned, function (key, assignedValue) { | ||
var origValue = baseValue[key]; | ||
var value = resultValue[key]; | ||
var op = !assignedValue ? "remove" : key in baseValue ? "replace" : "add"; | ||
if (origValue === baseValue && op === "replace") return; | ||
var path = basepath.concat(key); | ||
patches.push(op === "remove" ? { op: op, path: path } : { op: op, path: path, value: value }); | ||
inversePatches.push(op === "add" ? { op: "remove", path: path } : op === "remove" ? { op: "add", path: path, value: origValue } : { op: "replace", path: path, value: origValue }); | ||
}); | ||
} | ||
function applyPatches(draft, patches) { | ||
var _loop = function _loop(i) { | ||
var patch = patches[i]; | ||
if (patch.path.length === 0 && patch.op === "replace") { | ||
draft = patch.value; | ||
} else { | ||
var path = patch.path.slice(); | ||
var key = path.pop(); | ||
var base = path.reduce(function (current, part) { | ||
if (!current) throw new Error("Cannot apply patch, path doesn't resolve: " + patch.path.join("/")); | ||
return current[part]; | ||
}, draft); | ||
if (!base) throw new Error("Cannot apply patch, path doesn't resolve: " + patch.path.join("/")); | ||
switch (patch.op) { | ||
case "replace": | ||
case "add": | ||
// TODO: add support is not extensive, it does not support insertion or `-` atm! | ||
base[key] = patch.value; | ||
break; | ||
case "remove": | ||
if (Array.isArray(base)) { | ||
if (key === base.length - 1) base.length -= 1;else throw new Error("Remove can only remove the last key of an array, index: " + key + ", length: " + base.length); | ||
} else delete base[key]; | ||
break; | ||
default: | ||
throw new Error("Unsupported patch operation: " + patch.op); | ||
} | ||
} | ||
}; | ||
for (var i = 0; i < patches.length; i++) { | ||
_loop(i); | ||
} | ||
return draft; | ||
} | ||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { | ||
@@ -94,3 +180,3 @@ return typeof obj; | ||
// given a base object, returns it if unmodified, or return the changed cloned if modified | ||
function finalize(base) { | ||
function finalize(base, path, patches, inversePatches) { | ||
if (isProxy(base)) { | ||
@@ -101,3 +187,5 @@ var state = base[PROXY_STATE]; | ||
state.finalized = true; | ||
return finalizeObject(useProxies ? state.copy : state.copy = shallowCopy(base), state); | ||
var result = finalizeObject(useProxies ? state.copy : state.copy = shallowCopy(base), state, path, patches, inversePatches); | ||
generatePatches(state, path, patches, inversePatches, state.base, result); | ||
return result; | ||
} else { | ||
@@ -111,6 +199,11 @@ return state.base; | ||
function finalizeObject(copy, state) { | ||
function finalizeObject(copy, state, path, patches, inversePatches) { | ||
var base = state.base; | ||
each(copy, function (prop, value) { | ||
if (value !== base[prop]) copy[prop] = finalize(value); | ||
if (value !== base[prop]) { | ||
// if there was an assignment on this property, we don't need to generate | ||
// patches for the subtree | ||
var _generatePatches = patches && !has(state.assigned, prop); | ||
copy[prop] = finalize(value, _generatePatches && path.concat(prop), _generatePatches && patches, inversePatches); | ||
} | ||
}); | ||
@@ -177,3 +270,4 @@ return freeze(copy); | ||
return { | ||
modified: false, | ||
modified: false, // this tree is modified (either this object or one of it's children) | ||
assigned: {}, // true: value was assigned to these props, false: was removed | ||
finalized: false, | ||
@@ -209,2 +303,4 @@ parent: parent, | ||
function set$1(state, prop, value) { | ||
// TODO: optimize | ||
state.assigned[prop] = true; | ||
if (!state.modified) { | ||
@@ -219,2 +315,3 @@ if (prop in state.base && is(state.base[prop], value) || has(state.proxies, prop) && state.proxies[prop] === value) return true; | ||
function deleteProperty(state, prop) { | ||
state.assigned[prop] = false; | ||
markChanged(state); | ||
@@ -247,5 +344,5 @@ delete state.copy[prop]; | ||
// creates a proxy for plain objects / arrays | ||
function createProxy(parentState, base) { | ||
function createProxy(parentState, base, key) { | ||
if (isProxy(base)) throw new Error("Immer bug. Plz report."); | ||
var state = createState(parentState, base); | ||
var state = createState(parentState, base, key); | ||
var proxy = Array.isArray(base) ? Proxy.revocable([state], arrayTraps) : Proxy.revocable(state, objectTraps); | ||
@@ -256,3 +353,3 @@ proxies.push(proxy); | ||
function produceProxy(baseState, producer) { | ||
function produceProxy(baseState, producer, patchListener) { | ||
if (isProxy(baseState)) { | ||
@@ -265,2 +362,4 @@ // See #100, don't nest producers | ||
proxies = []; | ||
var patches = patchListener && []; | ||
var inversePatches = patchListener && []; | ||
try { | ||
@@ -282,4 +381,8 @@ // create proxy for root | ||
result = finalize(_returnValue); | ||
if (patches) { | ||
patches.push({ op: "replace", path: [], value: result }); | ||
inversePatches.push({ op: "replace", path: [], value: baseState }); | ||
} | ||
} else { | ||
result = finalize(rootProxy); | ||
result = finalize(rootProxy, [], patches, inversePatches); | ||
} | ||
@@ -290,2 +393,3 @@ // revoke all proxies | ||
}); | ||
patchListener && patchListener(patches, inversePatches); | ||
return result; | ||
@@ -305,2 +409,3 @@ } finally { | ||
modified: false, | ||
assigned: {}, // true: value was assigned to these props, false: was removed | ||
hasCopy: false, | ||
@@ -335,2 +440,3 @@ parent: parent, | ||
assertUnfinished(state); | ||
state.assigned[prop] = true; // optimization; skip this if there is no listener | ||
if (!state.modified) { | ||
@@ -389,3 +495,3 @@ if (is(source$1(state)[prop], value)) return; | ||
// which it is not already know that they are changed (that is, only object for which no known key was changed) | ||
function markChanges() { | ||
function markChangesSweep() { | ||
// intentionally we process the proxies in reverse order; | ||
@@ -404,2 +510,53 @@ // ideally we start by processing leafs in the tree, because if a child has changed, we don't have to check the parent anymore | ||
function markChangesRecursively(object) { | ||
if (!object || (typeof object === "undefined" ? "undefined" : _typeof(object)) !== "object") return; | ||
var state = object[PROXY_STATE]; | ||
if (!state) return; | ||
var proxy = state.proxy, | ||
base = state.base; | ||
if (Array.isArray(object)) { | ||
if (hasArrayChanges(state)) { | ||
markChanged$1(state); | ||
state.assigned.length = true; | ||
if (proxy.length < base.length) for (var i = proxy.length; i < base.length; i++) { | ||
state.assigned[i] = false; | ||
} else for (var _i = base.length; _i < proxy.length; _i++) { | ||
state.assigned[_i] = true; | ||
}each(proxy, function (index, child) { | ||
if (!state.assigned[index]) markChangesRecursively(child); | ||
}); | ||
} | ||
} else { | ||
var _diffKeys = diffKeys(base, proxy), | ||
added = _diffKeys.added, | ||
removed = _diffKeys.removed; | ||
if (added.length > 0 || removed.length > 0) markChanged$1(state); | ||
each(added, function (_, key) { | ||
state.assigned[key] = true; | ||
}); | ||
each(removed, function (_, key) { | ||
state.assigned[key] = false; | ||
}); | ||
each(proxy, function (key, child) { | ||
if (!state.assigned[key]) markChangesRecursively(child); | ||
}); | ||
} | ||
} | ||
function diffKeys(from, to) { | ||
// TODO: optimize | ||
var a = Object.keys(from); | ||
var b = Object.keys(to); | ||
return { | ||
added: b.filter(function (key) { | ||
return a.indexOf(key) === -1; | ||
}), | ||
removed: a.filter(function (key) { | ||
return b.indexOf(key) === -1; | ||
}) | ||
}; | ||
} | ||
function hasObjectChanges(state) { | ||
@@ -429,3 +586,3 @@ var baseKeys = Object.keys(state.base); | ||
function produceEs5(baseState, producer) { | ||
function produceEs5(baseState, producer, patchListener) { | ||
if (isProxy(baseState)) { | ||
@@ -438,2 +595,4 @@ // See #100, don't nest producers | ||
states = []; | ||
var patches = patchListener && []; | ||
var inversePatches = patchListener && []; | ||
try { | ||
@@ -448,5 +607,2 @@ // create proxy for root | ||
}); | ||
// find and mark all changes (for parts not done yet) | ||
// TODO: store states by depth, to be able guarantee processing leaves first | ||
markChanges(); | ||
var result = void 0; | ||
@@ -458,3 +614,11 @@ // check whether the draft was modified and/or a value was returned | ||
result = finalize(_returnValue); | ||
} else result = finalize(rootProxy); | ||
if (patches) { | ||
patches.push({ op: "replace", path: [], value: result }); | ||
inversePatches.push({ op: "replace", path: [], value: baseState }); | ||
} | ||
} else { | ||
if (patchListener) markChangesRecursively(rootProxy); | ||
markChangesSweep(); // this one is more efficient if we don't need to know which attributes have changed | ||
result = finalize(rootProxy, [], patches, inversePatches); | ||
} | ||
// make sure all proxies become unusable | ||
@@ -464,2 +628,3 @@ each(states, function (_, state) { | ||
}); | ||
patchListener && patchListener(patches, inversePatches); | ||
return result; | ||
@@ -504,7 +669,8 @@ } finally { | ||
* @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 produces here | ||
* @returns {any} a new state, or the base state if nothing was modified | ||
*/ | ||
function produce(baseState, producer) { | ||
function produce(baseState, producer, patchListener) { | ||
// prettier-ignore | ||
if (arguments.length !== 1 && arguments.length !== 2) throw new Error("produce expects 1 or 2 arguments, got " + arguments.length); | ||
if (arguments.length < 1 || arguments.length > 3) throw new Error("produce expects 1 to 3 arguments, got " + arguments.length); | ||
@@ -534,2 +700,3 @@ // curried invocation | ||
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"); | ||
} | ||
@@ -544,9 +711,12 @@ | ||
if (!isProxyable(baseState)) throw new Error("the first argument to an immer producer should be a primitive, plain object or array, got " + (typeof baseState === "undefined" ? "undefined" : _typeof(baseState)) + ": \"" + baseState + "\""); | ||
return getUseProxies() ? produceProxy(baseState, producer) : produceEs5(baseState, producer); | ||
return getUseProxies() ? produceProxy(baseState, producer, patchListener) : produceEs5(baseState, producer, patchListener); | ||
} | ||
var applyPatches$1 = produce(applyPatches); | ||
exports.produce = produce; | ||
exports['default'] = produce; | ||
exports.applyPatches = applyPatches$1; | ||
exports.setAutoFreeze = setAutoFreeze; | ||
exports.setUseProxies = setUseProxies; | ||
//# sourceMappingURL=immer.js.map |
@@ -0,1 +1,87 @@ | ||
function generatePatches(state, basepath, patches, inversePatches, baseValue, resultValue) { | ||
if (patches) if (Array.isArray(baseValue)) generateArrayPatches(state, basepath, patches, inversePatches, baseValue, resultValue);else generateObjectPatches(state, basepath, patches, inversePatches, baseValue, resultValue); | ||
} | ||
function generateArrayPatches(state, basepath, patches, inversePatches, baseValue, resultValue) { | ||
var shared = Math.min(baseValue.length, resultValue.length); | ||
for (var i = 0; i < shared; i++) { | ||
if (state.assigned[i] && baseValue[i] !== resultValue[i]) { | ||
var path = basepath.concat(i); | ||
patches.push({ op: "replace", path: path, value: resultValue[i] }); | ||
inversePatches.push({ op: "replace", path: path, value: baseValue[i] }); | ||
} | ||
} | ||
if (shared < resultValue.length) { | ||
// stuff was added | ||
for (var _i = shared; _i < resultValue.length; _i++) { | ||
var _path = basepath.concat(_i); | ||
patches.push({ op: "add", path: _path, value: resultValue[_i] }); | ||
} | ||
inversePatches.push({ | ||
op: "replace", | ||
path: basepath.concat("length"), | ||
value: baseValue.length | ||
}); | ||
} else if (shared < baseValue.length) { | ||
// stuff was removed | ||
patches.push({ | ||
op: "replace", | ||
path: basepath.concat("length"), | ||
value: resultValue.length | ||
}); | ||
for (var _i2 = shared; _i2 < baseValue.length; _i2++) { | ||
var _path2 = basepath.concat(_i2); | ||
inversePatches.push({ op: "add", path: _path2, value: baseValue[_i2] }); | ||
} | ||
} | ||
} | ||
function generateObjectPatches(state, basepath, patches, inversePatches, baseValue, resultValue) { | ||
each(state.assigned, function (key, assignedValue) { | ||
var origValue = baseValue[key]; | ||
var value = resultValue[key]; | ||
var op = !assignedValue ? "remove" : key in baseValue ? "replace" : "add"; | ||
if (origValue === baseValue && op === "replace") return; | ||
var path = basepath.concat(key); | ||
patches.push(op === "remove" ? { op: op, path: path } : { op: op, path: path, value: value }); | ||
inversePatches.push(op === "add" ? { op: "remove", path: path } : op === "remove" ? { op: "add", path: path, value: origValue } : { op: "replace", path: path, value: origValue }); | ||
}); | ||
} | ||
function applyPatches(draft, patches) { | ||
var _loop = function _loop(i) { | ||
var patch = patches[i]; | ||
if (patch.path.length === 0 && patch.op === "replace") { | ||
draft = patch.value; | ||
} else { | ||
var path = patch.path.slice(); | ||
var key = path.pop(); | ||
var base = path.reduce(function (current, part) { | ||
if (!current) throw new Error("Cannot apply patch, path doesn't resolve: " + patch.path.join("/")); | ||
return current[part]; | ||
}, draft); | ||
if (!base) throw new Error("Cannot apply patch, path doesn't resolve: " + patch.path.join("/")); | ||
switch (patch.op) { | ||
case "replace": | ||
case "add": | ||
// TODO: add support is not extensive, it does not support insertion or `-` atm! | ||
base[key] = patch.value; | ||
break; | ||
case "remove": | ||
if (Array.isArray(base)) { | ||
if (key === base.length - 1) base.length -= 1;else throw new Error("Remove can only remove the last key of an array, index: " + key + ", length: " + base.length); | ||
} else delete base[key]; | ||
break; | ||
default: | ||
throw new Error("Unsupported patch operation: " + patch.op); | ||
} | ||
} | ||
}; | ||
for (var i = 0; i < patches.length; i++) { | ||
_loop(i); | ||
} | ||
return draft; | ||
} | ||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { | ||
@@ -89,3 +175,3 @@ return typeof obj; | ||
// given a base object, returns it if unmodified, or return the changed cloned if modified | ||
function finalize(base) { | ||
function finalize(base, path, patches, inversePatches) { | ||
if (isProxy(base)) { | ||
@@ -96,3 +182,5 @@ var state = base[PROXY_STATE]; | ||
state.finalized = true; | ||
return finalizeObject(useProxies ? state.copy : state.copy = shallowCopy(base), state); | ||
var result = finalizeObject(useProxies ? state.copy : state.copy = shallowCopy(base), state, path, patches, inversePatches); | ||
generatePatches(state, path, patches, inversePatches, state.base, result); | ||
return result; | ||
} else { | ||
@@ -106,6 +194,11 @@ return state.base; | ||
function finalizeObject(copy, state) { | ||
function finalizeObject(copy, state, path, patches, inversePatches) { | ||
var base = state.base; | ||
each(copy, function (prop, value) { | ||
if (value !== base[prop]) copy[prop] = finalize(value); | ||
if (value !== base[prop]) { | ||
// if there was an assignment on this property, we don't need to generate | ||
// patches for the subtree | ||
var _generatePatches = patches && !has(state.assigned, prop); | ||
copy[prop] = finalize(value, _generatePatches && path.concat(prop), _generatePatches && patches, inversePatches); | ||
} | ||
}); | ||
@@ -172,3 +265,4 @@ return freeze(copy); | ||
return { | ||
modified: false, | ||
modified: false, // this tree is modified (either this object or one of it's children) | ||
assigned: {}, // true: value was assigned to these props, false: was removed | ||
finalized: false, | ||
@@ -204,2 +298,4 @@ parent: parent, | ||
function set$1(state, prop, value) { | ||
// TODO: optimize | ||
state.assigned[prop] = true; | ||
if (!state.modified) { | ||
@@ -214,2 +310,3 @@ if (prop in state.base && is(state.base[prop], value) || has(state.proxies, prop) && state.proxies[prop] === value) return true; | ||
function deleteProperty(state, prop) { | ||
state.assigned[prop] = false; | ||
markChanged(state); | ||
@@ -242,5 +339,5 @@ delete state.copy[prop]; | ||
// creates a proxy for plain objects / arrays | ||
function createProxy(parentState, base) { | ||
function createProxy(parentState, base, key) { | ||
if (isProxy(base)) throw new Error("Immer bug. Plz report."); | ||
var state = createState(parentState, base); | ||
var state = createState(parentState, base, key); | ||
var proxy = Array.isArray(base) ? Proxy.revocable([state], arrayTraps) : Proxy.revocable(state, objectTraps); | ||
@@ -251,3 +348,3 @@ proxies.push(proxy); | ||
function produceProxy(baseState, producer) { | ||
function produceProxy(baseState, producer, patchListener) { | ||
if (isProxy(baseState)) { | ||
@@ -260,2 +357,4 @@ // See #100, don't nest producers | ||
proxies = []; | ||
var patches = patchListener && []; | ||
var inversePatches = patchListener && []; | ||
try { | ||
@@ -277,4 +376,8 @@ // create proxy for root | ||
result = finalize(_returnValue); | ||
if (patches) { | ||
patches.push({ op: "replace", path: [], value: result }); | ||
inversePatches.push({ op: "replace", path: [], value: baseState }); | ||
} | ||
} else { | ||
result = finalize(rootProxy); | ||
result = finalize(rootProxy, [], patches, inversePatches); | ||
} | ||
@@ -285,2 +388,3 @@ // revoke all proxies | ||
}); | ||
patchListener && patchListener(patches, inversePatches); | ||
return result; | ||
@@ -300,2 +404,3 @@ } finally { | ||
modified: false, | ||
assigned: {}, // true: value was assigned to these props, false: was removed | ||
hasCopy: false, | ||
@@ -330,2 +435,3 @@ parent: parent, | ||
assertUnfinished(state); | ||
state.assigned[prop] = true; // optimization; skip this if there is no listener | ||
if (!state.modified) { | ||
@@ -384,3 +490,3 @@ if (is(source$1(state)[prop], value)) return; | ||
// which it is not already know that they are changed (that is, only object for which no known key was changed) | ||
function markChanges() { | ||
function markChangesSweep() { | ||
// intentionally we process the proxies in reverse order; | ||
@@ -399,2 +505,53 @@ // ideally we start by processing leafs in the tree, because if a child has changed, we don't have to check the parent anymore | ||
function markChangesRecursively(object) { | ||
if (!object || (typeof object === "undefined" ? "undefined" : _typeof(object)) !== "object") return; | ||
var state = object[PROXY_STATE]; | ||
if (!state) return; | ||
var proxy = state.proxy, | ||
base = state.base; | ||
if (Array.isArray(object)) { | ||
if (hasArrayChanges(state)) { | ||
markChanged$1(state); | ||
state.assigned.length = true; | ||
if (proxy.length < base.length) for (var i = proxy.length; i < base.length; i++) { | ||
state.assigned[i] = false; | ||
} else for (var _i = base.length; _i < proxy.length; _i++) { | ||
state.assigned[_i] = true; | ||
}each(proxy, function (index, child) { | ||
if (!state.assigned[index]) markChangesRecursively(child); | ||
}); | ||
} | ||
} else { | ||
var _diffKeys = diffKeys(base, proxy), | ||
added = _diffKeys.added, | ||
removed = _diffKeys.removed; | ||
if (added.length > 0 || removed.length > 0) markChanged$1(state); | ||
each(added, function (_, key) { | ||
state.assigned[key] = true; | ||
}); | ||
each(removed, function (_, key) { | ||
state.assigned[key] = false; | ||
}); | ||
each(proxy, function (key, child) { | ||
if (!state.assigned[key]) markChangesRecursively(child); | ||
}); | ||
} | ||
} | ||
function diffKeys(from, to) { | ||
// TODO: optimize | ||
var a = Object.keys(from); | ||
var b = Object.keys(to); | ||
return { | ||
added: b.filter(function (key) { | ||
return a.indexOf(key) === -1; | ||
}), | ||
removed: a.filter(function (key) { | ||
return b.indexOf(key) === -1; | ||
}) | ||
}; | ||
} | ||
function hasObjectChanges(state) { | ||
@@ -424,3 +581,3 @@ var baseKeys = Object.keys(state.base); | ||
function produceEs5(baseState, producer) { | ||
function produceEs5(baseState, producer, patchListener) { | ||
if (isProxy(baseState)) { | ||
@@ -433,2 +590,4 @@ // See #100, don't nest producers | ||
states = []; | ||
var patches = patchListener && []; | ||
var inversePatches = patchListener && []; | ||
try { | ||
@@ -443,5 +602,2 @@ // create proxy for root | ||
}); | ||
// find and mark all changes (for parts not done yet) | ||
// TODO: store states by depth, to be able guarantee processing leaves first | ||
markChanges(); | ||
var result = void 0; | ||
@@ -453,3 +609,11 @@ // check whether the draft was modified and/or a value was returned | ||
result = finalize(_returnValue); | ||
} else result = finalize(rootProxy); | ||
if (patches) { | ||
patches.push({ op: "replace", path: [], value: result }); | ||
inversePatches.push({ op: "replace", path: [], value: baseState }); | ||
} | ||
} else { | ||
if (patchListener) markChangesRecursively(rootProxy); | ||
markChangesSweep(); // this one is more efficient if we don't need to know which attributes have changed | ||
result = finalize(rootProxy, [], patches, inversePatches); | ||
} | ||
// make sure all proxies become unusable | ||
@@ -459,2 +623,3 @@ each(states, function (_, state) { | ||
}); | ||
patchListener && patchListener(patches, inversePatches); | ||
return result; | ||
@@ -499,7 +664,8 @@ } finally { | ||
* @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 produces here | ||
* @returns {any} a new state, or the base state if nothing was modified | ||
*/ | ||
function produce(baseState, producer) { | ||
function produce(baseState, producer, patchListener) { | ||
// prettier-ignore | ||
if (arguments.length !== 1 && arguments.length !== 2) throw new Error("produce expects 1 or 2 arguments, got " + arguments.length); | ||
if (arguments.length < 1 || arguments.length > 3) throw new Error("produce expects 1 to 3 arguments, got " + arguments.length); | ||
@@ -529,2 +695,3 @@ // curried invocation | ||
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"); | ||
} | ||
@@ -539,7 +706,9 @@ | ||
if (!isProxyable(baseState)) throw new Error("the first argument to an immer producer should be a primitive, plain object or array, got " + (typeof baseState === "undefined" ? "undefined" : _typeof(baseState)) + ": \"" + baseState + "\""); | ||
return getUseProxies() ? produceProxy(baseState, producer) : produceEs5(baseState, producer); | ||
return getUseProxies() ? produceProxy(baseState, producer, patchListener) : produceEs5(baseState, producer, patchListener); | ||
} | ||
export { produce, setAutoFreeze, setUseProxies }; | ||
var applyPatches$1 = produce(applyPatches); | ||
export { produce, applyPatches$1 as applyPatches, setAutoFreeze, setUseProxies }; | ||
export default produce; | ||
//# sourceMappingURL=immer.module.js.map |
@@ -1,2 +0,2 @@ | ||
var e,r;e=this,r=function(e){"use strict";var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n="undefined"!=typeof Symbol?Symbol("immer-proxy-state"):"__$immer_state",t="An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.";var o=!("undefined"!=typeof process&&"production"===process.env.NODE_ENV||"verifyMinified"!==function(){}.name),i="undefined"!=typeof Proxy;function f(e){return!!e&&!!e[n]}function u(e){if(!e)return!1;if("object"!==(void 0===e?"undefined":r(e)))return!1;if(Array.isArray(e))return!0;var n=Object.getPrototypeOf(e);return null===n||n===Object.prototype}function a(e){return o&&Object.freeze(e),e}var c=Object.assign||function(e,r){for(var n in r)p(r,n)&&(e[n]=r[n]);return e};function s(e){if(Array.isArray(e))return e.slice();var r=void 0===e.__proto__?Object.create(null):{};return c(r,e)}function d(e,r){if(Array.isArray(e))for(var n=0;n<e.length;n++)r(n,e[n]);else for(var t in e)r(t,e[t])}function p(e,r){return Object.prototype.hasOwnProperty.call(e,r)}function y(e){if(f(e)){var r=e[n];return!0===r.modified?!0===r.finalized?r.copy:(r.finalized=!0,t=i?r.copy:r.copy=s(e),o=r.base,d(t,function(e,r){r!==o[e]&&(t[e]=y(r))}),a(t)):r.base}var t,o;return function e(r){if(!u(r))return;if(Object.isFrozen(r))return;d(r,function(n,t){f(t)?r[n]=y(t):e(t)});a(r)}(e),e}function l(e,r){return e===r?0!==e||1/e==1/r:e!=e&&r!=r}var v=null,b={get:function(e,r){if(r===n)return e;if(e.modified){var t=e.copy[r];return t===e.base[r]&&u(t)?e.copy[r]=w(e,t):t}if(p(e.proxies,r))return e.proxies[r];var o=e.base[r];return!f(o)&&u(o)?e.proxies[r]=w(e,o):o},has:function(e,r){return r in h(e)},ownKeys:function(e){return Reflect.ownKeys(h(e))},set:function(e,r,n){if(!e.modified){if(r in e.base&&l(e.base[r],n)||p(e.proxies,r)&&e.proxies[r]===n)return!0;g(e)}return e.copy[r]=n,!0},deleteProperty:function(e,r){return g(e),delete e.copy[r],!0},getOwnPropertyDescriptor:function(e,r){var n=e.modified?e.copy:p(e.proxies,r)?e.proxies:e.base,t=Reflect.getOwnPropertyDescriptor(n,r);!t||Array.isArray(n)&&"length"===r||(t.configurable=!0);return t},defineProperty:function(){throw new Error("Immer does not support defining properties on draft objects.")},setPrototypeOf:function(){throw new Error("Immer does not support `setPrototypeOf()`.")}},m={};function h(e){return!0===e.modified?e.copy:e.base}function g(e){e.modified||(e.modified=!0,e.copy=s(e.base),Object.assign(e.copy,e.proxies),e.parent&&g(e.parent))}function w(e,r){if(f(r))throw new Error("Immer bug. Plz report.");var n={modified:!1,finalized:!1,parent:e,base:r,copy:void 0,proxies:{}},t=Array.isArray(r)?Proxy.revocable([n],m):Proxy.revocable(n,b);return v.push(t),t.proxy}d(b,function(e,r){m[e]=function(){return arguments[0]=arguments[0][0],r.apply(this,arguments)}});var O={},j=null;function x(e){return e.hasCopy?e.copy:e.base}function P(e){e.modified||(e.modified=!0,e.parent&&P(e.parent))}function A(e){e.hasCopy||(e.hasCopy=!0,e.copy=s(e.base))}function E(e,r){var t=s(r);d(r,function(e){var r;Object.defineProperty(t,""+e,O[r=""+e]||(O[r]={configurable:!0,enumerable:!0,get:function(){return function(e,r){z(e);var n=x(e)[r];return!e.finalizing&&n===e.base[r]&&u(n)?(A(e),e.copy[r]=E(e,n)):n}(this[n],r)},set:function(e){!function(e,r,n){if(z(e),!e.modified){if(l(x(e)[r],n))return;P(e),A(e)}e.copy[r]=n}(this[n],r,e)}}))});var o,i,f,a={modified:!1,hasCopy:!1,parent:e,base:r,proxy:t,copy:void 0,finished:!1,finalizing:!1,finalized:!1};return o=t,i=n,f=a,Object.defineProperty(o,i,{value:f,enumerable:!1,writable:!0}),j.push(a),t}function z(e){if(!0===e.finished)throw new Error("Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? "+JSON.stringify(e.copy||e.base))}function _(e){var r=e.proxy;if(r.length!==e.base.length)return!0;var n=Object.getOwnPropertyDescriptor(r,r.length-1);return!(!n||n.get)}function S(e,o){if(f(e)){var i=o.call(e,e);return void 0===i?e:i}var u=j;j=[];try{var a=E(void 0,e),c=o.call(a,a);d(j,function(e,r){r.finalizing=!0}),function(){for(var e=j.length-1;e>=0;e--){var n=j[e];!1===n.modified&&(Array.isArray(n.base)?_(n)&&P(n):(t=n,o=Object.keys(t.base),i=Object.keys(t.proxy),function(e,n){if(l(e,n))return!0;if("object"!==(void 0===e?"undefined":r(e))||null===e||"object"!==(void 0===n?"undefined":r(n))||null===n)return!1;var t=Object.keys(e),o=Object.keys(n);if(t.length!==o.length)return!1;for(var i=0;i<t.length;i++)if(!hasOwnProperty.call(n,t[i])||!l(e[t[i]],n[t[i]]))return!1;return!0}(o,i)||P(n)))}var t,o,i}();var s=void 0;if(void 0!==c&&c!==a){if(a[n].modified)throw new Error(t);s=y(c)}else s=y(a);return d(j,function(e,r){r.finished=!0}),s}finally{j=u}}function k(e,o){if(1!==arguments.length&&2!==arguments.length)throw new Error("produce expects 1 or 2 arguments, got "+arguments.length);if("function"==typeof e){if("function"==typeof o)throw new Error("if first argument is a function (curried invocation), the second argument to produce cannot be a function");var a=o,c=e;return function(){var e=arguments;return k(void 0===e[0]&&void 0!==a?a:e[0],function(r){return e[0]=r,c.apply(r,e)})}}if("function"!=typeof o)throw new Error("if first argument is not a function, the second argument to produce should be a function");if("object"!==(void 0===e?"undefined":r(e))||null===e){var s=o(e);return void 0===s?e:s}if(!u(e))throw new Error("the first argument to an immer producer should be a primitive, plain object or array, got "+(void 0===e?"undefined":r(e))+': "'+e+'"');return i?function(e,r){if(f(e)){var o=r.call(e,e);return void 0===o?e:o}var i=v;v=[];try{var u=w(void 0,e),a=r.call(u,u),c=void 0;if(void 0!==a&&a!==u){if(u[n].modified)throw new Error(t);c=y(a)}else c=y(u);return d(v,function(e,r){return r.revoke()}),c}finally{v=i}}(e,o):S(e,o)}e.produce=k,e.default=k,e.setAutoFreeze=function(e){o=e},e.setUseProxies=function(e){i=e},Object.defineProperty(e,"__esModule",{value:!0})},"object"==typeof exports&&"undefined"!=typeof module?r(exports):"function"==typeof define&&define.amd?define(["exports"],r):r(e.immer={}); | ||
var e,r;e=this,r=function(e){"use strict";function r(e,r,n,t,o,i){var a,f,u,c,p;n&&(Array.isArray(o)?function(e,r,n,t,o,i){for(var a=Math.min(o.length,i.length),f=0;f<a;f++)if(e.assigned[f]&&o[f]!==i[f]){var u=r.concat(f);n.push({op:"replace",path:u,value:i[f]}),t.push({op:"replace",path:u,value:o[f]})}if(a<i.length){for(var c=a;c<i.length;c++){var p=r.concat(c);n.push({op:"add",path:p,value:i[c]})}t.push({op:"replace",path:r.concat("length"),value:o.length})}else if(a<o.length){n.push({op:"replace",path:r.concat("length"),value:i.length});for(var s=a;s<o.length;s++){var d=r.concat(s);t.push({op:"add",path:d,value:o[s]})}}}(e,r,n,t,o,i):(a=r,f=n,u=t,c=o,p=i,d(e.assigned,function(e,r){var n=c[e],t=p[e],o=r?e in c?"replace":"add":"remove";if(n!==c||"replace"!==o){var i=a.concat(e);f.push("remove"===o?{op:o,path:i}:{op:o,path:i,value:t}),u.push("add"===o?{op:"remove",path:i}:"remove"===o?{op:"add",path:i,value:n}:{op:"replace",path:i,value:n})}})))}var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t="undefined"!=typeof Symbol?Symbol("immer-proxy-state"):"__$immer_state",o="An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.";var i=!("undefined"!=typeof process&&"production"===process.env.NODE_ENV||"verifyMinified"!==function(){}.name),a="undefined"!=typeof Proxy;function f(e){return!!e&&!!e[t]}function u(e){if(!e)return!1;if("object"!==(void 0===e?"undefined":n(e)))return!1;if(Array.isArray(e))return!0;var r=Object.getPrototypeOf(e);return null===r||r===Object.prototype}function c(e){return i&&Object.freeze(e),e}var p=Object.assign||function(e,r){for(var n in r)l(r,n)&&(e[n]=r[n]);return e};function s(e){if(Array.isArray(e))return e.slice();var r=void 0===e.__proto__?Object.create(null):{};return p(r,e)}function d(e,r){if(Array.isArray(e))for(var n=0;n<e.length;n++)r(n,e[n]);else for(var t in e)r(t,e[t])}function l(e,r){return Object.prototype.hasOwnProperty.call(e,r)}function h(e,n,o,i){if(f(e)){var p=e[t];if(!0===p.modified){if(!0===p.finalized)return p.copy;p.finalized=!0;var v=(y=a?p.copy:p.copy=s(e),b=n,m=o,w=i,O=(g=p).base,d(y,function(e,r){if(r!==O[e]){var n=m&&!l(g.assigned,e);y[e]=h(r,n&&b.concat(e),n&&m,w)}}),c(y));return r(p,n,o,i,p.base,v),v}return p.base}var y,g,b,m,w,O;return function e(r){if(!u(r))return;if(Object.isFrozen(r))return;d(r,function(n,t){f(t)?r[n]=h(t):e(t)});c(r)}(e),e}function v(e,r){return e===r?0!==e||1/e==1/r:e!=e&&r!=r}var y=null,g={get:function(e,r){if(r===t)return e;if(e.modified){var n=e.copy[r];return n===e.base[r]&&u(n)?e.copy[r]=O(e,n):n}if(l(e.proxies,r))return e.proxies[r];var o=e.base[r];return!f(o)&&u(o)?e.proxies[r]=O(e,o):o},has:function(e,r){return r in m(e)},ownKeys:function(e){return Reflect.ownKeys(m(e))},set:function(e,r,n){if(e.assigned[r]=!0,!e.modified){if(r in e.base&&v(e.base[r],n)||l(e.proxies,r)&&e.proxies[r]===n)return!0;w(e)}return e.copy[r]=n,!0},deleteProperty:function(e,r){return e.assigned[r]=!1,w(e),delete e.copy[r],!0},getOwnPropertyDescriptor:function(e,r){var n=e.modified?e.copy:l(e.proxies,r)?e.proxies:e.base,t=Reflect.getOwnPropertyDescriptor(n,r);!t||Array.isArray(n)&&"length"===r||(t.configurable=!0);return t},defineProperty:function(){throw new Error("Immer does not support defining properties on draft objects.")},setPrototypeOf:function(){throw new Error("Immer does not support `setPrototypeOf()`.")}},b={};function m(e){return!0===e.modified?e.copy:e.base}function w(e){e.modified||(e.modified=!0,e.copy=s(e.base),Object.assign(e.copy,e.proxies),e.parent&&w(e.parent))}function O(e,r,n){if(f(r))throw new Error("Immer bug. Plz report.");var t={modified:!1,assigned:{},finalized:!1,parent:e,base:r,copy:void 0,proxies:{}},o=Array.isArray(r)?Proxy.revocable([t],b):Proxy.revocable(t,g);return y.push(o),o.proxy}d(g,function(e,r){b[e]=function(){return arguments[0]=arguments[0][0],r.apply(this,arguments)}});var j={},x=null;function A(e){return e.hasCopy?e.copy:e.base}function P(e){e.modified||(e.modified=!0,e.parent&&P(e.parent))}function E(e){e.hasCopy||(e.hasCopy=!0,e.copy=s(e.base))}function k(e,r){var n=s(r);d(r,function(e){var r;Object.defineProperty(n,""+e,j[r=""+e]||(j[r]={configurable:!0,enumerable:!0,get:function(){return function(e,r){z(e);var n=A(e)[r];return!e.finalizing&&n===e.base[r]&&u(n)?(E(e),e.copy[r]=k(e,n)):n}(this[t],r)},set:function(e){!function(e,r,n){if(z(e),e.assigned[r]=!0,!e.modified){if(v(A(e)[r],n))return;P(e),E(e)}e.copy[r]=n}(this[t],r,e)}}))});var o,i,a,f={modified:!1,assigned:{},hasCopy:!1,parent:e,base:r,proxy:n,copy:void 0,finished:!1,finalizing:!1,finalized:!1};return o=n,i=t,a=f,Object.defineProperty(o,i,{value:a,enumerable:!1,writable:!0}),x.push(f),n}function z(e){if(!0===e.finished)throw new Error("Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? "+JSON.stringify(e.copy||e.base))}function _(e){if(e&&"object"===(void 0===e?"undefined":n(e))){var r=e[t];if(r){var o,i,a,f,u=r.proxy,c=r.base;if(Array.isArray(e)){if(S(r)){if(P(r),r.assigned.length=!0,u.length<c.length)for(var p=u.length;p<c.length;p++)r.assigned[p]=!1;else for(var s=c.length;s<u.length;s++)r.assigned[s]=!0;d(u,function(e,n){r.assigned[e]||_(n)})}}else{var l=(o=c,i=u,a=Object.keys(o),{added:(f=Object.keys(i)).filter(function(e){return-1===a.indexOf(e)}),removed:a.filter(function(e){return-1===f.indexOf(e)})}),h=l.added,v=l.removed;(h.length>0||v.length>0)&&P(r),d(h,function(e,n){r.assigned[n]=!0}),d(v,function(e,n){r.assigned[n]=!1}),d(u,function(e,n){r.assigned[e]||_(n)})}}}}function S(e){var r=e.proxy;if(r.length!==e.base.length)return!0;var n=Object.getOwnPropertyDescriptor(r,r.length-1);return!(!n||n.get)}function C(e,r,i){if(f(e)){var a=r.call(e,e);return void 0===a?e:a}var u=x;x=[];var c=i&&[],p=i&&[];try{var s=k(void 0,e),l=r.call(s,s);d(x,function(e,r){r.finalizing=!0});var y=void 0;if(void 0!==l&&l!==s){if(s[t].modified)throw new Error(o);y=h(l),c&&(c.push({op:"replace",path:[],value:y}),p.push({op:"replace",path:[],value:e}))}else i&&_(s),function(){for(var e=x.length-1;e>=0;e--){var r=x[e];!1===r.modified&&(Array.isArray(r.base)?S(r)&&P(r):(t=r,o=Object.keys(t.base),i=Object.keys(t.proxy),function(e,r){if(v(e,r))return!0;if("object"!==(void 0===e?"undefined":n(e))||null===e||"object"!==(void 0===r?"undefined":n(r))||null===r)return!1;var t=Object.keys(e),o=Object.keys(r);if(t.length!==o.length)return!1;for(var i=0;i<t.length;i++)if(!hasOwnProperty.call(r,t[i])||!v(e[t[i]],r[t[i]]))return!1;return!0}(o,i)||P(r)))}var t,o,i}(),y=h(s,[],c,p);return d(x,function(e,r){r.finished=!0}),i&&i(c,p),y}finally{x=u}}function D(e,r,i){if(arguments.length<1||arguments.length>3)throw new Error("produce expects 1 to 3 arguments, got "+arguments.length);if("function"==typeof e){if("function"==typeof r)throw new Error("if first argument is a function (curried invocation), the second argument to produce cannot be a function");var c=r,p=e;return function(){var e=arguments;return D(void 0===e[0]&&void 0!==c?c:e[0],function(r){return e[0]=r,p.apply(r,e)})}}if("function"!=typeof r)throw new Error("if first argument is not a function, the second argument to produce should be a function");if(void 0!==i&&"function"!=typeof i)throw new Error("the third argument of a producer should not be set or a function");if("object"!==(void 0===e?"undefined":n(e))||null===e){var s=r(e);return void 0===s?e:s}if(!u(e))throw new Error("the first argument to an immer producer should be a primitive, plain object or array, got "+(void 0===e?"undefined":n(e))+': "'+e+'"');return a?function(e,r,n){if(f(e)){var i=r.call(e,e);return void 0===i?e:i}var a=y;y=[];var u=n&&[],c=n&&[];try{var p=O(void 0,e),s=r.call(p,p),l=void 0;if(void 0!==s&&s!==p){if(p[t].modified)throw new Error(o);l=h(s),u&&(u.push({op:"replace",path:[],value:l}),c.push({op:"replace",path:[],value:e}))}else l=h(p,[],u,c);return d(y,function(e,r){return r.revoke()}),n&&n(u,c),l}finally{y=a}}(e,r,i):C(e,r,i)}var I=D(function(e,r){for(var n=function(n){var t=r[n];if(0===t.path.length&&"replace"===t.op)e=t.value;else{var o=t.path.slice(),i=o.pop(),a=o.reduce(function(e,r){if(!e)throw new Error("Cannot apply patch, path doesn't resolve: "+t.path.join("/"));return e[r]},e);if(!a)throw new Error("Cannot apply patch, path doesn't resolve: "+t.path.join("/"));switch(t.op){case"replace":case"add":a[i]=t.value;break;case"remove":if(Array.isArray(a)){if(i!==a.length-1)throw new Error("Remove can only remove the last key of an array, index: "+i+", length: "+a.length);a.length-=1}else delete a[i];break;default:throw new Error("Unsupported patch operation: "+t.op)}}},t=0;t<r.length;t++)n(t);return e});e.produce=D,e.default=D,e.applyPatches=I,e.setAutoFreeze=function(e){i=e},e.setUseProxies=function(e){a=e},Object.defineProperty(e,"__esModule",{value:!0})},"object"==typeof exports&&"undefined"!=typeof module?r(exports):"function"==typeof define&&define.amd?define(["exports"],r):r(e.immer={}); | ||
//# sourceMappingURL=immer.umd.js.map |
{ | ||
"name": "immer", | ||
"version": "1.4.0", | ||
"version": "1.5.0", | ||
"description": "Create your next immutable state by mutating the current one", | ||
@@ -14,3 +14,3 @@ "main": "dist/immer.js", | ||
"test": "jest", | ||
"test:perf": "NODE_ENV=production yarn-or-npm build && cd __preformance_tests__ && babel-node add-data.js && babel-node todo.js && babel-node incremental.js", | ||
"test:perf": "NODE_ENV=production yarn-or-npm build && cd __performance_tests__ && babel-node add-data.js && babel-node todo.js && babel-node incremental.js", | ||
"test:flow": "yarn-or-npm flow check", | ||
@@ -17,0 +17,0 @@ "coveralls": "jest --coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", |
@@ -223,2 +223,83 @@ # Immer | ||
## Patches | ||
During the run of a producer, Immer can record all the patches that would replay the changes made by the reducer. | ||
This is a very powerful tool if you want to fork your state temporarily, and replay the changes to the original. | ||
Patches are useful in few scenarios: | ||
* To exchange incremental updates with other parties, for example over websockets | ||
* For debugging / traces, to see precisely how state is changed over time | ||
* As basis for undo/redo or as approach to replay changes on a slightly different state tree | ||
To help with replaying patches, `applyPatches` comes in handy. Here is an example how patches could be used | ||
to record the incremental updates and (inverse) apply them: | ||
```javascript | ||
import produce, {applyPatches} from "immer" | ||
let state = { | ||
name: "Micheal", | ||
age: 32 | ||
} | ||
// Let's assume the user is in a wizard, and we don't know whether | ||
// his changes should be end up in the base state ultimately or not... | ||
let fork = state | ||
// all the changes the user made in the wizard | ||
let changes = [] | ||
// the inverse of all the changes made in the wizard | ||
let inverseChanges = [] | ||
fork = produce( | ||
fork, | ||
draft => { | ||
draft.age = 33 | ||
}, | ||
// The third argument to produce is a callback to which the patches will be fed | ||
(patches, inversePatches) => { | ||
changes.push(...patches) | ||
inverseChanges.push(...inversePatches) | ||
} | ||
) | ||
// In the mean time, our original state is replaced, as, for example, | ||
// some changes were received from the server | ||
state = produce(state, draft => { | ||
draft.name = "Michel" | ||
}) | ||
// When the wizard finishes (successfully) we can replay the changes that were in the fork onto the *new* state! | ||
state = applyPatches(state, changes) | ||
// state now contains the changes from both code paths! | ||
expect(state).toEqual({ | ||
name: "Michel", // changed by the server | ||
age: 33 // changed by the wizard | ||
}) | ||
// Finally, even after finishing the wizard, the user might change his mind and undo his changes... | ||
state = applyPatches(state, inverseChanges) | ||
expect(state).toEqual({ | ||
name: "Michel", // Not reverted | ||
age: 32 // Reverted | ||
}) | ||
``` | ||
The generated patches are similar (but not the same) to the [RFC-6902 JSON patch standard](http://tools.ietf.org/html/rfc6902), except that the `path` property is an array, rather than a string. | ||
This makes processing patches easier. If you want to normalize to the official specification, `patch.path = patch.path.join("/")` should do the trick. Anyway, this is what a bunch of patches and their inverse could look like: | ||
```json | ||
[ | ||
{ "op": "replace", "path": ["profile"], "value": { "name": "Veria", "age": 5 }}, | ||
{ "op": "remove", "path": ["tags", 3] } | ||
] | ||
``` | ||
```json | ||
[ | ||
{ "op": "replace", "path": ["profile"], "value": { "name": "Noa", "age": 6 }}, | ||
{ "op": "add", "path": ["tags", 3], "value": "kiddo"}, | ||
] | ||
``` | ||
## Auto freezing | ||
@@ -356,2 +437,3 @@ | ||
* [bey](https://github.com/jamiebuilds/bey) _Simple immutable state for React using Immer_ | ||
* [immer-wieder](https://github.com/drcmda/immer-wieder#readme) _State management lib that combines React 16 Context and immer for Redux semantics_ | ||
* ... and [many more](https://www.npmjs.com/browse/depended/immer) | ||
@@ -417,17 +499,14 @@ | ||
Here is a [simple benchmark](__performance_tests__/todo.js) on the performance of Immer. This test takes 100.000 todo items, and updates 10.000 of them. _Freeze_ indicates that the state tree has been frozen after producing it. This is a _development_ best practice, as it prevents developers from accidentally modifying the state tree. | ||
Here is a [simple benchmark](__performance_tests__/todo.js) on the performance of Immer. This test takes 50,000 todo items, and updates 5,000 of them. _Freeze_ indicates that the state tree has been frozen after producing it. This is a _development_ best practice, as it prevents developers from accidentally modifying the state tree. | ||
These tests were executed on Node 8.4.0. Use `yarn test:perf` to reproduce them locally. | ||
These tests were executed on Node 9.3.0. Use `yarn test:perf` to reproduce them locally. | ||
![performance.png](images/performance.png) | ||
Some observations: | ||
Most important observation: | ||
* From `immer` perspective, this benchmark is a _worst case_ scenario, because the root collection it has to proxy is really large relatively to the rest of the data set. | ||
* The _mutate_, and _deepclone, mutate_ benchmarks establish a baseline on how expensive changing the data is, without immutability (or structural sharing in the deep clone case). | ||
* The _reducer_ and _naive reducer_ are implemented in typical Redux style reducers. The "smart" implementation slices the collection first, and then maps and freezes only the relevant todos. The "naive" implementation just maps over and processes the entire collection. | ||
* Immer with proxies is roughly speaking twice as slow as a hand written reducer. This is in practice negligible. | ||
* Immer with proxies is roughly speaking twice to three times slower as a hand written reducer (the above test case is worst case, se `yarn test:perf` for more tests). This is in practice negligible. | ||
* Immer is roughly as fast as ImmutableJS. However, the _immutableJS + toJS_ makes clear the cost that often needs to be paid later; converting the immutableJS objects back to plain objects, to be able to pass them to components, over the network etc... (And there is also the upfront cost of converting data received from e.g. the server to immutable JS) | ||
* The ES5 implementation of Immer is significantly slower. For most reducers this won't matter, but reducers that process large amounts of data might benefit from not (or only partially) using an Immer producer. Luckily, Immer is fully opt-in. | ||
* The peaks in the _frozen_ versions of _just mutate_, _deepclone_ and _naive reducer_ come from the fact that they recursively freeze the full state tree, while the other test cases only freeze the modified parts of the tree. | ||
* Generating patches doesn't significantly slow immer down | ||
* The ES5 fallback implementation is roughly twice as slow as the proxy implementation, in some cases worse. | ||
@@ -434,0 +513,0 @@ ## FAQ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
237719
1375
549