You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

345 lines
10 KiB

// TypeScript Version 3.1.6
import * as invariant from 'invariant';
const hasOwnProperty = Object.prototype.hasOwnProperty;
const splice = Array.prototype.splice;
const toString = Object.prototype.toString;
const type = obj => toString.call(obj)
.slice(8, -1);
const assign = Object.assign || /* istanbul ignore next */ ((target, source) => {
getAllKeys(source)
.forEach(key => {
if (hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
});
return target;
});
const getAllKeys = typeof Object.getOwnPropertySymbols === 'function' ?
obj => Object.keys(obj)
.concat(Object.getOwnPropertySymbols(obj) as any) :
/* istanbul ignore next */ obj => Object.keys(obj);
/* istanbul ignore next */
function copy(object) {
if (Array.isArray(object)) {
return assign(object.constructor(object.length), object);
} else if (type(object) === 'Map') {
return new Map(object);
} else if (type(object) === 'Set') {
return new Set(object);
} else if (object && typeof object === 'object') {
const prototype = Object.getPrototypeOf(object);
return assign(Object.create(prototype), object);
} else {
return object;
}
}
function newContext() {
const commands = assign({}, defaultCommands);
update.extend = <T>(directive: string, fn: (param: any, old: T) => T) => {
commands[directive] = fn;
};
update.isEquals = (a, b) => a === b;
return update;
function update<T, C extends CustomCommands<object> = never>(
object: T,
$spec: Spec<T, C>,
): T {
const spec = (typeof $spec === 'function') ? { $apply: $spec } : $spec;
if (!(Array.isArray(object) && Array.isArray(spec))) {
invariant(
!Array.isArray(spec),
'update(): You provided an invalid spec to update(). The spec may ' +
'not contain an array except as the value of $set, $push, $unshift, ' +
'$splice or any custom command allowing an array value.',
);
}
invariant(
typeof spec === 'object' && spec !== null,
'update(): You provided an invalid spec to update(). The spec and ' +
'every included key path must be plain objects containing one of the ' +
'following commands: %s.',
Object.keys(commands)
.join(', '),
);
let nextObject = object;
getAllKeys(spec)
.forEach(key => {
if (hasOwnProperty.call(commands, key)) {
const objectWasNextObject = object === nextObject;
nextObject = commands[key](spec[key], nextObject, spec, object);
if (objectWasNextObject && update.isEquals(nextObject, object)) {
nextObject = object;
}
} else {
const nextValueForKey =
type(object) === 'Map'
? update((object as any as Map<any, any>).get(key), spec[key])
: update(object[key], spec[key]);
const nextObjectValue =
type(nextObject) === 'Map'
? (nextObject as any as Map<any, any>).get(key)
: nextObject[key];
if (!update.isEquals(nextValueForKey, nextObjectValue)
|| typeof nextValueForKey === 'undefined'
&& !hasOwnProperty.call(object, key)
) {
if (nextObject === object) {
nextObject = copy(object);
}
if (type(nextObject) === 'Map') {
(nextObject as any as Map<any, any>).set(key, nextValueForKey);
} else {
nextObject[key] = nextValueForKey;
}
}
}
});
return nextObject;
}
}
const defaultCommands = {
$push: (value, nextObject, spec) => {
invariantPushAndUnshift(nextObject, spec, '$push');
return value.length ? nextObject.concat(value) : nextObject;
},
$unshift: (value, nextObject, spec) => {
invariantPushAndUnshift(nextObject, spec, '$unshift');
return value.length ? value.concat(nextObject) : nextObject;
},
$splice: (value, nextObject, spec, originalObject) => {
invariantSplices(nextObject, spec);
value.forEach(args => {
invariantSplice(args);
if (nextObject === originalObject && args.length) {
nextObject = copy(originalObject);
}
splice.apply(nextObject, args);
});
return nextObject;
},
$set: (value, _nextObject, spec) => {
invariantSet(spec);
return value;
},
$toggle: (targets, nextObject) => {
invariantSpecArray(targets, '$toggle');
const nextObjectCopy = targets.length ? copy(nextObject) : nextObject;
targets.forEach(target => {
nextObjectCopy[target] = !nextObject[target];
});
return nextObjectCopy;
},
$unset: (value, nextObject, _spec, originalObject) => {
invariantSpecArray(value, '$unset');
value.forEach(key => {
if (Object.hasOwnProperty.call(nextObject, key)) {
if (nextObject === originalObject) {
nextObject = copy(originalObject);
}
// tslint:disable-next-line:no-dynamic-delete
delete nextObject[key];
}
});
return nextObject;
},
$add: (values, nextObject, _spec, originalObject) => {
invariantMapOrSet(nextObject, '$add');
invariantSpecArray(values, '$add');
if (type(nextObject) === 'Map') {
values.forEach(pair => {
const key = pair[0];
const value = pair[1];
/* istanbul ignore next */
if (nextObject === originalObject && nextObject.get(key) !== value) {
nextObject = copy(originalObject);
}
nextObject.set(key, value);
});
} else {
values.forEach(value => {
if (nextObject === originalObject && !nextObject.has(value)) {
nextObject = copy(originalObject);
}
nextObject.add(value);
});
}
return nextObject;
},
$remove: (value, nextObject, _spec, originalObject) => {
invariantMapOrSet(nextObject, '$remove');
invariantSpecArray(value, '$remove');
value.forEach(key => {
if (nextObject === originalObject && nextObject.has(key)) {
nextObject = copy(originalObject);
}
nextObject.delete(key);
});
return nextObject;
},
$merge: (value, nextObject, _spec, originalObject) => {
invariantMerge(nextObject, value);
getAllKeys(value)
.forEach(key => {
if (value[key] !== nextObject[key]) {
if (nextObject === originalObject) {
nextObject = copy(originalObject);
}
nextObject[key] = value[key];
}
});
return nextObject;
},
$apply: (value, original) => {
invariantApply(value);
return value(original);
},
};
const defaultExport = newContext();
(defaultExport as any).default = defaultExport;
(defaultExport as any).newContext = newContext
export = defaultExport;
// invariants
function invariantPushAndUnshift(value, spec, command) {
invariant(
Array.isArray(value),
'update(): expected target of %s to be an array; got %s.',
command,
value,
);
invariantSpecArray(spec[command], command);
}
function invariantSpecArray(spec, command) {
invariant(
Array.isArray(spec),
'update(): expected spec of %s to be an array; got %s. ' +
'Did you forget to wrap your parameter in an array?',
command,
spec,
);
}
function invariantSplices(value, spec) {
invariant(
Array.isArray(value),
'Expected $splice target to be an array; got %s',
value,
);
invariantSplice(spec.$splice);
}
function invariantSplice(value) {
invariant(
Array.isArray(value),
'update(): expected spec of $splice to be an array of arrays; got %s. ' +
'Did you forget to wrap your parameters in an array?',
value,
);
}
function invariantApply(fn) {
invariant(
typeof fn === 'function',
'update(): expected spec of $apply to be a function; got %s.',
fn,
);
}
function invariantSet(spec) {
invariant(
Object.keys(spec).length === 1,
'Cannot have more than one key in an object with $set',
);
}
function invariantMerge(target, specValue) {
invariant(
specValue && typeof specValue === 'object',
'update(): $merge expects a spec of type \'object\'; got %s',
specValue,
);
invariant(
target && typeof target === 'object',
'update(): $merge expects a target of type \'object\'; got %s',
target,
);
}
function invariantMapOrSet(target, command) {
const typeOfTarget = type(target);
invariant(
typeOfTarget === 'Map' || typeOfTarget === 'Set',
'update(): %s expects a target of type Set or Map; got %s',
command,
typeOfTarget,
);
}
// Usage with custom commands is as follows:
//
// interface MyCommands {
// $foo: string;
// }
//
// update<Foo, CustomCommands<MyCommands>>(..., { $foo: "bar" });
//
// It is suggested that if you use custom commands frequently, you wrap and re-export a
// properly-typed version of `update`:
//
// function myUpdate<T>(object: T, spec: Spec<T, CustomCommands<MyCommands>>) {
// return update(object, spec);
// }
//
// See https://github.com/kolodny/immutability-helper/pull/108 for explanation of why this
// type exists.
type CustomCommands<T> = T & { __noInferenceCustomCommandsBrand: any };
type Spec<T, C extends CustomCommands<object> = never> =
| (
T extends (Array<infer U> | ReadonlyArray<infer U>) ? ArraySpec<U, C> :
T extends (Map<infer K, infer V> | ReadonlyMap<infer K, infer V>) ? MapSpec<K, V> :
T extends (Set<infer W> | ReadonlySet<infer W>) ? SetSpec<W> :
T extends object ? ObjectSpec<T, C> :
never
)
| { $set: T }
| { $apply: (v: T) => T }
| ((v: T) => T)
| (C extends CustomCommands<infer O> ? O : never);
type ArraySpec<T, C extends CustomCommands<object>> =
| { $push: T[] }
| { $unshift: T[] }
| { $splice: Array<[number, number?] | [number, number, ...T[]]> }
| { [index: string]: Spec<T, C> }; // Note that this does not type check properly if index: number.
type MapSpec<K, V> =
| { $add: Array<[K, V]> }
| { $remove: K[] };
type SetSpec<T> =
| { $add: T[] }
| { $remove: T[] };
type ObjectSpec<T, C extends CustomCommands<object>> =
| { $toggle: Array<keyof T> }
| { $unset: Array<keyof T> }
| { $merge: Partial<T> }
| { [K in keyof T]?: Spec<T[K], C> };