mutate-cow 7.0.1 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,18 +17,11 @@ const newAnimals = mutate(animals)
17
17
 
18
18
  This module allows you to update an immutable object as if it were mutable. It has copy-on-write semantics, so properties are only changed if you write to them. (In fact, if you perform no writes, the same object is returned back.) This makes it useful in conjuction with libraries like React, where state may be compared by reference.
19
19
 
20
- `mutate-cow` provides useful features that other packages don't:
21
-
22
- * All property descriptors from the immutable object are preserved in the copy.
23
- * All extensibility information from the immutable object is preserved in the copy. Combined with the above point, this means that sealed objects stay sealed and frozen objects stay frozen.
24
- * Arrays, objects, and class instances are supported for mutation.
25
- * Flow and TypeScript definitions are provided.
26
-
27
20
  No cows were harmed in the making of this code.
28
21
 
29
22
  ## API
30
23
 
31
- ### const ctx = mutate(source)
24
+ ### const ctx = mutate(source, /* strict = */ false)
32
25
 
33
26
  Returns a "context" object which can modify a copy of `source`.
34
27
 
@@ -37,6 +30,12 @@ const foo = deepFreeze({bar: {baz: []}});
37
30
  const ctx = mutate(foo);
38
31
  ````
39
32
 
33
+ By default, you can mutate primitves, arrays, and plain objects. However, if `strict` is set to true, the following features are enabled:
34
+
35
+ * All property descriptors from the immutable object are preserved in the copy.
36
+ * All extensibility information from the immutable object is preserved in the copy. Combined with the above point, this means that sealed objects stay sealed and frozen objects stay frozen.
37
+ * Class instances are supported for mutation.
38
+
40
39
  ### ctx.read()
41
40
 
42
41
  Returns the current working copy of the context's `source` object, or just `source` if no changes were made.
@@ -132,10 +131,10 @@ Returns a boolean indicating whether `ctx` has been revoked.
132
131
 
133
132
  ### ctx.final()
134
133
 
135
- This is the same as `read`, except it also revokes the context and restores all property descriptors and extensibility information. This is what you call to get the final copy.
134
+ This is the same as `read`, except it also revokes the context, and in strict mode restores all property descriptors and extensibility information. This is what you call to get the final copy.
136
135
 
137
136
  ```js
138
- const copy = mutate(foo).set('bar', 'baz', 'qux').final();
137
+ const copy = mutate(foo, /* strict = */ true).set('bar', 'baz', 'qux').final();
139
138
  Object.isFrozen(copy) === true; // since `foo` was frozen, `copy` will be too
140
139
  ````
141
140
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mutate-cow",
3
- "version": "7.0.1",
3
+ "version": "8.1.0",
4
4
  "description": "Update immutable objects as if they were mutable with copy-on-write",
5
5
  "keywords": [
6
6
  "immutable",
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "devDependencies": {
20
20
  "benchmark": "2.1.4",
21
- "flow-bin": "0.275.0",
21
+ "flow-bin": "0.290.0",
22
22
  "immer": "10.1.1",
23
23
  "mutative": "1.2.0",
24
24
  "tsd": "0.29.0"
package/src/index.js CHANGED
@@ -5,10 +5,12 @@
5
5
  * in the file named "LICENSE" at the root directory of this distribution.
6
6
  */
7
7
 
8
+ const EMPTY_OBJECT = Object.freeze({});
9
+
8
10
  const NATIVE_CODE_REGEXP = /^function \w*\(\) \{\s*\[native code\]\s*\}$/m;
9
11
 
10
12
  const STATUS_NONE = 1;
11
- const STATUS_CHANGED = 2;
13
+ const STATUS_MUTABLE = 2;
12
14
  const STATUS_REVOKED = 3;
13
15
  const STATUS_STALE = 4;
14
16
 
@@ -20,40 +22,41 @@ function isPrimitive(value) {
20
22
  return (type !== 'function' && type !== 'object');
21
23
  }
22
24
 
23
- function getCloneableType(value) {
24
- if (isPrimitive(value)) {
25
- return 1;
26
- }
27
- if (typeof value !== 'object') {
28
- return 0;
25
+ function isCloneableObject(object) {
26
+ if (typeof object === 'function') {
27
+ return false;
29
28
  }
30
- let proto = Object.getPrototypeOf(value);
29
+ let proto = Object.getPrototypeOf(object);
31
30
  while (proto) {
32
31
  let ctor = proto.constructor;
33
- // A Generator object's constructor is an object.
34
- if (ctor && typeof ctor === 'object') {
35
- ctor = ctor.constructor;
36
- }
37
- if (
38
- typeof ctor === 'function' &&
39
- ctor.name !== 'Array' &&
40
- ctor.name !== 'Object' &&
41
- NATIVE_CODE_REGEXP.test(Function.prototype.toString.call(ctor))
42
- ) {
43
- return 0;
32
+ if (ctor) {
33
+ // A Generator object's constructor is an object.
34
+ if (typeof ctor === 'object') {
35
+ ctor = ctor.constructor;
36
+ }
37
+ if (
38
+ typeof ctor === 'function' &&
39
+ ctor.name !== 'Array' &&
40
+ ctor.name !== 'Object' &&
41
+ NATIVE_CODE_REGEXP.test(Function.prototype.toString.call(ctor))
42
+ ) {
43
+ return false;
44
+ }
44
45
  }
45
46
  proto = Object.getPrototypeOf(proto);
46
47
  }
47
- return 2;
48
+ return true;
48
49
  }
49
50
 
50
- function throwIfTypeNotCloneable(cloneableType) {
51
- if (cloneableType === 0) {
52
- throw new Error(
53
- 'Only plain objects, arrays, and class instances ' +
54
- 'can be cloned. Primitives, functions, and built-ins ' +
55
- 'are unsupported.',
56
- );
51
+ function printConstructor(object) {
52
+ const ctor = object.constructor;
53
+ switch (typeof ctor) {
54
+ case 'function':
55
+ return ctor.name;
56
+ case 'object':
57
+ return Object.prototype.toString.call(ctor);
58
+ default:
59
+ return '[primitive constructor]';
57
60
  }
58
61
  }
59
62
 
@@ -68,120 +71,50 @@ function restoreDescriptors(copy, changedDescriptors) {
68
71
  }
69
72
  }
70
73
 
71
- function clone(source, callbacks) {
72
- const cloneableType = getCloneableType(source);
73
- throwIfTypeNotCloneable(cloneableType);
74
- if (cloneableType === 1) {
75
- return source;
76
- }
77
- const proto = Object.getPrototypeOf(source);
78
- let changedDescriptors = [];
79
- let copy;
80
- if (Array.isArray(source)) {
81
- copy = Reflect.construct(Array, source, proto.constructor);
82
- } else {
83
- copy = Object.create(proto);
84
- }
85
- const ownKeys = Reflect.ownKeys(source);
86
- for (let i = 0; i < ownKeys.length; i++) {
87
- const key = ownKeys[i];
88
- const descriptor = Object.getOwnPropertyDescriptor(source, key);
89
- let origDesc;
90
- if (descriptor.configurable === false) {
91
- descriptor.configurable = true;
92
- origDesc = {configurable: false};
93
- }
94
- if (descriptor.writable === false) {
95
- descriptor.writable = true;
96
- origDesc = {...origDesc, writable: false};
97
- }
98
- if (origDesc) {
99
- changedDescriptors.push([key, origDesc]);
100
- }
101
- Reflect.defineProperty(copy, key, descriptor);
102
- }
103
- if (changedDescriptors.length) {
104
- callbacks.push({
105
- func: restoreDescriptors,
106
- args: [copy, changedDescriptors],
107
- });
108
- }
109
- if (Object.isFrozen(source)) {
110
- callbacks.push({
111
- func: Object.freeze,
112
- args: [copy],
113
- });
114
- } else if (Object.isSealed(source)) {
115
- callbacks.push({
116
- func: Object.seal,
117
- args: [copy],
118
- });
119
- } else if (!Object.isExtensible(source)) {
120
- callbacks.push({
121
- func: Object.preventExtensions,
122
- args: [copy],
123
- });
124
- }
125
- return copy;
126
- }
127
-
128
74
  export class CowContext {
129
75
  constructor(source, prop, parent) {
130
76
  this._source = source;
131
77
  this._prop = prop;
132
78
  this._parent = parent;
133
- this._callbacks = [];
134
79
  this._result = null;
135
80
  this._status = STATUS_NONE;
136
81
  this._children = null;
137
82
  }
138
83
 
139
- _copyForWrite() {
140
- const status = this._status;
141
- if (
142
- status === STATUS_CHANGED ||
143
- status === STATUS_REVOKED
144
- ) {
84
+ _copyForWrite(forceObject) {
85
+ if (this._status === STATUS_MUTABLE || this._status === STATUS_REVOKED) {
145
86
  return;
146
87
  }
147
- const stack = [];
148
- let parent = this;
149
- while (parent && parent._status !== STATUS_CHANGED) {
150
- stack.push(parent);
151
- parent = parent._parent;
152
- }
153
- for (let i = stack.length - 1; i >= 0; i--) {
154
- const context = stack[i];
155
- if (!context._result) {
156
- context._result = clone(context._getSource(), context._callbacks);
157
- }
158
- if (context._parent) {
159
- context._parent._result[context._prop] = context._result;
88
+ if (!this._result) {
89
+ const source = this._getSource();
90
+ if (isPrimitive(source)) {
91
+ this._result = forceObject ? {} : source;
92
+ } else if (!isCloneableObject(source)) {
93
+ throw new Error(
94
+ printConstructor(source) +
95
+ ' objects are not supported for cloning.',
96
+ );
97
+ } else {
98
+ this._result = this._cloneSourceObject(source);
160
99
  }
161
- context._status = STATUS_CHANGED;
162
100
  }
101
+ const parent = this._parent;
102
+ if (parent) {
103
+ parent._copyForWrite(/* forceObject = */ true);
104
+ parent._result[this._prop] = this._result;
105
+ }
106
+ this._status = STATUS_MUTABLE;
163
107
  }
164
108
 
165
- _getPropDescriptor(target, prop) {
166
- const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
167
- if (descriptor) {
168
- if (descriptor.get) {
169
- throw new Error('Getters are unsupported.');
170
- }
171
- if (descriptor.set) {
172
- throw new Error('Setters are unsupported.');
173
- }
174
- return descriptor;
109
+ _cloneSourceObject(source) {
110
+ if (Array.isArray(source)) {
111
+ return source.slice();
175
112
  }
113
+ return {...source};
176
114
  }
177
115
 
178
116
  _getPropValue(prop) {
179
- const target = this.read();
180
- const descriptor = this._getPropDescriptor(target, prop);
181
- if (descriptor) {
182
- return descriptor.value;
183
- }
184
- return Reflect.get(target, prop);
117
+ return Reflect.get(this._read() ?? EMPTY_OBJECT, prop);
185
118
  }
186
119
 
187
120
  _getSource() {
@@ -204,14 +137,18 @@ export class CowContext {
204
137
  }
205
138
  }
206
139
 
140
+ _read() {
141
+ return this._status === STATUS_MUTABLE ? this._result : this._getSource();
142
+ }
143
+
207
144
  read() {
208
145
  this._throwIfRevoked();
209
- return this._status === STATUS_CHANGED ? this._result : this._getSource();
146
+ return this._read();
210
147
  }
211
148
 
212
149
  write() {
213
150
  this._throwIfRevoked();
214
- this._copyForWrite();
151
+ this._copyForWrite(/* forceObject = */ false);
215
152
  return this._result;
216
153
  }
217
154
 
@@ -229,12 +166,13 @@ export class CowContext {
229
166
  return child;
230
167
  }
231
168
 
232
- child = new CowContext(value, prop, this);
169
+ child = new this.constructor(value, prop, this);
233
170
  children.set(prop, child);
234
171
  return child;
235
172
  }
236
173
 
237
174
  get(...props) {
175
+ this._throwIfRevoked();
238
176
  let ctx = this;
239
177
  for (const prop of props) {
240
178
  ctx = ctx._get(prop);
@@ -245,45 +183,67 @@ export class CowContext {
245
183
  _replace(value) {
246
184
  const parent = this._parent;
247
185
  if (parent) {
248
- parent.set(this._prop, value);
186
+ parent._setIfChanged(this._prop, value);
249
187
  } else {
250
188
  this._source = value;
189
+ this._status = STATUS_NONE;
190
+ this._result = null;
191
+ // Child source values must be invalidated, because they can
192
+ // reference a previous copy we made.
193
+ this._setAllChildrenAsStale();
251
194
  }
252
195
  }
253
196
 
254
197
  _set(prop, newValue) {
255
- const descriptor = this._getPropDescriptor(this.read(), prop);
256
- if (descriptor === undefined || !Object.is(descriptor.value, newValue)) {
257
- this._copyForWrite();
258
- this._result[prop] = newValue;
259
-
260
- // Child source values must be invalidated, because they can
261
- // reference a previous copy we made.
262
- const children = this._children;
263
- if (children) {
264
- const child = children.get(prop);
265
- if (child) {
266
- child._source = null;
267
- child._callbacks = [];
268
- child._result = null;
269
- child._status = STATUS_STALE;
270
- }
198
+ this._copyForWrite(/* forceObject = */ true);
199
+ this._result[prop] = newValue;
200
+
201
+ // Child source values must be invalidated, because they can
202
+ // reference a previous copy we made.
203
+ const children = this._children;
204
+ if (children) {
205
+ const child = children.get(prop);
206
+ if (child) {
207
+ child._setStale();
271
208
  }
272
209
  }
210
+ }
273
211
 
274
- return this;
212
+ _setIfChanged(prop, newValue) {
213
+ const object = this._read() ?? EMPTY_OBJECT;
214
+ if (
215
+ !Object.hasOwn(object, prop) ||
216
+ !Object.is(Reflect.get(object, prop), newValue)
217
+ ) {
218
+ this._set(prop, newValue);
219
+ }
220
+ }
221
+
222
+ _setStale() {
223
+ this._source = null;
224
+ this._result = null;
225
+ this._status = STATUS_STALE;
226
+ this._setAllChildrenAsStale();
227
+ }
228
+
229
+ _setAllChildrenAsStale() {
230
+ const children = this._children;
231
+ if (children) {
232
+ for (const child of children.values()) {
233
+ child._setStale();
234
+ }
235
+ }
275
236
  }
276
237
 
277
238
  set(...args) {
278
- this._throwIfRevoked();
279
239
  const newValue = args.pop();
280
240
  const hasProps = args.length > 0;
281
- const lastProp = hasProps ? args.pop() : undefined;
282
- const ctx = hasProps ? this.get(...args) : this;
283
241
  if (hasProps) {
284
- ctx._set(lastProp, newValue);
242
+ const lastProp = args.pop();
243
+ this.get(...args)._setIfChanged(lastProp, newValue);
285
244
  } else {
286
- ctx._replace(newValue);
245
+ this._throwIfRevoked();
246
+ this._replace(newValue);
287
247
  }
288
248
  return this;
289
249
  }
@@ -294,6 +254,21 @@ export class CowContext {
294
254
  return this;
295
255
  }
296
256
 
257
+ dangerouslySetAsMutable() {
258
+ this._throwIfRevoked();
259
+ const source = this._getSource();
260
+ // N.B. This may be (dangerously) equal to `source`.
261
+ const mutableValue = this._read();
262
+ const parent = this._parent;
263
+ if (parent) {
264
+ parent._set(this._prop, mutableValue);
265
+ }
266
+ this._source = source;
267
+ this._result = mutableValue;
268
+ this._status = STATUS_MUTABLE;
269
+ this._setAllChildrenAsStale();
270
+ }
271
+
297
272
  parent() {
298
273
  this._throwIfRevoked();
299
274
  return this._parent;
@@ -308,27 +283,31 @@ export class CowContext {
308
283
  return root;
309
284
  }
310
285
 
311
- revoke() {
286
+ _revoke(recursive) {
312
287
  if (this.isRevoked()) {
313
288
  return;
314
289
  }
315
290
  if (this._parent) {
316
291
  this._parent._children.delete(this._prop);
317
292
  }
318
- if (this._children) {
319
- for (const child of this._children.values()) {
320
- child.revoke();
293
+ if (recursive && this._children) {
294
+ const childrenToRevoke = [...this._children.values()];
295
+ for (const child of childrenToRevoke) {
296
+ child._revoke(true);
321
297
  }
322
- this._children = null;
323
298
  }
299
+ this._children = null;
324
300
  this._source = null;
325
301
  this._prop = null;
326
302
  this._parent = null;
327
- this._callbacks = null;
328
303
  this._result = null;
329
304
  this._status = STATUS_REVOKED;
330
305
  }
331
306
 
307
+ revoke() {
308
+ this._revoke(/* recursive = */ true);
309
+ }
310
+
332
311
  isRevoked() {
333
312
  return this._status === STATUS_REVOKED;
334
313
  }
@@ -336,17 +315,13 @@ export class CowContext {
336
315
  final() {
337
316
  this._throwIfRevoked();
338
317
  if (this._children) {
339
- for (const child of this._children.values()) {
318
+ const childrenToFinalize = [...this._children.values()];
319
+ for (const child of childrenToFinalize) {
340
320
  child.final();
341
321
  }
342
322
  }
343
- const result = this.read();
344
- const callbacks = this._callbacks;
345
- this.revoke();
346
- for (let i = 0; i < callbacks.length; i++) {
347
- const {func, args} = callbacks[i];
348
- func(...args);
349
- }
323
+ const result = this._read();
324
+ this._revoke(/* recursive = */ false);
350
325
  return result;
351
326
  }
352
327
 
@@ -355,7 +330,106 @@ export class CowContext {
355
330
  }
356
331
  }
357
332
 
358
- export default function mutate(source) {
359
- throwIfTypeNotCloneable(getCloneableType(source));
360
- return new CowContext(source, null, null);
333
+ class CowContextStrict extends CowContext {
334
+ constructor(source, prop, parent) {
335
+ super(source, prop, parent);
336
+ this._callbacks = null;
337
+ }
338
+
339
+ _replace(value) {
340
+ super._replace(value);
341
+ if (!this._parent) {
342
+ this._callbacks = null;
343
+ }
344
+ }
345
+
346
+ _setStale() {
347
+ super._setStale();
348
+ this._callbacks = null;
349
+ }
350
+
351
+ _cloneSourceObject(source) {
352
+ if (!this._callbacks) {
353
+ this._callbacks = [];
354
+ }
355
+ const proto = Object.getPrototypeOf(source);
356
+ let changedDescriptors = [];
357
+ let copy;
358
+ if (Array.isArray(source)) {
359
+ copy = Reflect.construct(Array, source, proto.constructor);
360
+ } else {
361
+ copy = Object.create(proto);
362
+ }
363
+ const ownKeys = Reflect.ownKeys(source);
364
+ for (let i = 0; i < ownKeys.length; i++) {
365
+ const key = ownKeys[i];
366
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
367
+ let origDesc;
368
+ if (descriptor.configurable === false) {
369
+ descriptor.configurable = true;
370
+ origDesc = {configurable: false};
371
+ }
372
+ if (descriptor.writable === false) {
373
+ descriptor.writable = true;
374
+ origDesc = {...origDesc, writable: false};
375
+ }
376
+ if (origDesc) {
377
+ changedDescriptors.push([key, origDesc]);
378
+ }
379
+ Reflect.defineProperty(copy, key, descriptor);
380
+ }
381
+ if (changedDescriptors.length) {
382
+ this._callbacks.push({
383
+ func: restoreDescriptors,
384
+ args: [copy, changedDescriptors],
385
+ });
386
+ }
387
+ if (Object.isFrozen(source)) {
388
+ this._callbacks.push({
389
+ func: Object.freeze,
390
+ args: [copy],
391
+ });
392
+ } else if (Object.isSealed(source)) {
393
+ this._callbacks.push({
394
+ func: Object.seal,
395
+ args: [copy],
396
+ });
397
+ } else if (!Object.isExtensible(source)) {
398
+ this._callbacks.push({
399
+ func: Object.preventExtensions,
400
+ args: [copy],
401
+ });
402
+ }
403
+ return copy;
404
+ }
405
+
406
+ dangerouslySetAsMutable() {
407
+ super.dangerouslySetAsMutable();
408
+ this._callbacks = null;
409
+ }
410
+
411
+ revoke() {
412
+ super.revoke();
413
+ this._callbacks = null;
414
+ }
415
+
416
+ final() {
417
+ const callbacks = this._callbacks;
418
+ const result = super.final();
419
+ if (callbacks) {
420
+ for (let i = 0; i < callbacks.length; i++) {
421
+ const {func, args} = callbacks[i];
422
+ func(...args);
423
+ }
424
+ }
425
+ return result;
426
+ }
427
+ }
428
+
429
+ export default function mutate(source, strict = false) {
430
+ if (strict) {
431
+ return new CowContextStrict(source, null, null);
432
+ } else {
433
+ return new CowContext(source, null, null);
434
+ }
361
435
  }
package/src/index.js.flow CHANGED
@@ -101,6 +101,7 @@ declare export class CowContext<
101
101
  update<const K1: KeyType<T>, const K2: KeyType2<T, K1>, const K3: KeyType3<T, K1, K2>, const K4: KeyType4<T, K1, K2, K3>, const K5: KeyType5<T, K1, K2, K3, K4>, const K6: KeyType6<T, K1, K2, K3, K4, K5>>(prop1: K1, prop2: K2, prop3: K3, prop4: K4, prop5: K5, prop6: K6, updater: ($Get6<T, K1, K2, K3, K4, K5, K6, this>) => mixed): this;
102
102
  update<const K1: KeyType<T>, const K2: KeyType2<T, K1>, const K3: KeyType3<T, K1, K2>, const K4: KeyType4<T, K1, K2, K3>, const K5: KeyType5<T, K1, K2, K3, K4>, const K6: KeyType6<T, K1, K2, K3, K4, K5>, const K7: KeyType7<T, K1, K2, K3, K4, K5, K6>>(prop1: K1, prop2: K2, prop3: K3, prop4: K4, prop5: K5, prop6: K6, prop7: K7, updater: ($Get7<T, K1, K2, K3, K4, K5, K6, K7, this>) => mixed): this;
103
103
  update<const K1: KeyType<T>, const K2: KeyType2<T, K1>, const K3: KeyType3<T, K1, K2>, const K4: KeyType4<T, K1, K2, K3>, const K5: KeyType5<T, K1, K2, K3, K4>, const K6: KeyType6<T, K1, K2, K3, K4, K5>, const K7: KeyType7<T, K1, K2, K3, K4, K5, K6>, const K8: KeyType8<T, K1, K2, K3, K4, K5, K6, K7>>(prop1: K1, prop2: K2, prop3: K3, prop4: K4, prop5: K5, prop6: K6, prop7: K7, prop8: K8, updater: ($Get8<T, K1, K2, K3, K4, K5, K6, K7, K8, this>) => mixed): this;
104
+ dangerouslySetAsMutable(): void;
104
105
  parent(): ParentContext;
105
106
  root(): GetCowContextRoot<this>;
106
107
  revoke(): void;
@@ -111,4 +112,5 @@ declare export class CowContext<
111
112
 
112
113
  declare export default function mutate<T>(
113
114
  source: T,
115
+ strict?: boolean,
114
116
  ): CowRootContext<T>;
package/types/index.d.ts CHANGED
@@ -45,6 +45,7 @@ declare class CowContext<
45
45
  get<Path extends ReadonlyArray<PropertyKey>>(...path: Path): NestedContext<T, ParentContext, Path>;
46
46
  set<Path extends ReadonlyArray<PropertyKey>>(...args: [...Path, NestedProp<T, Path>]): this;
47
47
  update<Path extends ReadonlyArray<PropertyKey>>(...args: [...Path, (childContext: NestedContext<T, ParentContext, Path>) => unknown]): this;
48
+ dangerouslySetAsMutable(): void;
48
49
  parent(): ParentContext;
49
50
  root(): GetCowContextRoot<this>;
50
51
  revoke(): void;
@@ -55,6 +56,7 @@ declare class CowContext<
55
56
 
56
57
  declare function mutate<T>(
57
58
  source: T,
59
+ strict?: boolean,
58
60
  ): CowRootContext<T>;
59
61
 
60
62
  export {CowRootContext, CowAnyContext, CowContext};