Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

immutable-ops

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

immutable-ops - npm Package Compare versions

Comparing version 0.4.2 to 0.5.0

immutable-ops.sublime-project

448

lib/index.js

@@ -1,38 +0,21 @@

'use strict';
import curry from 'ramda/src/curry';
import placeholder from 'ramda/src/__';
Object.defineProperty(exports, '__esModule', {
value: true
});
exports.canMutate = canMutate;
exports['default'] = getImmutableOps;
function forOwn(obj, fn) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
fn(obj[key], key);
}
}
}
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
function isArrayLike(value) {
return value && typeof value === 'object' && typeof value.length === 'number' && value.length >= 0 && value.length % 1 === 0;
}
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } }
const OWNER_ID_TAG = '@@_______immutableOpsOwnerID';
var _lodashForOwn = require('lodash/forOwn');
var _lodashForOwn2 = _interopRequireDefault(_lodashForOwn);
var _lodashIsArrayLike = require('lodash/isArrayLike');
var _lodashIsArrayLike2 = _interopRequireDefault(_lodashIsArrayLike);
var _ramdaSrcCurry = require('ramda/src/curry');
var _ramdaSrcCurry2 = _interopRequireDefault(_ramdaSrcCurry);
var _ramdaSrcWrap = require('ramda/src/wrap');
var _ramdaSrcWrap2 = _interopRequireDefault(_ramdaSrcWrap);
var _ramdaSrc__ = require('ramda/src/__');
var _ramdaSrc__2 = _interopRequireDefault(_ramdaSrc__);
var MUTABILITY_TAG = '@@_______canMutate';
function fastArrayCopy(arr) {
var copied = new Array(arr.length);
for (var i = 0; i < arr.length; i++) {
const copied = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
copied[i] = arr[i];

@@ -43,9 +26,14 @@ }

function canMutate(obj) {
return obj.hasOwnProperty(MUTABILITY_TAG);
export function canMutate(obj, ownerID) {
if (!ownerID) return false;
return obj[OWNER_ID_TAG] === ownerID;
}
function addCanMutateTag(opts, obj) {
Object.defineProperty(obj, MUTABILITY_TAG, {
value: true,
const newOwnerID = typeof Symbol === 'function' ? () => Symbol('ownerID') : () => ({});
export const getBatchToken = newOwnerID;
function addOwnerID(obj, ownerID) {
Object.defineProperty(obj, OWNER_ID_TAG, {
value: ownerID,
configurable: true,

@@ -55,17 +43,9 @@ enumerable: false

opts.batchManager.addMutated(obj);
return obj;
}
function removeCanMutateTag(obj) {
delete obj[MUTABILITY_TAG];
return obj;
}
function prepareNewObject(opts, instance) {
if (opts.batchManager.isWithMutations()) {
addCanMutateTag(opts, instance);
function prepareNewObject(instance, ownerID) {
if (ownerID) {
addOwnerID(instance, ownerID);
}
opts.createdObjects++;
return instance;

@@ -81,3 +61,3 @@ }

var PATH_SEPARATOR = '.';
const PATH_SEPARATOR = '.';

@@ -95,3 +75,3 @@ function normalizePath(pathArg) {

function mutableSet(opts, key, value, obj) {
function mutableSet(key, value, obj) {
obj[key] = value;

@@ -101,7 +81,7 @@ return obj;

function mutableSetIn(opts, _pathArg, value, obj) {
var originalPathArg = normalizePath(_pathArg);
function mutableSetIn(_pathArg, value, obj) {
const originalPathArg = normalizePath(_pathArg);
var pathLen = originalPathArg.length;
originalPathArg.reduce(function (acc, curr, idx) {
const pathLen = originalPathArg.length;
originalPathArg.reduce((acc, curr, idx) => {
if (idx === pathLen - 1) {

@@ -112,7 +92,7 @@ acc[curr] = value;

var currType = typeof acc[curr];
const currType = typeof acc[curr];
if (currType === 'undefined') {
var newObj = {};
prepareNewObject(opts, newObj);
const newObj = {};
prepareNewObject(newObj, null);
acc[curr] = newObj;

@@ -126,4 +106,4 @@ return newObj;

var pathRepr = originalPathArg[idx - 1] + '.' + curr;
throw new Error('A non-object value was encountered when traversing setIn path at ' + pathRepr + '.');
const pathRepr = `${ originalPathArg[idx - 1] }.${ curr }`;
throw new Error(`A non-object value was encountered when traversing setIn path at ${ pathRepr }.`);
});

@@ -134,9 +114,9 @@

function valueInPath(opts, _pathArg, obj) {
var pathArg = normalizePath(_pathArg);
function valueInPath(_pathArg, obj) {
const pathArg = normalizePath(_pathArg);
var acc = obj;
for (var i = 0; i < pathArg.length; i++) {
var curr = pathArg[i];
var currRef = acc[curr];
let acc = obj;
for (let i = 0; i < pathArg.length; i++) {
const curr = pathArg[i];
const currRef = acc[curr];
if (i === pathArg.length - 1) {

@@ -154,13 +134,20 @@ return currRef;

function immutableSetIn(opts, _pathArg, value, obj) {
var pathArg = normalizePath(_pathArg);
function immutableSetIn(ownerID, _pathArg, value, obj) {
const pathArg = normalizePath(_pathArg);
var currentValue = valueInPath(opts, pathArg, obj);
const currentValue = valueInPath(pathArg, obj);
if (value === currentValue) return obj;
var pathLen = pathArg.length;
var acc = Object.assign(prepareNewObject(opts, {}), obj);
var rootObj = acc;
const pathLen = pathArg.length;
pathArg.forEach(function (curr, idx) {
let acc;
if (canMutate(obj, ownerID)) {
acc = obj;
} else {
acc = Object.assign(prepareNewObject({}, ownerID), obj);
}
const rootObj = acc;
pathArg.forEach((curr, idx) => {
if (idx === pathLen - 1) {

@@ -171,10 +158,10 @@ acc[curr] = value;

var currRef = acc[curr];
var currType = typeof currRef;
const currRef = acc[curr];
const currType = typeof currRef;
if (currType === 'object') {
if (canMutate(currRef)) {
if (canMutate(currRef, ownerID)) {
acc = currRef;
} else {
var newObj = prepareNewObject(opts, {});
const newObj = prepareNewObject({}, ownerID);
acc[curr] = Object.assign(newObj, currRef);

@@ -187,3 +174,3 @@ acc = newObj;

if (currType === 'undefined') {
var newObj = prepareNewObject(opts, {});
const newObj = prepareNewObject({}, ownerID);
acc[curr] = newObj;

@@ -194,4 +181,4 @@ acc = newObj;

var pathRepr = pathArg[idx - 1] + '.' + curr;
throw new Error('A non-object value was encountered when traversing setIn path at ' + pathRepr + '.');
const pathRepr = `${ pathArg[idx - 1] }.${ curr }`;
throw new Error(`A non-object value was encountered when traversing setIn path at ${ pathRepr }.`);
});

@@ -202,15 +189,15 @@

function mutableMerge(isDeep, opts, _mergeObjs, baseObj) {
var mergeObjs = forceArray(_mergeObjs);
function mutableMerge(isDeep, _mergeObjs, baseObj) {
const mergeObjs = forceArray(_mergeObjs);
if (opts.deep) {
mergeObjs.forEach(function (mergeObj) {
(0, _lodashForOwn2['default'])(mergeObj, function (value, key) {
if (isDeep) {
mergeObjs.forEach(mergeObj => {
forOwn(mergeObj, (value, key) => {
if (isDeep && baseObj.hasOwnProperty(key)) {
var assignValue = undefined;
let assignValue;
if (typeof value === 'object') {
assignValue = canMutate(value) ? mutableMerge(isDeep, opts, [value], baseObj[key]) : immutableMerge(isDeep, opts, [value], baseObj[key]); // eslint-disable-line
assignValue = mutableMerge(isDeep, [value], baseObj[key]);
} else {
assignValue = value;
}
assignValue = value;
}

@@ -224,3 +211,3 @@ baseObj[key] = assignValue;

} else {
Object.assign.apply(Object, [baseObj].concat(_toConsumableArray(mergeObjs)));
Object.assign(baseObj, ...mergeObjs);
}

@@ -231,8 +218,8 @@

var mutableShallowMerge = mutableMerge.bind(null, false);
var mutableDeepMerge = mutableMerge.bind(null, true);
const mutableShallowMerge = mutableMerge.bind(null, false);
const mutableDeepMerge = mutableMerge.bind(null, true);
function mutableOmit(opts, _keys, obj) {
var keys = forceArray(_keys);
keys.forEach(function (key) {
function mutableOmit(_keys, obj) {
const keys = forceArray(_keys);
keys.forEach(key => {
delete obj[key];

@@ -247,24 +234,24 @@ });

function immutableMerge(isDeep, opts, _mergeObjs, obj) {
if (canMutate(obj)) return mutableMerge(isDeep, opts, _mergeObjs, obj);
var mergeObjs = forceArray(_mergeObjs);
function immutableMerge(isDeep, ownerID, _mergeObjs, obj) {
if (canMutate(obj, ownerID)) return mutableMerge(isDeep, _mergeObjs, obj);
const mergeObjs = forceArray(_mergeObjs);
var hasChanges = false;
var nextObject = obj;
let hasChanges = false;
let nextObject = obj;
var willChange = function willChange() {
const willChange = () => {
if (!hasChanges) {
hasChanges = true;
nextObject = Object.assign({}, obj);
prepareNewObject(opts, nextObject);
prepareNewObject(nextObject, ownerID);
}
};
mergeObjs.forEach(function (mergeObj) {
(0, _lodashForOwn2['default'])(mergeObj, function (mergeValue, key) {
mergeObjs.forEach(mergeObj => {
forOwn(mergeObj, (mergeValue, key) => {
if (isDeep && obj.hasOwnProperty(key)) {
var currentValue = nextObject[key];
const currentValue = nextObject[key];
if (typeof mergeValue === 'object' && !(mergeValue instanceof Array)) {
if (_shouldMergeKey(nextObject, mergeObj, key)) {
var recursiveMergeResult = immutableMerge(isDeep, opts, mergeValue, currentValue);
const recursiveMergeResult = immutableMerge(isDeep, ownerID, mergeValue, currentValue);

@@ -289,13 +276,13 @@ if (recursiveMergeResult !== currentValue) {

var immutableDeepMerge = immutableMerge.bind(null, true);
var immutableShallowMerge = immutableMerge.bind(null, false);
const immutableDeepMerge = immutableMerge.bind(null, true);
const immutableShallowMerge = immutableMerge.bind(null, false);
function immutableArrSet(opts, index, value, arr) {
if (canMutate(arr)) return mutableSet(opts, index, value, arr);
function immutableArrSet(ownerID, index, value, arr) {
if (canMutate(arr, ownerID)) return mutableSet(index, value, arr);
if (arr[index] === value) return arr;
var newArr = fastArrayCopy(arr);
const newArr = fastArrayCopy(arr);
newArr[index] = value;
prepareNewObject(opts, newArr);
prepareNewObject(newArr, ownerID);

@@ -305,10 +292,10 @@ return newArr;

function immutableSet(opts, key, value, obj) {
if ((0, _lodashIsArrayLike2['default'])(obj)) return immutableArrSet(opts, key, value, obj);
if (canMutate(obj)) return mutableSet(opts, key, value, obj);
function immutableSet(ownerID, key, value, obj) {
if (isArrayLike(obj)) return immutableArrSet(ownerID, key, value, obj);
if (canMutate(obj, ownerID)) return mutableSet(key, value, obj);
if (obj[key] === value) return obj;
var newObj = Object.assign({}, obj);
prepareNewObject(opts, newObj);
const newObj = Object.assign({}, obj);
prepareNewObject(newObj, ownerID);
newObj[key] = value;

@@ -318,9 +305,7 @@ return newObj;

function immutableOmit(opts, _keys, obj) {
if (canMutate(obj)) return mutableOmit(opts, _keys, obj);
function immutableOmit(ownerID, _keys, obj) {
if (canMutate(obj, ownerID)) return mutableOmit(_keys, obj);
var keys = forceArray(_keys);
var keysInObj = keys.filter(function (key) {
return obj.hasOwnProperty(key);
});
const keys = forceArray(_keys);
const keysInObj = keys.filter(key => obj.hasOwnProperty(key));

@@ -330,21 +315,21 @@ // None of the keys were in the object, so we can return `obj`.

var newObj = Object.assign({}, obj);
keysInObj.forEach(function (key) {
const newObj = Object.assign({}, obj);
keysInObj.forEach(key => {
delete newObj[key];
});
prepareNewObject(opts, newObj);
prepareNewObject(newObj, ownerID);
return newObj;
}
function mutableArrPush(opts, _vals, arr) {
var vals = forceArray(_vals);
arr.push.apply(arr, _toConsumableArray(vals));
function mutableArrPush(_vals, arr) {
const vals = forceArray(_vals);
arr.push(...vals);
return arr;
}
function mutableArrFilter(opts, func, arr) {
var currIndex = 0;
var originalIndex = 0;
function mutableArrFilter(func, arr) {
let currIndex = 0;
let originalIndex = 0;
while (currIndex < arr.length) {
var item = arr[currIndex];
const item = arr[currIndex];
if (!func(item, originalIndex)) {

@@ -361,19 +346,19 @@ arr.splice(currIndex, 1);

function mutableArrSplice(opts, index, deleteCount, _vals, arr) {
var vals = forceArray(_vals);
arr.splice.apply(arr, [index, deleteCount].concat(_toConsumableArray(vals)));
function mutableArrSplice(index, deleteCount, _vals, arr) {
const vals = forceArray(_vals);
arr.splice(index, deleteCount, ...vals);
return arr;
}
function mutableArrInsert(opts, index, _vals, arr) {
return mutableArrSplice(opts, index, 0, _vals, arr);
function mutableArrInsert(index, _vals, arr) {
return mutableArrSplice(index, 0, _vals, arr);
}
function immutableArrSplice(opts, index, deleteCount, _vals, arr) {
if (canMutate(arr)) return mutableArrSplice(opts, index, deleteCount, _vals, arr);
function immutableArrSplice(ownerID, index, deleteCount, _vals, arr) {
if (canMutate(arr, ownerID)) return mutableArrSplice(index, deleteCount, _vals, arr);
var vals = forceArray(_vals);
var newArr = arr.slice();
prepareNewObject(opts, newArr);
newArr.splice.apply(newArr, [index, deleteCount].concat(_toConsumableArray(vals)));
const vals = forceArray(_vals);
const newArr = arr.slice();
prepareNewObject(newArr, ownerID);
newArr.splice(index, deleteCount, ...vals);

@@ -383,22 +368,22 @@ return newArr;

function immutableArrInsert(opts, index, _vals, arr) {
if (canMutate(arr)) return mutableArrInsert(opts, index, _vals, arr);
return immutableArrSplice(opts, index, 0, _vals, arr);
function immutableArrInsert(ownerID, index, _vals, arr) {
if (canMutate(arr, ownerID)) return mutableArrInsert(index, _vals, arr);
return immutableArrSplice(ownerID, index, 0, _vals, arr);
}
function immutableArrPush(opts, vals, arr) {
return immutableArrInsert(opts, arr.length, vals, arr);
function immutableArrPush(ownerID, vals, arr) {
return immutableArrInsert(ownerID, arr.length, vals, arr);
}
function immutableArrFilter(opts, func, arr) {
if (canMutate(arr)) return mutableArrFilter(opts, func, arr);
var newArr = arr.filter(func);
function immutableArrFilter(ownerID, func, arr) {
if (canMutate(arr, ownerID)) return mutableArrFilter(func, arr);
const newArr = arr.filter(func);
if (newArr.length === arr.length) return arr;
prepareNewObject(opts, newArr);
prepareNewObject(newArr, ownerID);
return newArr;
}
var operations = {
const immutableOperations = {
// object operations

@@ -417,123 +402,68 @@ merge: immutableShallowMerge,

// both
set: immutableSet,
set: immutableSet
};
mutable: {
// object operations
merge: mutableShallowMerge,
deepMerge: mutableDeepMerge,
omit: mutableOmit,
setIn: mutableSetIn,
const mutableOperations = {
// object operations
merge: mutableShallowMerge,
deepMerge: mutableDeepMerge,
omit: mutableOmit,
setIn: mutableSetIn,
// array operations
insert: mutableArrInsert,
push: mutableArrPush,
filter: mutableArrFilter,
splice: mutableArrSplice,
// array operations
insert: mutableArrInsert,
push: mutableArrPush,
filter: mutableArrFilter,
splice: mutableArrSplice,
// both
set: mutableSet
}
// both
set: mutableSet
};
function bindOperationsToOptions(opsObj, opts) {
var boundOperations = {};
export function getImmutableOps() {
const immutableOps = Object.assign({}, immutableOperations);
forOwn(immutableOps, (value, key) => {
immutableOps[key] = curry(value.bind(null, null));
});
(0, _lodashForOwn2['default'])(opsObj, function (value, key) {
if (typeof value === 'object') {
boundOperations[key] = bindOperationsToOptions(value, opts);
} else {
boundOperations[key] = value.bind(null, opts);
const mutableOps = Object.assign({}, mutableOperations);
forOwn(mutableOps, (value, key) => {
mutableOps[key] = curry(value);
});
if (opts.curried) {
boundOperations[key] = (0, _ramdaSrcCurry2['default'])(boundOperations[key]);
}
}
const batchOps = Object.assign({}, immutableOperations);
forOwn(batchOps, (value, key) => {
batchOps[key] = curry(value);
});
return boundOperations;
}
function batched(_token, _fn) {
let token;
let fn;
function getBatchManager() {
var previousSessionStack = [];
var currMutatedObjects = null;
var objectsCreated = 0;
return {
open: function open() {
if (currMutatedObjects !== null) {
previousSessionStack.push(currMutatedObjects);
}
currMutatedObjects = [];
},
isWithMutations: function isWithMutations() {
return currMutatedObjects !== null;
},
addMutated: function addMutated(obj) {
currMutatedObjects.push(obj);
objectsCreated++;
},
getMutatedObjects: function getMutatedObjects() {
return currMutatedObjects;
},
getObjectsCreatedCount: function getObjectsCreatedCount() {
return objectsCreated;
},
close: function close() {
if (currMutatedObjects !== null) {
currMutatedObjects.forEach(removeCanMutateTag);
if (previousSessionStack.length) {
currMutatedObjects = previousSessionStack.pop();
} else {
currMutatedObjects = null;
}
objectsCreated = 0;
}
if (typeof _token === 'function') {
fn = _token;
token = getBatchToken();
} else {
token = _token;
fn = _fn;
}
};
}
function getImmutableOps(userOpts) {
var defaultOpts = {
curried: true,
batchManager: getBatchManager()
};
var opts = Object.assign({ createdObjects: 0 }, defaultOpts, userOpts || {});
var boundOperations = bindOperationsToOptions(operations, opts);
function batchWrapper() {
var func = arguments[0];
var args = Array.prototype.slice.call(arguments, 1);
opts.batchManager.open();
var returnValue = func.apply(null, args);
opts.batchManager.close();
return returnValue;
const immutableOpsBoundToToken = Object.assign({}, immutableOperations);
forOwn(immutableOpsBoundToToken, (value, key) => {
immutableOpsBoundToToken[key] = curry(value.bind(null, token));
});
return fn(immutableOpsBoundToToken);
}
boundOperations.batched = batchWrapper;
boundOperations.batch = (0, _ramdaSrcWrap2['default'])(_ramdaSrc__2['default'], batchWrapper);
boundOperations.createdObjectsCount = function () {
return opts.createdObjects;
};
boundOperations.getMutatedObjects = opts.batchManager.getMutatedObjects;
boundOperations.__ = _ramdaSrc__2['default'];
boundOperations.open = opts.batchManager.open;
boundOperations.close = opts.batchManager.close;
boundOperations.getBatchManager = getBatchManager;
return Object.assign(immutableOps, {
mutable: mutableOps,
batch: batchOps,
batched,
__: placeholder,
getBatchToken
});
}
boundOperations.useBatchManager = function (manager) {
opts.batchManager.close();
opts.batchManager = manager;
boundOperations.open = manager.open;
boundOperations.close = manager.close;
boundOperations.getMutatedObjects = manager.getMutatedObjects;
};
export const ops = getImmutableOps();
return boundOperations;
}
export default ops;

@@ -1,66 +0,34 @@

'use strict';
import chai from 'chai';
import sinonChai from 'sinon-chai';
import freeze from 'deep-freeze';
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
import { ops, canMutate, getBatchToken } from '../index';
var _chai = require('chai');
chai.use(sinonChai);
const { expect } = chai;
var _chai2 = _interopRequireDefault(_chai);
describe('batched', () => {
it('works', () => {
const res = ops.batched(batchOps => {
const obj = {};
const result = batchOps.set('a', 1, obj);
expect(result).to.deep.equal({ a: 1 });
expect(result).not.to.equal(obj);
var _sinonChai = require('sinon-chai');
var _sinonChai2 = _interopRequireDefault(_sinonChai);
var _index = require('../index');
var _index2 = _interopRequireDefault(_index);
var _deepFreeze = require('deep-freeze');
var _deepFreeze2 = _interopRequireDefault(_deepFreeze);
var _ramdaSrcCompose = require('ramda/src/compose');
var _ramdaSrcCompose2 = _interopRequireDefault(_ramdaSrcCompose);
_chai2['default'].use(_sinonChai2['default']);
var expect = _chai2['default'].expect;
describe('operations', function () {
var ops = undefined;
beforeEach(function () {
ops = (0, _index2['default'])();
});
it('wrapBatched', function () {
var pushFour = ops.push(4);
var pushFive = ops.push(5);
var arr = (0, _deepFreeze2['default'])([1, 2, 3]);
var pusher = ops.batch((0, _ramdaSrcCompose2['default'])(pushFive, pushFour));
expect(pusher).to.be.a('function');
var result = pusher(arr);
expect(result).to.deep.equal([1, 2, 3, 4, 5]);
});
it('useBatchManager', function () {
var myManager = ops.getBatchManager();
ops.useBatchManager(myManager);
var pusher = ops.push(0);
var batchOperation = ops.batch(function (arr) {
var first = pusher(arr);
expect(ops.getMutatedObjects()).to.equal(myManager.getMutatedObjects());
expect(ops.getMutatedObjects()[0]).to.equal(first);
return first;
const result2 = batchOps.omit('a', result);
expect(result2).to.equal(result);
expect(result2).to.deep.equal({});
return result2;
});
batchOperation([]);
expect(res).to.deep.equal({});
});
});
describe('object', function () {
describe('batched mutations', function () {
it('deepMerges', function () {
var baseObj = (0, _deepFreeze2['default'])({
describe('operations', () => {
describe('object', () => {
describe('batched mutations', () => {
const token = getBatchToken();
it('deepMerges', () => {
const baseObj = freeze({
change: 'Tommi',

@@ -73,3 +41,3 @@ dontChange: 25,

});
var mergeObj = (0, _deepFreeze2['default'])({
const mergeObj = freeze({
change: 'None',

@@ -82,12 +50,9 @@ add: 'US',

});
var result = undefined;
var merger = ops.deepMerge(mergeObj);
ops.batched(function () {
result = merger(baseObj);
expect((0, _index.canMutate)(result)).to.be['true'];
expect((0, _index.canMutate)(result.deeper)).to.be['true'];
});
const merger = ops.batch.deepMerge(token, mergeObj);
const result = merger(baseObj);
expect(canMutate(result, token)).to.be.true;
expect(canMutate(result.deeper, token)).to.be.true;
expect((0, _index.canMutate)(result)).to.be['false'];
expect((0, _index.canMutate)(result.deeper)).to.be['false'];
expect(canMutate(result, getBatchToken())).to.be.false;
expect(canMutate(result.deeper, getBatchToken())).to.be.false;

@@ -106,4 +71,4 @@ expect(result).to.not.equal(baseObj);

it('omits a single key', function () {
var obj = (0, _deepFreeze2['default'])({
it('omits a single key', () => {
const obj = freeze({
name: 'Tommi',

@@ -113,16 +78,16 @@ age: 25

var result = undefined;
var omitter = ops.omit('age');
const omitter = ops.batch.omit(token, 'age');
ops.batched(function () {
result = omitter(obj);
expect((0, _index.canMutate)(result)).to.be['true'];
});
const result = omitter(obj);
expect(canMutate(result, token)).to.be.true;
expect((0, _index.canMutate)(result)).to.be['false'];
expect(canMutate(result, getBatchToken())).to.be.false;
expect(result).to.not.contain.keys(['age']);
// Further modification should mutate the existing object.
expect(ops.batch.omit(token, 'name', result)).to.equal(result);
});
it('omits an array of keys', function () {
var obj = (0, _deepFreeze2['default'])({
it('omits an array of keys', () => {
const obj = freeze({
name: 'Tommi',

@@ -132,17 +97,16 @@ age: 25

var result = undefined;
const omitter = ops.batch.omit(token, ['age']);
const result = omitter(obj);
var omitter = ops.omit(['age']);
ops.batched(function () {
result = omitter(obj);
expect(canMutate(result, token)).to.be.true;
expect((0, _index.canMutate)(result)).to.be['true'];
});
expect(canMutate(result, getBatchToken())).to.be.false;
expect(result).to.not.contain.keys(['age']);
expect((0, _index.canMutate)(result)).to.be['false'];
expect(result).to.not.contain.keys(['age']);
// Further modification should mutate the existing object.
expect(ops.batch.omit(token, ['name'], result)).to.equal(result);
});
it('sets a value', function () {
var obj = (0, _deepFreeze2['default'])({
it('sets a value', () => {
const obj = freeze({
one: 1,

@@ -153,10 +117,7 @@ two: 500,

var result = undefined;
ops.batched(function () {
result = ops.set('two', 5, obj);
const result = ops.batch.set(token, 'two', 5, obj);
expect((0, _index.canMutate)(result)).to.be['true'];
result = ops.set('two', 2, result);
});
expect(result).to.deep.equal({
expect(canMutate(result, token)).to.be.true;
const result2 = ops.batch.set(token, 'two', 2, result);
expect(result2).to.deep.equal({
one: 1,

@@ -166,6 +127,8 @@ two: 2,

});
expect(result).to.equal(result2);
});
it('sets a value in path', function () {
var obj = (0, _deepFreeze2['default'])({
it('sets a value in path', () => {
const obj = freeze({
first: {

@@ -180,24 +143,24 @@ second: {

});
var result = undefined;
var setter = ops.setIn('first.second.value', 'anotherValue');
const setter = ops.batch.setIn(token, 'first.second.value', 'anotherValue');
ops.batched(function () {
result = setter(obj);
const result = setter(obj);
expect(canMutate(result, token)).to.be.true;
expect((0, _index.canMutate)(result)).to.be['true'];
});
expect((0, _index.canMutate)(result)).to.be['false'];
expect(canMutate(result, getBatchToken())).to.be.false;
expect(result).not.to.equal(obj);
expect(result.first.second.value).to.equal('anotherValue');
expect(result.maintain).to.be['true'];
expect(result.first.maintain).to.be['true'];
expect(result.first.second.maintain).to.be['true'];
expect(result.maintain).to.be.true;
expect(result.first.maintain).to.be.true;
expect(result.first.second.maintain).to.be.true;
const result2 = ops.batch.setIn(token, 'first.second.value', 'secondAnotherValue', result);
expect(result).to.equal(result2);
expect(result2.first.second.value).to.equal('secondAnotherValue');
});
});
describe('immutable ops', function () {
it('deepMerges', function () {
var baseObj = (0, _deepFreeze2['default'])({
describe('immutable ops', () => {
it('deepMerges', () => {
const baseObj = freeze({
change: 'Tommi',

@@ -210,3 +173,3 @@ dontChange: 25,

});
var mergeObj = (0, _deepFreeze2['default'])({
const mergeObj = freeze({
change: 'None',

@@ -220,7 +183,7 @@ add: 'US',

var merger = ops.deepMerge(mergeObj);
var result = merger(baseObj);
const merger = ops.deepMerge(mergeObj);
const result = merger(baseObj);
expect((0, _index.canMutate)(result)).to.be['false'];
expect((0, _index.canMutate)(result.deeper)).to.be['false'];
expect(canMutate(result)).to.be.false;
expect(canMutate(result.deeper)).to.be.false;

@@ -239,4 +202,4 @@ expect(result).to.not.equal(baseObj);

it('deepMerges and returns initial object when no values changed', function () {
var baseObj = (0, _deepFreeze2['default'])({
it('deepMerges and returns initial object when no values changed', () => {
const baseObj = freeze({
deep: {

@@ -246,3 +209,3 @@ dontChange: 'John'

});
var mergeObj = (0, _deepFreeze2['default'])({
const mergeObj = freeze({
deep: {

@@ -253,8 +216,8 @@ dontChange: 'John'

var result = ops.deepMerge(mergeObj, baseObj);
const result = ops.deepMerge(mergeObj, baseObj);
expect(result).to.equal(baseObj);
});
it('omits a single key', function () {
var obj = (0, _deepFreeze2['default'])({
it('omits a single key', () => {
const obj = freeze({
name: 'Tommi',

@@ -264,11 +227,11 @@ age: 25

var omitter = ops.omit('age');
var result = omitter(obj);
const omitter = ops.omit('age');
const result = omitter(obj);
expect((0, _index.canMutate)(result)).to.be['false'];
expect(canMutate(result)).to.be.false;
expect(result).to.not.contain.keys(['age']);
});
it('omits a single key, returns same object if no value changes', function () {
var obj = (0, _deepFreeze2['default'])({
it('omits a single key, returns same object if no value changes', () => {
const obj = freeze({
name: 'Tommi',

@@ -278,8 +241,8 @@ age: 25

var result = ops.omit('location', obj);
const result = ops.omit('location', obj);
expect(result).to.equal(obj);
});
it('omits an array of keys', function () {
var obj = (0, _deepFreeze2['default'])({
it('omits an array of keys', () => {
const obj = freeze({
name: 'Tommi',

@@ -289,11 +252,11 @@ age: 25

var omitter = ops.omit(['age']);
var result = omitter(obj);
const omitter = ops.omit(['age']);
const result = omitter(obj);
expect((0, _index.canMutate)(result)).to.be['false'];
expect(canMutate(result)).to.be.false;
expect(result).to.not.contain.keys(['age']);
});
it('sets a value', function () {
var obj = (0, _deepFreeze2['default'])({
it('sets a value', () => {
const obj = freeze({
name: 'Tommi',

@@ -303,3 +266,3 @@ age: 25

var result = ops.set('age', 26, obj);
const result = ops.set('age', 26, obj);
expect(result).to.deep.equal({

@@ -311,4 +274,4 @@ name: 'Tommi',

it('sets a value and returns the initial value of no changes', function () {
var obj = (0, _deepFreeze2['default'])({
it('sets a value and returns the initial value of no changes', () => {
const obj = freeze({
name: 'Tommi',

@@ -318,8 +281,8 @@ age: 25

var result = ops.set('age', 25, obj);
const result = ops.set('age', 25, obj);
expect(result).to.equal(obj);
});
it('sets a value in path', function () {
var obj = (0, _deepFreeze2['default'])({
it('sets a value in path', () => {
const obj = freeze({
first: {

@@ -335,16 +298,16 @@ second: {

var setter = ops.setIn('first.second.value', 'anotherValue');
const setter = ops.setIn('first.second.value', 'anotherValue');
var result = setter(obj);
const result = setter(obj);
expect((0, _index.canMutate)(result)).to.be['false'];
expect(canMutate(result)).to.be.false;
expect(result).not.to.equal(obj);
expect(result.first.second.value).to.equal('anotherValue');
expect(result.maintain).to.be['true'];
expect(result.first.maintain).to.be['true'];
expect(result.first.second.maintain).to.be['true'];
expect(result.maintain).to.be.true;
expect(result.first.maintain).to.be.true;
expect(result.first.second.maintain).to.be.true;
});
it('sets a value in path but returns same object if no value changes', function () {
var obj = (0, _deepFreeze2['default'])({
it('sets a value in path but returns same object if no value changes', () => {
const obj = freeze({
first: {

@@ -360,3 +323,3 @@ second: {

var result = ops.setIn('first.second.value', 'value', obj);
const result = ops.setIn('first.second.value', 'value', obj);
expect(result).to.equal(obj);

@@ -367,88 +330,97 @@ });

describe('array', function () {
describe('batched mutations', function () {
it('push', function () {
var push = ops.push;
var arr = (0, _deepFreeze2['default'])([5, 4]);
var pusher = push((0, _deepFreeze2['default'])([1, 2, 3]));
var result = ops.batched(function () {
return pusher(arr);
});
describe('array', () => {
describe('batched mutations', () => {
const token = getBatchToken();
it('push', () => {
const push = ops.batch.push;
const arr = freeze([5, 4]);
const pusher = push(token, freeze([1, 2, 3]));
const result = pusher(arr);
expect(result).to.not.equal(arr);
expect(result).to.deep.equal([5, 4, 1, 2, 3]);
const result2 = push(token, [4, 5], result);
expect(result).to.equal(result2);
expect(result2).to.deep.equal([5, 4, 1, 2, 3, 4, 5]);
});
it('insert', function () {
var insert = ops.insert;
var arr = (0, _deepFreeze2['default'])([1, 2, 5]);
var inserter = insert(2, (0, _deepFreeze2['default'])([3, 4]));
var result = ops.batched(function () {
return inserter(arr);
});
it('insert', () => {
const insert = ops.batch.insert;
const arr = freeze([1, 2, 5]);
const inserter = insert(token, 2, freeze([3, 4]));
const result = inserter(arr);
expect(result).to.deep.equal([1, 2, 3, 4, 5]);
const result2 = ops.batch.insert(token, 2, [1000], result);
expect(result).to.equal(result2);
expect(result2).to.deep.equal([1, 2, 1000, 3, 4, 5]);
});
it('filter', function () {
var arr = (0, _deepFreeze2['default'])([0, 1, 2, 3]);
var result = undefined;
it('filter', () => {
const arr = freeze([0, 1, 2, 3]);
const result = ops.batch.filter(token, item => item % 2 === 0, arr);
expect(canMutate(result, token)).to.be.true;
ops.batched(function () {
result = ops.filter(function (item) {
return item % 2 === 0;
}, arr);
expect((0, _index.canMutate)(result)).to.be['true'];
});
expect(result).to.deep.equal([0, 2]);
expect(canMutate(result, getBatchToken())).to.be.false;
expect(result).to.deep.equal([0, 2]);
expect((0, _index.canMutate)(result)).to.be['false'];
const result2 = ops.batch.filter(token, item => item === 2, result);
expect(result2).to.equal(result);
expect(result2).to.deep.equal([2]);
});
it('set', function () {
var arr = (0, _deepFreeze2['default'])([1, 2, 987, 4]);
it('set', () => {
const arr = freeze([1, 2, 987, 4]);
var result = ops.batched(function () {
var setter = ops.set(2, 3);
var res = setter(arr);
expect((0, _index.canMutate)(res)).to.be['true'];
return res;
});
const setter = ops.batch.set(token, 2, 3);
const result = setter(arr);
expect(canMutate(result, token)).to.be.true;
expect((0, _index.canMutate)(result)).to.be['false'];
expect(canMutate(result, getBatchToken())).to.be.false;
expect(result).to.deep.equal([1, 2, 3, 4]);
const result2 = ops.batch.set(token, 2, 1000, result);
expect(result).to.equal(result2);
expect(result2).to.deep.equal([1, 2, 1000, 4]);
});
it('splice with deletions', function () {
var splice = ops.splice;
var arr = (0, _deepFreeze2['default'])([1, 2, 3, 3, 3, 4]);
var splicer = splice(2, 2, []);
it('splice with deletions', () => {
const splice = ops.batch.splice;
const arr = freeze([1, 2, 3, 3, 3, 4]);
const splicer = splice(token, 2, 2, []);
var result = ops.batched(function () {
return splicer(arr);
});
const result = splicer(arr);
expect(result).to.deep.equal([1, 2, 3, 4]);
const result2 = ops.batch.splice(token, 2, 1, [], result);
expect(result2).to.equal(result);
expect(result2).to.deep.equal([1, 2, 4]);
});
it('splice with additions', function () {
var splice = ops.splice;
var arr = (0, _deepFreeze2['default'])([1, 5]);
var splicer = splice(1, 0, [2, 3, 4]);
it('splice with additions', () => {
const splice = ops.batch.splice;
const arr = freeze([1, 5]);
const splicer = splice(token, 1, 0, [2, 3, 4]);
var result = ops.batched(function () {
return splicer(arr);
});
const result = splicer(arr);
expect(result).to.deep.equal([1, 2, 3, 4, 5]);
const result2 = ops.batch.splice(token, 0, 1, [1000], result);
expect(result).to.equal(result2);
expect(result2).to.deep.equal([1000, 2, 3, 4, 5]);
});
});
describe('immutable ops', function () {
it('push', function () {
var push = ops.push;
var arr = (0, _deepFreeze2['default'])([5, 4]);
var pusher = push((0, _deepFreeze2['default'])([1, 2, 3]));
var result = pusher(arr);
describe('immutable ops', () => {
it('push', () => {
const push = ops.push;
const arr = freeze([5, 4]);
const pusher = push(freeze([1, 2, 3]));
const result = pusher(arr);

@@ -460,7 +432,7 @@ expect(result).to.not.equal(arr);

it('insert', function () {
var insert = ops.insert;
var arr = (0, _deepFreeze2['default'])([1, 2, 5]);
var inserter = insert(2, (0, _deepFreeze2['default'])([3, 4]));
var result = inserter(arr);
it('insert', () => {
const insert = ops.insert;
const arr = freeze([1, 2, 5]);
const inserter = insert(2, freeze([3, 4]));
const result = inserter(arr);

@@ -470,52 +442,48 @@ expect(result).to.deep.equal([1, 2, 3, 4, 5]);

it('filter', function () {
var arr = (0, _deepFreeze2['default'])([0, 1, 2, 3]);
it('filter', () => {
const arr = freeze([0, 1, 2, 3]);
var result = ops.filter(function (item) {
return item % 2 === 0;
}, arr);
const result = ops.filter(item => item % 2 === 0, arr);
expect(result).to.deep.equal([0, 2]);
expect((0, _index.canMutate)(result)).to.be['false'];
expect(canMutate(result)).to.be.false;
});
it('filter with no effect should return initial array', function () {
var arr = (0, _deepFreeze2['default'])([0, 1, 2, 3]);
var result = ops.filter(function (item) {
return item < 4;
}, arr);
it('filter with no effect should return initial array', () => {
const arr = freeze([0, 1, 2, 3]);
const result = ops.filter(item => item < 4, arr);
expect(result).to.equal(arr);
});
it('set', function () {
var arr = (0, _deepFreeze2['default'])([1, 2, 987, 4]);
it('set', () => {
const arr = freeze([1, 2, 987, 4]);
var result = ops.set(2, 3, arr);
const result = ops.set(2, 3, arr);
expect((0, _index.canMutate)(result)).to.be['false'];
expect(canMutate(result)).to.be.false;
expect(result).to.deep.equal([1, 2, 3, 4]);
});
it('set with no effect should return initial array', function () {
var arr = (0, _deepFreeze2['default'])([1, 2, 3, 4]);
it('set with no effect should return initial array', () => {
const arr = freeze([1, 2, 3, 4]);
var result = ops.set(2, 3, arr);
const result = ops.set(2, 3, arr);
expect(result).to.equal(arr);
});
it('splice with deletions', function () {
var splice = ops.splice;
var arr = (0, _deepFreeze2['default'])([1, 2, 3, 3, 3, 4]);
var splicer = splice(2, 2, []);
it('splice with deletions', () => {
const splice = ops.splice;
const arr = freeze([1, 2, 3, 3, 3, 4]);
const splicer = splice(2, 2, []);
var result = splicer(arr);
const result = splicer(arr);
expect(result).to.deep.equal([1, 2, 3, 4]);
});
it('splice with additions', function () {
var splice = ops.splice;
var arr = (0, _deepFreeze2['default'])([1, 5]);
var splicer = splice(1, 0, [2, 3, 4]);
it('splice with additions', () => {
const splice = ops.splice;
const arr = freeze([1, 5]);
const splicer = splice(1, 0, [2, 3, 4]);
var result = splicer(arr);
const result = splicer(arr);

@@ -522,0 +490,0 @@ expect(result).to.deep.equal([1, 2, 3, 4, 5]);

{
"name": "immutable-ops",
"version": "0.4.2",
"version": "0.5.0",
"description": "A collection of functions to perform immutable operations on plain JavaScript objects",

@@ -18,17 +18,17 @@ "main": "lib/index.js",

"devDependencies": {
"babel": "^5.8.24",
"babel-core": "^5.8.24",
"babel-eslint": "^4.1.5",
"chai": "^3.0.0",
"babel-cli": "^6.18.0",
"babel-core": "^6.18.2",
"babel-eslint": "^7.1.0",
"chai": "^3.5.0",
"deep-freeze": "0.0.1",
"eslint": "^1.10.1",
"eslint-config-airbnb": "1.0.0",
"mocha": "^2.2.5",
"sinon": "^1.17.2",
"eslint": "^3.10.0",
"eslint-config-airbnb-base": "10.0.1",
"eslint-plugin-import": "^2.2.0",
"mocha": "^3.1.2",
"sinon": "^1.17.6",
"sinon-chai": "^2.8.0"
},
"dependencies": {
"lodash": "^4.2.1",
"ramda": "^0.19.1"
"ramda": "^0.22.1"
}
}

@@ -27,3 +27,3 @@ immutable-ops

import compose from 'ramda/src/compose';
import getOps from 'immutable-ops';
import ops from 'immutable-ops';

@@ -47,19 +47,5 @@ // These are all the available functions.

// Batch mutations
// Run a function batched
batched,
// Wrap a function to be executed as a batch
batch,
// Open a batch
open,
// Close a batch
close,
// Placeholder for currying.
__,
} = getOps({
// These are the default options.
curried: true
});
} = ops;

@@ -79,12 +65,18 @@ const arr = [1, 2, 3];

const result = pushFourAndFive(arr);
// Two new arrays were created during `pushFourAndFive` eecution.
// Two new arrays were created during `pushFourAndFive` execution.
expect(result).to.deep.equal([1, 2, 3, 4, 5]);
const batchedPushFourAndFive = ops.batch(pushFourAndFive);
const sameResult = batchedPushFourAndFive(arr);
// Only one new array is created during `batchedPushFourAndFive` execution.
// `immutable-ops` keeps track of objects mutated during the wrapped
// function, and applies the same operations with mutations to those objects.
expect(result).to.deep.equal([1, 2, 3, 4, 5]);
// Only one new array is created.
const sameResult = ops.batched(batchedOps => {
// batchedOps is able to keep track of mutated
// objects.
return compose(
batchedOps.push(5),
batchedOps.push(4)
)(arr);
});
expect(sameResult).to.deep.equal([1, 2, 3, 4, 5]);
```

@@ -94,40 +86,56 @@

You can run operations in a mutation batch by calling `ops.batched(func)` with a function, and you can create a batch-wrapped function with `const batchedFunc = ops.batch(funcToWrap)`.
A batch token is supplied by the user at the start of a batch, or created by `immutable-ops`. Each newly created object within a batch is tagged with that token. If a batch using token `X` operates on an object that is tagged with token `X`, it is free to mutate it. You can think of it as an ownership; the batch owns the newly created object and therefore is free to mutate it. New batches use a token `Y` that will never be equal to the previous token.
When `immutable-ops` creates a new object or array during batched mutations to preserve immutability, it tags it as a mutable object (by adding an unenumerable `@@_____canMutate` property) and pushes its reference to an array of `mutatedObjects`. All consecutive functions applied will execute a mutating operations for objects that have the tag. This applies for tagged objects found in nested structures too.
Tags are not removed; They are assigned to a non-enumerable property `@@_______immutableOpsOwnerID` which should avoid any collisions.
When the function finishes executing, `immutable-ops` loops through the `mutatedObjects` array, removing the tag properties from each object, and clearing the `mutatedObjects` array.
This token strategy is similar to what ImmutableJS uses to track batches.
The overhead of keeping track of mutated objects should be a sufficient tradeoff to creating lots of new objects to applyi multiple consecutive operations, unless you're working on a really big set of data.
**Manually using batch tokens**
## Currying
`ops.batch` gives you access to all the `immutable-ops` functions that take a token as their additional first argument. Otherwise they are identical to the functions found in `ops` directly.
All operations are curried by default. If you don't want them to be curried, pass `{ curried: false }` to `getImmutableOps()`. Functions are curried with `ramda.curry`. In addition to normal currying behaviour, you can use the `ramda` placeholder variable available in `ops.__` to specify parameters you want to pass arguments for later. Example:
```javascript
const removeNFromHead = ops.splice(/* startIndex */ 0, /* deleteCount */ops.__, /* valsToAdd */[]);
const removeTwoFromHead = removeNFromHead(2);
const arr = [1, 2, 3];
import ops from 'immutable-ops';
const token = ops.getBatchToken();
console.log(removeTwoFromHead(arr));
// [3];
// This object has no batch token, since it was not created by immutable-ops.
const obj = {a: 1, b: 2};
// obj2 is a newly created object tagged with the token.
const obj2 = ops.batch.set(token, 'a', 10, obj);
expect(obj).to.not.equal(obj2)
// Because we operate on obj2 that has the same token as
// we passed to the function, obj2 is mutated.
const obj3 = ops.batch.set(token, 'b', 20, obj2);
expect(obj2).to.equal(obj3);
```
## Batched Mutations API
### batched(functionToRun)
**Handling batch tokens implicitly**
Executes `functionToRun` as a batched mutation and returns the return value of `functionToRun`.`functionToRun` will be called without arguments. During `functionToRun` execution, `immutable-ops` will keep track of new objects created during operations, and apply further operations with mutations to those objects.
```javascript
import ops from 'immutable-ops';
### batch(functionToWrap)
const obj = {a: 1, b: 2};
Like `batched`, but returns a function that wraps `functionToWrap` to be executed as a batch. `functionToWrap` is also curried. When `functionToWrap` is executed (all arguments are passed), all operations run during its execution will apply mutations instead of creating new objects whenever possible.
const obj3 = ops.batched(batchedOps => {
// batchedOps has functions that are bound to a new batch token.
const obj2 = batchedOps.set('a', 10, obj);
return batchedOps.set('b', 20, obj2);
});
```
### open()
## Currying
Opens a batch session. From this point on, any operations done through the `ops` instance that `open` was called from will be applied mutatively **if** the object it's operating on was created after opening the session.
All operations are curried by default. Functions are curried with `ramda.curry`. In addition to normal currying behaviour, you can use the `ramda` placeholder variable available in `ops.__` to specify parameters you want to pass arguments for later. Example:
### close()
```javascript
const removeNFromHead = ops.splice(/* startIndex */ 0, /* deleteCount */ops.__, /* valsToAdd */[]);
const removeTwoFromHead = removeNFromHead(2);
const arr = [1, 2, 3];
Closes the current batch session.
console.log(removeTwoFromHead(arr));
// [3];
```

@@ -318,4 +326,21 @@ ## Object API

## Changelog
## 0.5.0: Major Changes
- **BREAKING**: No `getImmutableOps` function, which was the main export, is exported anymore because options were removed. Now the object containing the operation functions is exported directly.
- **BREAKING**: removed option to choose whether operations are curried. Functions are now always curried.
- **BREAKING**: former batched mutations API totally replaced.
- **BREAKING**: batched mutations implementation changed.
Previously newly created objects were tagged with a "can mutate" tag, and references to those objects were kept in a list. After the batch was finished, the list was processed by removing the tags from each object in the list.
Now a batch token is created at the start of a batch (or supplied by the user). Each newly created object is tagged with that token. If a batch using token `X` operates on an object that is tagged with token `X`, it is free to mutate it. New batches use a token `Y` that will never be equal to the previous token.
Tags are not removed anymore; They are assigned to a non-enumerable property `@@_______immutableOpsOwnerID` which should avoid any collisions.
This token strategy is similar to what ImmutableJS uses to track batches.
## License
MIT. See `LICENSE`

@@ -1,9 +0,22 @@

import forOwn from 'lodash/forOwn';
import isArrayLike from 'lodash/isArrayLike';
import curry from 'ramda/src/curry';
import wrap from 'ramda/src/wrap';
import placeholder from 'ramda/src/__';
const MUTABILITY_TAG = '@@_______canMutate';
function forOwn(obj, fn) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
fn(obj[key], key);
}
}
}
function isArrayLike(value) {
return value
&& typeof value === 'object'
&& typeof value.length === 'number'
&& value.length >= 0
&& value.length % 1 === 0;
}
const OWNER_ID_TAG = '@@_______immutableOpsOwnerID';
function fastArrayCopy(arr) {

@@ -17,9 +30,16 @@ const copied = new Array(arr.length);

export function canMutate(obj) {
return obj.hasOwnProperty(MUTABILITY_TAG);
export function canMutate(obj, ownerID) {
if (!ownerID) return false;
return obj[OWNER_ID_TAG] === ownerID;
}
function addCanMutateTag(opts, obj) {
Object.defineProperty(obj, MUTABILITY_TAG, {
value: true,
const newOwnerID = typeof Symbol === 'function'
? () => Symbol('ownerID')
: () => ({});
export const getBatchToken = newOwnerID;
function addOwnerID(obj, ownerID) {
Object.defineProperty(obj, OWNER_ID_TAG, {
value: ownerID,
configurable: true,

@@ -29,17 +49,9 @@ enumerable: false,

opts.batchManager.addMutated(obj);
return obj;
}
function removeCanMutateTag(obj) {
delete obj[MUTABILITY_TAG];
return obj;
}
function prepareNewObject(opts, instance) {
if (opts.batchManager.isWithMutations()) {
addCanMutateTag(opts, instance);
function prepareNewObject(instance, ownerID) {
if (ownerID) {
addOwnerID(instance, ownerID);
}
opts.createdObjects++;
return instance;

@@ -68,3 +80,3 @@ }

function mutableSet(opts, key, value, obj) {
function mutableSet(key, value, obj) {
obj[key] = value;

@@ -74,3 +86,3 @@ return obj;

function mutableSetIn(opts, _pathArg, value, obj) {
function mutableSetIn(_pathArg, value, obj) {
const originalPathArg = normalizePath(_pathArg);

@@ -89,3 +101,3 @@

const newObj = {};
prepareNewObject(opts, newObj);
prepareNewObject(newObj, null);
acc[curr] = newObj;

@@ -106,3 +118,3 @@ return newObj;

function valueInPath(opts, _pathArg, obj) {
function valueInPath(_pathArg, obj) {
const pathArg = normalizePath(_pathArg);

@@ -126,10 +138,17 @@

function immutableSetIn(opts, _pathArg, value, obj) {
function immutableSetIn(ownerID, _pathArg, value, obj) {
const pathArg = normalizePath(_pathArg);
const currentValue = valueInPath(opts, pathArg, obj);
const currentValue = valueInPath(pathArg, obj);
if (value === currentValue) return obj;
const pathLen = pathArg.length;
let acc = Object.assign(prepareNewObject(opts, {}), obj);
let acc;
if (canMutate(obj, ownerID)) {
acc = obj;
} else {
acc = Object.assign(prepareNewObject({}, ownerID), obj);
}
const rootObj = acc;

@@ -147,6 +166,6 @@

if (currType === 'object') {
if (canMutate(currRef)) {
if (canMutate(currRef, ownerID)) {
acc = currRef;
} else {
const newObj = prepareNewObject(opts, {});
const newObj = prepareNewObject({}, ownerID);
acc[curr] = Object.assign(newObj, currRef);

@@ -159,3 +178,3 @@ acc = newObj;

if (currType === 'undefined') {
const newObj = prepareNewObject(opts, {});
const newObj = prepareNewObject({}, ownerID);
acc[curr] = newObj;

@@ -173,6 +192,6 @@ acc = newObj;

function mutableMerge(isDeep, opts, _mergeObjs, baseObj) {
function mutableMerge(isDeep, _mergeObjs, baseObj) {
const mergeObjs = forceArray(_mergeObjs);
if (opts.deep) {
if (isDeep) {
mergeObjs.forEach(mergeObj => {

@@ -183,5 +202,3 @@ forOwn(mergeObj, (value, key) => {

if (typeof value === 'object') {
assignValue = canMutate(value)
? mutableMerge(isDeep, opts, [value], baseObj[key])
: immutableMerge(isDeep, opts, [value], baseObj[key]); // eslint-disable-line
assignValue = mutableMerge(isDeep, [value], baseObj[key]);
} else {

@@ -207,3 +224,3 @@ assignValue = value;

function mutableOmit(opts, _keys, obj) {
function mutableOmit(_keys, obj) {
const keys = forceArray(_keys);

@@ -220,4 +237,4 @@ keys.forEach(key => {

function immutableMerge(isDeep, opts, _mergeObjs, obj) {
if (canMutate(obj)) return mutableMerge(isDeep, opts, _mergeObjs, obj);
function immutableMerge(isDeep, ownerID, _mergeObjs, obj) {
if (canMutate(obj, ownerID)) return mutableMerge(isDeep, _mergeObjs, obj);
const mergeObjs = forceArray(_mergeObjs);

@@ -232,3 +249,3 @@

nextObject = Object.assign({}, obj);
prepareNewObject(opts, nextObject);
prepareNewObject(nextObject, ownerID);
}

@@ -243,3 +260,3 @@ };

if (_shouldMergeKey(nextObject, mergeObj, key)) {
const recursiveMergeResult = immutableMerge(isDeep, opts, mergeValue, currentValue);
const recursiveMergeResult = immutableMerge(isDeep, ownerID, mergeValue, currentValue);

@@ -267,4 +284,4 @@ if (recursiveMergeResult !== currentValue) {

function immutableArrSet(opts, index, value, arr) {
if (canMutate(arr)) return mutableSet(opts, index, value, arr);
function immutableArrSet(ownerID, index, value, arr) {
if (canMutate(arr, ownerID)) return mutableSet(index, value, arr);

@@ -275,3 +292,3 @@ if (arr[index] === value) return arr;

newArr[index] = value;
prepareNewObject(opts, newArr);
prepareNewObject(newArr, ownerID);

@@ -281,5 +298,5 @@ return newArr;

function immutableSet(opts, key, value, obj) {
if (isArrayLike(obj)) return immutableArrSet(opts, key, value, obj);
if (canMutate(obj)) return mutableSet(opts, key, value, obj);
function immutableSet(ownerID, key, value, obj) {
if (isArrayLike(obj)) return immutableArrSet(ownerID, key, value, obj);
if (canMutate(obj, ownerID)) return mutableSet(key, value, obj);

@@ -289,3 +306,3 @@ if (obj[key] === value) return obj;

const newObj = Object.assign({}, obj);
prepareNewObject(opts, newObj);
prepareNewObject(newObj, ownerID);
newObj[key] = value;

@@ -295,4 +312,4 @@ return newObj;

function immutableOmit(opts, _keys, obj) {
if (canMutate(obj)) return mutableOmit(opts, _keys, obj);
function immutableOmit(ownerID, _keys, obj) {
if (canMutate(obj, ownerID)) return mutableOmit(_keys, obj);

@@ -309,7 +326,7 @@ const keys = forceArray(_keys);

});
prepareNewObject(opts, newObj);
prepareNewObject(newObj, ownerID);
return newObj;
}
function mutableArrPush(opts, _vals, arr) {
function mutableArrPush(_vals, arr) {
const vals = forceArray(_vals);

@@ -320,3 +337,3 @@ arr.push(...vals);

function mutableArrFilter(opts, func, arr) {
function mutableArrFilter(func, arr) {
let currIndex = 0;

@@ -337,3 +354,3 @@ let originalIndex = 0;

function mutableArrSplice(opts, index, deleteCount, _vals, arr) {
function mutableArrSplice(index, deleteCount, _vals, arr) {
const vals = forceArray(_vals);

@@ -344,12 +361,12 @@ arr.splice(index, deleteCount, ...vals);

function mutableArrInsert(opts, index, _vals, arr) {
return mutableArrSplice(opts, index, 0, _vals, arr);
function mutableArrInsert(index, _vals, arr) {
return mutableArrSplice(index, 0, _vals, arr);
}
function immutableArrSplice(opts, index, deleteCount, _vals, arr) {
if (canMutate(arr)) return mutableArrSplice(opts, index, deleteCount, _vals, arr);
function immutableArrSplice(ownerID, index, deleteCount, _vals, arr) {
if (canMutate(arr, ownerID)) return mutableArrSplice(index, deleteCount, _vals, arr);
const vals = forceArray(_vals);
const newArr = arr.slice();
prepareNewObject(opts, newArr);
prepareNewObject(newArr, ownerID);
newArr.splice(index, deleteCount, ...vals);

@@ -360,13 +377,13 @@

function immutableArrInsert(opts, index, _vals, arr) {
if (canMutate(arr)) return mutableArrInsert(opts, index, _vals, arr);
return immutableArrSplice(opts, index, 0, _vals, arr);
function immutableArrInsert(ownerID, index, _vals, arr) {
if (canMutate(arr, ownerID)) return mutableArrInsert(index, _vals, arr);
return immutableArrSplice(ownerID, index, 0, _vals, arr);
}
function immutableArrPush(opts, vals, arr) {
return immutableArrInsert(opts, arr.length, vals, arr);
function immutableArrPush(ownerID, vals, arr) {
return immutableArrInsert(ownerID, arr.length, vals, arr);
}
function immutableArrFilter(opts, func, arr) {
if (canMutate(arr)) return mutableArrFilter(opts, func, arr);
function immutableArrFilter(ownerID, func, arr) {
if (canMutate(arr, ownerID)) return mutableArrFilter(func, arr);
const newArr = arr.filter(func);

@@ -376,7 +393,7 @@

prepareNewObject(opts, newArr);
prepareNewObject(newArr, ownerID);
return newArr;
}
const operations = {
const immutableOperations = {
// object operations

@@ -396,120 +413,68 @@ merge: immutableShallowMerge,

set: immutableSet,
};
mutable: {
// object operations
merge: mutableShallowMerge,
deepMerge: mutableDeepMerge,
omit: mutableOmit,
setIn: mutableSetIn,
const mutableOperations = {
// object operations
merge: mutableShallowMerge,
deepMerge: mutableDeepMerge,
omit: mutableOmit,
setIn: mutableSetIn,
// array operations
insert: mutableArrInsert,
push: mutableArrPush,
filter: mutableArrFilter,
splice: mutableArrSplice,
// array operations
insert: mutableArrInsert,
push: mutableArrPush,
filter: mutableArrFilter,
splice: mutableArrSplice,
// both
set: mutableSet,
},
// both
set: mutableSet,
};
function bindOperationsToOptions(opsObj, opts) {
const boundOperations = {};
forOwn(opsObj, (value, key) => {
if (typeof value === 'object') {
boundOperations[key] = bindOperationsToOptions(value, opts);
} else {
boundOperations[key] = value.bind(null, opts);
export function getImmutableOps() {
const immutableOps = Object.assign({}, immutableOperations);
forOwn(immutableOps, (value, key) => {
immutableOps[key] = curry(value.bind(null, null));
});
if (opts.curried) {
boundOperations[key] = curry(boundOperations[key]);
}
}
const mutableOps = Object.assign({}, mutableOperations);
forOwn(mutableOps, (value, key) => {
mutableOps[key] = curry(value);
});
return boundOperations;
}
const batchOps = Object.assign({}, immutableOperations);
forOwn(batchOps, (value, key) => {
batchOps[key] = curry(value);
});
function getBatchManager() {
const previousSessionStack = [];
let currMutatedObjects = null;
let objectsCreated = 0;
function batched(_token, _fn) {
let token;
let fn;
return {
open() {
if (currMutatedObjects !== null) {
previousSessionStack.push(currMutatedObjects);
}
currMutatedObjects = [];
},
if (typeof _token === 'function') {
fn = _token;
token = getBatchToken();
} else {
token = _token;
fn = _fn;
}
isWithMutations() {
return currMutatedObjects !== null;
},
const immutableOpsBoundToToken = Object.assign({}, immutableOperations);
forOwn(immutableOpsBoundToToken, (value, key) => {
immutableOpsBoundToToken[key] = curry(value.bind(null, token));
});
return fn(immutableOpsBoundToToken);
}
addMutated(obj) {
currMutatedObjects.push(obj);
objectsCreated++;
},
getMutatedObjects() {
return currMutatedObjects;
},
getObjectsCreatedCount() {
return objectsCreated;
},
close() {
if (currMutatedObjects !== null) {
currMutatedObjects.forEach(removeCanMutateTag);
if (previousSessionStack.length) {
currMutatedObjects = previousSessionStack.pop();
} else {
currMutatedObjects = null;
}
objectsCreated = 0;
}
},
};
return Object.assign(immutableOps, {
mutable: mutableOps,
batch: batchOps,
batched,
__: placeholder,
getBatchToken,
});
}
export default function getImmutableOps(userOpts) {
const defaultOpts = {
curried: true,
batchManager: getBatchManager(),
};
export const ops = getImmutableOps();
const opts = Object.assign({ createdObjects: 0 }, defaultOpts, (userOpts || {}));
const boundOperations = bindOperationsToOptions(operations, opts);
function batchWrapper() {
const func = arguments[0];
const args = Array.prototype.slice.call(arguments, 1);
opts.batchManager.open();
const returnValue = func.apply(null, args);
opts.batchManager.close();
return returnValue;
}
boundOperations.batched = batchWrapper;
boundOperations.batch = wrap(placeholder, batchWrapper);
boundOperations.createdObjectsCount = () => opts.createdObjects;
boundOperations.getMutatedObjects = opts.batchManager.getMutatedObjects;
boundOperations.__ = placeholder;
boundOperations.open = opts.batchManager.open;
boundOperations.close = opts.batchManager.close;
boundOperations.getBatchManager = getBatchManager;
boundOperations.useBatchManager = manager => {
opts.batchManager.close();
opts.batchManager = manager;
boundOperations.open = manager.open;
boundOperations.close = manager.close;
boundOperations.getMutatedObjects = manager.getMutatedObjects;
};
return boundOperations;
}
export default ops;
import chai from 'chai';
import sinonChai from 'sinon-chai';
import getOps, { canMutate } from '../index';
import freeze from 'deep-freeze';
import compose from 'ramda/src/compose';
import { ops, canMutate, getBatchToken } from '../index';
chai.use(sinonChai);
const { expect } = chai;
describe('operations', () => {
let ops;
describe('batched', () => {
it('works', () => {
const res = ops.batched(batchOps => {
const obj = {};
const result = batchOps.set('a', 1, obj);
expect(result).to.deep.equal({ a: 1 });
expect(result).not.to.equal(obj);
beforeEach(() => {
ops = getOps();
});
it('wrapBatched', () => {
const pushFour = ops.push(4);
const pushFive = ops.push(5);
const arr = freeze([1, 2, 3]);
const pusher = ops.batch(compose(pushFive, pushFour));
expect(pusher).to.be.a('function');
const result = pusher(arr);
expect(result).to.deep.equal([1, 2, 3, 4, 5]);
});
it('useBatchManager', () => {
const myManager = ops.getBatchManager();
ops.useBatchManager(myManager);
const pusher = ops.push(0);
const batchOperation = ops.batch((arr) => {
const first = pusher(arr);
expect(ops.getMutatedObjects()).to.equal(myManager.getMutatedObjects());
expect(ops.getMutatedObjects()[0]).to.equal(first);
return first;
const result2 = batchOps.omit('a', result);
expect(result2).to.equal(result);
expect(result2).to.deep.equal({});
return result2;
});
batchOperation([]);
expect(res).to.deep.equal({});
});
});
describe('operations', () => {
describe('object', () => {
describe('batched mutations', () => {
const token = getBatchToken();
it('deepMerges', () => {

@@ -63,12 +49,9 @@ const baseObj = freeze({

});
let result;
const merger = ops.deepMerge(mergeObj);
ops.batched(() => {
result = merger(baseObj);
expect(canMutate(result)).to.be.true;
expect(canMutate(result.deeper)).to.be.true;
});
const merger = ops.batch.deepMerge(token, mergeObj);
const result = merger(baseObj);
expect(canMutate(result, token)).to.be.true;
expect(canMutate(result.deeper, token)).to.be.true;
expect(canMutate(result)).to.be.false;
expect(canMutate(result.deeper)).to.be.false;
expect(canMutate(result, getBatchToken())).to.be.false;
expect(canMutate(result.deeper, getBatchToken())).to.be.false;

@@ -93,12 +76,12 @@ expect(result).to.not.equal(baseObj);

let result;
const omitter = ops.omit('age');
const omitter = ops.batch.omit(token, 'age');
ops.batched(() => {
result = omitter(obj);
expect(canMutate(result)).to.be.true;
});
const result = omitter(obj);
expect(canMutate(result, token)).to.be.true;
expect(canMutate(result)).to.be.false;
expect(canMutate(result, getBatchToken())).to.be.false;
expect(result).to.not.contain.keys(['age']);
// Further modification should mutate the existing object.
expect(ops.batch.omit(token, 'name', result)).to.equal(result);
});

@@ -112,13 +95,12 @@

let result;
const omitter = ops.batch.omit(token, ['age']);
const result = omitter(obj);
const omitter = ops.omit(['age']);
ops.batched(() => {
result = omitter(obj);
expect(canMutate(result, token)).to.be.true;
expect(canMutate(result)).to.be.true;
});
expect(canMutate(result, getBatchToken())).to.be.false;
expect(result).to.not.contain.keys(['age']);
expect(canMutate(result)).to.be.false;
expect(result).to.not.contain.keys(['age']);
// Further modification should mutate the existing object.
expect(ops.batch.omit(token, ['name'], result)).to.equal(result);
});

@@ -133,10 +115,7 @@

let result;
ops.batched(() => {
result = ops.set('two', 5, obj);
const result = ops.batch.set(token, 'two', 5, obj);
expect(canMutate(result)).to.be.true;
result = ops.set('two', 2, result);
});
expect(result).to.deep.equal({
expect(canMutate(result, token)).to.be.true;
const result2 = ops.batch.set(token, 'two', 2, result);
expect(result2).to.deep.equal({
one: 1,

@@ -146,2 +125,4 @@ two: 2,

});
expect(result).to.equal(result2);
});

@@ -160,13 +141,9 @@

});
let result;
const setter = ops.setIn('first.second.value', 'anotherValue');
const setter = ops.batch.setIn(token, 'first.second.value', 'anotherValue');
ops.batched(() => {
result = setter(obj);
const result = setter(obj);
expect(canMutate(result, token)).to.be.true;
expect(canMutate(result)).to.be.true;
});
expect(canMutate(result)).to.be.false;
expect(canMutate(result, getBatchToken())).to.be.false;
expect(result).not.to.equal(obj);

@@ -177,2 +154,6 @@ expect(result.first.second.value).to.equal('anotherValue');

expect(result.first.second.maintain).to.be.true;
const result2 = ops.batch.setIn(token, 'first.second.value', 'secondAnotherValue', result);
expect(result).to.equal(result2);
expect(result2.first.second.value).to.equal('secondAnotherValue');
});

@@ -335,9 +316,11 @@ });

describe('array', () =>{
describe('array', () => {
describe('batched mutations', () => {
const token = getBatchToken();
it('push', () => {
const push = ops.push;
const push = ops.batch.push;
const arr = freeze([5, 4]);
const pusher = push(freeze([1, 2, 3]));
const result = ops.batched(() => pusher(arr));
const pusher = push(token, freeze([1, 2, 3]));
const result = pusher(arr);

@@ -347,11 +330,19 @@ expect(result).to.not.equal(arr);

expect(result).to.deep.equal([5, 4, 1, 2, 3]);
const result2 = push(token, [4, 5], result);
expect(result).to.equal(result2);
expect(result2).to.deep.equal([5, 4, 1, 2, 3, 4, 5]);
});
it('insert', () => {
const insert = ops.insert;
const insert = ops.batch.insert;
const arr = freeze([1, 2, 5]);
const inserter = insert(2, freeze([3, 4]));
const result = ops.batched(() => inserter(arr));
const inserter = insert(token, 2, freeze([3, 4]));
const result = inserter(arr);
expect(result).to.deep.equal([1, 2, 3, 4, 5]);
const result2 = ops.batch.insert(token, 2, [1000], result);
expect(result).to.equal(result2);
expect(result2).to.deep.equal([1, 2, 1000, 3, 4, 5]);
});

@@ -361,11 +352,11 @@

const arr = freeze([0, 1, 2, 3]);
let result;
const result = ops.batch.filter(token, item => item % 2 === 0, arr);
expect(canMutate(result, token)).to.be.true;
ops.batched(() => {
result = ops.filter(item => item % 2 === 0, arr);
expect(canMutate(result)).to.be.true;
});
expect(result).to.deep.equal([0, 2]);
expect(canMutate(result, getBatchToken())).to.be.false;
expect(result).to.deep.equal([0, 2]);
expect(canMutate(result)).to.be.false;
const result2 = ops.batch.filter(token, item => item === 2, result);
expect(result2).to.equal(result);
expect(result2).to.deep.equal([2]);
});

@@ -376,31 +367,40 @@

const result = ops.batched(() => {
const setter = ops.set(2, 3);
const res = setter(arr);
expect(canMutate(res)).to.be.true;
return res;
});
const setter = ops.batch.set(token, 2, 3);
const result = setter(arr);
expect(canMutate(result, token)).to.be.true;
expect(canMutate(result)).to.be.false;
expect(canMutate(result, getBatchToken())).to.be.false;
expect(result).to.deep.equal([1, 2, 3, 4]);
const result2 = ops.batch.set(token, 2, 1000, result);
expect(result).to.equal(result2);
expect(result2).to.deep.equal([1, 2, 1000, 4]);
});
it('splice with deletions', () => {
const splice = ops.splice;
const splice = ops.batch.splice;
const arr = freeze([1, 2, 3, 3, 3, 4]);
const splicer = splice(2, 2, []);
const splicer = splice(token, 2, 2, []);
const result = ops.batched(() => splicer(arr));
const result = splicer(arr);
expect(result).to.deep.equal([1, 2, 3, 4]);
const result2 = ops.batch.splice(token, 2, 1, [], result);
expect(result2).to.equal(result);
expect(result2).to.deep.equal([1, 2, 4]);
});
it('splice with additions', () => {
const splice = ops.splice;
const splice = ops.batch.splice;
const arr = freeze([1, 5]);
const splicer = splice(1, 0, [2, 3, 4]);
const splicer = splice(token, 1, 0, [2, 3, 4]);
const result = ops.batched(() => splicer(arr));
const result = splicer(arr);
expect(result).to.deep.equal([1, 2, 3, 4, 5]);
const result2 = ops.batch.splice(token, 0, 1, [1000], result);
expect(result).to.equal(result2);
expect(result2).to.deep.equal([1000, 2, 3, 4, 5]);
});

@@ -407,0 +407,0 @@ });

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc