From 20bc897494734348f2c46f2501fa32b2a3e476a0 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Fri, 5 Jan 2018 05:19:43 +0100 Subject: [PATCH 01/12] type verification test --- __tests__/base.js | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/__tests__/base.js b/__tests__/base.js index 3909d743..e82d4f26 100644 --- a/__tests__/base.js +++ b/__tests__/base.js @@ -54,16 +54,31 @@ function runBaseTest(name, lib, freeze) { expect(nextState.nested).toBe(baseState.nested) }) - it("deep change bubbles up", () => { - const nextState = immer(baseState, s => { - s.anObject.nested.yummie = false + if ( + ("should preserve type", + () => { + const nextState = immer(baseState, s => { + expect(Array.isArray(s)).toBe(true) + expect(s.protoType).toBe(Object) + s.anArray.push(3) + s.aProp = "hello world" + expect(Array.isArray(s)).toBe(true) + expect(s.protoType).toBe(Object) + }) + expect(Array.isArray(nextState)).toBe(true) + expect(nextState.protoType).toBe(Object) + }) + ) + it("deep change bubbles up", () => { + const nextState = immer(baseState, s => { + s.anObject.nested.yummie = false + }) + expect(nextState).not.toBe(baseState) + expect(nextState.anObject).not.toBe(baseState.anObject) + expect(baseState.anObject.nested.yummie).toBe(true) + expect(nextState.anObject.nested.yummie).toBe(false) + expect(nextState.anArray).toBe(baseState.anArray) }) - expect(nextState).not.toBe(baseState) - expect(nextState.anObject).not.toBe(baseState.anObject) - expect(baseState.anObject.nested.yummie).toBe(true) - expect(nextState.anObject.nested.yummie).toBe(false) - expect(nextState.anArray).toBe(baseState.anArray) - }) it("can add props", () => { const nextState = immer(baseState, s => { From a75875136c4e88d8a296b512f3c84c90f3114dfc Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Fri, 5 Jan 2018 06:11:57 +0100 Subject: [PATCH 02/12] WIP --- __tests__/base.js | 11 ++- immer.js | 191 +++++++++++++++++++++------------------------- 2 files changed, 93 insertions(+), 109 deletions(-) diff --git a/__tests__/base.js b/__tests__/base.js index e82d4f26..71d39959 100644 --- a/__tests__/base.js +++ b/__tests__/base.js @@ -4,9 +4,9 @@ import * as immerEs5 from "../es5" import deepFreeze from "deep-freeze" runBaseTest("proxy (no freeze)", immerProxy, false) -runBaseTest("proxy (autofreeze)", immerProxy, true) -runBaseTest("es5 (no freeze)", immerEs5, false) -runBaseTest("es5 (autofreeze)", immerEs5, true) +// runBaseTest("proxy (autofreeze)", immerProxy, true) +// runBaseTest("es5 (no freeze)", immerEs5, false) +// runBaseTest("es5 (autofreeze)", immerEs5, true) function runBaseTest(name, lib, freeze) { describe(`base functionality - ${name}`, () => { @@ -345,3 +345,8 @@ function enumerableOnly(x) { // this can be done better... return JSON.parse(JSON.stringify(x)) } + +// TODO: test problem scenarios +// nesting immmer +// non-trees +// complex objects / functions diff --git a/immer.js b/immer.js index b2bb4ba1..23d67e02 100644 --- a/immer.js +++ b/immer.js @@ -12,7 +12,8 @@ if (typeof Proxy === "undefined") * @property {Function} revoke */ -const IMMER_PROXY = Symbol("immer-proxy") // TODO: create per closure, to avoid sharing proxies between multiple immer version +const IS_PROXY = Symbol("immer-proxy") // TODO: create per closure, to avoid sharing proxies between multiple immer version +const PROXY_STATE = Symbol("immer-proxy-state") // TODO: create per closure, to avoid sharing proxies between multiple immer version // This property indicates that the current object is cloned for another object, // to make sure the proxy of a frozen object is writeable @@ -31,50 +32,84 @@ let autoFreeze = true * @returns {any} a new state, or the base state if nothing was modified */ function immer(baseState, thunk) { - /** - * Maps baseState objects to revocable proxies - * @type {Map} - */ - const revocableProxies = new Map() - // Maps baseState objects to their copies + class State { + // /** @type {boolean} */ + // modified + // /** @type {State} */ + // parent + // /** @type {any} */ + // base + // /** @type {any} */ + // copy + // /** @type {any} */ + // proxies + + constructor(parent, base) { + this.modified + this.parent = parent + this.base = base + this.proxies = {} + } + + get source() { + return this.modified === true ? this.copy : this.base + } - const copies = new Map() + get(prop) { + const proxy = this.proxies[prop] + if (proxy) return proxy + const value = this.source[prop] + if (!isProxy(value) && isProxyable(value)) + return (this.proxies[prop] = createProxy(this, value)) + return value + } + + set(prop, value) { + if (!this.modified) { + if (this.proxies[prop] === value || this.base[prop] === value) + return + this.markChanged() + } + if (isProxy(value)) this.proxies[prop] = value + this.copy[prop] = value + } + + markChanged() { + if (!this.modified) { + this.modified = true + this.copy = Object.assign({}, this.base) + if (this.parent) this.parent.markChanged() + } + } + } const objectTraps = { get(target, prop) { - if (prop === IMMER_PROXY) return target - return createProxy(getCurrentSource(target)[prop]) + if (prop === IS_PROXY) return true + if (prop === PROXY_STATE) return target + return target.get(prop) }, has(target, prop) { - return prop in getCurrentSource(target) + return prop in target.source }, ownKeys(target) { - return Reflect.ownKeys(getCurrentSource(target)) + return Reflect.ownKeys(target.source) }, set(target, prop, value) { - const current = createProxy(getCurrentSource(target)[prop]) - const newValue = createProxy(value) - if (current !== newValue) { - const copy = getOrCreateCopy(target) - copy[prop] = isProxy(newValue) - ? newValue[IMMER_PROXY] - : newValue - } + target.set(prop, value) return true }, deleteProperty(target, property) { - const copy = getOrCreateCopy(target) - delete copy[property] + target.markChanged() + delete target.copy[property] return true }, getOwnPropertyDescriptor(target, prop) { - return Reflect.getOwnPropertyDescriptor( - getCurrentSource(target), - prop - ) + return Reflect.getOwnPropertyDescriptor(target.source, prop) }, defineProperty(target, property, descriptor) { - Object.defineProperty(getOrCreateCopy(target), property, descriptor) + target.markChanged() + Object.defineProperty(target.copy, property, descriptor) return true }, setPrototypeOf() { @@ -82,104 +117,42 @@ function immer(baseState, thunk) { } } - // creates a copy for a base object if there ain't one - function getOrCreateCopy(base) { - let copy = copies.get(base) - if (copy) return copy - const cloneTarget = base[CLONE_TARGET] - if (cloneTarget) { - // base is a clone already (source was frozen), no need to create addtional copy - copies.set(cloneTarget, base) - return base - } - // create a fresh copy - copy = Array.isArray(base) ? base.slice() : Object.assign({}, base) - copies.set(base, copy) - return copy - } - - // returns the current source of truth for a base object - function getCurrentSource(base) { - const copy = copies.get(base) - return copy || base - } - // creates a proxy for plain objects / arrays - function createProxy(base) { - if (isPlainObject(base) || Array.isArray(base)) { - if (isProxy(base)) return base // avoid double wrapping - if (revocableProxies.has(base)) - return revocableProxies.get(base).proxy - let proxyTarget - // special case, if the base tree is frozen, we cannot modify it's proxy it in strict mode, clone first. - if (Object.isFrozen(base)) { - proxyTarget = Array.isArray(base) - ? base.slice() - : Object.assign({}, base) - Object.defineProperty(proxyTarget, CLONE_TARGET, { - enumerable: false, - value: base, - configurable: true - }) - } else { - proxyTarget = base - } - // create the proxy - const revocableProxy = Proxy.revocable(proxyTarget, objectTraps) - revocableProxies.set(base, revocableProxy) - return revocableProxy.proxy - } - return base - } - - // checks if the given base object has modifications, either because it is modified, or - // because one of it's children is - function hasChanges(base) { - const proxy = revocableProxies.get(base) - if (!proxy) return false // nobody did read this object - if (copies.has(base)) return true // a copy was created, so there are changes - // look deeper - const keys = Object.keys(base) - for (let i = 0; i < keys.length; i++) { - const value = base[keys[i]] - if ( - (Array.isArray(value) || isPlainObject(value)) && - hasChanges(value) - ) - return true - } - return false + function createProxy(parentState, base) { + const state = new State(parentState, base) + return new Proxy(state, objectTraps) } // given a base object, returns it if unmodified, or return the changed cloned if modified function finalize(base) { - if (isPlainObject(base)) return finalizeObject(base) - if (Array.isArray(base)) return finalizeArray(base) + if (isProxy(base)) { + const state = base[PROXY_STATE] + if (state.modified === true) { + if (isPlainObject(base)) return finalizeObject(state) + if (Array.isArray(base)) return finalizeArray(state) + } else return state.base + } return base } - function finalizeObject(thing) { - if (!hasChanges(thing)) return thing - const copy = getOrCreateCopy(thing) // TODO: getOrCreate is weird here.. + function finalizeObject(state) { + const copy = state.copy Object.keys(copy).forEach(prop => { copy[prop] = finalize(copy[prop]) }) - delete copy[CLONE_TARGET] return freeze(copy) } function finalizeArray(thing) { - if (!hasChanges(thing)) return thing - const copy = getOrCreateCopy(thing) // TODO: getOrCreate is weird here.. + const copy = state.copy copy.forEach((value, index) => { copy[index] = finalize(copy[index]) }) - delete copy[CLONE_TARGET] return freeze(copy) } // create proxy for root - const rootClone = createProxy(baseState) + const rootClone = createProxy(undefined, baseState) // execute the thunk const maybeVoidReturn = thunk(rootClone) //values either than undefined will trigger warning; @@ -189,9 +162,9 @@ function immer(baseState, thunk) { ) // console.log(`proxies: ${revocableProxies.size}, copies: ${copies.size}`) // revoke all proxies - revoke(revocableProxies) + // TODO: revoke(revocableProxies) // and finalize the modified proxy - return finalize(baseState) + return finalize(rootClone) } /** @@ -212,7 +185,13 @@ function isPlainObject(value) { } function isProxy(value) { - return !!value && !!value[IMMER_PROXY] + return !!value && !!value[IS_PROXY] +} + +function isProxyable(value) { + if (!value) return false + if (typeof value !== "object") return false + return Array.isArray(value) || isPlainObject(value) } function freeze(value) { From cab47bb1ee640d22c7974a7e15aead22b9f2f7e3 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Fri, 5 Jan 2018 21:44:45 +0100 Subject: [PATCH 03/12] WIP on arrays --- immer.js | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/immer.js b/immer.js index 23d67e02..7fb7d4b8 100644 --- a/immer.js +++ b/immer.js @@ -77,7 +77,9 @@ function immer(baseState, thunk) { markChanged() { if (!this.modified) { this.modified = true - this.copy = Object.assign({}, this.base) + this.copy = Array.isArray(this.base) + ? this.base.slice() + : Object.assign({}, this.base) if (this.parent) this.parent.markChanged() } } @@ -117,10 +119,50 @@ function immer(baseState, thunk) { } } + const arrayTraps = { + get(target, prop) { + if (prop === IS_PROXY) return true + if (prop === PROXY_STATE) return target[0] + return target[0].get(prop) + }, + has(target, prop) { + return prop in target[0].source + }, + ownKeys(target) { + return Reflect.ownKeys(target[0].source) + }, + set(target, prop, value) { + target[0].set(prop, value) + return true + }, + deleteProperty(target, property) { + target[0].markChanged() + delete target[0].copy[property] + return true + }, + getOwnPropertyDescriptor(target, prop) { + return Reflect.getOwnPropertyDescriptor(target[0].source, prop) + }, + defineProperty(target, property, descriptor) { + target[0].markChanged() + Object.defineProperty(target[0].copy, property, descriptor) + return true + }, + setPrototypeOf() { + throw new Error("Don't even try this...") + } + } + // creates a proxy for plain objects / arrays function createProxy(parentState, base) { const state = new State(parentState, base) - return new Proxy(state, objectTraps) + if (Array.isArray(base)) { + // Proxy should be created with an array to make it an array for JS + // so... here you have it! + return new Proxy([state], arrayTraps) + } else { + return new Proxy(state, objectTraps) + } } // given a base object, returns it if unmodified, or return the changed cloned if modified From 82ac66cb70f9c5363634c315d8881af3d1da67fa Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Fri, 5 Jan 2018 23:30:55 +0100 Subject: [PATCH 04/12] Allmost feature complete! --- __tests__/base.js | 6 +++--- immer.js | 52 +++++++++++++++++++++++++++++++---------------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/__tests__/base.js b/__tests__/base.js index 71d39959..0acc2ab0 100644 --- a/__tests__/base.js +++ b/__tests__/base.js @@ -4,9 +4,9 @@ import * as immerEs5 from "../es5" import deepFreeze from "deep-freeze" runBaseTest("proxy (no freeze)", immerProxy, false) -// runBaseTest("proxy (autofreeze)", immerProxy, true) -// runBaseTest("es5 (no freeze)", immerEs5, false) -// runBaseTest("es5 (autofreeze)", immerEs5, true) +runBaseTest("proxy (autofreeze)", immerProxy, true) +runBaseTest("es5 (no freeze)", immerEs5, false) +runBaseTest("es5 (autofreeze)", immerEs5, true) function runBaseTest(name, lib, freeze) { describe(`base functionality - ${name}`, () => { diff --git a/immer.js b/immer.js index 7fb7d4b8..26b86c0c 100644 --- a/immer.js +++ b/immer.js @@ -45,9 +45,10 @@ function immer(baseState, thunk) { // proxies constructor(parent, base) { - this.modified + this.modified = false this.parent = parent this.base = base + this.copy = undefined this.proxies = {} } @@ -56,30 +57,39 @@ function immer(baseState, thunk) { } get(prop) { - const proxy = this.proxies[prop] - if (proxy) return proxy - const value = this.source[prop] - if (!isProxy(value) && isProxyable(value)) - return (this.proxies[prop] = createProxy(this, value)) - return value + if (this.modified) { + const value = this.copy[prop] + if (!isProxy(value) && isProxyable(value)) + return (this.copy[prop] = createProxy(this, value)) + return value + } else { + if (prop in this.proxies) return this.proxies[prop] + const value = this.base[prop] + if (!isProxy(value) && isProxyable(value)) + return (this.proxies[prop] = createProxy(this, value)) + return value + } } set(prop, value) { if (!this.modified) { - if (this.proxies[prop] === value || this.base[prop] === value) + if ( + (prop in this.base && this.base[prop] === value) || + (prop in this.proxies && this.proxies[prop] === value) + ) return this.markChanged() } - if (isProxy(value)) this.proxies[prop] = value this.copy[prop] = value } markChanged() { if (!this.modified) { this.modified = true - this.copy = Array.isArray(this.base) + this.copy = Array.isArray(this.base) // TODO: eliminate those isArray checks? ? this.base.slice() : Object.assign({}, this.base) + Object.assign(this.copy, this.proxies) // yup that works for arrays as well if (this.parent) this.parent.markChanged() } } @@ -107,7 +117,11 @@ function immer(baseState, thunk) { return true }, getOwnPropertyDescriptor(target, prop) { - return Reflect.getOwnPropertyDescriptor(target.source, prop) + // TOOD: move to state + const owner = target.modified + ? target.copy + : prop in target.proxies ? target.proxies : target.base + return Reflect.getOwnPropertyDescriptor(owner, prop) }, defineProperty(target, property, descriptor) { target.markChanged() @@ -140,8 +154,12 @@ function immer(baseState, thunk) { delete target[0].copy[property] return true }, - getOwnPropertyDescriptor(target, prop) { - return Reflect.getOwnPropertyDescriptor(target[0].source, prop) + getOwnPropertyDescriptor(target_, prop) { + const target = target_[0] + const owner = target.modified + ? target.copy + : prop in target.proxies ? target.proxies : target.base + return Reflect.getOwnPropertyDescriptor(owner, prop) }, defineProperty(target, property, descriptor) { target[0].markChanged() @@ -170,8 +188,8 @@ function immer(baseState, thunk) { if (isProxy(base)) { const state = base[PROXY_STATE] if (state.modified === true) { - if (isPlainObject(base)) return finalizeObject(state) - if (Array.isArray(base)) return finalizeArray(state) + if (isPlainObject(state.base)) return finalizeObject(state) + if (Array.isArray(state.base)) return finalizeArray(state) } else return state.base } return base @@ -180,12 +198,12 @@ function immer(baseState, thunk) { function finalizeObject(state) { const copy = state.copy Object.keys(copy).forEach(prop => { - copy[prop] = finalize(copy[prop]) + copy[prop] = finalize(copy[prop]) // TODO: make sure no new proxies are generated! }) return freeze(copy) } - function finalizeArray(thing) { + function finalizeArray(state) { const copy = state.copy copy.forEach((value, index) => { copy[index] = finalize(copy[index]) From 4e7f44a948f510c4165ad06ee00b081d9657ad84 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Sat, 6 Jan 2018 23:05:59 +0100 Subject: [PATCH 05/12] Fixed remaining failing tests --- immer.js | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/immer.js b/immer.js index 26b86c0c..91e2c907 100644 --- a/immer.js +++ b/immer.js @@ -32,6 +32,8 @@ let autoFreeze = true * @returns {any} a new state, or the base state if nothing was modified */ function immer(baseState, thunk) { + const revocableProxies = [] + class State { // /** @type {boolean} */ // modified @@ -121,7 +123,9 @@ function immer(baseState, thunk) { const owner = target.modified ? target.copy : prop in target.proxies ? target.proxies : target.base - return Reflect.getOwnPropertyDescriptor(owner, prop) + const descriptor = Reflect.getOwnPropertyDescriptor(owner, prop) + if (descriptor) descriptor.configurable = true // XXX: is this really needed? + return descriptor }, defineProperty(target, property, descriptor) { target.markChanged() @@ -155,11 +159,14 @@ function immer(baseState, thunk) { return true }, getOwnPropertyDescriptor(target_, prop) { + // TOOD: move to state const target = target_[0] const owner = target.modified ? target.copy : prop in target.proxies ? target.proxies : target.base - return Reflect.getOwnPropertyDescriptor(owner, prop) + const descriptor = Reflect.getOwnPropertyDescriptor(owner, prop) + if (descriptor) descriptor.configurable = true // XXX: is this really needed? + return descriptor }, defineProperty(target, property, descriptor) { target[0].markChanged() @@ -174,13 +181,16 @@ function immer(baseState, thunk) { // creates a proxy for plain objects / arrays function createProxy(parentState, base) { const state = new State(parentState, base) + let proxy if (Array.isArray(base)) { // Proxy should be created with an array to make it an array for JS // so... here you have it! - return new Proxy([state], arrayTraps) + proxy = Proxy.revocable([state], arrayTraps) } else { - return new Proxy(state, objectTraps) + proxy = Proxy.revocable(state, objectTraps) } + revocableProxies.push(proxy) + return proxy.proxy } // given a base object, returns it if unmodified, or return the changed cloned if modified @@ -220,22 +230,11 @@ function immer(baseState, thunk) { console.warn( `Immer callback expects no return value. However ${typeof maybeVoidReturn} was returned` ) - // console.log(`proxies: ${revocableProxies.size}, copies: ${copies.size}`) - // revoke all proxies - // TODO: revoke(revocableProxies) // and finalize the modified proxy - return finalize(rootClone) -} - -/** - * Revoke all the proxies stored in the revocableProxies map - * - * @param {Map} revocableProxies - */ -function revoke(revocableProxies) { - for (var revocableProxy of revocableProxies.values()) { - revocableProxy.revoke() - } + const res = finalize(rootClone) + // revoke all proxies + revocableProxies.forEach(p => p.revoke()) + return res } function isPlainObject(value) { From 24b5d423008fc6ab7c20fdfef6771fb89fef3f2e Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Mon, 8 Jan 2018 09:03:41 +0100 Subject: [PATCH 06/12] Simple optimization in ES5 --- es5.js | 4 ++-- immer.js | 11 ----------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/es5.js b/es5.js index 7c2b6d47..80821add 100644 --- a/es5.js +++ b/es5.js @@ -100,7 +100,7 @@ function immer(baseState, thunk) { } function createObjectProxy(base) { - const proxy = {} + const proxy = Object.assign({}, base) Object.keys(base).forEach(prop => Object.defineProperty(proxy, prop, createPropertyProxy(prop)) ) @@ -108,7 +108,7 @@ function immer(baseState, thunk) { } function createArrayProxy(base) { - const proxy = [] + const proxy = new Array(base.length) for (let i = 0; i < base.length; i++) Object.defineProperty(proxy, "" + i, createPropertyProxy("" + i)) return proxy diff --git a/immer.js b/immer.js index 91e2c907..61e855b8 100644 --- a/immer.js +++ b/immer.js @@ -35,17 +35,6 @@ function immer(baseState, thunk) { const revocableProxies = [] class State { - // /** @type {boolean} */ - // modified - // /** @type {State} */ - // parent - // /** @type {any} */ - // base - // /** @type {any} */ - // copy - // /** @type {any} */ - // proxies - constructor(parent, base) { this.modified = false this.parent = parent From fbeeadee3ddba5f10c64c559c830018111d111a8 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Mon, 8 Jan 2018 09:17:55 +0100 Subject: [PATCH 07/12] Code cleanup --- immer.js | 188 +++++++++++++++++++++++++------------------------------ 1 file changed, 86 insertions(+), 102 deletions(-) diff --git a/immer.js b/immer.js index 61e855b8..a7880309 100644 --- a/immer.js +++ b/immer.js @@ -6,20 +6,70 @@ if (typeof Proxy === "undefined") "Immer requires `Proxy` to be available, but it seems to be not available on your platform. Consider requiring immer '\"immer/es5\"' instead." ) -/** - * @typedef {Object} RevocableProxy - * @property {any} proxy - * @property {Function} revoke - */ - -const IS_PROXY = Symbol("immer-proxy") // TODO: create per closure, to avoid sharing proxies between multiple immer version const PROXY_STATE = Symbol("immer-proxy-state") // TODO: create per closure, to avoid sharing proxies between multiple immer version +let autoFreeze = true -// This property indicates that the current object is cloned for another object, -// to make sure the proxy of a frozen object is writeable -const CLONE_TARGET = Symbol("immer-clone-target") +const objectTraps = { + get(target, prop) { + if (prop === PROXY_STATE) return target + return target.get(prop) + }, + has(target, prop) { + return prop in target.source + }, + ownKeys(target) { + return Reflect.ownKeys(target.source) + }, + set(target, prop, value) { + target.set(prop, value) + return true + }, + deleteProperty(target, prop) { + target.deleteProp(prop) + return true + }, + getOwnPropertyDescriptor(target, prop) { + return target.getOwnPropertyDescriptor(prop) + }, + defineProperty(target, property, descriptor) { + target.defineProperty(property, descriptor) + return true + }, + setPrototypeOf() { + throw new Error("Don't even try this...") + } +} -let autoFreeze = true +const arrayTraps = { + get(target, prop) { + if (prop === PROXY_STATE) return target[0] + return target[0].get(prop) + }, + has(target, prop) { + return prop in target[0].source + }, + ownKeys(target) { + return Reflect.ownKeys(target[0].source) + }, + set(target, prop, value) { + target[0].set(prop, value) + return true + }, + deleteProperty(target, prop) { + target[0].deleteProp(prop) + return true + }, + getOwnPropertyDescriptor(target, prop) { + return target[0].getOwnPropertyDescriptor(prop) + }, + defineProperty(target, property, descriptor) { + target[0].defineProperty(property, descriptor) + return true + }, + setPrototypeOf() { + throw new Error("Don't even try this...") + } +} /** * Immer takes a state, and runs a function against it. @@ -74,6 +124,25 @@ function immer(baseState, thunk) { this.copy[prop] = value } + deleteProp(prop) { + this.markChanged() + delete this.copy[prop] + } + + getOwnPropertyDescriptor(prop) { + const owner = this.modified + ? this.copy + : prop in this.proxies ? this.proxies : this.base + const descriptor = Reflect.getOwnPropertyDescriptor(owner, prop) + if (descriptor) descriptor.configurable = true // XXX: is this really needed? + return descriptor + } + + defineProperty(property, descriptor) { + this.markChanged() + Object.defineProperty(this.copy, property, descriptor) + } + markChanged() { if (!this.modified) { this.modified = true @@ -86,87 +155,6 @@ function immer(baseState, thunk) { } } - const objectTraps = { - get(target, prop) { - if (prop === IS_PROXY) return true - if (prop === PROXY_STATE) return target - return target.get(prop) - }, - has(target, prop) { - return prop in target.source - }, - ownKeys(target) { - return Reflect.ownKeys(target.source) - }, - set(target, prop, value) { - target.set(prop, value) - return true - }, - deleteProperty(target, property) { - target.markChanged() - delete target.copy[property] - return true - }, - getOwnPropertyDescriptor(target, prop) { - // TOOD: move to state - const owner = target.modified - ? target.copy - : prop in target.proxies ? target.proxies : target.base - const descriptor = Reflect.getOwnPropertyDescriptor(owner, prop) - if (descriptor) descriptor.configurable = true // XXX: is this really needed? - return descriptor - }, - defineProperty(target, property, descriptor) { - target.markChanged() - Object.defineProperty(target.copy, property, descriptor) - return true - }, - setPrototypeOf() { - throw new Error("Don't even try this...") - } - } - - const arrayTraps = { - get(target, prop) { - if (prop === IS_PROXY) return true - if (prop === PROXY_STATE) return target[0] - return target[0].get(prop) - }, - has(target, prop) { - return prop in target[0].source - }, - ownKeys(target) { - return Reflect.ownKeys(target[0].source) - }, - set(target, prop, value) { - target[0].set(prop, value) - return true - }, - deleteProperty(target, property) { - target[0].markChanged() - delete target[0].copy[property] - return true - }, - getOwnPropertyDescriptor(target_, prop) { - // TOOD: move to state - const target = target_[0] - const owner = target.modified - ? target.copy - : prop in target.proxies ? target.proxies : target.base - const descriptor = Reflect.getOwnPropertyDescriptor(owner, prop) - if (descriptor) descriptor.configurable = true // XXX: is this really needed? - return descriptor - }, - defineProperty(target, property, descriptor) { - target[0].markChanged() - Object.defineProperty(target[0].copy, property, descriptor) - return true - }, - setPrototypeOf() { - throw new Error("Don't even try this...") - } - } - // creates a proxy for plain objects / arrays function createProxy(parentState, base) { const state = new State(parentState, base) @@ -187,8 +175,8 @@ function immer(baseState, thunk) { if (isProxy(base)) { const state = base[PROXY_STATE] if (state.modified === true) { - if (isPlainObject(state.base)) return finalizeObject(state) if (Array.isArray(state.base)) return finalizeArray(state) + return finalizeObject(state) } else return state.base } return base @@ -197,7 +185,7 @@ function immer(baseState, thunk) { function finalizeObject(state) { const copy = state.copy Object.keys(copy).forEach(prop => { - copy[prop] = finalize(copy[prop]) // TODO: make sure no new proxies are generated! + copy[prop] = finalize(copy[prop]) }) return freeze(copy) } @@ -226,20 +214,16 @@ function immer(baseState, thunk) { return res } -function isPlainObject(value) { - if (value === null || typeof value !== "object") return false - const proto = Object.getPrototypeOf(value) - return proto === Object.prototype || proto === null -} - function isProxy(value) { - return !!value && !!value[IS_PROXY] + return !!value && !!value[PROXY_STATE] } function isProxyable(value) { if (!value) return false if (typeof value !== "object") return false - return Array.isArray(value) || isPlainObject(value) + if (Array.isArray(value)) return true + const proto = Object.getPrototypeOf(value) + return (proto === proto) === null || Object.prototype } function freeze(value) { From ef94110ddc2dc19bd1bc1654e69dd0b0bd1dc653 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Mon, 8 Jan 2018 09:54:08 +0100 Subject: [PATCH 08/12] Cleaner ES5 impl --- es5.js | 187 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 92 insertions(+), 95 deletions(-) diff --git a/es5.js b/es5.js index 80821add..0bddb711 100644 --- a/es5.js +++ b/es5.js @@ -1,9 +1,7 @@ "use strict" // @ts-check -const PROXY_TARGET = Symbol("immer-proxy") -const CHANGED_STATE = Symbol("immer-changed-state") -const PARENT = Symbol("immer-parent") +const PROXY_STATE = Symbol("immer-proxy-state") // TODO: create per closure, to avoid sharing proxies between multiple immer version let autoFreeze = true @@ -21,42 +19,67 @@ function immer(baseState, thunk) { let finalizing = false let finished = false const descriptors = {} - const proxies = [] + const states = [] - // creates a proxy for plain objects / arrays - function createProxy(base, parent) { - let proxy - if (isPlainObject(base)) proxy = createObjectProxy(base) - else if (Array.isArray(base)) proxy = createArrayProxy(base) - else throw new Error("Expected a plain object or array") - createHiddenProperty(proxy, PROXY_TARGET, base) - createHiddenProperty(proxy, CHANGED_STATE, false) - createHiddenProperty(proxy, PARENT, parent) - proxies.push(proxy) - return proxy - } + class State { + constructor(parent, proxy, base) { + this.modified = false + this.hasCopy = false + this.parent = parent + this.base = base + this.proxy = proxy + this.copy = undefined + } - function assertUnfinished() { - if (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?" - ) + get source() { + return this.hasCopy ? this.copy : this.base + } + + get(prop) { + assertUnfinished() + const value = this.source[prop] + if (!finalizing && !isProxy(value) && isProxyable(value)) { + this.prepareCopy() + return (this.copy[prop] = createProxy(this, value)) + } + return value + } + + set(prop, value) { + assertUnfinished() + if (!this.modified) { + if (this.source[prop] === value) return + this.markChanged() + } + this.prepareCopy() + this.copy[prop] = value + } + + markChanged() { + if (!this.modified) { + this.modified = true + if (this.parent) this.parent.markChanged() + } + } + + prepareCopy() { + if (this.hasCopy) return + this.hasCopy = true + this.copy = Array.isArray(this.base) + ? this.base.slice() + : Object.assign({}, this.base) + } } - function proxySet(proxy, prop, value) { - // immer func not ended? - assertUnfinished() - // actually a change? - if (Object.is(proxy[prop], value)) return - // mark changed - markDirty(proxy) - // and stop proxying, we know this object has changed - Object.defineProperty(proxy, prop, { - enumerable: true, - writable: true, - configurable: true, - value: value - }) + // creates a proxy for plain objects / arrays + function createProxy(parent, base) { + let proxy + if (Array.isArray(base)) proxy = createArrayProxy(base) + else proxy = createObjectProxy(base) + const state = new State(parent, proxy, base) + createHiddenProperty(proxy, PROXY_STATE, state) + states.push(state) + return proxy } function createPropertyProxy(prop) { @@ -66,34 +89,10 @@ function immer(baseState, thunk) { configurable: true, enumerable: true, get() { - assertUnfinished() - // find the target object - const target = this[PROXY_TARGET] - // find the original value - const value = target[prop] - // if we are finalizing, don't bother creating proxies, just return base value - if (finalizing) return value - // if not proxy-able, return value - if (!isPlainObject(value) && !Array.isArray(value)) - return value - // otherwise, create proxy - const proxy = createProxy(value, this) - // and make sure this proxy is returned from this prop in the future if read - // (write behavior as is) - Object.defineProperty(this, prop, { - configurable: true, - enumerable: true, - get() { - return proxy - }, - set(value) { - proxySet(this, prop, value) - } - }) - return proxy + return this[PROXY_STATE].get(prop) }, set(value) { - proxySet(this, prop, value) + this[PROXY_STATE].set(prop, value) } }) ) @@ -114,6 +113,13 @@ function immer(baseState, thunk) { return proxy } + function assertUnfinished() { + if (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?" + ) + } + // this sounds very expensive, but actually it is not that extensive in practice // as it will only visit proxies, and only do key-based change detection for objects for // which it is not already know that they are changed (that is, only object for which no known key was changed) @@ -121,41 +127,39 @@ function immer(baseState, thunk) { // intentionally we process the proxies in reverse order; // ideally we start by processing leafs in the tree, because if a child has changed, we don't have to check the parent anymore // reverse order of proxy creation approximates this - for (let i = proxies.length - 1; i >= 0; i--) { - const proxy = proxies[i] - if ( - proxy[CHANGED_STATE] === false && - ((isPlainObject(proxy) && hasObjectChanges(proxy)) || - (Array.isArray(proxy) && hasArrayChanges(proxy))) - ) { - markDirty(proxy) + for (let i = states.length - 1; i >= 0; i--) { + const state = states[i] + if (state.modified === false) { + if (Array.isArray(state.base)) { + if (hasArrayChanges(state)) state.markChanged() + } else if (hasObjectChanges(state)) state.markChanged() } } } - function hasObjectChanges(proxy) { - const baseKeys = Object.keys(proxy[PROXY_TARGET]) - const keys = Object.keys(proxy) + function hasObjectChanges(state) { + const baseKeys = Object.keys(state.base) + const keys = Object.keys(state.proxy) return !shallowEqual(baseKeys, keys) } - function hasArrayChanges(proxy) { - return proxy[PROXY_TARGET].length !== proxy.length + function hasArrayChanges(state) { + return state.proxy.length !== state.base.length } function finalize(proxy) { // given a base object, returns it if unmodified, or return the changed cloned if modified if (!isProxy(proxy)) return proxy - if (!proxy[CHANGED_STATE]) return proxy[PROXY_TARGET] // return the original target - if (isPlainObject(proxy)) return finalizeObject(proxy) + const state = proxy[PROXY_STATE] + if (state.modified === false) return state.base // return the original target if (Array.isArray(proxy)) return finalizeArray(proxy) - throw new Error("Illegal state") + return finalizeObject(proxy) } function finalizeObject(proxy) { - const res = {} - Object.keys(proxy).forEach(prop => { - res[prop] = finalize(proxy[prop]) + const res = Object.assign({}, proxy) + Object.keys(res).forEach(prop => { + res[prop] = finalize(res[prop]) }) return freeze(res) } @@ -165,7 +169,7 @@ function immer(baseState, thunk) { } // create proxy for root - const rootClone = createProxy(baseState, undefined) + const rootClone = createProxy(undefined, baseState) // execute the thunk const maybeVoidReturn = thunk(rootClone) //values either than undefined will trigger warning; @@ -183,23 +187,16 @@ function immer(baseState, thunk) { return res } -function markDirty(proxy) { - proxy[CHANGED_STATE] = true - let parent = proxy - while ((parent = parent[PARENT])) { - if (parent[CHANGED_STATE] === true) return - parent[CHANGED_STATE] = true - } +function isProxy(value) { + return !!value && !!value[PROXY_STATE] } -function isPlainObject(value) { - if (value === null || typeof value !== "object") return false +function isProxyable(value) { + if (!value) return false + if (typeof value !== "object") return false + if (Array.isArray(value)) return true const proto = Object.getPrototypeOf(value) - return proto === Object.prototype || proto === null -} - -function isProxy(value) { - return !!(value && value[PROXY_TARGET]) + return (proto === proto) === null || Object.prototype } function freeze(value) { From d750a374198312e32bf71ac217fe0b88601f00ae Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Mon, 8 Jan 2018 10:55:09 +0100 Subject: [PATCH 09/12] Added some more performance tests --- __performance_tests__/todo.js | 91 +++++++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 19 deletions(-) diff --git a/__performance_tests__/todo.js b/__performance_tests__/todo.js index b4fef0a1..02d49b7b 100644 --- a/__performance_tests__/todo.js +++ b/__performance_tests__/todo.js @@ -3,6 +3,12 @@ import immerProxy, {setAutoFreeze as setAutoFreezeProxy} from ".." import immerEs5, {setAutoFreeze as setAutoFreezeEs5} from "../es5" import cloneDeep from "lodash.clonedeep" import {List, Record} from "immutable" +import deepFreeze from "deep-freeze" + +function freeze(x) { + Object.freeze(x) + return x +} describe("performance", () => { const MAX = 100000 @@ -21,14 +27,7 @@ describe("performance", () => { } // Produce the frozen bazeState - frozenBazeState = baseState.map(todo => { - const newTodo = {...todo} - newTodo.someThingCompletelyIrrelevant = todo.someThingCompletelyIrrelevant.slice() - Object.freeze(newTodo.someThingCompletelyIrrelevant) - Object.freeze(newTodo) - return newTodo - }) - Object.freeze(frozenBazeState) + frozenBazeState = deepFreeze(cloneDeep(baseState)) // generate immutalbeJS base state const todoRecord = Record({ @@ -56,7 +55,15 @@ describe("performance", () => { } }) - measure("handcrafted reducer", () => { + measure("deepclone, then mutate, then freeze", () => { + const draft = cloneDeep(baseState) + for (let i = 0; i < MAX * MODIFY_FACTOR; i++) { + draft[i].done = true + } + deepFreeze(draft) + }) + + measure("handcrafted reducer (no freeze)", () => { const nextState = [ ...baseState.slice(0, MAX * MODIFY_FACTOR).map(todo => ({ ...todo, @@ -66,6 +73,42 @@ describe("performance", () => { ] }) + measure("handcrafted reducer (with freeze)", () => { + const nextState = freeze([ + ...baseState.slice(0, MAX * MODIFY_FACTOR).map(todo => + freeze({ + ...todo, + done: true + }) + ), + ...baseState.slice(MAX * MODIFY_FACTOR) + ]) + }) + + measure("naive handcrafted reducer (without freeze)", () => { + const nextState = baseState.map((todo, index) => { + if (index < MAX * MODIFY_FACTOR) + return { + ...todo, + done: true + } + else return todo + }) + }) + + measure("naive handcrafted reducer (with freeze)", () => { + const nextState = deepFreeze( + baseState.map((todo, index) => { + if (index < MAX * MODIFY_FACTOR) + return { + ...todo, + done: true + } + else return todo + }) + ) + }) + measure("immutableJS", () => { let state = immutableJsBaseState state.withMutations(state => { @@ -75,13 +118,14 @@ describe("performance", () => { }) }) - measure("immer (proxy) - with autofreeze", () => { - setAutoFreezeProxy(true) - immerProxy(frozenBazeState, draft => { - for (let i = 0; i < MAX * MODIFY_FACTOR; i++) { - draft[i].done = true - } - }) + measure("immutableJS + toJS", () => { + let state = immutableJsBaseState + .withMutations(state => { + for (let i = 0; i < MAX * MODIFY_FACTOR; i++) { + state.setIn([i, "done"], true) + } + }) + .toJS() }) measure("immer (proxy) - without autofreeze", () => { @@ -94,9 +138,9 @@ describe("performance", () => { setAutoFreezeProxy(true) }) - measure("immer (es5) - with autofreeze", () => { - setAutoFreezeEs5(true) - immerEs5(frozenBazeState, draft => { + measure("immer (proxy) - with autofreeze", () => { + setAutoFreezeProxy(true) + immerProxy(frozenBazeState, draft => { for (let i = 0; i < MAX * MODIFY_FACTOR; i++) { draft[i].done = true } @@ -112,4 +156,13 @@ describe("performance", () => { }) setAutoFreezeEs5(true) }) + + measure("immer (es5) - with autofreeze", () => { + setAutoFreezeEs5(true) + immerEs5(frozenBazeState, draft => { + for (let i = 0; i < MAX * MODIFY_FACTOR; i++) { + draft[i].done = true + } + }) + }) }) From 1fea836544ac361b514897171fae2f43c0f00fcb Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Mon, 8 Jan 2018 11:51:31 +0100 Subject: [PATCH 10/12] Added performance explanation --- __performance_tests__/todo.js | 23 ++++++++++++---- immer.png => images/immer.png | Bin images/performance.png | Bin 0 -> 38426 bytes readme.md | 48 ++++++++++++++-------------------- 4 files changed, 38 insertions(+), 33 deletions(-) rename immer.png => images/immer.png (100%) create mode 100644 images/performance.png diff --git a/__performance_tests__/todo.js b/__performance_tests__/todo.js index 02d49b7b..9dc71866 100644 --- a/__performance_tests__/todo.js +++ b/__performance_tests__/todo.js @@ -42,11 +42,24 @@ describe("performance", () => { test(name, fn) } - measure("just mutate", () => { - for (let i = 0; i < MAX * MODIFY_FACTOR; i++) { - baseState[i].done = true - } - }) + { + const draft = cloneDeep(baseState) + measure("just mutate", () => { + for (let i = 0; i < MAX * MODIFY_FACTOR; i++) { + draft[i].done = true + } + }) + } + + { + const draft = cloneDeep(baseState) + measure("just mutate, freeze", () => { + for (let i = 0; i < MAX * MODIFY_FACTOR; i++) { + draft[i].done = true + } + deepFreeze(draft) + }) + } measure("deepclone, then mutate", () => { const draft = cloneDeep(baseState) diff --git a/immer.png b/images/immer.png similarity index 100% rename from immer.png rename to images/immer.png diff --git a/images/performance.png b/images/performance.png new file mode 100644 index 0000000000000000000000000000000000000000..3c59bd1e5df769e6ec23d670799a1d978d31eb78 GIT binary patch literal 38426 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 Xq|Ze8tvZXLc88fJ=F_eiIqdvDe?a|I literal 0 HcmV?d00001 diff --git a/readme.md b/readme.md index 44e9ddc6..c24ec8f2 100644 --- a/readme.md +++ b/readme.md @@ -11,7 +11,7 @@ The basic idea is that you will apply all your changes to a temporarily _draftSt Once all your mutations are completed, immer will produce the _nextState_ based on the mutations to the draft state. This means that you can interact with your data by simply modifying it, while keeping all the benefits of immutable data. -![immer.png](immer.png) +![immer.png](images/immer.png) Using immer is like having a personal assistant; he takes a letter (the current state), and gives you a copy (draft) to jot changes onto. Once you are done, the assistant will take your draft and produce the real immutable, final letter for you (the next state). @@ -233,33 +233,6 @@ function updateObjectInArray(array, action) { } ``` -## Performance - -Here is a [simple benchmark](__tests__/performance.js) on the performance of `immer`. -This test takes 100.000 todo items, and updates 10.000 of them. -These tests were executed on Node 8.4.0. - -Use `yarn test:perf` to reproduce them locally - -``` - ✓ just mutate (3ms) - (No immutability at all) - ✓ deepclone, then mutate (409ms) - (Clone entire tree, then mutate (no structural sharing!)) - ✓ handcrafted reducer (17ms) - (Implement it as typical Redux reducer, with slices and spread operator) - ✓ immutableJS (60ms) - (Use immutableJS and leverage `withMutations` for best performance) - ✓ immer (proxy) - with autofreeze (305ms) - (Immer, with auto freeze enabled, default implementation) - ✓ immer (proxy) - without autofreeze (149ms) - (Immer, with auto freeze disabled, default implementation) - ✓ immer (es5) - with autofreeze (436ms) - (Immer, with auto freeze enabled, compatibility implementation) - ✓ immer (es5) - without autofreeze (336ms) - (Immer, with auto freeze disabled, default implementation) -``` - ## Limitations * Currently, only tree shaped states are supported. Cycles could potentially be supported as well (PR's welcome) @@ -270,6 +243,25 @@ Use `yarn test:perf` to reproduce them locally * Make sure to modify the draft state you get passed in in the callback function, not the original current state that was passed as the first argument to `immer`! * Since immer uses proxies, reading huge amounts of data from state comes with an overhead. If this ever becomes an issue (measure before you optimize!), do the current state analysis before entering the `immer` block or read from the `currentState` rather than the `draftState` +## Performance + +Here is a [simple benchmark](__tests__/performance.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. + +These tests were executed on Node 8.4.0. +Use `yarn test:perf` to reproduce them locally. + +![immer.png](images/immer.png) + +Some observations: +* 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 _reducure_ 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 negclectable. +* 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 implentation 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 peeks 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. + ## Credits Special thanks goes to @Mendix, which supports it's employees to experiment completely freely two full days a month, which formed the kick-start for this project. \ No newline at end of file From 97ce2d4cc3ca03b8b88d40a918265355d328ce0c Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Mon, 8 Jan 2018 20:03:59 +0100 Subject: [PATCH 11/12] Don't auto add formatted files, it breaks partial commits --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 73713190..4598b9cd 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "test": "jest", "test:perf": "node --expose-gc node_modules/jest-cli/bin/jest.js --verbose --testRegex '__performance_tests__/.*'", - "prettier": "prettier \"*/**/*.js\" --ignore-path ./.prettierignore --write && git add . && git status" + "prettier": "prettier \"*/**/*.js\" --ignore-path ./.prettierignore --write" }, "pre-commit": [ "prettier" From e49d0a2bade7aa04b31fb92e2df3ad7e54d83152 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Mon, 8 Jan 2018 20:04:10 +0100 Subject: [PATCH 12/12] Added tests --- __tests__/base.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/__tests__/base.js b/__tests__/base.js index 0acc2ab0..73f509e2 100644 --- a/__tests__/base.js +++ b/__tests__/base.js @@ -148,6 +148,16 @@ function runBaseTest(name, lib, freeze) { ]) }) + it("can delete array items", () => { + const nextState = immer(baseState, s => { + s.anArray.length = 3 + }) + expect(nextState).not.toBe(baseState) + expect(nextState.anObject).toBe(baseState.anObject) + expect(nextState.anArray).not.toBe(baseState.anArray) + expect(nextState.anArray).toEqual([3, 2, {c: 3}]) + }) + it("should support sorting arrays", () => { const nextState = immer(baseState, s => { s.anArray[2].c = 4 @@ -238,6 +248,7 @@ function runBaseTest(name, lib, freeze) { delete s.anObject obj.coffee = true s.messy = obj + debugger }) expect(nextState).not.toBe(baseState) expect(nextState.anArray).toBe(baseState.anArray)