Skip to content

Commit

Permalink
feat: passing drafts to produce
Browse files Browse the repository at this point in the history
Improve support for nested producers by allowing drafts to be passed
as or in the base state.

DETAILS:

- Added `state.scope` for knowing when a draft is owned by the current
  producer or not.

- Objects included in the return value of a nested producer are
  never auto-freezed, because they may contain drafts from a parent producer.

- Drafts are always finalized by the producer that created them.

- Now that `state.base` can be a draft, we must ensure `state.finalized`
  is false before creating drafts in the `get` handler. This only affects ES2015+ proxies.
  • Loading branch information
aleclarson committed Jan 4, 2019
1 parent 483a27a commit 9c53415
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 35 deletions.
25 changes: 17 additions & 8 deletions src/es5.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,37 @@ export function willFinalize(result, baseDraft, needPatches) {
}

export function createDraft(base, parent) {
if (isDraft(base)) throw new Error("This should never happen. Please report: https://github.com/mweststrate/immer/issues/new") // prettier-ignore

const draft = shallowCopy(base)
let draft
if (isDraft(base)) {
const state = base[DRAFT_STATE]
// Avoid creating new drafts when copying.
state.finalizing = true
draft = shallowCopy(state.draft)
state.finalizing = false
} else {
draft = shallowCopy(base)
}
each(base, prop => {
Object.defineProperty(draft, "" + prop, createPropertyProxy("" + prop))
})

// See "proxy.js" for property documentation.
const state = {
scope: parent ? parent.scope : currentScope(),
modified: false,
finalizing: false,
finalizing: false, // es5 only
finalized: false,
assigned: {}, // true: value was assigned to these props, false: was removed
assigned: {},
parent,
base,
draft,
copy: null,
revoke,
revoked: false
revoked: false // es5 only
}

createHiddenProperty(draft, DRAFT_STATE, state)
currentScope().push(state)
state.scope.push(state)
return draft
}

Expand All @@ -74,7 +83,7 @@ function get(state, prop) {

function set(state, prop, value) {
assertUnrevoked(state)
state.assigned[prop] = true // optimization; skip this if there is no listener
state.assigned[prop] = true
if (!state.modified) {
if (is(source(state)[prop], value)) return
markChanged(state)
Expand Down
17 changes: 15 additions & 2 deletions src/immer.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ export class Immer {
if (Object.isFrozen(draft)) return draft
return this.finalizeTree(draft)
}
// Never finalize drafts owned by an outer scope.
if (state.scope !== this.currentScope()) {
return draft
}
if (!state.modified) return state.base
if (!state.finalized) {
state.finalized = true
Expand All @@ -139,7 +143,13 @@ export class Immer {
assigned[prop] || this.onDelete(state, prop)
}
if (this.onCopy) this.onCopy(state)
if (this.autoFreeze) Object.freeze(state.copy)

// Nested producers must never auto-freeze their result,
// because it may contain drafts from parent producers.
if (this.autoFreeze && this.scopes.length === 1) {
Object.freeze(state.copy)
}

if (patches) generatePatches(state, path, patches, inversePatches)
}
return state.copy
Expand Down Expand Up @@ -168,8 +178,11 @@ export class Immer {
patches && inDraft && !state.assigned[prop]
? this.finalize(value, path.concat(prop), patches, inversePatches)
: this.finalize(value)

// Unchanged drafts are ignored.
if (inDraft && value === state.base[prop]) return
}
// Unchanged base state is always ignored.
// Unchanged draft properties are ignored.
else if (inDraft && is(value, state.base[prop])) {
return
}
Expand Down
63 changes: 38 additions & 25 deletions src/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,27 @@ export const currentScope = () => scopes[scopes.length - 1]
export function willFinalize() {}

export function createDraft(base, parent) {
if (isDraft(base)) throw new Error("This should never happen. Please report: https://github.com/mweststrate/immer/issues/new") // prettier-ignore

const state = {
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
// Track which produce call this is associated with.
scope: parent ? parent.scope : currentScope(),
// True for both shallow and deep changes.
modified: false,
// Used during finalization.
finalized: false,
// Track which properties have been assigned (true) or deleted (false).
assigned: {},
// The parent draft state.
parent,
// The base state.
base,
draft: null, // the root proxy
drafts: {}, // proxied properties
// The base proxy.
draft: null,
// Any property proxies.
drafts: {},
// The base copy with any updated values.
copy: null,
revoke: null,
finalized: false
// Called by the `produce` function.
revoke: null
}

const {revoke, proxy} = Array.isArray(base)
Expand All @@ -41,7 +50,7 @@ export function createDraft(base, parent) {
state.draft = proxy
state.revoke = revoke

currentScope().push(state)
state.scope.push(state)
return proxy
}

Expand Down Expand Up @@ -86,25 +95,30 @@ arrayTraps.set = function(state, prop, value) {
}

function source(state) {
return state.modified === true ? state.copy : state.base
return state.copy || state.base
}

function get(state, prop) {
if (prop === DRAFT_STATE) return state
let {drafts} = state

// Check for existing draft in unmodified state.
if (!state.modified && has(drafts, prop)) {
return drafts[prop]
}

const value = source(state)[prop]
if (state.finalized || !isDraftable(value)) return value

// Check for existing draft in modified state.
if (state.modified) {
const value = state.copy[prop]
if (value === state.base[prop] && isDraftable(value))
// only create proxy if it is not yet a proxy, and not a new object
// (new objects don't need proxying, they will be processed in finalize anyway)
return (state.copy[prop] = createDraft(value, state))
return value
} else {
if (has(state.drafts, prop)) return state.drafts[prop]
const value = state.base[prop]
if (!isDraft(value) && isDraftable(value))
return (state.drafts[prop] = createDraft(value, state))
return value
// Assigned values are never drafted. This catches any drafts we created, too.
if (value !== state.base[prop]) return value
// Store drafts on the copy (when one exists).
drafts = state.copy
}

return (drafts[prop] = createDraft(value, state))
}

function set(state, prop, value) {
Expand Down Expand Up @@ -154,9 +168,8 @@ function defineProperty() {
function markChanged(state) {
if (!state.modified) {
state.modified = true
state.copy = shallowCopy(state.base)
// copy the drafts over the base-copy
assign(state.copy, state.drafts) // yup that works for arrays as well
state.copy = assign(shallowCopy(state.base), state.drafts)
state.drafts = null
if (state.parent) markChanged(state.parent)
}
}

0 comments on commit 9c53415

Please sign in to comment.