Skip to content

Commit

Permalink
feat: add Immer class
Browse files Browse the repository at this point in the history
Every instance of the `Immer` class provides a bound `produce` function. As explained in #254, this class makes room for advanced configuration (if we need it) and allows for interop between Immer-using libraries that have different needs.

I took the time to refactor while I was implementing this, so the diff may look
more overwhelming than it is. :)

Summary:

1. Renamed `immer.js` to `index.js` so the `Immer` class can have `immer.js`

2. Use the word "draft" in place of "proxy" in most places, because "draft" is
   a higher level of abstraction. It also makes sense to use the same semantics
   both internally and externally.

3. Moved `finalize` logic to the `Immer` class

4. Inlined the `freeze` function as only one callsite existed

5. Inlined the `createState` functions in both `proxy.js` and `es5.js`

6. Extract repeated code from `produceEs5` and `produceProxy` (which have since
   been removed) into the `Immer` class

7. The `es5.js` and `proxy.js` modules now have different exports:
    - `scopes`: an array of nested `produce` calls, where each value is an array
      of unrevoked proxies
    - `currentScope()`: shortcut for getting the last value of the `scopes` array
    - `createDraft()`: a renamed `createProxy` with the arguments swapped
    - `willFinalize()`: called right after the recipe function returns (only
      used by ES5 proxies)

8. Changed some "draft state" properties:
    - removed `hasCopy` in ES5 state (checking for truthiness of `copy` does the job)
    - renamed `proxy` to `draft` in ES5 state
    - renamed `finished` to `revoked` in ES5 state
    - renamed `proxies` to `drafts` in Proxy state
    - added `revoke` method (called by the `Immer` class)
    - added `draft` property to Proxy state
    - use null literals instead of undefined

9. Delay creation of `patches` and `inversePatches` arrays until the recipe
   function returns. This avoids array allocations when a rollback is performed
   by throwing.

10. Simplified `generatePatches` by removing the last two arguments, since they
    can be obtained from the `state` argument.
  • Loading branch information
aleclarson committed Dec 15, 2018
1 parent 66e51e1 commit ecd04dc
Show file tree
Hide file tree
Showing 11 changed files with 400 additions and 454 deletions.
10 changes: 5 additions & 5 deletions __tests__/curry.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ function runTests(name, useProxies) {
setUseProxies(useProxies)

it("should check arguments", () => {
expect(() => produce()).toThrow(/produce expects 1 to 3 arguments/)
let error = /if first argument is not a function, the second argument to produce should be a function/
expect(() => produce()).toThrow(error)
expect(() => produce({})).toThrow(error)

expect(() => produce({}, {})).toThrow(/should be a function/)
expect(() => produce({}, () => {}, [])).toThrow(
/third argument of a producer/
)
expect(() => produce({}, {})).toThrow(/should be a function/)
expect(() => produce({})).toThrow(
/if first argument is not a function, the second argument to produce should be a function/
)
})

it("should support currying", () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe("applyPatches", () => {
expect(() => {
const patch = {op: "remove", path: [0]}
applyPatches([1, 2], [patch])
}).toThrowError(/^Remove can only remove the last key of an array/)
}).toThrowError(/^Only the last index of an array can be removed/)
})
})

Expand Down
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import babel from "rollup-plugin-babel"

function getConfig(dest, format, ugly) {
const conf = {
input: "src/immer.js",
input: "src/index.js",
output: {
exports: "named",
file: dest,
Expand Down
122 changes: 9 additions & 113 deletions src/common.js
Original file line number Diff line number Diff line change
@@ -1,69 +1,26 @@
import {generatePatches} from "./patches"

export const NOTHING =
typeof Symbol !== "undefined"
? Symbol("immer-nothing")
: {["immer-nothing"]: true}

export const PROXY_STATE =
typeof Symbol !== "undefined"
? Symbol("immer-proxy-state")
: "__$immer_state"

export const RETURNED_AND_MODIFIED_ERROR =
"An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."

function verifyMinified() {}

const inProduction =
(typeof process !== "undefined" && process.env.NODE_ENV === "production") ||
verifyMinified.name !== "verifyMinified"

let autoFreeze = !inProduction
let useProxies = typeof Proxy !== "undefined" && typeof Reflect !== "undefined"

/**
* Automatically freezes any state trees generated by immer.
* This protects against accidental modifications of the state tree outside of an immer function.
* This comes with a performance impact, so it is recommended to disable this option in production.
* It is by default enabled.
*
* @returns {void}
*/
export function setAutoFreeze(enableAutoFreeze) {
autoFreeze = enableAutoFreeze
}

export function setUseProxies(value) {
useProxies = value
}

export function getUseProxies() {
return useProxies
}
export const DRAFT_STATE =
typeof Symbol !== "undefined" ? Symbol("immer-state") : "__$immer_state"

export function isProxy(value) {
return !!value && !!value[PROXY_STATE]
export function isDraft(value) {
return !!value && !!value[DRAFT_STATE]
}

export function isProxyable(value) {
export function isDraftable(value) {
if (!value) return false
if (typeof value !== "object") return false
if (Array.isArray(value)) return true
const proto = Object.getPrototypeOf(value)
return proto === null || proto === Object.prototype
}

export function freeze(value) {
if (autoFreeze) {
Object.freeze(value)
}
return value
}

export function original(value) {
if (value && value[PROXY_STATE]) {
return value[PROXY_STATE].base
if (value && value[DRAFT_STATE]) {
return value[DRAFT_STATE].base
}
// otherwise return undefined
}
Expand All @@ -87,77 +44,16 @@ export function shallowCopy(value) {

export function each(value, cb) {
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) cb(i, value[i])
for (let i = 0; i < value.length; i++) cb(i, value[i], value)
} else {
for (let key in value) cb(key, value[key])
for (let key in value) cb(key, value[key], value)
}
}

export function has(thing, prop) {
return Object.prototype.hasOwnProperty.call(thing, prop)
}

// given a base object, returns it if unmodified, or return the changed cloned if modified
export function finalize(base, path, patches, inversePatches) {
if (isProxy(base)) {
const state = base[PROXY_STATE]
if (state.modified === true) {
if (state.finalized === true) return state.copy
state.finalized = true
const result = finalizeObject(
useProxies ? state.copy : (state.copy = shallowCopy(base)),
state,
path,
patches,
inversePatches
)
generatePatches(
state,
path,
patches,
inversePatches,
state.base,
result
)
return result
} else {
return state.base
}
}
finalizeNonProxiedObject(base)
return base
}

function finalizeObject(copy, state, path, patches, inversePatches) {
const base = state.base
each(copy, (prop, value) => {
if (value !== base[prop]) {
// if there was an assignment on this property, we don't need to generate
// patches for the subtree
const generatePatches = patches && !has(state.assigned, prop)
copy[prop] = finalize(
value,
generatePatches && path.concat(prop),
generatePatches && patches,
inversePatches
)
}
})
return freeze(copy)
}

function finalizeNonProxiedObject(parent) {
// If finalize is called on an object that was not a proxy, it means that it is an object that was not there in the original
// tree and it could contain proxies at arbitrarily places. Let's find and finalize them as well
if (!isProxyable(parent)) return
if (Object.isFrozen(parent)) return
each(parent, (i, child) => {
if (isProxy(child)) {
parent[i] = finalize(child)
} else finalizeNonProxiedObject(child)
})
}

export function is(x, y) {
// From: https://github.com/facebook/fbjs/blob/c69904a511b900266935168223063dd8772dfc40/packages/fbjs/src/core/shallowEqual.js
if (x === y) {
Expand Down
Loading

0 comments on commit ecd04dc

Please sign in to comment.