├── .travis.yml ├── .gitignore ├── CHANGELOG.md ├── Makefile ├── LICENSE ├── package.json ├── icepick.min.js ├── icepick.dev.js ├── icepick.js ├── README.md └── icepick.test.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | sudo: false 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .project 4 | .DS_Store 5 | .settings 6 | .externalToolBuilders 7 | .nyc_output 8 | coverage 9 | tmp 10 | npm-debug.log 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v2.4.0 2 | - Handle objects with `null` prototypes 3 | 4 | # v2.3.1 5 | - Add a LICENSE file 6 | 7 | # v2.3.0 8 | - Added `dissocIn`/`unsetIn`, the analog to `assocIn`/`setIne` 9 | 10 | # v2.2.1 11 | - Make `map` and `filter` work properly with `chain` 12 | 13 | # v2.2.0 14 | - Compile `icepick.min.js` and `icepick.dev.js` down to ES5. 15 | 16 | # v2.1.1 17 | - Fix readme display on npmjs.org 18 | 19 | # v2.1.0 20 | - Improved performance 21 | - Added `icepick.min.js` and `icepick.dev.js` for direct use in browsers 22 | 23 | # v2.0.0 24 | - Breaking: require use of ES6, drop support for ES5-only environments 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | MAKEFLAGS += --no-print-directory --silent 3 | export PATH := ./node_modules/.bin/:$(PATH):./bin/ 4 | 5 | setup: 6 | npm install --quiet > /dev/null; true 7 | 8 | default: ci 9 | 10 | lint: setup 11 | standard icepick.js icepick.test.js 12 | 13 | dev: test 14 | 15 | watch: dev 16 | 17 | test: setup 18 | tap icepick.test.js -R spec --100 19 | 20 | coverage: test 21 | 22 | pre-commit: lint 23 | 24 | ci: lint test 25 | 26 | .PHONY: release-patch release-minor release-major 27 | release-patch release-minor release-major: ci 28 | git push 29 | npm version $(@:release-%=%) 30 | npm publish 31 | 32 | .PHONY: ci clean dev doc help lint release setup test 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2105 Alexander Early 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icepick", 3 | "version": "2.4.0", 4 | "description": "Utilities for treating frozen JavaScript objects as persistent immutable collections.", 5 | "main": "icepick.js", 6 | "scripts": { 7 | "dist:prod": "buble icepick.js | browserify - --standalone icepick -g [ envify purge --NODE_ENV production ] | uglifyjs -mc > icepick.min.js", 8 | "dist:dev": "buble icepick.js | browserify - --standalone icepick -g [ envify purge --NODE_ENV development ] | uglifyjs -mc > icepick.dev.js", 9 | "preversion": "npm run dist:prod && npm run dist:dev", 10 | "pretest": "standard icepick.js icepick.test.js", 11 | "test": "tap icepick.test.js -R spec --100" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "ssh://git@github.com/aearly/icepick" 16 | }, 17 | "keywords": [ 18 | "freeze", 19 | "frozen", 20 | "immutable", 21 | "immutability", 22 | "persistent", 23 | "collections", 24 | "structural", 25 | "sharing", 26 | "update", 27 | "redux", 28 | "flux", 29 | "store", 30 | "react", 31 | "mori", 32 | "clojure" 33 | ], 34 | "author": "Alexander Early ", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/aearly/icepick/issues" 38 | }, 39 | "homepage": "https://github.com/aearly/icepick", 40 | "devDependencies": { 41 | "browserify": "^14.4.0", 42 | "buble": "^0.16.0", 43 | "envify": "^4.1.0", 44 | "standard": "^10.0.2", 45 | "tap": "^10.7.0", 46 | "uglify-es": "^3.0.25" 47 | }, 48 | "dependencies": {} 49 | } 50 | -------------------------------------------------------------------------------- /icepick.min.js: -------------------------------------------------------------------------------- 1 | !function(n){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).icepick=n()}}(function(){return function n(r,e,t){function o(u,i){if(!e[u]){if(!r[u]){var s="function"==typeof require&&require;if(!i&&s)return s(u,!0);if(c)return c(u,!0);var f=new Error("Cannot find module '"+u+"'");throw f.code="MODULE_NOT_FOUND",f}var a=e[u]={exports:{}};r[u][0].call(a.exports,function(n){var e=r[u][1][n];return o(e||n)},a,a.exports,n,r,e,t)}return e[u].exports}for(var c="function"==typeof require&&require,u=0;u0;)r[e]=arguments[e+1];var t=[].concat(n),o=r.map(d);return t.splice.apply(t,o),h(t)},e.slice=function(n,r,e){var t=n.slice(r,e);return h(t)},["map","filter"].forEach(function(n){e[n]=function(r,e){var t=e[n](r);return h(t)},e[n].displayName="icepick."+n}),e.extend=e.assign=function(n){for(var r=[],e=arguments.length-1;e-- >0;)r[e]=arguments[e+1];var t=r.reduce(o,n);return h(t)},e.merge=c;var y={value:function(){return this.val},thru:function(n){return this.val=d(n(this.val)),this}};Object.keys(e).forEach(function(n){n.match(/^(map|filter)$/)?y[n]=function(r){return this.val=e[n](r,this.val),this}:y[n]=function(){for(var r=[],t=arguments.length;t--;)r[t]=arguments[t];return this.val=e[n].apply(e,[this.val].concat(r)),this}}),e.chain=function(n){var r=Object.create(y);return r.val=n,r}},{}]},{},[1])(1)}); 2 | -------------------------------------------------------------------------------- /icepick.dev.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).icepick=e()}}(function(){return function e(n,r,t){function o(u,i){if(!r[u]){if(!n[u]){var f="function"==typeof require&&require;if(!i&&f)return f(u,!0);if(c)return c(u,!0);var s=new Error("Cannot find module '"+u+"'");throw s.code="MODULE_NOT_FOUND",s}var a=r[u]={exports:{}};n[u][0].call(a.exports,function(e){var r=n[u][1][e];return o(r||e)},a,a.exports,e,n,r,t)}return r[u].exports}for(var c="function"==typeof require&&require,u=0;u0;)n[r]=arguments[r+1];var t=[].concat(e),o=n.map(h);return t.splice.apply(t,o),v(t)},r.slice=function(e,n,r){var t=e.slice(n,r);return v(t)},["map","filter"].forEach(function(e){r[e]=function(n,r){var t=r[e](n);return v(t)},r[e].displayName="icepick."+e}),r.extend=r.assign=function(e){for(var n=[],r=arguments.length-1;r-- >0;)n[r]=arguments[r+1];var t=n.reduce(o,e);return v(t)},r.merge=c;var b={value:function(){return this.val},thru:function(e){return this.val=h(e(this.val)),this}};Object.keys(r).forEach(function(e){e.match(/^(map|filter)$/)?b[e]=function(n){return this.val=r[e](n,this.val),this}:b[e]=function(){for(var n=[],t=arguments.length;t--;)n[t]=arguments[t];return this.val=r[e].apply(r,[this.val].concat(n)),this}}),r.chain=function(e){var n=Object.create(b);return n.val=e,n}},{}]},{},[1])(1)}); 2 | -------------------------------------------------------------------------------- /icepick.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This allows you to work with object hierarchies that have been frozen 3 | * with Object.freeze(). "get" operations can use the normal JS syntax, 4 | * but operations that modify the data will have to return partial copies of 5 | * the structure. The portions of the structure that did not change will 6 | * === their previous values. 7 | * 8 | * Inspired by clojure/mori and Immutable.js 9 | */ 10 | 11 | 'use strict' 12 | 13 | const i = exports 14 | 15 | const identity = coll => coll 16 | 17 | // we only care about objects or arrays for now 18 | const weCareAbout = val => val !== null && 19 | (Array.isArray(val) || 20 | // This will skip objects created with `new Foo()` 21 | // and objects created with `Object.create(proto)` 22 | // The benefit is ignoring DOM elements and event emitters, 23 | // which are often circular. 24 | isObjectLike(val)) 25 | 26 | const isObjectLike = val => typeof val === 'object' && 27 | (val.constructor === Object || 28 | val.constructor == null) && 29 | (Object.getPrototypeOf(val) === Object.prototype || 30 | Object.getPrototypeOf(val) === null) 31 | 32 | const forKeys = (obj, iter) => { 33 | let idx, keys 34 | if (Array.isArray(obj)) { 35 | idx = obj.length 36 | while (idx--) { 37 | iter(idx) 38 | } 39 | return 40 | } 41 | keys = Object.keys(obj) 42 | idx = keys.length 43 | while (idx--) { 44 | iter(keys[idx]) 45 | } 46 | } 47 | 48 | const cloneObj = obj => { 49 | const newObj = obj.constructor == null ? Object.create(null) : {} 50 | const keys = Object.keys(obj) 51 | let idx = keys.length 52 | let key 53 | while (idx--) { 54 | key = keys[idx] 55 | newObj[key] = obj[key] 56 | } 57 | return newObj 58 | } 59 | 60 | const clone = (coll) => { 61 | if (Array.isArray(coll)) { 62 | return coll.slice() 63 | } else { 64 | return cloneObj(coll) 65 | } 66 | } 67 | 68 | const freezeIfNeeded = process.env.NODE_ENV === 'production' 69 | ? identity 70 | : coll => { 71 | if (weCareAbout(coll) && !Object.isFrozen(coll)) { 72 | return baseFreeze(coll) 73 | } 74 | return coll 75 | } 76 | 77 | const _freeze = process.env.NODE_ENV === 'production' 78 | ? identity 79 | : coll => { 80 | if (typeof coll === 'object') { 81 | return Object.freeze(coll) 82 | } else { 83 | return coll 84 | } 85 | } 86 | 87 | const prevNodes = [] 88 | 89 | const baseFreeze = (coll) => { 90 | if (prevNodes.some(val => val === coll)) { 91 | throw new Error('object has a reference cycle') 92 | } 93 | prevNodes.push(coll) 94 | forKeys(coll, key => { 95 | const prop = coll[key] 96 | if (weCareAbout(prop)) { 97 | baseFreeze(prop) 98 | } 99 | }) 100 | prevNodes.pop() 101 | 102 | Object.freeze(coll) 103 | return coll 104 | } 105 | 106 | /** 107 | * recrursively freeze an object and all its child objects 108 | * @param {Object|Array} coll 109 | * @return {Object|Array} 110 | */ 111 | exports.freeze = process.env.NODE_ENV === 'production' 112 | ? identity 113 | : baseFreeze 114 | 115 | /** 116 | * recursively un-freeze an object, by cloning frozen collections 117 | * @param {[type]} coll [description] 118 | * @return {[type]} [description] 119 | */ 120 | exports.thaw = function thaw (coll) { 121 | if (!weCareAbout(coll) || !Object.isFrozen(coll)) return coll 122 | 123 | const newColl = Array.isArray(coll) 124 | ? new Array(coll.length) 125 | : {} 126 | 127 | forKeys(coll, key => { 128 | newColl[key] = thaw(coll[key]) 129 | }) 130 | return newColl 131 | } 132 | 133 | /** 134 | * set a value on an object or array 135 | * @param {Object|Array} coll 136 | * @param {String|Number} key Key or index 137 | * @param {Object} value 138 | * @return {Object|Array} new object hierarchy with modifications 139 | */ 140 | exports.assoc = function assoc (coll, key, value) { 141 | if (coll[key] === value) { 142 | return _freeze(coll) 143 | } 144 | 145 | const newObj = clone(coll) 146 | 147 | newObj[key] = freezeIfNeeded(value) 148 | 149 | return _freeze(newObj) 150 | } 151 | exports.set = exports.assoc 152 | 153 | /** 154 | * un-set a value on an object or array 155 | * @param {Object|Array} coll 156 | * @param {String|Number} key Key or Index 157 | * @return {Object|Array} New object or array 158 | */ 159 | exports.dissoc = function dissoc (coll, key) { 160 | const newObj = clone(coll) 161 | 162 | delete newObj[key] 163 | 164 | return _freeze(newObj) 165 | } 166 | exports.unset = exports.dissoc 167 | 168 | /** 169 | * set a value deep in a hierarchical structure 170 | * @param {Object|Array} coll 171 | * @param {Array} path A list of keys to traverse 172 | * @param {Object} value 173 | * @return {Object|Array} new object hierarchy with modifications 174 | */ 175 | exports.assocIn = function assocIn (coll, path, value) { 176 | const key0 = path[0] 177 | if (path.length === 1) { 178 | // simplest case is a 1-element array. Just a simple assoc. 179 | return i.assoc(coll, key0, value) 180 | } else { 181 | // break the problem down. Assoc this object with the first key 182 | // and the result of assocIn with the rest of the keys 183 | return i.assoc(coll, key0, assocIn(coll[key0] || {}, path.slice(1), value)) 184 | } 185 | } 186 | exports.setIn = exports.assocIn 187 | 188 | /** 189 | * un-set a value on an object or array 190 | * @param {Object|Array} coll 191 | * @param {Array} path A list of keys to traverse 192 | * @return {Object|Array} New object or array 193 | */ 194 | exports.dissocIn = function dissocIn (coll, path) { 195 | const key0 = path[0] 196 | if (!coll.hasOwnProperty(key0)) { 197 | return coll 198 | } 199 | if (path.length === 1) { 200 | // simplest case is a 1-element array. Just a simple dissoc. 201 | return i.dissoc(coll, key0) 202 | } else { 203 | // break the problem down. Assoc this object with the first key 204 | // and the result of dissocIn with the rest of the keys 205 | return i.assoc(coll, key0, dissocIn(coll[key0], path.slice(1))) 206 | } 207 | } 208 | exports.unsetIn = exports.dissocIn 209 | 210 | /** 211 | * get an object from a hierachy based on an array of keys 212 | * @param {Object|Array} coll 213 | * @param {Array} path list of keys 214 | * @return {Object} value, or undefined 215 | */ 216 | function baseGet (coll, path) { 217 | return (path || []).reduce((curr, key) => { 218 | if (!curr) { return } 219 | return curr[key] 220 | }, coll) 221 | } 222 | 223 | exports.getIn = baseGet 224 | 225 | /** 226 | * Update a value in a hierarchy 227 | * @param {Object|Array} coll 228 | * @param {Array} path list of keys 229 | * @param {Function} callback The existing value with be passed to this. 230 | * Return the new value to set 231 | * @return {Object|Array} new object hierarchy with modifications 232 | */ 233 | exports.updateIn = function updateIn (coll, path, callback) { 234 | const existingVal = baseGet(coll, path) 235 | return i.assocIn(coll, path, callback(existingVal)) 236 | }; 237 | 238 | // generate wrappers for the mutative array methods 239 | ['push', 'unshift', 'pop', 'shift', 'reverse', 'sort'] 240 | .forEach((methodName) => { 241 | exports[methodName] = function (arr, val) { 242 | const newArr = [...arr] 243 | 244 | newArr[methodName](freezeIfNeeded(val)) 245 | 246 | return _freeze(newArr) 247 | } 248 | 249 | exports[methodName].displayName = 'icepick.' + methodName 250 | }) 251 | 252 | // splice is special because it is variadic 253 | exports.splice = function splice (arr, ..._args) { 254 | const newArr = [...arr] 255 | const args = _args.map(freezeIfNeeded) 256 | 257 | newArr.splice.apply(newArr, args) 258 | 259 | return _freeze(newArr) 260 | } 261 | 262 | // slice is non-mutative 263 | exports.slice = function slice (arr, arg1, arg2) { 264 | const newArr = arr.slice(arg1, arg2) 265 | 266 | return _freeze(newArr) 267 | }; 268 | 269 | ['map', 'filter'].forEach((methodName) => { 270 | exports[methodName] = function (fn, arr) { 271 | const newArr = arr[methodName](fn) 272 | 273 | return _freeze(newArr) 274 | } 275 | 276 | exports[methodName].displayName = 'icepick.' + methodName 277 | }) 278 | 279 | exports.extend = 280 | exports.assign = function assign (obj, ...objs) { 281 | const newObj = objs.reduce(singleAssign, obj) 282 | 283 | return _freeze(newObj) 284 | } 285 | 286 | function singleAssign (obj1, obj2) { 287 | return Object.keys(obj2).reduce((obj, key) => { 288 | return i.assoc(obj, key, obj2[key]) 289 | }, obj1) 290 | } 291 | 292 | exports.merge = merge 293 | function merge (target, source, resolver) { 294 | if (target == null || source == null) { 295 | return target 296 | } 297 | return Object.keys(source).reduce((obj, key) => { 298 | const sourceVal = source[key] 299 | const targetVal = obj[key] 300 | 301 | const resolvedSourceVal = 302 | resolver ? resolver(targetVal, sourceVal, key) : sourceVal 303 | 304 | if (weCareAbout(sourceVal) && weCareAbout(targetVal)) { 305 | // if they are both frozen and reference equal, assume they are deep equal 306 | if ( 307 | resolvedSourceVal === targetVal && 308 | ( 309 | process.env.NODE_ENV === 'production' || 310 | ( 311 | Object.isFrozen(resolvedSourceVal) && 312 | Object.isFrozen(targetVal) 313 | ) 314 | ) 315 | ) { 316 | return obj 317 | } 318 | if (Array.isArray(sourceVal)) { 319 | return i.assoc(obj, key, resolvedSourceVal) 320 | } 321 | // recursively merge pairs of objects 322 | return assocIfDifferent(obj, key, 323 | merge(targetVal, resolvedSourceVal, resolver)) 324 | } 325 | 326 | // primitive values, stuff with prototypes 327 | return assocIfDifferent(obj, key, resolvedSourceVal) 328 | }, target) 329 | } 330 | 331 | function assocIfDifferent (target, key, value) { 332 | if (target[key] === value) { 333 | return target 334 | } 335 | return i.assoc(target, key, value) 336 | } 337 | 338 | const chainProto = { 339 | value: function value () { 340 | return this.val 341 | }, 342 | thru: function thru (fn) { 343 | this.val = freezeIfNeeded(fn(this.val)) 344 | return this 345 | } 346 | } 347 | 348 | Object.keys(exports).forEach((methodName) => { 349 | if (methodName.match(/^(map|filter)$/)) { 350 | chainProto[methodName] = function (fn) { 351 | this.val = exports[methodName](fn, this.val) 352 | return this 353 | } 354 | return 355 | } 356 | chainProto[methodName] = function (...args) { 357 | this.val = exports[methodName](this.val, ...args) 358 | return this 359 | } 360 | }) 361 | 362 | exports.chain = function chain (val) { 363 | const wrapped = Object.create(chainProto) 364 | wrapped.val = val 365 | return wrapped 366 | } 367 | 368 | // for testing 369 | if (process.env.NODE_ENV !== 'development' && 370 | process.env.NODE_ENV !== 'production') { 371 | exports._weCareAbout = weCareAbout 372 | } 373 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # icepick 2 | 3 | A tiny (1kb min/gzipped), zero-dependency library for treating frozen JavaScript objects as persistent immutable collections. 4 | 5 | [![Build Status via Travis CI](https://travis-ci.org/aearly/icepick.svg?branch=master)](https://travis-ci.org/aearly/icepick) [![NPM version](http://img.shields.io/npm/v/icepick.svg)](https://www.npmjs.org/package/icepick) [![Coverage Status](https://coveralls.io/repos/aearly/icepick/badge.svg?branch=)](https://coveralls.io/r/aearly/icepick?branch=) 6 | 7 | ## Motivation 8 | 9 | `Object.freeze()` is a quick and easy way to get immutable collections in plain JavaScript. If you recursively freeze an object hierarchy, you have a nice structure you can pass around without fear of mutation. The problem is that if you want to modify properties inside this hierarchical collection, you have to return a new copy with the properties changed. 10 | 11 | A quick and dirty way to do this is to just `_.cloneDeep()` or `JSON.parse(JSON.stringify())` your object, set the new properties, and re-freeze, but this operation is expensive, especially if you are only changing a single property in a large structure. It also means that all the branches that did not have an update will be new objects. 12 | 13 | Instead, what `icepick` does is provide functions that allow you to "modify" a frozen structure by returning a partial clone using structural sharing. Only collections in the structure that had a child change will be changed. This is very similar to how Clojure's [persistent data structures](https://en.wikipedia.org/wiki/Persistent_data_structure) work, albeit more primitive. 14 | 15 | `icepick` uses structural sharing at the object or array level. Unlike Clojure, `icepick` does not use [tries](https://en.wikipedia.org/wiki/Trie) to store objects or arrays, so updates will be less efficient. This is to maintain JavaScript interoperability at all times. Also, for smaller collections, the overhead of creating and managing a trie structure is slower than simply cloning the entire collection. However, using very large collections (e.g.collections with more than 1000 elements) with `icepick` could lead to performance problems. 16 | 17 | Structural sharing is useful wherever you can avoid expensive computation if you can quickly detect if the source data has changed. For example, `shouldComponentUpdate` in a React component. If you are using a frozen hierarchical object to build a system of React components, you can be confident that a component doesn't need to update if its current `props` strictly equal the `nextProps`. 18 | 19 | ## API 20 | 21 | * `freeze` 22 | * `thaw` 23 | * `assoc` 24 | * `set` 25 | * `dissoc` 26 | * `unset` 27 | * `assocIn` 28 | * `setIn` 29 | * `getIn` 30 | * `updateIn` 31 | * `push` 32 | * `unshift` 33 | * `pop` 34 | * `shift` 35 | * `reverse` 36 | * `sort` 37 | * `splice` 38 | * `slice` 39 | * `map` 40 | * `filter` 41 | * `assign` 42 | * `extend` 43 | * `merge` 44 | * `chain` 45 | 46 | 47 | ### Usage 48 | 49 | `icepick` is provided as a CommonJS module with no dependencies. It is designed for use in Node, or with module loaders like Browserify or Webpack. To use as a global or with require.js, use `icepick.min.js` or `icepick.dev.js` directly in a browser. 50 | 51 | ```bash 52 | $ npm install icepick --save 53 | ``` 54 | 55 | ```javascript 56 | "use strict"; // so attempted modifications of frozen objects will throw errors 57 | 58 | var icepick = require("icepick"); 59 | ``` 60 | 61 | The API is heavily influenced from Clojure/mori. In the contexts of these docs "collection" means a plain, frozen `Object` or `Array`. Only JSON-style collections are supported. Functions, Dates, RegExps, DOM elements, and others are left as-is, and could mutate if they exist in your hierarchy. 62 | 63 | If you set `process.env.NODE_ENV` to `"production"` in your build, using `envify` or its equivalent, freezing objects will be skipped. This can improve performance for your production build. 64 | 65 | ### freeze(collection) 66 | 67 | Recursively freeze a collection and all its child collections with `Object.freeze()`. Values that are not plain Arrays or Objects will be ignored, including objects created with custom constructors (e.g. `new MyClass()`). Does not allow reference cycles. 68 | 69 | ```javascript 70 | var coll = { 71 | a: "foo", 72 | b: [1, 2, 3], 73 | c: { 74 | d: "bar" 75 | } 76 | }; 77 | 78 | icepick.freeze(coll); 79 | 80 | coll.c.d = "baz"; // throws Error 81 | 82 | var circular = {bar: {}}; 83 | circular.bar.foo = circular; 84 | 85 | icepick.freeze(circular); // throws Error 86 | ``` 87 | 88 | ### thaw(collection) 89 | 90 | Recursively un-freeze a collection by creating a partial clone. Object that are not frozen or that have custom prototypes are left as-is. This is useful when interfacing with other libraries. 91 | 92 | ```javascript 93 | var coll = icepick.freeze({a: "foo", b: [1, 2, 3], c: {d: "bar"}, e: new Foo() }); 94 | var thawed = icepick.thaw(coll); 95 | 96 | assert(!Object.isFrozen(thawed)); 97 | assert(!Object.isFrozen(thawed.c)); 98 | assert(thawed.c !== coll.c); 99 | assert(thawed.e === coll.e); 100 | ``` 101 | 102 | 103 | ### assoc(collection, key, value) 104 | 105 | *alias: `set`* 106 | 107 | Set a value in a collection. If `value` is a collection, it will be recursively frozen (if not already). In the case that the collection is an Array, the key is the array index. 108 | 109 | ```javascript 110 | var coll = {a: 1, b: 2}; 111 | 112 | var newColl = icepick.assoc(coll, "b", 3); // {a: 1, b: 3} 113 | 114 | 115 | var arr = ["a", "b", "c"]; 116 | 117 | var newArr = icepick.assoc(arr, 2, "d"); // ["a", "b", "d"] 118 | ``` 119 | 120 | 121 | ### dissoc(collection, key) 122 | 123 | *alias: `unset`* 124 | 125 | The opposite of `assoc`. Remove the value with the `key` from the collection. If used on an array, it will create a sparse array. 126 | 127 | ```javascript 128 | var coll = {a: 1, b: 2, c: 3}; 129 | 130 | var newColl = icepick.dissoc(coll, "b"); // {a: 1, c: 3} 131 | 132 | var arr = ["a", "b", "c"]; 133 | 134 | var newArr = icepick.dissoc(arr, 2); // ["a", , "c"] 135 | ``` 136 | 137 | 138 | ### dissocIn(collection, path) 139 | 140 | *alias: `unsetIn`* 141 | 142 | The opposite of `assocIn`. Remove a value inside a hierarchical collection. `path` is an array of keys inside the object. Returns a partial copy of the original collection. 143 | 144 | ```javascript 145 | var coll = {a: 1, b: {d: 5, e: 7}, c: 3}; 146 | 147 | var newColl = icepick.dissocIn(coll, ["b", "d"]); // {a: 1, {b: {e: 7}}, c: 3} 148 | 149 | var coll = {a: 1, b: {d: 5}, c: 3}; 150 | 151 | var newColl = icepick.dissocIn(coll, ["b", "d"]); // {a: 1, {b: {}}, c: 3} 152 | 153 | var arr = ["a", "b", "c"]; 154 | 155 | var newArr = icepick.dissoc(arr, [2]); // ["a", , "c"] 156 | ``` 157 | 158 | 159 | ### assocIn(collection, path, value) 160 | 161 | *alias: `setIn`* 162 | 163 | Set a value inside a hierarchical collection. `path` is an array of keys inside the object. Returns a partial copy of the original collection. Intermediate objects will be created if they don't exist. 164 | 165 | ```javascript 166 | var coll = { 167 | a: "foo", 168 | b: [1, 2, 3], 169 | c: { 170 | d: "bar" 171 | } 172 | }; 173 | 174 | var newColl = icepick.assocIn(coll, ["c", "d"], "baz"); 175 | 176 | assert(newColl.c.d === "baz"); 177 | assert(newColl.b === coll.b); 178 | 179 | var coll = {}; 180 | var newColl = icepick.assocIn(coll, ["a", "b", "c"], 1); 181 | assert(newColl.a.b.c === 1); 182 | ``` 183 | 184 | ### getIn(collection, path) 185 | 186 | Get a value inside a hierarchical collection using a path of keys. Returns `undefined` if the value does not exist. A convenience method -- in most cases plain JS syntax will be simpler. 187 | 188 | ```javascript 189 | var coll = icepick.freeze([ 190 | {a: 1}, 191 | {b: 2} 192 | ]); 193 | 194 | var result = icepick.getIn(coll, [1, "b"]); // 2 195 | ``` 196 | 197 | ### updateIn(collection, path, callback) 198 | 199 | Update a value inside a hierarchical collection. The `path` is the same as in `assocIn`. The previous value will be passed to the `callback` function, and `callback` should return the new value. If the value does not exist, `undefined` will be passed. If not all of the intermediate collections exist, an error will be thrown. 200 | 201 | ```javascript 202 | var coll = icepick.freeze([ 203 | {a: 1}, 204 | {b: 2} 205 | ]); 206 | 207 | var newColl = icepick.updateIn(coll, [1, "b"], function (val) { 208 | return val * 2; 209 | }); // [ {a: 1}, {b: 4} ] 210 | ``` 211 | 212 | ### assign(coll1, coll2, ...) 213 | 214 | *alias: extend* 215 | 216 | Similar to `Object.assign`, this function shallowly merges several objects together. Properties of the objects that are Objects or Arrays are deeply frozen. 217 | 218 | ```javascript 219 | var obj1 = {a: 1, b: 2, c: 3}; 220 | var obj2 = {c: 4, d: 5}; 221 | 222 | var result = icepick.assign(obj1, obj2); // {a: 1, b: 2, c: 4, d: 5} 223 | assert(obj1 !== result); // true 224 | ``` 225 | 226 | ### merge(target, source, [associator]) 227 | 228 | Deeply merge a `source` object into `target`, similar to Lodash.merge. Child collections that are both frozen and reference equal will be assumed to be deeply equal. Arrays from the `source` object will completely replace those in the `target` object if the two differ. If nothing changed, the original reference will not change. Returns a frozen object, and works with both unfrozen and frozen objects. 229 | 230 | ```javascript 231 | var defaults = {a: 1, c: {d: 1, e: [1, 2, 3], f: {g: 1}}}; 232 | var obj = {c: {d: 2, e: [2], f: null}}; 233 | 234 | var result1 = icepick.merge(defaults, obj); // {a: 1, c: {d: 2, e: [2]}, f: null} 235 | 236 | var obj2 = {c: {d: 2}}; 237 | var result2 = icepick.merge(result1, obj2); 238 | 239 | assert(result1 === result2); // true 240 | 241 | ``` 242 | 243 | An optional `resolver` function can be given as the third argument to change the way values are merged. For example, if you'd prefer that Array values from source be concatenated to target (instead of the source Array just replacing the target Array): 244 | 245 | ```javascript 246 | var o1 = icepick.freeze({a: 1, b: {c: [1, 1]}, d: 1}); 247 | var o2 = icepick.freeze({a: 2, b: {c: [2]}}); 248 | 249 | function resolver(targetVal, sourceVal, key) { 250 | if (Array.isArray(targetVal) && sourceVal) { 251 | return targetVal.concat(sourceVal); 252 | } else { 253 | return sourceVal; 254 | } 255 | } 256 | 257 | var result3 = icepick.merge(o1, o2, resolver); 258 | assert(result === {a: 2, b: {c: [1, 1, 2]}, d: 1}); 259 | ``` 260 | 261 | The `resolver` function receives three arguments: the value from the target object, the value from the source object, and the key of the value being merged. 262 | 263 | ### Array.prototype methods 264 | 265 | * push 266 | * pop 267 | * shift 268 | * unshift 269 | * reverse 270 | * sort 271 | * splice 272 | 273 | Each of these mutative Array prototype methods have been converted: 274 | 275 | ```javascript 276 | var a = [1]; 277 | a = icepick.push(a, 2); // [1, 2]; 278 | a = icepick.unshift(a, 0); // [0, 1, 2]; 279 | a = icepick.pop(a); // [0, 1]; 280 | a = icepick.shift(a); // [1]; 281 | ``` 282 | 283 | * slice(arr, start, [end]) 284 | 285 | `slice` is also provided as a convenience, even though it does not mutate the original array. It freezes its result, however. 286 | 287 | * map(fn, array) 288 | * filter(fn, array) 289 | 290 | These non-mutative functions that return new arrays are also wrapped for convenience. Their results are frozen. Note that the mapping or filtering function is passed first, for easier partial application. 291 | 292 | ```javascript 293 | icepick.map(function (v) {return v * 2}, [1, 2, 3]); // [2, 4, 6] 294 | 295 | var removeEvens = _.partial(icepick.filter, function (v) { return v % 2; }); 296 | 297 | removeEvens([1, 2, 3]); // [1, 3] 298 | ``` 299 | 300 | Array methods like `find` or `indexOf` are not added to `icepick`, because you can just use them directly on the array: 301 | 302 | ```js 303 | var arr = icepick.freeze([{a: 1}, {b: 2}]); 304 | 305 | arr.find(function (item) { return item.b != null; }); // {b: 2} 306 | ``` 307 | 308 | ### chain(coll) 309 | 310 | Wrap a collection in a wrapper that allows calling icepick function as chainable methods, similar to [`lodash.chain`](https://lodash.com/docs#chain). This is convenient when you need to perform multiple operations on a collection at one time. The result of calling each method is passed to the next method in the chain as the first argument. To retrieve the result, call `wrapped.value()`. Unlike `lodash.chain`, you must always call `.value()` to get the result, the methods are not lazily evaluated, and intermediate collections are always created (but this may change in the future). 311 | 312 | ```javascript 313 | var o = { 314 | a: [1, 2, 3], 315 | b: {c: 1}, 316 | d: 4 317 | }; 318 | 319 | var result = icepick.chain(o) 320 | .assocIn(["a", 2], 4) 321 | .merge({b: {c: 2, c2: 3}}) 322 | .assoc("e", 2) 323 | .dissoc("d") 324 | .value(); 325 | 326 | expect(result).to.eql({ 327 | a: [1, 2, 4], 328 | b: {c: 2, c2: 3}, 329 | e: 2 330 | }); 331 | ``` 332 | 333 | The wrapper also contains an additional `thru` method for performing arbitrary updates on the current wrapped value. 334 | 335 | ```js 336 | var result = icepick.chain([1, 2]) 337 | .push(3) 338 | .thru(function (val) { 339 | return [0].concat(val) 340 | }) 341 | .value(); // [0, 1, 2, 3] 342 | ``` 343 | 344 | ## FAQ 345 | 346 | ### Why not just use Immutable.js or mori? 347 | 348 | Those two libraries introduce their own types. If you need to share a frozen data structure with other libraries or other 3rd-party code, you force those downstream from you to use Immutable.js or mori (and in the case of mori, the exact version you use). Also, since you can build your data structures using plain JS, creating the initial representation is faster. The overhead of`Object.freeze()` is negligible. 349 | 350 | ### How does this differ from `React.addons.update` or `seamless-immutable`. 351 | 352 | All three of these libraries are very similar in their goals -- provide incremental updates of plain JS objects. They mainly differ in their APIs. 353 | 354 | [`React.addons.update`](https://facebook.github.io/react/docs/update.html) provides a single function to which you pass an object of commands. While this can be convenient to do many updates in a single batch, the syntax of the command object is very cumbersome, especially when dealing with computed property names. It also does not freeze the objects it operates on, leaving them open to modifications elsewhere in your code. 355 | 356 | [`seamless-immutable`](https://github.com/rtfeldman/seamless-immutable) is the most similar to `icepick`. Its main difference is that it adds methods to the prototypes of objects, and overrides array built-ins like `map` and `filter` to return frozen objects. It also adds a couple utility functions, like `asMutable` and `merge`. `icepick` does not modify the methods or properties of collections in order to function, it merely provides a set of functions to operate on them, similar to Lodash, Underscore, or Ramda. This means that when passing frozen objects to third-party libraries, they will be able to `map` over them and obtain mutable arrays. `seamless-immutable` handles `Date`s, which `icepick` leaves as-is currently (as well as any other objects with custom constructors). `icepick` will detect circular references within an object and throw an Error, `seamless-immutable` will run into infinite recursion in such a case. 357 | 358 | 359 | ### Isn't this horribly slow? 360 | 361 | It is faster than deeply cloning an object. Since it does not touch portions of a data structure that did not change, it can help you optimize expensive calculations elsewhere (such as rendering a component in the DOM). It is also faster than mori[1](https://github.com/aearly/icepick-benchmarks). 362 | 363 | [Here are some performance profiles](https://github.com/aearly/icepick-benchmarks) of various immutable libraries, icepick is faster than most, except for writes to collections with more than 100 elements. 364 | 365 | ### Won't this leak memory? 366 | 367 | Garbage collection in modern JS engines can clean up the intermediate Objects and Arrays that are no longer needed. I need to profile memory across a wider range of browsers, but V8 can definitely handle it. Working with a collection that is about 200kb as JSON, the GC phase is only 8ms after a few hundred updates. Memory usage does fluctuate a few MBs though, but it always resets to the baseline. 368 | 369 | ## License 370 | 371 | MIT 372 | -------------------------------------------------------------------------------- /icepick.test.js: -------------------------------------------------------------------------------- 1 | const i = require('./icepick') 2 | const tap = require('tap') 3 | 4 | function test (what, how) { 5 | tap.test(what, assert => { 6 | how(assert) 7 | assert.end() 8 | }) 9 | } 10 | 11 | test('icepick', assert => { 12 | 'use strict' 13 | 14 | test('freeze', assert => { 15 | test('should work', assert => { 16 | const a = i.freeze({asdf: 'foo', zxcv: {asdf: 'bar'}}) 17 | 18 | assert.equal(a.asdf, 'foo') 19 | assert.equal(Object.isFrozen(a), true) 20 | 21 | assert.throws(function () { 22 | a.asdf = 'bar' 23 | }) 24 | 25 | assert.throws(function () { 26 | a.zxcv.asdf = 'qux' 27 | }) 28 | 29 | assert.throws(function () { 30 | a.qwer = 'bar' 31 | }) 32 | }) 33 | 34 | test('should not work with cyclical objects', assert => { 35 | let a = {} 36 | a.a = a 37 | 38 | assert.throws(() => i.freeze(a)) 39 | 40 | a = {b: {}} 41 | a.b.a = a 42 | assert.throws(() => i.freeze(a)) 43 | }) 44 | }) 45 | 46 | test('thaw', assert => { 47 | function Foo () {} 48 | 49 | test('should thaw objects', assert => { 50 | const o = i.freeze({ 51 | a: {}, 52 | b: 1, 53 | c: new Foo(), 54 | d: [{e: 1}] 55 | }) 56 | 57 | const thawed = i.thaw(o) 58 | 59 | assert.same(thawed, o) 60 | assert.equal(Object.isFrozen(thawed), false) 61 | assert.equal(Object.isFrozen(thawed.a), false) 62 | assert.notEqual(o.a, thawed.a) 63 | assert.notEqual(o.d, thawed.d) 64 | assert.notEqual(o.d[0], thawed.d[0]) 65 | assert.equal(o.c, thawed.c) 66 | }) 67 | }) 68 | 69 | test('assoc', assert => { 70 | test('should work with objects', assert => { 71 | const o = i.freeze({a: 1, b: 2, c: 3}) 72 | let result = i.assoc(o, 'b', 4) 73 | 74 | assert.same(result, {a: 1, b: 4, c: 3}) 75 | 76 | result = i.assoc(o, 'd', 4) 77 | assert.same(result, {a: 1, b: 2, c: 3, d: 4}) 78 | }) 79 | 80 | test('should freeze objects you assoc', assert => { 81 | const o = i.freeze({a: 1, b: 2, c: 3}) 82 | const result = i.assoc(o, 'b', {d: 5}) 83 | 84 | assert.same(result, {a: 1, b: {d: 5}, c: 3}) 85 | 86 | assert.ok(Object.isFrozen(result.b)) 87 | }) 88 | 89 | test('should work with arrays', assert => { 90 | const a = i.freeze([1, 2, 3]) 91 | let result = i.assoc(a, 1, 4) 92 | 93 | assert.same(result, [1, 4, 3]) 94 | 95 | result = i.assoc(a, '1', 4) 96 | assert.same(result, [1, 4, 3]) 97 | 98 | result = i.assoc(a, 3, 4) 99 | assert.same(result, [1, 2, 3, 4]) 100 | }) 101 | 102 | test('should freeze arrays you assoc', assert => { 103 | const o = i.freeze({a: 1, b: 2, c: 3}) 104 | const result = i.assoc(o, 'b', [1, 2]) 105 | 106 | assert.same(result, {a: 1, b: [1, 2], c: 3}) 107 | 108 | assert.ok(Object.isFrozen(result.b)) 109 | }) 110 | 111 | test('should return a frozen copy', assert => { 112 | const o = i.freeze({a: 1, b: 2, c: 3}) 113 | const result = i.assoc(o, 'b', 4) 114 | 115 | assert.notEqual(result, o) 116 | assert.ok(Object.isFrozen(result)) 117 | }) 118 | 119 | test('should not modify child objects', assert => { 120 | const o = i.freeze({a: 1, b: 2, c: {a: 4}}) 121 | const result = i.assoc(o, 'b', 4) 122 | 123 | assert.equal(result.c, o.c) 124 | }) 125 | 126 | test('should keep references the same if nothing changes', assert => { 127 | const o = i.freeze({a: 1}) 128 | const result = i.assoc(o, 'a', 1) 129 | assert.equal(result, o) 130 | }) 131 | 132 | test('should work with Object.create(null)', assert => { 133 | const o = Object.create(null) 134 | o.b = 2 135 | const result = i.assoc(o, 'a', 1) 136 | assert.same(result, {a: 1, b: 2}) 137 | assert.equal(result.constructor, undefined) 138 | assert.equal(Object.getPrototypeOf(result), null) 139 | }) 140 | 141 | test('should be aliased as set', assert => { 142 | assert.equal(i.set, i.assoc) 143 | }) 144 | }) 145 | 146 | test('dissoc', assert => { 147 | test('should work with objecs', assert => { 148 | const o = i.freeze({a: 1, b: 2, c: 3}) 149 | const result = i.dissoc(o, 'b') 150 | 151 | assert.same(result, {a: 1, c: 3}) 152 | }) 153 | 154 | test('should work with arrays (poorly)', assert => { 155 | const a = i.freeze([1, 2, 3]) 156 | const result = i.dissoc(a, 1) 157 | 158 | // assert.same(result, [1, , 3]) 159 | assert.same(Object.keys(result), [0, 2]) 160 | assert.equal(result[0], 1) 161 | assert.equal(result[1], undefined) 162 | assert.equal(result[2], 3) 163 | }) 164 | 165 | test('should be aliased as unset', assert => { 166 | assert.equal(i.unset, i.dissoc) 167 | }) 168 | }) 169 | 170 | test('assocIn', assert => { 171 | test('should work recursively', assert => { 172 | const o = i.freeze({a: 1, b: 2, c: {a: 4}}) 173 | const result = i.assocIn(o, ['c', 'a'], 5) 174 | 175 | assert.same(result, {a: 1, b: 2, c: {a: 5}}) 176 | }) 177 | 178 | test('should work recursively (deeper)', assert => { 179 | const o = i.freeze({ 180 | a: 1, 181 | b: {a: 2}, 182 | c: [ 183 | { 184 | a: 3, 185 | b: 4 186 | }, 187 | {a: 4} 188 | ] 189 | }) 190 | const result = i.assocIn(o, ['c', 0, 'a'], 8) 191 | 192 | assert.equal(result.c[0].a, 8) 193 | assert.notEqual(result, o) 194 | assert.equal(result.b, o.b) 195 | assert.notEqual(result.c, o.c) 196 | assert.notEqual(result.c[0], o.c[0]) 197 | assert.equal(result.c[0].b, o.c[0].b) 198 | assert.equal(result.c[1], o.c[1]) 199 | }) 200 | 201 | test("should create collections if they don't exist", assert => { 202 | const result = i.assocIn({}, ['a', 'b', 'c'], 1) 203 | assert.same(result, {a: {b: {c: 1}}}) 204 | }) 205 | 206 | test('should be aliased as setIn', assert => { 207 | assert.equal(i.setIn, i.assocIn) 208 | }) 209 | 210 | test('should keep references the same if nothing changes', assert => { 211 | const o = i.freeze({a: {b: 1}}) 212 | const result = i.assocIn(o, ['a', 'b'], 1) 213 | assert.equal(result, o) 214 | }) 215 | }) 216 | 217 | test('dissocIn', assert => { 218 | test('should work recursively', assert => { 219 | const o = i.freeze({a: 1, b: 2, c: {a: 4}}) 220 | const result = i.dissocIn(o, ['c', 'a']) 221 | 222 | assert.same(result, {a: 1, b: 2, c: {}}) 223 | }) 224 | 225 | test('should work recursively (deeper)', assert => { 226 | const o = i.freeze({ 227 | a: 1, 228 | b: {a: 2}, 229 | c: [ 230 | { 231 | a: 3, 232 | b: 4 233 | }, 234 | {a: 4} 235 | ] 236 | }) 237 | const result = i.dissocIn(o, ['c', 0, 'a']) 238 | 239 | assert.equal(result.c[0].a, undefined) 240 | assert.notEqual(result, o) 241 | assert.equal(result.b, o.b) 242 | assert.notEqual(result.c, o.c) 243 | assert.notEqual(result.c[0], o.c[0]) 244 | assert.equal(result.c[0].b, o.c[0].b) 245 | assert.equal(result.c[1], o.c[1]) 246 | }) 247 | 248 | test("should not create collections if they don't exist", assert => { 249 | const result = i.dissocIn({}, ['a', 'b', 'c']) 250 | assert.same(result, {}) 251 | }) 252 | 253 | test('should be aliased as unsetIn', assert => { 254 | assert.equal(i.unsetIn, i.dissocIn) 255 | }) 256 | 257 | test('should keep references the same if nothing changes', assert => { 258 | const o = i.freeze({a: {b: 1}}) 259 | const result = i.dissocIn(o, ['a', 'b', 'c']) 260 | assert.equal(result, o) 261 | }) 262 | }) 263 | 264 | test('getIn', assert => { 265 | test('should work', assert => { 266 | const o = i.freeze({ 267 | a: 0, 268 | b: {a: 2}, 269 | c: [ 270 | {a: 3, b: 4}, 271 | {a: 4} 272 | ] 273 | }) 274 | assert.equal(i.getIn(o, ['c', 0, 'b']), 4) 275 | assert.equal(i.getIn(o, ['a']), 0) 276 | }) 277 | 278 | test('should work without a path', assert => { 279 | const o = i.freeze({a: {b: 1}}) 280 | assert.equal(i.getIn(o), o) 281 | }) 282 | 283 | test('should return undefined for a non-existant path', assert => { 284 | const o = i.freeze({ 285 | a: 1, 286 | b: {a: 2}, 287 | c: [ 288 | {a: 3, b: 4}, 289 | {a: 4} 290 | ] 291 | }) 292 | 293 | assert.equal(i.getIn(o, ['q']), undefined) 294 | assert.equal(i.getIn(o, ['a', 's', 'd']), undefined) 295 | }) 296 | 297 | test('should return undefined for a non-existant path (null)', assert => { 298 | const o = i.freeze({ 299 | a: null 300 | }) 301 | 302 | assert.equal(i.getIn(o, ['a', 'b']), undefined) 303 | }) 304 | }) 305 | 306 | test('updateIn', assert => { 307 | test('should work', assert => { 308 | const o = i.freeze({a: 1, b: 2, c: {a: 4}}) 309 | const result = i.updateIn(o, ['c', 'a'], function (num) { 310 | return num * 2 311 | }) 312 | 313 | assert.same(result, {a: 1, b: 2, c: {a: 8}}) 314 | }) 315 | 316 | test("should create collections if they don't exist", assert => { 317 | const result = i.updateIn({}, ['a', 1, 'c'], function (val) { 318 | assert.equal(val, undefined) 319 | return 1 320 | }) 321 | assert.same(result, {a: {'1': {c: 1}}}) 322 | }) 323 | 324 | test('should keep references the same if nothing changes', assert => { 325 | const o = i.freeze({a: 1}) 326 | const result = i.updateIn(o, ['a', 'b'], function (v) { return v }) 327 | assert.equal(result, o) 328 | }) 329 | }) 330 | 331 | test('Array methods', assert => { 332 | test('push', assert => { 333 | const a = i.freeze([1, 2]) 334 | const result = i.push(a, 3) 335 | 336 | assert.same(result, [1, 2, 3]) 337 | assert.ok(Object.isFrozen(result)) 338 | }) 339 | 340 | test('push (with object)', assert => { 341 | const a = i.freeze([1, 2]) 342 | const result = i.push(a, {b: 1}) 343 | 344 | assert.same(result, [1, 2, {b: 1}]) 345 | assert.ok(Object.isFrozen(result)) 346 | assert.ok(Object.isFrozen(result[2])) 347 | }) 348 | 349 | test('unshift', assert => { 350 | const a = i.freeze([1, 2]) 351 | const result = i.unshift(a, 3) 352 | 353 | assert.same(result, [3, 1, 2]) 354 | assert.ok(Object.isFrozen(result)) 355 | }) 356 | 357 | test('unshift (with object)', assert => { 358 | const a = i.freeze([1, 2]) 359 | const result = i.unshift(a, [0]) 360 | 361 | assert.same(result, [[0], 1, 2]) 362 | assert.ok(Object.isFrozen(result)) 363 | assert.ok(Object.isFrozen(result[0])) 364 | }) 365 | 366 | test('pop', assert => { 367 | const a = i.freeze([1, 2]) 368 | const result = i.pop(a) 369 | 370 | assert.same(result, [1]) 371 | assert.ok(Object.isFrozen(result)) 372 | }) 373 | 374 | test('shift', assert => { 375 | const a = i.freeze([1, 2]) 376 | const result = i.shift(a) 377 | 378 | assert.same(result, [2]) 379 | assert.ok(Object.isFrozen(result)) 380 | }) 381 | 382 | test('reverse', assert => { 383 | const a = i.freeze([1, 2, 3]) 384 | const result = i.reverse(a) 385 | 386 | assert.same(result, [3, 2, 1]) 387 | assert.ok(Object.isFrozen(result)) 388 | }) 389 | 390 | test('sort', assert => { 391 | const a = i.freeze([4, 1, 2, 3]) 392 | const result = i.sort(a) 393 | 394 | assert.same(result, [1, 2, 3, 4]) 395 | assert.ok(Object.isFrozen(result)) 396 | }) 397 | 398 | test('splice', assert => { 399 | const a = i.freeze([1, 2, 3]) 400 | const result = i.splice(a, 1, 1, 4) 401 | 402 | assert.same(result, [1, 4, 3]) 403 | assert.ok(Object.isFrozen(result)) 404 | }) 405 | 406 | test('splice (with object)', assert => { 407 | const a = i.freeze([1, 2, 3]) 408 | const result = i.splice(a, 1, 1, {b: 1}, {b: 2}) 409 | 410 | assert.same(result, [1, {b: 1}, {b: 2}, 3]) 411 | assert.ok(Object.isFrozen(result)) 412 | assert.ok(Object.isFrozen(result[1])) 413 | assert.ok(Object.isFrozen(result[2])) 414 | }) 415 | 416 | test('slice', assert => { 417 | const a = i.freeze([1, 2, 3]) 418 | const result = i.slice(a, 1, 2) 419 | 420 | assert.same(result, [2]) 421 | assert.ok(Object.isFrozen(result)) 422 | }) 423 | 424 | test('map', assert => { 425 | const a = i.freeze([1, 2, 3]) 426 | const result = i.map(function (v) { return v * 2 }, a) 427 | 428 | assert.same(result, [2, 4, 6]) 429 | assert.ok(Object.isFrozen(result)) 430 | }) 431 | 432 | test('filter', assert => { 433 | const a = i.freeze([1, 2, 3]) 434 | const result = i.filter(function (v) { return v % 2 }, a) 435 | 436 | assert.same(result, [1, 3]) 437 | assert.ok(Object.isFrozen(result)) 438 | }) 439 | }) 440 | 441 | test('assign', assert => { 442 | test('should work', assert => { 443 | const o = i.freeze({a: 1, b: 2, c: 3}) 444 | let result = i.assign(o, {'b': 3, 'c': 4}) 445 | assert.same(result, {a: 1, b: 3, c: 4}) 446 | assert.notEqual(result, o) 447 | result = i.assign(o, {'d': 4}) 448 | assert.same(result, {a: 1, b: 2, c: 3, d: 4}) 449 | }) 450 | 451 | test('should work with multiple args', assert => { 452 | const o = i.freeze({a: 1, b: 2, c: 3}) 453 | const result = i.assign(o, {'b': 3, 'c': 4}, {'d': 4}) 454 | assert.same(result, {a: 1, b: 3, c: 4, d: 4}) 455 | }) 456 | 457 | test('should keep references the same if nothing changes', assert => { 458 | const o = i.freeze({a: 1}) 459 | const result = i.assign(o, {a: 1}) 460 | assert.equal(result, o) 461 | }) 462 | }) 463 | 464 | test('merge', assert => { 465 | test('should merge nested objects', assert => { 466 | const o1 = i.freeze({a: 1, b: {c: 1, d: 1}}) 467 | const o2 = i.freeze({a: 1, b: {c: 2}, e: 2}) 468 | 469 | const result = i.merge(o1, o2) 470 | assert.same(result, {a: 1, b: {c: 2, d: 1}, e: 2}) 471 | }) 472 | 473 | test('should replace arrays', assert => { 474 | const o1 = i.freeze({a: 1, b: {c: [1, 1]}, d: 1}) 475 | const o2 = i.freeze({a: 2, b: {c: [2]}}) 476 | 477 | const result = i.merge(o1, o2) 478 | assert.same(result, {a: 2, b: {c: [2]}, d: 1}) 479 | }) 480 | 481 | test('should overwrite with nulls', assert => { 482 | const o1 = i.freeze({a: 1, b: {c: [1, 1]}}) 483 | const o2 = i.freeze({a: 2, b: {c: null}}) 484 | 485 | const result = i.merge(o1, o2) 486 | assert.same(result, {a: 2, b: {c: null}}) 487 | }) 488 | 489 | test('should overwrite primitives with objects', assert => { 490 | const o1 = i.freeze({a: 1, b: 1}) 491 | const o2 = i.freeze({a: 2, b: {c: 2}}) 492 | 493 | const result = i.merge(o1, o2) 494 | assert.same(result, {a: 2, b: {c: 2}}) 495 | }) 496 | 497 | test('should overwrite objects with primitives', assert => { 498 | const o1 = i.freeze({a: 1, b: {c: 2}}) 499 | const o2 = i.freeze({a: 1, b: 2}) 500 | 501 | const result = i.merge(o1, o2) 502 | assert.same(result, {a: 1, b: 2}) 503 | }) 504 | 505 | test('should keep references the same if nothing changes', assert => { 506 | const o1 = i.freeze({a: 1, b: {c: 1, d: 1, e: [1]}}) 507 | const o2 = i.freeze({a: 1, b: {c: 1, d: 1, e: o1.b.e}}) 508 | const result = i.merge(o1, o2) 509 | assert.equal(result, o1) 510 | assert.equal(result.b, o1.b) 511 | }) 512 | 513 | test('should handle undefined parameters', assert => { 514 | assert.same(i.merge({}, undefined), {}) 515 | assert.same(i.merge(undefined, {}), undefined) 516 | }) 517 | 518 | test('custom associator', assert => { 519 | test('should use the custom associator', assert => { 520 | const o1 = i.freeze({a: 1, b: {c: [1, 1]}, d: 1}) 521 | const o2 = i.freeze({a: 2, b: {c: [2]}}) 522 | 523 | function resolver (targetVal, sourceVal) { 524 | if (Array.isArray(targetVal) && sourceVal) { 525 | return targetVal.concat(sourceVal) 526 | } else { 527 | return sourceVal 528 | } 529 | } 530 | 531 | const result = i.merge(o1, o2, resolver) 532 | assert.same(result, {a: 2, b: {c: [1, 1, 2]}, d: 1}) 533 | }) 534 | }) 535 | }) 536 | }) 537 | 538 | test('chain', assert => { 539 | test('should wrap and unwrap a value', assert => { 540 | const a = [1, 2, 3] 541 | const result = i.chain(a).value() 542 | assert.same(result, a) 543 | }) 544 | 545 | test('should work with a simple operation', assert => { 546 | const a = [1, 2, 3] 547 | const result = i.chain(a) 548 | .assoc(1, 4) 549 | .value() 550 | assert.same(result, [1, 4, 3]) 551 | assert.notEqual(result, a) 552 | assert.ok(Object.isFrozen(result)) 553 | }) 554 | 555 | test('should work with multiple operations', assert => { 556 | const a = [1, 2, 3] 557 | const result = i.chain(a) 558 | .assoc(1, 4) 559 | .reverse() 560 | .pop() 561 | .push(5) 562 | .value() 563 | assert.same(result, [3, 4, 5]) 564 | assert.notEqual(result, a) 565 | assert.ok(Object.isFrozen(result)) 566 | }) 567 | 568 | test('should work with multiple operations (more complicated)', assert => { 569 | const o = { 570 | a: [1, 2, 3], 571 | b: {c: 1}, 572 | d: 4 573 | } 574 | const result = i.chain(o) 575 | .assocIn(['a', 2], 4) 576 | .merge({b: {c: 2, c2: 3}}) 577 | .assoc('e', 2) 578 | .dissoc('d') 579 | .value() 580 | assert.same(result, { 581 | a: [1, 2, 4], 582 | b: {c: 2, c2: 3}, 583 | e: 2 584 | }) 585 | assert.notEqual(result, o) 586 | assert.ok(Object.isFrozen(result)) 587 | }) 588 | 589 | test('should have a thru method', assert => { 590 | const o = [1, 2] 591 | const result = i.chain(o) 592 | .push(3) 593 | .thru(function (val) { 594 | return [0].concat(val) 595 | }) 596 | .value() 597 | assert.ok(Object.isFrozen(result)) 598 | assert.same(result, [0, 1, 2, 3]) 599 | }) 600 | 601 | test('should work with map and filter', assert => { 602 | const o = [1, 2, 3] 603 | const result = i.chain(o) 604 | .map(val => val * 2) 605 | .filter(val => val > 2) 606 | .value() 607 | assert.ok(Object.isFrozen(result)) 608 | assert.same(result, [4, 6]) 609 | }) 610 | }) 611 | 612 | test('production mode', assert => { 613 | let oldEnv 614 | oldEnv = process.env.NODE_ENV 615 | process.env.NODE_ENV = 'production' 616 | delete require.cache[require.resolve('./icepick')] 617 | const i = require('./icepick') 618 | 619 | assert.tearDown(function () { 620 | process.env.NODE_ENV = oldEnv 621 | }) 622 | 623 | test('should not freeze objects', assert => { 624 | const result = i.freeze({}) 625 | assert.equal(Object.isFrozen(result), false) 626 | }) 627 | 628 | test("should not freeze objects that are assoc'd", assert => { 629 | const result = i.assoc({}, 'a', {}) 630 | assert.equal(Object.isFrozen(result), false) 631 | assert.equal(Object.isFrozen(result.a), false) 632 | }) 633 | 634 | test('merge should keep references the same if nothing changes', assert => { 635 | const o1 = i.freeze({a: 1, b: {c: 1, d: 1, e: [1]}}) 636 | const o2 = i.freeze({a: 1, b: {c: 1, d: 1, e: o1.b.e}}) 637 | const result = i.merge(o1, o2) 638 | assert.equal(result, o1) 639 | assert.equal(result.b, o1.b) 640 | }) 641 | }) 642 | 643 | test('internals', assert => { 644 | test('_weCareAbout', assert => { 645 | function Foo () {} 646 | class Bar {} 647 | 648 | test('should care about objects', assert => { 649 | assert.equal(i._weCareAbout({}), true) 650 | }) 651 | test('should care about arrays', assert => { 652 | assert.equal(i._weCareAbout([]), true) 653 | }) 654 | test('should not care about dates', assert => { 655 | assert.equal(i._weCareAbout(new Date()), false) 656 | }) 657 | test('should not care about null', assert => { 658 | assert.equal(i._weCareAbout(null), false) 659 | }) 660 | test('should not care about undefined', assert => { 661 | assert.equal(i._weCareAbout(undefined), false) 662 | }) 663 | test('should not care about class instances', assert => { 664 | assert.equal(i._weCareAbout(new Foo()), false) 665 | }) 666 | test('should not care about class instances (2)', assert => { 667 | assert.equal(i._weCareAbout(new Bar()), false) 668 | }) 669 | test('should not care about objects created with Object.create()', assert => { 670 | assert.equal(i._weCareAbout(Object.create(Foo.prototype)), false) 671 | }) 672 | test('should not care about objects created with Object.create({})', assert => { 673 | assert.equal(i._weCareAbout(Object.create({ 674 | foo: function () {} 675 | })), false) 676 | }) 677 | test('should care about objects with null prototypes', assert => { 678 | assert.equal(i._weCareAbout(Object.create(null)), true) 679 | }) 680 | }) 681 | }) 682 | --------------------------------------------------------------------------------