mutative
Version:
A JavaScript library for efficient immutable updates
1,417 lines (1,400 loc) • 63.6 kB
JavaScript
const Operation = {
Remove: 'remove',
Replace: 'replace',
Add: 'add',
};
// Don't use `Symbol()` just for 3rd party access the draft
const PROXY_DRAFT = Symbol.for('__MUTATIVE_PROXY_DRAFT__');
const RAW_RETURN_SYMBOL = Symbol('__MUTATIVE_RAW_RETURN_SYMBOL__');
const iteratorSymbol = Symbol.iterator;
const dataTypes = {
mutable: 'mutable',
immutable: 'immutable',
};
const internal = {};
function has(target, key) {
return target instanceof Map
? target.has(key)
: Object.prototype.hasOwnProperty.call(target, key);
}
function getDescriptor(target, key) {
if (key in target) {
let prototype = Reflect.getPrototypeOf(target);
while (prototype) {
const descriptor = Reflect.getOwnPropertyDescriptor(prototype, key);
if (descriptor)
return descriptor;
prototype = Reflect.getPrototypeOf(prototype);
}
}
return;
}
function isBaseSetInstance(obj) {
return Object.getPrototypeOf(obj) === Set.prototype;
}
function isBaseMapInstance(obj) {
return Object.getPrototypeOf(obj) === Map.prototype;
}
function latest(proxyDraft) {
var _a;
return (_a = proxyDraft.copy) !== null && _a !== void 0 ? _a : proxyDraft.original;
}
/**
* Check if the value is a draft
*/
function isDraft(target) {
return !!getProxyDraft(target);
}
function getProxyDraft(value) {
if (typeof value !== 'object')
return null;
return value === null || value === void 0 ? void 0 : value[PROXY_DRAFT];
}
function getValue(value) {
var _a;
const proxyDraft = getProxyDraft(value);
return proxyDraft ? ((_a = proxyDraft.copy) !== null && _a !== void 0 ? _a : proxyDraft.original) : value;
}
/**
* Check if a value is draftable
*/
function isDraftable(value, options) {
if (!value || typeof value !== 'object')
return false;
let markResult;
return (Object.getPrototypeOf(value) === Object.prototype ||
Array.isArray(value) ||
value instanceof Map ||
value instanceof Set ||
(!!(options === null || options === void 0 ? void 0 : options.mark) &&
((markResult = options.mark(value, dataTypes)) === dataTypes.immutable ||
typeof markResult === 'function')));
}
function getPath(target, path = []) {
if (Object.hasOwnProperty.call(target, 'key')) {
// check if the parent is a draft and the original value is not equal to the current value
const parentCopy = target.parent.copy;
const proxyDraft = getProxyDraft(get(parentCopy, target.key));
if (proxyDraft !== null && (proxyDraft === null || proxyDraft === void 0 ? void 0 : proxyDraft.original) !== target.original) {
return null;
}
const isSet = target.parent.type === 3 /* DraftType.Set */;
const key = isSet
? Array.from(target.parent.setMap.keys()).indexOf(target.key)
: target.key;
// check if the key is still in the next state parent
if (!((isSet && parentCopy.size > key) || has(parentCopy, key)))
return null;
path.push(key);
}
if (target.parent) {
return getPath(target.parent, path);
}
// `target` is root draft.
path.reverse();
try {
// check if the path is valid
resolvePath(target.copy, path);
}
catch (e) {
return null;
}
return path;
}
function getType(target) {
if (Array.isArray(target))
return 1 /* DraftType.Array */;
if (target instanceof Map)
return 2 /* DraftType.Map */;
if (target instanceof Set)
return 3 /* DraftType.Set */;
return 0 /* DraftType.Object */;
}
function get(target, key) {
return getType(target) === 2 /* DraftType.Map */ ? target.get(key) : target[key];
}
function set(target, key, value) {
const type = getType(target);
if (type === 2 /* DraftType.Map */) {
target.set(key, value);
}
else {
target[key] = value;
}
}
function peek(target, key) {
const state = getProxyDraft(target);
const source = state ? latest(state) : target;
return source[key];
}
function isEqual(x, y) {
if (x === y) {
return x !== 0 || 1 / x === 1 / y;
}
else {
return x !== x && y !== y;
}
}
function revokeProxy(proxyDraft) {
if (!proxyDraft)
return;
while (proxyDraft.finalities.revoke.length > 0) {
const revoke = proxyDraft.finalities.revoke.pop();
revoke();
}
}
// handle JSON Pointer path with spec https://www.rfc-editor.org/rfc/rfc6901
function escapePath(path, pathAsArray) {
return pathAsArray
? path
: ['']
.concat(path)
.map((_item) => {
const item = `${_item}`;
if (item.indexOf('/') === -1 && item.indexOf('~') === -1)
return item;
return item.replace(/~/g, '~0').replace(/\//g, '~1');
})
.join('/');
}
function unescapePath(path) {
if (Array.isArray(path))
return path;
return path
.split('/')
.map((_item) => _item.replace(/~1/g, '/').replace(/~0/g, '~'))
.slice(1);
}
function resolvePath(base, path) {
for (let index = 0; index < path.length - 1; index += 1) {
const key = path[index];
// use `index` in Set draft
base = get(getType(base) === 3 /* DraftType.Set */ ? Array.from(base) : base, key);
if (typeof base !== 'object') {
throw new Error(`Cannot resolve patch at '${path.join('/')}'.`);
}
}
return base;
}
function strictCopy(target) {
const copy = Object.create(Object.getPrototypeOf(target));
Reflect.ownKeys(target).forEach((key) => {
let desc = Reflect.getOwnPropertyDescriptor(target, key);
if (desc.enumerable && desc.configurable && desc.writable) {
copy[key] = target[key];
return;
}
// for freeze
if (!desc.writable) {
desc.writable = true;
desc.configurable = true;
}
if (desc.get || desc.set)
desc = {
configurable: true,
writable: true,
enumerable: desc.enumerable,
value: target[key],
};
Reflect.defineProperty(copy, key, desc);
});
return copy;
}
const propIsEnum = Object.prototype.propertyIsEnumerable;
function shallowCopy(original, options) {
let markResult;
if (Array.isArray(original)) {
return Array.prototype.concat.call(original);
}
else if (original instanceof Set) {
if (!isBaseSetInstance(original)) {
const SubClass = Object.getPrototypeOf(original).constructor;
return new SubClass(original.values());
}
return Set.prototype.difference
? Set.prototype.difference.call(original, new Set())
: new Set(original.values());
}
else if (original instanceof Map) {
if (!isBaseMapInstance(original)) {
const SubClass = Object.getPrototypeOf(original).constructor;
return new SubClass(original);
}
return new Map(original);
}
else if ((options === null || options === void 0 ? void 0 : options.mark) &&
((markResult = options.mark(original, dataTypes)),
markResult !== undefined) &&
markResult !== dataTypes.mutable) {
if (markResult === dataTypes.immutable) {
return strictCopy(original);
}
else if (typeof markResult === 'function') {
if ((options.enablePatches || options.enableAutoFreeze)) {
throw new Error(`You can't use mark and patches or auto freeze together.`);
}
return markResult();
}
throw new Error(`Unsupported mark result: ${markResult}`);
}
else if (typeof original === 'object' &&
Object.getPrototypeOf(original) === Object.prototype) {
// For best performance with shallow copies,
// don't use `Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));` by default.
const copy = {};
Object.keys(original).forEach((key) => {
copy[key] = original[key];
});
Object.getOwnPropertySymbols(original).forEach((key) => {
if (propIsEnum.call(original, key)) {
copy[key] = original[key];
}
});
return copy;
}
else {
throw new Error(`Please check mark() to ensure that it is a stable marker draftable function.`);
}
}
function ensureShallowCopy(target) {
if (target.copy)
return;
target.copy = shallowCopy(target.original, target.options);
}
function deepClone(target) {
if (!isDraftable(target))
return getValue(target);
if (Array.isArray(target))
return target.map(deepClone);
if (target instanceof Map) {
const iterable = Array.from(target.entries()).map(([k, v]) => [
k,
deepClone(v),
]);
if (!isBaseMapInstance(target)) {
const SubClass = Object.getPrototypeOf(target).constructor;
return new SubClass(iterable);
}
return new Map(iterable);
}
if (target instanceof Set) {
const iterable = Array.from(target).map(deepClone);
if (!isBaseSetInstance(target)) {
const SubClass = Object.getPrototypeOf(target).constructor;
return new SubClass(iterable);
}
return new Set(iterable);
}
const copy = Object.create(Object.getPrototypeOf(target));
for (const key in target)
copy[key] = deepClone(target[key]);
return copy;
}
function cloneIfNeeded(target) {
return isDraft(target) ? deepClone(target) : target;
}
function markChanged(proxyDraft) {
var _a;
proxyDraft.assignedMap = (_a = proxyDraft.assignedMap) !== null && _a !== void 0 ? _a : new Map();
if (!proxyDraft.operated) {
proxyDraft.operated = true;
if (proxyDraft.parent) {
markChanged(proxyDraft.parent);
}
}
}
function throwFrozenError() {
throw new Error('Cannot modify frozen object');
}
function deepFreeze(target, subKey, updatedValues, stack, keys) {
{
updatedValues = updatedValues !== null && updatedValues !== void 0 ? updatedValues : new WeakMap();
stack = stack !== null && stack !== void 0 ? stack : [];
keys = keys !== null && keys !== void 0 ? keys : [];
const value = updatedValues.has(target)
? updatedValues.get(target)
: target;
if (stack.length > 0) {
const index = stack.indexOf(value);
if (value && typeof value === 'object' && index !== -1) {
if (stack[0] === value) {
throw new Error(`Forbids circular reference`);
}
throw new Error(`Forbids circular reference: ~/${keys
.slice(0, index)
.map((key, index) => {
if (typeof key === 'symbol')
return `[${key.toString()}]`;
const parent = stack[index];
if (typeof key === 'object' &&
(parent instanceof Map || parent instanceof Set))
return Array.from(parent.keys()).indexOf(key);
return key;
})
.join('/')}`);
}
stack.push(value);
keys.push(subKey);
}
else {
stack.push(value);
}
}
if (Object.isFrozen(target) || isDraft(target)) {
{
stack.pop();
keys.pop();
}
return;
}
const type = getType(target);
switch (type) {
case 2 /* DraftType.Map */:
for (const [key, value] of target) {
deepFreeze(key, key, updatedValues, stack, keys);
deepFreeze(value, key, updatedValues, stack, keys);
}
target.set = target.clear = target.delete = throwFrozenError;
break;
case 3 /* DraftType.Set */:
for (const value of target) {
deepFreeze(value, value, updatedValues, stack, keys);
}
target.add = target.clear = target.delete = throwFrozenError;
break;
case 1 /* DraftType.Array */:
Object.freeze(target);
let index = 0;
for (const value of target) {
deepFreeze(value, index, updatedValues, stack, keys);
index += 1;
}
break;
default:
Object.freeze(target);
// ignore non-enumerable or symbol properties
Object.keys(target).forEach((name) => {
const value = target[name];
deepFreeze(value, name, updatedValues, stack, keys);
});
}
{
stack.pop();
keys.pop();
}
}
function forEach(target, iter) {
const type = getType(target);
if (type === 0 /* DraftType.Object */) {
Reflect.ownKeys(target).forEach((key) => {
iter(key, target[key], target);
});
}
else if (type === 1 /* DraftType.Array */) {
let index = 0;
for (const entry of target) {
iter(index, entry, target);
index += 1;
}
}
else {
target.forEach((entry, index) => iter(index, entry, target));
}
}
function handleValue(target, handledSet, options) {
if (isDraft(target) ||
!isDraftable(target, options) ||
handledSet.has(target) ||
Object.isFrozen(target))
return;
const isSet = target instanceof Set;
const setMap = isSet ? new Map() : undefined;
handledSet.add(target);
forEach(target, (key, value) => {
var _a;
if (isDraft(value)) {
const proxyDraft = getProxyDraft(value);
ensureShallowCopy(proxyDraft);
// A draft where a child node has been changed, or assigned a value
const updatedValue = ((_a = proxyDraft.assignedMap) === null || _a === void 0 ? void 0 : _a.size) || proxyDraft.operated
? proxyDraft.copy
: proxyDraft.original;
// final update value
set(isSet ? setMap : target, key, updatedValue);
}
else {
handleValue(value, handledSet, options);
}
});
if (setMap) {
const set = target;
const values = Array.from(set);
set.clear();
values.forEach((value) => {
set.add(setMap.has(value) ? setMap.get(value) : value);
});
}
}
function finalizeAssigned(proxyDraft, key) {
// handle the draftable assigned values, and the value is not a draft
const copy = proxyDraft.type === 3 /* DraftType.Set */ ? proxyDraft.setMap : proxyDraft.copy;
if (proxyDraft.finalities.revoke.length > 1 &&
proxyDraft.assignedMap.get(key) &&
copy) {
handleValue(get(copy, key), proxyDraft.finalities.handledSet, proxyDraft.options);
}
}
function finalizeSetValue(target) {
if (target.type === 3 /* DraftType.Set */ && target.copy) {
target.copy.clear();
target.setMap.forEach((value) => {
target.copy.add(getValue(value));
});
}
}
function finalizePatches(target, generatePatches, patches, inversePatches) {
const shouldFinalize = target.operated &&
target.assignedMap &&
target.assignedMap.size > 0 &&
!target.finalized;
if (shouldFinalize) {
if (patches && inversePatches) {
const basePath = getPath(target);
if (basePath) {
generatePatches(target, basePath, patches, inversePatches);
}
}
target.finalized = true;
}
}
function markFinalization(target, key, value, generatePatches) {
const proxyDraft = getProxyDraft(value);
if (proxyDraft) {
// !case: assign the draft value
if (!proxyDraft.callbacks) {
proxyDraft.callbacks = [];
}
proxyDraft.callbacks.push((patches, inversePatches) => {
var _a;
const copy = target.type === 3 /* DraftType.Set */ ? target.setMap : target.copy;
if (isEqual(get(copy, key), value)) {
let updatedValue = proxyDraft.original;
if (proxyDraft.copy) {
updatedValue = proxyDraft.copy;
}
finalizeSetValue(target);
finalizePatches(target, generatePatches, patches, inversePatches);
if (target.options.enableAutoFreeze) {
target.options.updatedValues =
(_a = target.options.updatedValues) !== null && _a !== void 0 ? _a : new WeakMap();
target.options.updatedValues.set(updatedValue, proxyDraft.original);
}
// final update value
set(copy, key, updatedValue);
}
});
if (target.options.enableAutoFreeze) {
// !case: assign the draft value in cross draft tree
if (proxyDraft.finalities !== target.finalities) {
target.options.enableAutoFreeze = false;
}
}
}
if (isDraftable(value, target.options)) {
// !case: assign the non-draft value
target.finalities.draft.push(() => {
const copy = target.type === 3 /* DraftType.Set */ ? target.setMap : target.copy;
if (isEqual(get(copy, key), value)) {
finalizeAssigned(target, key);
}
});
}
}
function generateArrayPatches(proxyState, basePath, patches, inversePatches, pathAsArray) {
let { original, assignedMap, options } = proxyState;
let copy = proxyState.copy;
if (copy.length < original.length) {
[original, copy] = [copy, original];
[patches, inversePatches] = [inversePatches, patches];
}
for (let index = 0; index < original.length; index += 1) {
if (assignedMap.get(index.toString()) && copy[index] !== original[index]) {
const _path = basePath.concat([index]);
const path = escapePath(_path, pathAsArray);
patches.push({
op: Operation.Replace,
path,
// If it is a draft, it needs to be deep cloned, and it may also be non-draft.
value: cloneIfNeeded(copy[index]),
});
inversePatches.push({
op: Operation.Replace,
path,
// If it is a draft, it needs to be deep cloned, and it may also be non-draft.
value: cloneIfNeeded(original[index]),
});
}
}
for (let index = original.length; index < copy.length; index += 1) {
const _path = basePath.concat([index]);
const path = escapePath(_path, pathAsArray);
patches.push({
op: Operation.Add,
path,
// If it is a draft, it needs to be deep cloned, and it may also be non-draft.
value: cloneIfNeeded(copy[index]),
});
}
if (original.length < copy.length) {
// https://www.rfc-editor.org/rfc/rfc6902#appendix-A.4
// For performance, here we only generate an operation that replaces the length of the array,
// which is inconsistent with JSON Patch specification
const { arrayLengthAssignment = true } = options.enablePatches;
if (arrayLengthAssignment) {
const _path = basePath.concat(['length']);
const path = escapePath(_path, pathAsArray);
inversePatches.push({
op: Operation.Replace,
path,
value: original.length,
});
}
else {
for (let index = copy.length; original.length < index; index -= 1) {
const _path = basePath.concat([index - 1]);
const path = escapePath(_path, pathAsArray);
inversePatches.push({
op: Operation.Remove,
path,
});
}
}
}
}
function generatePatchesFromAssigned({ original, copy, assignedMap }, basePath, patches, inversePatches, pathAsArray) {
assignedMap.forEach((assignedValue, key) => {
const originalValue = get(original, key);
const value = cloneIfNeeded(get(copy, key));
const op = !assignedValue
? Operation.Remove
: has(original, key)
? Operation.Replace
: Operation.Add;
if (isEqual(originalValue, value) && op === Operation.Replace)
return;
const _path = basePath.concat(key);
const path = escapePath(_path, pathAsArray);
patches.push(op === Operation.Remove ? { op, path } : { op, path, value });
inversePatches.push(op === Operation.Add
? { op: Operation.Remove, path }
: op === Operation.Remove
? { op: Operation.Add, path, value: originalValue }
: { op: Operation.Replace, path, value: originalValue });
});
}
function generateSetPatches({ original, copy }, basePath, patches, inversePatches, pathAsArray) {
let index = 0;
original.forEach((value) => {
if (!copy.has(value)) {
const _path = basePath.concat([index]);
const path = escapePath(_path, pathAsArray);
patches.push({
op: Operation.Remove,
path,
value,
});
inversePatches.unshift({
op: Operation.Add,
path,
value,
});
}
index += 1;
});
index = 0;
copy.forEach((value) => {
if (!original.has(value)) {
const _path = basePath.concat([index]);
const path = escapePath(_path, pathAsArray);
patches.push({
op: Operation.Add,
path,
value,
});
inversePatches.unshift({
op: Operation.Remove,
path,
value,
});
}
index += 1;
});
}
function generatePatches(proxyState, basePath, patches, inversePatches) {
const { pathAsArray = true } = proxyState.options.enablePatches;
switch (proxyState.type) {
case 0 /* DraftType.Object */:
case 2 /* DraftType.Map */:
return generatePatchesFromAssigned(proxyState, basePath, patches, inversePatches, pathAsArray);
case 1 /* DraftType.Array */:
return generateArrayPatches(proxyState, basePath, patches, inversePatches, pathAsArray);
case 3 /* DraftType.Set */:
return generateSetPatches(proxyState, basePath, patches, inversePatches, pathAsArray);
}
}
let readable = false;
const checkReadable = (value, options, ignoreCheckDraftable = false) => {
if (typeof value === 'object' &&
value !== null &&
(!isDraftable(value, options) || ignoreCheckDraftable) &&
!readable) {
throw new Error(`Strict mode: Mutable data cannot be accessed directly, please use 'unsafe(callback)' wrap.`);
}
};
/**
* `unsafe(callback)` to access mutable data directly in strict mode.
*
* ## Example
*
* ```ts
* import { create, unsafe } from '../index';
*
* class Foobar {
* bar = 1;
* }
*
* const baseState = { foobar: new Foobar() };
* const state = create(
* baseState,
* (draft) => {
* unsafe(() => {
* draft.foobar.bar = 2;
* });
* },
* {
* strict: true,
* }
* );
*
* expect(state).toBe(baseState);
* expect(state.foobar).toBe(baseState.foobar);
* expect(state.foobar.bar).toBe(2);
* ```
*/
function unsafe(callback) {
readable = true;
let result;
try {
result = callback();
}
finally {
readable = false;
}
return result;
}
const mapHandler = {
get size() {
const current = latest(getProxyDraft(this));
return current.size;
},
has(key) {
return latest(getProxyDraft(this)).has(key);
},
set(key, value) {
const target = getProxyDraft(this);
const source = latest(target);
if (!source.has(key) || !isEqual(source.get(key), value)) {
ensureShallowCopy(target);
markChanged(target);
target.assignedMap.set(key, true);
target.copy.set(key, value);
markFinalization(target, key, value, generatePatches);
}
return this;
},
delete(key) {
if (!this.has(key)) {
return false;
}
const target = getProxyDraft(this);
ensureShallowCopy(target);
markChanged(target);
if (target.original.has(key)) {
target.assignedMap.set(key, false);
}
else {
target.assignedMap.delete(key);
}
target.copy.delete(key);
return true;
},
clear() {
const target = getProxyDraft(this);
if (!this.size)
return;
ensureShallowCopy(target);
markChanged(target);
target.assignedMap = new Map();
for (const [key] of target.original) {
target.assignedMap.set(key, false);
}
target.copy.clear();
},
forEach(callback, thisArg) {
const target = getProxyDraft(this);
latest(target).forEach((_value, _key) => {
callback.call(thisArg, this.get(_key), _key, this);
});
},
get(key) {
var _a, _b;
const target = getProxyDraft(this);
const value = latest(target).get(key);
const mutable = ((_b = (_a = target.options).mark) === null || _b === void 0 ? void 0 : _b.call(_a, value, dataTypes)) === dataTypes.mutable;
if (target.options.strict) {
checkReadable(value, target.options, mutable);
}
if (mutable) {
return value;
}
if (target.finalized || !isDraftable(value, target.options)) {
return value;
}
// drafted or reassigned
if (value !== target.original.get(key)) {
return value;
}
const draft = internal.createDraft({
original: value,
parentDraft: target,
key,
finalities: target.finalities,
options: target.options,
});
ensureShallowCopy(target);
target.copy.set(key, draft);
return draft;
},
keys() {
return latest(getProxyDraft(this)).keys();
},
values() {
const iterator = this.keys();
return {
[iteratorSymbol]: () => this.values(),
next: () => {
const result = iterator.next();
if (result.done)
return result;
const value = this.get(result.value);
return {
done: false,
value,
};
},
};
},
entries() {
const iterator = this.keys();
return {
[iteratorSymbol]: () => this.entries(),
next: () => {
const result = iterator.next();
if (result.done)
return result;
const value = this.get(result.value);
return {
done: false,
value: [result.value, value],
};
},
};
},
[iteratorSymbol]() {
return this.entries();
},
};
const mapHandlerKeys = Reflect.ownKeys(mapHandler);
const getNextIterator = (target, iterator, { isValuesIterator }) => () => {
var _a, _b;
const result = iterator.next();
if (result.done)
return result;
const key = result.value;
let value = target.setMap.get(key);
const currentDraft = getProxyDraft(value);
const mutable = ((_b = (_a = target.options).mark) === null || _b === void 0 ? void 0 : _b.call(_a, value, dataTypes)) === dataTypes.mutable;
if (target.options.strict) {
checkReadable(key, target.options, mutable);
}
if (!mutable &&
!currentDraft &&
isDraftable(key, target.options) &&
!target.finalized &&
target.original.has(key)) {
// draft a draftable original set item
const proxy = internal.createDraft({
original: key,
parentDraft: target,
key,
finalities: target.finalities,
options: target.options,
});
target.setMap.set(key, proxy);
value = proxy;
}
else if (currentDraft) {
// drafted
value = currentDraft.proxy;
}
return {
done: false,
value: isValuesIterator ? value : [value, value],
};
};
const setHandler = {
get size() {
const target = getProxyDraft(this);
return target.setMap.size;
},
has(value) {
const target = getProxyDraft(this);
// reassigned or non-draftable values
if (target.setMap.has(value))
return true;
ensureShallowCopy(target);
const valueProxyDraft = getProxyDraft(value);
// drafted
if (valueProxyDraft && target.setMap.has(valueProxyDraft.original))
return true;
return false;
},
add(value) {
const target = getProxyDraft(this);
if (!this.has(value)) {
ensureShallowCopy(target);
markChanged(target);
target.assignedMap.set(value, true);
target.setMap.set(value, value);
markFinalization(target, value, value, generatePatches);
}
return this;
},
delete(value) {
if (!this.has(value)) {
return false;
}
const target = getProxyDraft(this);
ensureShallowCopy(target);
markChanged(target);
const valueProxyDraft = getProxyDraft(value);
if (valueProxyDraft && target.setMap.has(valueProxyDraft.original)) {
// delete drafted
target.assignedMap.set(valueProxyDraft.original, false);
return target.setMap.delete(valueProxyDraft.original);
}
if (!valueProxyDraft && target.setMap.has(value)) {
// non-draftable values
target.assignedMap.set(value, false);
}
else {
// reassigned
target.assignedMap.delete(value);
}
// delete reassigned or non-draftable values
return target.setMap.delete(value);
},
clear() {
if (!this.size)
return;
const target = getProxyDraft(this);
ensureShallowCopy(target);
markChanged(target);
for (const value of target.original) {
target.assignedMap.set(value, false);
}
target.setMap.clear();
},
values() {
const target = getProxyDraft(this);
ensureShallowCopy(target);
const iterator = target.setMap.keys();
return {
[Symbol.iterator]: () => this.values(),
next: getNextIterator(target, iterator, { isValuesIterator: true }),
};
},
entries() {
const target = getProxyDraft(this);
ensureShallowCopy(target);
const iterator = target.setMap.keys();
return {
[Symbol.iterator]: () => this.entries(),
next: getNextIterator(target, iterator, {
isValuesIterator: false,
}),
};
},
keys() {
return this.values();
},
[iteratorSymbol]() {
return this.values();
},
forEach(callback, thisArg) {
const iterator = this.values();
let result = iterator.next();
while (!result.done) {
callback.call(thisArg, result.value, result.value, this);
result = iterator.next();
}
},
};
if (Set.prototype.difference) {
// for compatibility with new Set methods
// https://github.com/tc39/proposal-set-methods
// And `https://github.com/tc39/proposal-set-methods/blob/main/details.md#symbolspecies` has some details about the `@@species` symbol.
// So we can't use SubSet instance constructor to get the constructor of the SubSet instance.
Object.assign(setHandler, {
intersection(other) {
return Set.prototype.intersection.call(new Set(this.values()), other);
},
union(other) {
return Set.prototype.union.call(new Set(this.values()), other);
},
difference(other) {
return Set.prototype.difference.call(new Set(this.values()), other);
},
symmetricDifference(other) {
return Set.prototype.symmetricDifference.call(new Set(this.values()), other);
},
isSubsetOf(other) {
return Set.prototype.isSubsetOf.call(new Set(this.values()), other);
},
isSupersetOf(other) {
return Set.prototype.isSupersetOf.call(new Set(this.values()), other);
},
isDisjointFrom(other) {
return Set.prototype.isDisjointFrom.call(new Set(this.values()), other);
},
});
}
const setHandlerKeys = Reflect.ownKeys(setHandler);
const proxyHandler = {
get(target, key, receiver) {
var _a, _b;
const copy = (_a = target.copy) === null || _a === void 0 ? void 0 : _a[key];
// Improve draft reading performance by caching the draft copy.
if (copy && target.finalities.draftsCache.has(copy)) {
return copy;
}
if (key === PROXY_DRAFT)
return target;
let markResult;
if (target.options.mark) {
// handle `Uncaught TypeError: Method get Map.prototype.size called on incompatible receiver #<Map>`
// or `Uncaught TypeError: Method get Set.prototype.size called on incompatible receiver #<Set>`
const value = key === 'size' &&
(target.original instanceof Map || target.original instanceof Set)
? Reflect.get(target.original, key)
: Reflect.get(target.original, key, receiver);
markResult = target.options.mark(value, dataTypes);
if (markResult === dataTypes.mutable) {
if (target.options.strict) {
checkReadable(value, target.options, true);
}
return value;
}
}
const source = latest(target);
if (source instanceof Map && mapHandlerKeys.includes(key)) {
if (key === 'size') {
return Object.getOwnPropertyDescriptor(mapHandler, 'size').get.call(target.proxy);
}
const handle = mapHandler[key];
return handle.bind(target.proxy);
}
if (source instanceof Set && setHandlerKeys.includes(key)) {
if (key === 'size') {
return Object.getOwnPropertyDescriptor(setHandler, 'size').get.call(target.proxy);
}
const handle = setHandler[key];
return handle.bind(target.proxy);
}
if (!has(source, key)) {
const desc = getDescriptor(source, key);
return desc
? `value` in desc
? desc.value
: // !case: support for getter
(_b = desc.get) === null || _b === void 0 ? void 0 : _b.call(target.proxy)
: undefined;
}
const value = source[key];
if (target.options.strict) {
checkReadable(value, target.options);
}
if (target.finalized || !isDraftable(value, target.options)) {
return value;
}
// Ensure that the assigned values are not drafted
if (value === peek(target.original, key)) {
ensureShallowCopy(target);
target.copy[key] = createDraft({
original: target.original[key],
parentDraft: target,
key: target.type === 1 /* DraftType.Array */ ? Number(key) : key,
finalities: target.finalities,
options: target.options,
});
// !case: support for custom shallow copy function
if (typeof markResult === 'function') {
const subProxyDraft = getProxyDraft(target.copy[key]);
ensureShallowCopy(subProxyDraft);
// Trigger a custom shallow copy to update to a new copy
markChanged(subProxyDraft);
return subProxyDraft.copy;
}
return target.copy[key];
}
if (isDraft(value)) {
target.finalities.draftsCache.add(value);
}
return value;
},
set(target, key, value) {
var _a;
if (target.type === 3 /* DraftType.Set */ || target.type === 2 /* DraftType.Map */) {
throw new Error(`Map/Set draft does not support any property assignment.`);
}
let _key;
if (target.type === 1 /* DraftType.Array */ &&
key !== 'length' &&
!(Number.isInteger((_key = Number(key))) &&
_key >= 0 &&
(key === 0 || _key === 0 || String(_key) === String(key)))) {
throw new Error(`Only supports setting array indices and the 'length' property.`);
}
const desc = getDescriptor(latest(target), key);
if (desc === null || desc === void 0 ? void 0 : desc.set) {
// !case: cover the case of setter
desc.set.call(target.proxy, value);
return true;
}
const current = peek(latest(target), key);
const currentProxyDraft = getProxyDraft(current);
if (currentProxyDraft && isEqual(currentProxyDraft.original, value)) {
// !case: ignore the case of assigning the original draftable value to a draft
target.copy[key] = value;
target.assignedMap = (_a = target.assignedMap) !== null && _a !== void 0 ? _a : new Map();
target.assignedMap.set(key, false);
return true;
}
// !case: handle new props with value 'undefined'
if (isEqual(value, current) &&
(value !== undefined || has(target.original, key)))
return true;
ensureShallowCopy(target);
markChanged(target);
if (has(target.original, key) && isEqual(value, target.original[key])) {
// !case: handle the case of assigning the original non-draftable value to a draft
target.assignedMap.delete(key);
}
else {
target.assignedMap.set(key, true);
}
target.copy[key] = value;
markFinalization(target, key, value, generatePatches);
return true;
},
has(target, key) {
return key in latest(target);
},
ownKeys(target) {
return Reflect.ownKeys(latest(target));
},
getOwnPropertyDescriptor(target, key) {
const source = latest(target);
const descriptor = Reflect.getOwnPropertyDescriptor(source, key);
if (!descriptor)
return descriptor;
return {
writable: true,
configurable: target.type !== 1 /* DraftType.Array */ || key !== 'length',
enumerable: descriptor.enumerable,
value: source[key],
};
},
getPrototypeOf(target) {
return Reflect.getPrototypeOf(target.original);
},
setPrototypeOf() {
throw new Error(`Cannot call 'setPrototypeOf()' on drafts`);
},
defineProperty() {
throw new Error(`Cannot call 'defineProperty()' on drafts`);
},
deleteProperty(target, key) {
var _a;
if (target.type === 1 /* DraftType.Array */) {
return proxyHandler.set.call(this, target, key, undefined, target.proxy);
}
if (peek(target.original, key) !== undefined || key in target.original) {
// !case: delete an existing key
ensureShallowCopy(target);
markChanged(target);
target.assignedMap.set(key, false);
}
else {
target.assignedMap = (_a = target.assignedMap) !== null && _a !== void 0 ? _a : new Map();
// The original non-existent key has been deleted
target.assignedMap.delete(key);
}
if (target.copy)
delete target.copy[key];
return true;
},
};
function createDraft(createDraftOptions) {
const { original, parentDraft, key, finalities, options } = createDraftOptions;
const type = getType(original);
const proxyDraft = {
type,
finalized: false,
parent: parentDraft,
original,
copy: null,
proxy: null,
finalities,
options,
// Mapping of draft Set items to their corresponding draft values.
setMap: type === 3 /* DraftType.Set */
? new Map(original.entries())
: undefined,
};
// !case: undefined as a draft map key
if (key || 'key' in createDraftOptions) {
proxyDraft.key = key;
}
const { proxy, revoke } = Proxy.revocable(type === 1 /* DraftType.Array */ ? Object.assign([], proxyDraft) : proxyDraft, proxyHandler);
finalities.revoke.push(revoke);
proxyDraft.proxy = proxy;
if (parentDraft) {
const target = parentDraft;
target.finalities.draft.push((patches, inversePatches) => {
var _a, _b;
const oldProxyDraft = getProxyDraft(proxy);
// if target is a Set draft, `setMap` is the real Set copies proxy mapping.
let copy = target.type === 3 /* DraftType.Set */ ? target.setMap : target.copy;
const draft = get(copy, key);
const proxyDraft = getProxyDraft(draft);
if (proxyDraft) {
// assign the updated value to the copy object
let updatedValue = proxyDraft.original;
if (proxyDraft.operated) {
updatedValue = getValue(draft);
}
finalizeSetValue(proxyDraft);
finalizePatches(proxyDraft, generatePatches, patches, inversePatches);
if (target.options.enableAutoFreeze) {
target.options.updatedValues =
(_a = target.options.updatedValues) !== null && _a !== void 0 ? _a : new WeakMap();
target.options.updatedValues.set(updatedValue, proxyDraft.original);
}
// final update value
set(copy, key, updatedValue);
}
// !case: handle the deleted key
(_b = oldProxyDraft.callbacks) === null || _b === void 0 ? void 0 : _b.forEach((callback) => {
callback(patches, inversePatches);
});
});
}
else {
// !case: handle the root draft
const target = getProxyDraft(proxy);
target.finalities.draft.push((patches, inversePatches) => {
finalizeSetValue(target);
finalizePatches(target, generatePatches, patches, inversePatches);
});
}
return proxy;
}
internal.createDraft = createDraft;
function finalizeDraft(result, returnedValue, patches, inversePatches, enableAutoFreeze) {
var _a;
const proxyDraft = getProxyDraft(result);
const original = (_a = proxyDraft === null || proxyDraft === void 0 ? void 0 : proxyDraft.original) !== null && _a !== void 0 ? _a : result;
const hasReturnedValue = !!returnedValue.length;
if (proxyDraft === null || proxyDraft === void 0 ? void 0 : proxyDraft.operated) {
while (proxyDraft.finalities.draft.length > 0) {
const finalize = proxyDraft.finalities.draft.pop();
finalize(patches, inversePatches);
}
}
const state = hasReturnedValue
? returnedValue[0]
: proxyDraft
? proxyDraft.operated
? proxyDraft.copy
: proxyDraft.original
: result;
if (proxyDraft)
revokeProxy(proxyDraft);
if (enableAutoFreeze) {
deepFreeze(state, state, proxyDraft === null || proxyDraft === void 0 ? void 0 : proxyDraft.options.updatedValues);
}
return [
state,
patches && hasReturnedValue
? [{ op: Operation.Replace, path: [], value: returnedValue[0] }]
: patches,
inversePatches && hasReturnedValue
? [{ op: Operation.Replace, path: [], value: original }]
: inversePatches,
];
}
function draftify(baseState, options) {
var _a;
const finalities = {
draft: [],
revoke: [],
handledSet: new WeakSet(),
draftsCache: new WeakSet(),
};
let patches;
let inversePatches;
if (options.enablePatches) {
patches = [];
inversePatches = [];
}
const isMutable = ((_a = options.mark) === null || _a === void 0 ? void 0 : _a.call(options, baseState, dataTypes)) === dataTypes.mutable ||
!isDraftable(baseState, options);
const draft = isMutable
? baseState
: createDraft({
original: baseState,
parentDraft: null,
finalities,
options,
});
return [
draft,
(returnedValue = []) => {
const [finalizedState, finalizedPatches, finalizedInversePatches] = finalizeDraft(draft, returnedValue, patches, inversePatches, options.enableAutoFreeze);
return (options.enablePatches
? [finalizedState, finalizedPatches, finalizedInversePatches]
: finalizedState);
},
];
}
function handleReturnValue(options) {
const { rootDraft, value, useRawReturn = false, isRoot = true } = options;
forEach(value, (key, item, source) => {
const proxyDraft = getProxyDraft(item);
// just handle the draft which is created by the same rootDraft
if (proxyDraft &&
rootDraft &&
proxyDraft.finalities === rootDraft.finalities) {
options.isContainDraft = true;
const currentValue = proxyDraft.original;
// final update value, but just handle return value
if (source instanceof Set) {
const arr = Array.from(source);
source.clear();
arr.forEach((_item) => source.add(key === _item ? currentValue : _item));
}
else {
set(source, key, currentValue);
}
}
else if (typeof item === 'object' && item !== null) {
options.value = item;
options.isRoot = false;
handleReturnValue(options);
}
});
if (isRoot) {
if (!options.isContainDraft)
console.warn(`The return value does not contain any draft, please use 'rawReturn()' to wrap the return value to improve performance.`);
if (useRawReturn) {
console.warn(`The return value contains drafts, please don't use 'rawReturn()' to wrap the return value.`);
}
}
}
function getCurrent(target) {
var _a;
const proxyDraft = getProxyDraft(target);
if (!isDraftable(target, proxyDraft === null || proxyDraft === void 0 ? void 0 : proxyDraft.options))
return target;
const type = getType(target);
if (proxyDraft && !proxyDraft.operated)
return proxyDraft.original;
let currentValue;
function ensureShallowCopy() {
currentValue =
type === 2 /* DraftType.Map */
? !isBaseMapInstance(target)
? new (Object.getPrototypeOf(target).constructor)(target)
: new Map(target)
: type === 3 /* DraftType.Set */
? Array.from(proxyDraft.setMap.values())
: shallowCopy(target, proxyDraft === null || proxyDraft === void 0 ? void 0 : proxyDraft.options);
}
if (proxyDraft) {
// It's a proxy draft, let's create a shallow copy eagerly
proxyDraft.finalized = true;
try {
ensureShallowCopy();
}
finally {
proxyDraft.finalized = false;
}
}
else {
// It's not a proxy draft, let's use the target directly and let's see
// lazily if we need to create a shallow copy
currentValue = target;
}
forEach(currentValue, (key, value) => {
if (proxyDraft && isEqual(get(proxyDraft.original, key), value))
return;
const newValue = getCurrent(value);
if (newValue !== value) {
if (currentValue === target)
ensureShallowCopy();
set(currentValue, key, newValue);
}
});
if (type === 3 /* DraftType.Set */) {
const value = (_a = proxyDraft === null || proxyDraft === void 0 ? void 0 : proxyDraft.original) !== null && _a !== void 0 ? _a : currentValue;
return !isBaseSetInstance(value)
? new (Object.getPrototypeOf(value).constructor)(currentValue)
: new Set(currentValue);
}
return currentValue;
}
function current(target) {
if (!isDraft(target)) {
throw new Error(`current() is only used for Draft, parameter: ${target}`);
}
return getCurrent(target);
}
/**
* `makeCreator(options)` to make a creator function.
*
* ## Example
*
* ```ts
* import { makeCreator } from '../index';
*
* const baseState = { foo: { bar: 'str' }, arr: [] };
* const create = makeCreator({ enableAutoFreeze: true });
* const state = create(
* baseState,
* (draft) => {
* draft.foo.bar = 'str2';
* },
* );
*
* expect(state).toEqual({ foo: { bar: 's