This commit is contained in:
9
node_modules/immer/src/plugins/all.ts
generated
vendored
Normal file
9
node_modules/immer/src/plugins/all.ts
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import {enableES5} from "./es5"
|
||||
import {enableMapSet} from "./mapset"
|
||||
import {enablePatches} from "./patches"
|
||||
|
||||
export function enableAllPlugins() {
|
||||
enableES5()
|
||||
enableMapSet()
|
||||
enablePatches()
|
||||
}
|
||||
277
node_modules/immer/src/plugins/es5.ts
generated
vendored
Normal file
277
node_modules/immer/src/plugins/es5.ts
generated
vendored
Normal file
@@ -0,0 +1,277 @@
|
||||
import {
|
||||
ImmerState,
|
||||
Drafted,
|
||||
ES5ArrayState,
|
||||
ES5ObjectState,
|
||||
each,
|
||||
has,
|
||||
isDraft,
|
||||
latest,
|
||||
DRAFT_STATE,
|
||||
is,
|
||||
loadPlugin,
|
||||
ImmerScope,
|
||||
ProxyType,
|
||||
getCurrentScope,
|
||||
die,
|
||||
markChanged,
|
||||
objectTraps,
|
||||
ownKeys,
|
||||
getOwnPropertyDescriptors
|
||||
} from "../internal"
|
||||
|
||||
type ES5State = ES5ArrayState | ES5ObjectState
|
||||
|
||||
export function enableES5() {
|
||||
function willFinalizeES5_(
|
||||
scope: ImmerScope,
|
||||
result: any,
|
||||
isReplaced: boolean
|
||||
) {
|
||||
if (!isReplaced) {
|
||||
if (scope.patches_) {
|
||||
markChangesRecursively(scope.drafts_![0])
|
||||
}
|
||||
// This is faster when we don't care about which attributes changed.
|
||||
markChangesSweep(scope.drafts_)
|
||||
}
|
||||
// When a child draft is returned, look for changes.
|
||||
else if (
|
||||
isDraft(result) &&
|
||||
(result[DRAFT_STATE] as ES5State).scope_ === scope
|
||||
) {
|
||||
markChangesSweep(scope.drafts_)
|
||||
}
|
||||
}
|
||||
|
||||
function createES5Draft(isArray: boolean, base: any) {
|
||||
if (isArray) {
|
||||
const draft = new Array(base.length)
|
||||
for (let i = 0; i < base.length; i++)
|
||||
Object.defineProperty(draft, "" + i, proxyProperty(i, true))
|
||||
return draft
|
||||
} else {
|
||||
const descriptors = getOwnPropertyDescriptors(base)
|
||||
delete descriptors[DRAFT_STATE as any]
|
||||
const keys = ownKeys(descriptors)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key: any = keys[i]
|
||||
descriptors[key] = proxyProperty(
|
||||
key,
|
||||
isArray || !!descriptors[key].enumerable
|
||||
)
|
||||
}
|
||||
return Object.create(Object.getPrototypeOf(base), descriptors)
|
||||
}
|
||||
}
|
||||
|
||||
function createES5Proxy_<T>(
|
||||
base: T,
|
||||
parent?: ImmerState
|
||||
): Drafted<T, ES5ObjectState | ES5ArrayState> {
|
||||
const isArray = Array.isArray(base)
|
||||
const draft = createES5Draft(isArray, base)
|
||||
|
||||
const state: ES5ObjectState | ES5ArrayState = {
|
||||
type_: isArray ? ProxyType.ES5Array : (ProxyType.ES5Object as any),
|
||||
scope_: parent ? parent.scope_ : getCurrentScope(),
|
||||
modified_: false,
|
||||
finalized_: false,
|
||||
assigned_: {},
|
||||
parent_: parent,
|
||||
// base is the object we are drafting
|
||||
base_: base,
|
||||
// draft is the draft object itself, that traps all reads and reads from either the base (if unmodified) or copy (if modified)
|
||||
draft_: draft,
|
||||
copy_: null,
|
||||
revoked_: false,
|
||||
isManual_: false
|
||||
}
|
||||
|
||||
Object.defineProperty(draft, DRAFT_STATE, {
|
||||
value: state,
|
||||
// enumerable: false <- the default
|
||||
writable: true
|
||||
})
|
||||
return draft
|
||||
}
|
||||
|
||||
// property descriptors are recycled to make sure we don't create a get and set closure per property,
|
||||
// but share them all instead
|
||||
const descriptors: {[prop: string]: PropertyDescriptor} = {}
|
||||
|
||||
function proxyProperty(
|
||||
prop: string | number,
|
||||
enumerable: boolean
|
||||
): PropertyDescriptor {
|
||||
let desc = descriptors[prop]
|
||||
if (desc) {
|
||||
desc.enumerable = enumerable
|
||||
} else {
|
||||
descriptors[prop] = desc = {
|
||||
configurable: true,
|
||||
enumerable,
|
||||
get(this: any) {
|
||||
const state = this[DRAFT_STATE]
|
||||
if (__DEV__) assertUnrevoked(state)
|
||||
// @ts-ignore
|
||||
return objectTraps.get(state, prop)
|
||||
},
|
||||
set(this: any, value) {
|
||||
const state = this[DRAFT_STATE]
|
||||
if (__DEV__) assertUnrevoked(state)
|
||||
// @ts-ignore
|
||||
objectTraps.set(state, prop, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return desc
|
||||
}
|
||||
|
||||
// This looks expensive, but only proxies are visited, and only objects without known changes are scanned.
|
||||
function markChangesSweep(drafts: Drafted<any, ImmerState>[]) {
|
||||
// The natural order of drafts in the `scope` array is based on when they
|
||||
// were accessed. By processing drafts in reverse natural order, we have a
|
||||
// better chance of processing leaf nodes first. When a leaf node is known to
|
||||
// have changed, we can avoid any traversal of its ancestor nodes.
|
||||
for (let i = drafts.length - 1; i >= 0; i--) {
|
||||
const state: ES5State = drafts[i][DRAFT_STATE]
|
||||
if (!state.modified_) {
|
||||
switch (state.type_) {
|
||||
case ProxyType.ES5Array:
|
||||
if (hasArrayChanges(state)) markChanged(state)
|
||||
break
|
||||
case ProxyType.ES5Object:
|
||||
if (hasObjectChanges(state)) markChanged(state)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function markChangesRecursively(object: any) {
|
||||
if (!object || typeof object !== "object") return
|
||||
const state: ES5State | undefined = object[DRAFT_STATE]
|
||||
if (!state) return
|
||||
const {base_, draft_, assigned_, type_} = state
|
||||
if (type_ === ProxyType.ES5Object) {
|
||||
// Look for added keys.
|
||||
// probably there is a faster way to detect changes, as sweep + recurse seems to do some
|
||||
// unnecessary work.
|
||||
// also: probably we can store the information we detect here, to speed up tree finalization!
|
||||
each(draft_, key => {
|
||||
if ((key as any) === DRAFT_STATE) return
|
||||
// The `undefined` check is a fast path for pre-existing keys.
|
||||
if ((base_ as any)[key] === undefined && !has(base_, key)) {
|
||||
assigned_[key] = true
|
||||
markChanged(state)
|
||||
} else if (!assigned_[key]) {
|
||||
// Only untouched properties trigger recursion.
|
||||
markChangesRecursively(draft_[key])
|
||||
}
|
||||
})
|
||||
// Look for removed keys.
|
||||
each(base_, key => {
|
||||
// The `undefined` check is a fast path for pre-existing keys.
|
||||
if (draft_[key] === undefined && !has(draft_, key)) {
|
||||
assigned_[key] = false
|
||||
markChanged(state)
|
||||
}
|
||||
})
|
||||
} else if (type_ === ProxyType.ES5Array) {
|
||||
if (hasArrayChanges(state as ES5ArrayState)) {
|
||||
markChanged(state)
|
||||
assigned_.length = true
|
||||
}
|
||||
|
||||
if (draft_.length < base_.length) {
|
||||
for (let i = draft_.length; i < base_.length; i++) assigned_[i] = false
|
||||
} else {
|
||||
for (let i = base_.length; i < draft_.length; i++) assigned_[i] = true
|
||||
}
|
||||
|
||||
// Minimum count is enough, the other parts has been processed.
|
||||
const min = Math.min(draft_.length, base_.length)
|
||||
|
||||
for (let i = 0; i < min; i++) {
|
||||
// Only untouched indices trigger recursion.
|
||||
if (!draft_.hasOwnProperty(i)) {
|
||||
assigned_[i] = true
|
||||
}
|
||||
if (assigned_[i] === undefined) markChangesRecursively(draft_[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasObjectChanges(state: ES5ObjectState) {
|
||||
const {base_, draft_} = state
|
||||
|
||||
// Search for added keys and changed keys. Start at the back, because
|
||||
// non-numeric keys are ordered by time of definition on the object.
|
||||
const keys = ownKeys(draft_)
|
||||
for (let i = keys.length - 1; i >= 0; i--) {
|
||||
const key: any = keys[i]
|
||||
if (key === DRAFT_STATE) continue
|
||||
const baseValue = base_[key]
|
||||
// The `undefined` check is a fast path for pre-existing keys.
|
||||
if (baseValue === undefined && !has(base_, key)) {
|
||||
return true
|
||||
}
|
||||
// Once a base key is deleted, future changes go undetected, because its
|
||||
// descriptor is erased. This branch detects any missed changes.
|
||||
else {
|
||||
const value = draft_[key]
|
||||
const state: ImmerState = value && value[DRAFT_STATE]
|
||||
if (state ? state.base_ !== baseValue : !is(value, baseValue)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, no keys were added or changed.
|
||||
// Compare key count to determine if keys were deleted.
|
||||
const baseIsDraft = !!base_[DRAFT_STATE as any]
|
||||
return keys.length !== ownKeys(base_).length + (baseIsDraft ? 0 : 1) // + 1 to correct for DRAFT_STATE
|
||||
}
|
||||
|
||||
function hasArrayChanges(state: ES5ArrayState) {
|
||||
const {draft_} = state
|
||||
if (draft_.length !== state.base_.length) return true
|
||||
// See #116
|
||||
// If we first shorten the length, our array interceptors will be removed.
|
||||
// If after that new items are added, result in the same original length,
|
||||
// those last items will have no intercepting property.
|
||||
// So if there is no own descriptor on the last position, we know that items were removed and added
|
||||
// N.B.: splice, unshift, etc only shift values around, but not prop descriptors, so we only have to check
|
||||
// the last one
|
||||
// last descriptor can be not a trap, if the array was extended
|
||||
const descriptor = Object.getOwnPropertyDescriptor(
|
||||
draft_,
|
||||
draft_.length - 1
|
||||
)
|
||||
// descriptor can be null, but only for newly created sparse arrays, eg. new Array(10)
|
||||
if (descriptor && !descriptor.get) return true
|
||||
// if we miss a property, it has been deleted, so array probobaly changed
|
||||
for (let i = 0; i < draft_.length; i++) {
|
||||
if (!draft_.hasOwnProperty(i)) return true
|
||||
}
|
||||
// For all other cases, we don't have to compare, as they would have been picked up by the index setters
|
||||
return false
|
||||
}
|
||||
|
||||
function hasChanges_(state: ES5State) {
|
||||
return state.type_ === ProxyType.ES5Object
|
||||
? hasObjectChanges(state)
|
||||
: hasArrayChanges(state)
|
||||
}
|
||||
|
||||
function assertUnrevoked(state: any /*ES5State | MapState | SetState*/) {
|
||||
if (state.revoked_) die(3, JSON.stringify(latest(state)))
|
||||
}
|
||||
|
||||
loadPlugin("ES5", {
|
||||
createES5Proxy_,
|
||||
willFinalizeES5_,
|
||||
hasChanges_
|
||||
})
|
||||
}
|
||||
348
node_modules/immer/src/plugins/mapset.ts
generated
vendored
Normal file
348
node_modules/immer/src/plugins/mapset.ts
generated
vendored
Normal file
@@ -0,0 +1,348 @@
|
||||
// types only!
|
||||
import {
|
||||
ImmerState,
|
||||
AnyMap,
|
||||
AnySet,
|
||||
MapState,
|
||||
SetState,
|
||||
DRAFT_STATE,
|
||||
getCurrentScope,
|
||||
latest,
|
||||
iteratorSymbol,
|
||||
isDraftable,
|
||||
createProxy,
|
||||
loadPlugin,
|
||||
markChanged,
|
||||
ProxyType,
|
||||
die,
|
||||
each
|
||||
} from "../internal"
|
||||
|
||||
export function enableMapSet() {
|
||||
/* istanbul ignore next */
|
||||
var extendStatics = function(d: any, b: any): any {
|
||||
extendStatics =
|
||||
Object.setPrototypeOf ||
|
||||
({__proto__: []} instanceof Array &&
|
||||
function(d, b) {
|
||||
d.__proto__ = b
|
||||
}) ||
|
||||
function(d, b) {
|
||||
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]
|
||||
}
|
||||
return extendStatics(d, b)
|
||||
}
|
||||
|
||||
// Ugly hack to resolve #502 and inherit built in Map / Set
|
||||
function __extends(d: any, b: any): any {
|
||||
extendStatics(d, b)
|
||||
function __(this: any): any {
|
||||
this.constructor = d
|
||||
}
|
||||
d.prototype =
|
||||
// @ts-ignore
|
||||
((__.prototype = b.prototype), new __())
|
||||
}
|
||||
|
||||
const DraftMap = (function(_super) {
|
||||
__extends(DraftMap, _super)
|
||||
// Create class manually, cause #502
|
||||
function DraftMap(this: any, target: AnyMap, parent?: ImmerState): any {
|
||||
this[DRAFT_STATE] = {
|
||||
type_: ProxyType.Map,
|
||||
parent_: parent,
|
||||
scope_: parent ? parent.scope_ : getCurrentScope()!,
|
||||
modified_: false,
|
||||
finalized_: false,
|
||||
copy_: undefined,
|
||||
assigned_: undefined,
|
||||
base_: target,
|
||||
draft_: this as any,
|
||||
isManual_: false,
|
||||
revoked_: false
|
||||
} as MapState
|
||||
return this
|
||||
}
|
||||
const p = DraftMap.prototype
|
||||
|
||||
Object.defineProperty(p, "size", {
|
||||
get: function() {
|
||||
return latest(this[DRAFT_STATE]).size
|
||||
}
|
||||
// enumerable: false,
|
||||
// configurable: true
|
||||
})
|
||||
|
||||
p.has = function(key: any): boolean {
|
||||
return latest(this[DRAFT_STATE]).has(key)
|
||||
}
|
||||
|
||||
p.set = function(key: any, value: any) {
|
||||
const state: MapState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
if (!latest(state).has(key) || latest(state).get(key) !== value) {
|
||||
prepareMapCopy(state)
|
||||
markChanged(state)
|
||||
state.assigned_!.set(key, true)
|
||||
state.copy_!.set(key, value)
|
||||
state.assigned_!.set(key, true)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
p.delete = function(key: any): boolean {
|
||||
if (!this.has(key)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const state: MapState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
prepareMapCopy(state)
|
||||
markChanged(state)
|
||||
if (state.base_.has(key)) {
|
||||
state.assigned_!.set(key, false)
|
||||
} else {
|
||||
state.assigned_!.delete(key)
|
||||
}
|
||||
state.copy_!.delete(key)
|
||||
return true
|
||||
}
|
||||
|
||||
p.clear = function() {
|
||||
const state: MapState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
if (latest(state).size) {
|
||||
prepareMapCopy(state)
|
||||
markChanged(state)
|
||||
state.assigned_ = new Map()
|
||||
each(state.base_, key => {
|
||||
state.assigned_!.set(key, false)
|
||||
})
|
||||
state.copy_!.clear()
|
||||
}
|
||||
}
|
||||
|
||||
p.forEach = function(
|
||||
cb: (value: any, key: any, self: any) => void,
|
||||
thisArg?: any
|
||||
) {
|
||||
const state: MapState = this[DRAFT_STATE]
|
||||
latest(state).forEach((_value: any, key: any, _map: any) => {
|
||||
cb.call(thisArg, this.get(key), key, this)
|
||||
})
|
||||
}
|
||||
|
||||
p.get = function(key: any): any {
|
||||
const state: MapState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
const value = latest(state).get(key)
|
||||
if (state.finalized_ || !isDraftable(value)) {
|
||||
return value
|
||||
}
|
||||
if (value !== state.base_.get(key)) {
|
||||
return value // either already drafted or reassigned
|
||||
}
|
||||
// despite what it looks, this creates a draft only once, see above condition
|
||||
const draft = createProxy(state.scope_.immer_, value, state)
|
||||
prepareMapCopy(state)
|
||||
state.copy_!.set(key, draft)
|
||||
return draft
|
||||
}
|
||||
|
||||
p.keys = function(): IterableIterator<any> {
|
||||
return latest(this[DRAFT_STATE]).keys()
|
||||
}
|
||||
|
||||
p.values = function(): IterableIterator<any> {
|
||||
const iterator = this.keys()
|
||||
return {
|
||||
[iteratorSymbol]: () => this.values(),
|
||||
next: () => {
|
||||
const r = iterator.next()
|
||||
/* istanbul ignore next */
|
||||
if (r.done) return r
|
||||
const value = this.get(r.value)
|
||||
return {
|
||||
done: false,
|
||||
value
|
||||
}
|
||||
}
|
||||
} as any
|
||||
}
|
||||
|
||||
p.entries = function(): IterableIterator<[any, any]> {
|
||||
const iterator = this.keys()
|
||||
return {
|
||||
[iteratorSymbol]: () => this.entries(),
|
||||
next: () => {
|
||||
const r = iterator.next()
|
||||
/* istanbul ignore next */
|
||||
if (r.done) return r
|
||||
const value = this.get(r.value)
|
||||
return {
|
||||
done: false,
|
||||
value: [r.value, value]
|
||||
}
|
||||
}
|
||||
} as any
|
||||
}
|
||||
|
||||
p[iteratorSymbol] = function() {
|
||||
return this.entries()
|
||||
}
|
||||
|
||||
return DraftMap
|
||||
})(Map)
|
||||
|
||||
function proxyMap_<T extends AnyMap>(target: T, parent?: ImmerState): T {
|
||||
// @ts-ignore
|
||||
return new DraftMap(target, parent)
|
||||
}
|
||||
|
||||
function prepareMapCopy(state: MapState) {
|
||||
if (!state.copy_) {
|
||||
state.assigned_ = new Map()
|
||||
state.copy_ = new Map(state.base_)
|
||||
}
|
||||
}
|
||||
|
||||
const DraftSet = (function(_super) {
|
||||
__extends(DraftSet, _super)
|
||||
// Create class manually, cause #502
|
||||
function DraftSet(this: any, target: AnySet, parent?: ImmerState) {
|
||||
this[DRAFT_STATE] = {
|
||||
type_: ProxyType.Set,
|
||||
parent_: parent,
|
||||
scope_: parent ? parent.scope_ : getCurrentScope()!,
|
||||
modified_: false,
|
||||
finalized_: false,
|
||||
copy_: undefined,
|
||||
base_: target,
|
||||
draft_: this,
|
||||
drafts_: new Map(),
|
||||
revoked_: false,
|
||||
isManual_: false
|
||||
} as SetState
|
||||
return this
|
||||
}
|
||||
const p = DraftSet.prototype
|
||||
|
||||
Object.defineProperty(p, "size", {
|
||||
get: function() {
|
||||
return latest(this[DRAFT_STATE]).size
|
||||
}
|
||||
// enumerable: true,
|
||||
})
|
||||
|
||||
p.has = function(value: any): boolean {
|
||||
const state: SetState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
// bit of trickery here, to be able to recognize both the value, and the draft of its value
|
||||
if (!state.copy_) {
|
||||
return state.base_.has(value)
|
||||
}
|
||||
if (state.copy_.has(value)) return true
|
||||
if (state.drafts_.has(value) && state.copy_.has(state.drafts_.get(value)))
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
p.add = function(value: any): any {
|
||||
const state: SetState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
if (!this.has(value)) {
|
||||
prepareSetCopy(state)
|
||||
markChanged(state)
|
||||
state.copy_!.add(value)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
p.delete = function(value: any): any {
|
||||
if (!this.has(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const state: SetState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
prepareSetCopy(state)
|
||||
markChanged(state)
|
||||
return (
|
||||
state.copy_!.delete(value) ||
|
||||
(state.drafts_.has(value)
|
||||
? state.copy_!.delete(state.drafts_.get(value))
|
||||
: /* istanbul ignore next */ false)
|
||||
)
|
||||
}
|
||||
|
||||
p.clear = function() {
|
||||
const state: SetState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
if (latest(state).size) {
|
||||
prepareSetCopy(state)
|
||||
markChanged(state)
|
||||
state.copy_!.clear()
|
||||
}
|
||||
}
|
||||
|
||||
p.values = function(): IterableIterator<any> {
|
||||
const state: SetState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
prepareSetCopy(state)
|
||||
return state.copy_!.values()
|
||||
}
|
||||
|
||||
p.entries = function entries(): IterableIterator<[any, any]> {
|
||||
const state: SetState = this[DRAFT_STATE]
|
||||
assertUnrevoked(state)
|
||||
prepareSetCopy(state)
|
||||
return state.copy_!.entries()
|
||||
}
|
||||
|
||||
p.keys = function(): IterableIterator<any> {
|
||||
return this.values()
|
||||
}
|
||||
|
||||
p[iteratorSymbol] = function() {
|
||||
return this.values()
|
||||
}
|
||||
|
||||
p.forEach = function forEach(cb: any, thisArg?: any) {
|
||||
const iterator = this.values()
|
||||
let result = iterator.next()
|
||||
while (!result.done) {
|
||||
cb.call(thisArg, result.value, result.value, this)
|
||||
result = iterator.next()
|
||||
}
|
||||
}
|
||||
|
||||
return DraftSet
|
||||
})(Set)
|
||||
|
||||
function proxySet_<T extends AnySet>(target: T, parent?: ImmerState): T {
|
||||
// @ts-ignore
|
||||
return new DraftSet(target, parent)
|
||||
}
|
||||
|
||||
function prepareSetCopy(state: SetState) {
|
||||
if (!state.copy_) {
|
||||
// create drafts for all entries to preserve insertion order
|
||||
state.copy_ = new Set()
|
||||
state.base_.forEach(value => {
|
||||
if (isDraftable(value)) {
|
||||
const draft = createProxy(state.scope_.immer_, value, state)
|
||||
state.drafts_.set(value, draft)
|
||||
state.copy_!.add(draft)
|
||||
} else {
|
||||
state.copy_!.add(value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function assertUnrevoked(state: any /*ES5State | MapState | SetState*/) {
|
||||
if (state.revoked_) die(3, JSON.stringify(latest(state)))
|
||||
}
|
||||
|
||||
loadPlugin("MapSet", {proxyMap_, proxySet_})
|
||||
}
|
||||
305
node_modules/immer/src/plugins/patches.ts
generated
vendored
Normal file
305
node_modules/immer/src/plugins/patches.ts
generated
vendored
Normal file
@@ -0,0 +1,305 @@
|
||||
import {immerable} from "../immer"
|
||||
import {
|
||||
ImmerState,
|
||||
Patch,
|
||||
SetState,
|
||||
ES5ArrayState,
|
||||
ProxyArrayState,
|
||||
MapState,
|
||||
ES5ObjectState,
|
||||
ProxyObjectState,
|
||||
PatchPath,
|
||||
get,
|
||||
each,
|
||||
has,
|
||||
getArchtype,
|
||||
isSet,
|
||||
isMap,
|
||||
loadPlugin,
|
||||
ProxyType,
|
||||
Archtype,
|
||||
die,
|
||||
isDraft,
|
||||
isDraftable,
|
||||
NOTHING
|
||||
} from "../internal"
|
||||
|
||||
export function enablePatches() {
|
||||
const REPLACE = "replace"
|
||||
const ADD = "add"
|
||||
const REMOVE = "remove"
|
||||
|
||||
function generatePatches_(
|
||||
state: ImmerState,
|
||||
basePath: PatchPath,
|
||||
patches: Patch[],
|
||||
inversePatches: Patch[]
|
||||
): void {
|
||||
switch (state.type_) {
|
||||
case ProxyType.ProxyObject:
|
||||
case ProxyType.ES5Object:
|
||||
case ProxyType.Map:
|
||||
return generatePatchesFromAssigned(
|
||||
state,
|
||||
basePath,
|
||||
patches,
|
||||
inversePatches
|
||||
)
|
||||
case ProxyType.ES5Array:
|
||||
case ProxyType.ProxyArray:
|
||||
return generateArrayPatches(state, basePath, patches, inversePatches)
|
||||
case ProxyType.Set:
|
||||
return generateSetPatches(
|
||||
(state as any) as SetState,
|
||||
basePath,
|
||||
patches,
|
||||
inversePatches
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function generateArrayPatches(
|
||||
state: ES5ArrayState | ProxyArrayState,
|
||||
basePath: PatchPath,
|
||||
patches: Patch[],
|
||||
inversePatches: Patch[]
|
||||
) {
|
||||
let {base_, assigned_} = state
|
||||
let copy_ = state.copy_!
|
||||
|
||||
// Reduce complexity by ensuring `base` is never longer.
|
||||
if (copy_.length < base_.length) {
|
||||
// @ts-ignore
|
||||
;[base_, copy_] = [copy_, base_]
|
||||
;[patches, inversePatches] = [inversePatches, patches]
|
||||
}
|
||||
|
||||
// Process replaced indices.
|
||||
for (let i = 0; i < base_.length; i++) {
|
||||
if (assigned_[i] && copy_[i] !== base_[i]) {
|
||||
const path = basePath.concat([i])
|
||||
patches.push({
|
||||
op: REPLACE,
|
||||
path,
|
||||
// Need to maybe clone it, as it can in fact be the original value
|
||||
// due to the base/copy inversion at the start of this function
|
||||
value: clonePatchValueIfNeeded(copy_[i])
|
||||
})
|
||||
inversePatches.push({
|
||||
op: REPLACE,
|
||||
path,
|
||||
value: clonePatchValueIfNeeded(base_[i])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Process added indices.
|
||||
for (let i = base_.length; i < copy_.length; i++) {
|
||||
const path = basePath.concat([i])
|
||||
patches.push({
|
||||
op: ADD,
|
||||
path,
|
||||
// Need to maybe clone it, as it can in fact be the original value
|
||||
// due to the base/copy inversion at the start of this function
|
||||
value: clonePatchValueIfNeeded(copy_[i])
|
||||
})
|
||||
}
|
||||
if (base_.length < copy_.length) {
|
||||
inversePatches.push({
|
||||
op: REPLACE,
|
||||
path: basePath.concat(["length"]),
|
||||
value: base_.length
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This is used for both Map objects and normal objects.
|
||||
function generatePatchesFromAssigned(
|
||||
state: MapState | ES5ObjectState | ProxyObjectState,
|
||||
basePath: PatchPath,
|
||||
patches: Patch[],
|
||||
inversePatches: Patch[]
|
||||
) {
|
||||
const {base_, copy_} = state
|
||||
each(state.assigned_!, (key, assignedValue) => {
|
||||
const origValue = get(base_, key)
|
||||
const value = get(copy_!, key)
|
||||
const op = !assignedValue ? REMOVE : has(base_, key) ? REPLACE : ADD
|
||||
if (origValue === value && op === REPLACE) return
|
||||
const path = basePath.concat(key as any)
|
||||
patches.push(op === REMOVE ? {op, path} : {op, path, value})
|
||||
inversePatches.push(
|
||||
op === ADD
|
||||
? {op: REMOVE, path}
|
||||
: op === REMOVE
|
||||
? {op: ADD, path, value: clonePatchValueIfNeeded(origValue)}
|
||||
: {op: REPLACE, path, value: clonePatchValueIfNeeded(origValue)}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function generateSetPatches(
|
||||
state: SetState,
|
||||
basePath: PatchPath,
|
||||
patches: Patch[],
|
||||
inversePatches: Patch[]
|
||||
) {
|
||||
let {base_, copy_} = state
|
||||
|
||||
let i = 0
|
||||
base_.forEach((value: any) => {
|
||||
if (!copy_!.has(value)) {
|
||||
const path = basePath.concat([i])
|
||||
patches.push({
|
||||
op: REMOVE,
|
||||
path,
|
||||
value
|
||||
})
|
||||
inversePatches.unshift({
|
||||
op: ADD,
|
||||
path,
|
||||
value
|
||||
})
|
||||
}
|
||||
i++
|
||||
})
|
||||
i = 0
|
||||
copy_!.forEach((value: any) => {
|
||||
if (!base_.has(value)) {
|
||||
const path = basePath.concat([i])
|
||||
patches.push({
|
||||
op: ADD,
|
||||
path,
|
||||
value
|
||||
})
|
||||
inversePatches.unshift({
|
||||
op: REMOVE,
|
||||
path,
|
||||
value
|
||||
})
|
||||
}
|
||||
i++
|
||||
})
|
||||
}
|
||||
|
||||
function generateReplacementPatches_(
|
||||
baseValue: any,
|
||||
replacement: any,
|
||||
patches: Patch[],
|
||||
inversePatches: Patch[]
|
||||
): void {
|
||||
patches.push({
|
||||
op: REPLACE,
|
||||
path: [],
|
||||
value: replacement === NOTHING ? undefined : replacement
|
||||
})
|
||||
inversePatches.push({
|
||||
op: REPLACE,
|
||||
path: [],
|
||||
value: baseValue
|
||||
})
|
||||
}
|
||||
|
||||
function applyPatches_<T>(draft: T, patches: Patch[]): T {
|
||||
patches.forEach(patch => {
|
||||
const {path, op} = patch
|
||||
|
||||
let base: any = draft
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const parentType = getArchtype(base)
|
||||
let p = path[i]
|
||||
if (typeof p !== "string" && typeof p !== "number") {
|
||||
p = "" + p
|
||||
}
|
||||
|
||||
// See #738, avoid prototype pollution
|
||||
if (
|
||||
(parentType === Archtype.Object || parentType === Archtype.Array) &&
|
||||
(p === "__proto__" || p === "constructor")
|
||||
)
|
||||
die(24)
|
||||
if (typeof base === "function" && p === "prototype") die(24)
|
||||
base = get(base, p)
|
||||
if (typeof base !== "object") die(15, path.join("/"))
|
||||
}
|
||||
|
||||
const type = getArchtype(base)
|
||||
const value = deepClonePatchValue(patch.value) // used to clone patch to ensure original patch is not modified, see #411
|
||||
const key = path[path.length - 1]
|
||||
switch (op) {
|
||||
case REPLACE:
|
||||
switch (type) {
|
||||
case Archtype.Map:
|
||||
return base.set(key, value)
|
||||
/* istanbul ignore next */
|
||||
case Archtype.Set:
|
||||
die(16)
|
||||
default:
|
||||
// if value is an object, then it's assigned by reference
|
||||
// in the following add or remove ops, the value field inside the patch will also be modifyed
|
||||
// so we use value from the cloned patch
|
||||
// @ts-ignore
|
||||
return (base[key] = value)
|
||||
}
|
||||
case ADD:
|
||||
switch (type) {
|
||||
case Archtype.Array:
|
||||
return key === "-"
|
||||
? base.push(value)
|
||||
: base.splice(key as any, 0, value)
|
||||
case Archtype.Map:
|
||||
return base.set(key, value)
|
||||
case Archtype.Set:
|
||||
return base.add(value)
|
||||
default:
|
||||
return (base[key] = value)
|
||||
}
|
||||
case REMOVE:
|
||||
switch (type) {
|
||||
case Archtype.Array:
|
||||
return base.splice(key as any, 1)
|
||||
case Archtype.Map:
|
||||
return base.delete(key)
|
||||
case Archtype.Set:
|
||||
return base.delete(patch.value)
|
||||
default:
|
||||
return delete base[key]
|
||||
}
|
||||
default:
|
||||
die(17, op)
|
||||
}
|
||||
})
|
||||
|
||||
return draft
|
||||
}
|
||||
|
||||
// optimize: this is quite a performance hit, can we detect intelligently when it is needed?
|
||||
// E.g. auto-draft when new objects from outside are assigned and modified?
|
||||
// (See failing test when deepClone just returns obj)
|
||||
function deepClonePatchValue<T>(obj: T): T
|
||||
function deepClonePatchValue(obj: any) {
|
||||
if (!isDraftable(obj)) return obj
|
||||
if (Array.isArray(obj)) return obj.map(deepClonePatchValue)
|
||||
if (isMap(obj))
|
||||
return new Map(
|
||||
Array.from(obj.entries()).map(([k, v]) => [k, deepClonePatchValue(v)])
|
||||
)
|
||||
if (isSet(obj)) return new Set(Array.from(obj).map(deepClonePatchValue))
|
||||
const cloned = Object.create(Object.getPrototypeOf(obj))
|
||||
for (const key in obj) cloned[key] = deepClonePatchValue(obj[key])
|
||||
if (has(obj, immerable)) cloned[immerable] = obj[immerable]
|
||||
return cloned
|
||||
}
|
||||
|
||||
function clonePatchValueIfNeeded<T>(obj: T): T {
|
||||
if (isDraft(obj)) {
|
||||
return deepClonePatchValue(obj)
|
||||
} else return obj
|
||||
}
|
||||
|
||||
loadPlugin("Patches", {
|
||||
applyPatches_,
|
||||
generatePatches_,
|
||||
generateReplacementPatches_
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user