mutate-cow 7.0.1 → 8.0.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.0.0",
4
4
  "description": "Update immutable objects as if they were mutable with copy-on-write",
5
5
  "keywords": [
6
6
  "immutable",
package/src/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  const NATIVE_CODE_REGEXP = /^function \w*\(\) \{\s*\[native code\]\s*\}$/m;
9
9
 
10
10
  const STATUS_NONE = 1;
11
- const STATUS_CHANGED = 2;
11
+ const STATUS_MUTABLE = 2;
12
12
  const STATUS_REVOKED = 3;
13
13
  const STATUS_STALE = 4;
14
14
 
@@ -20,18 +20,18 @@ function isPrimitive(value) {
20
20
  return (type !== 'function' && type !== 'object');
21
21
  }
22
22
 
23
- function getCloneableType(value) {
24
- if (isPrimitive(value)) {
25
- return 1;
23
+ function isCloneableObject(object) {
24
+ if (typeof object === 'function') {
25
+ return false;
26
26
  }
27
- if (typeof value !== 'object') {
28
- return 0;
29
- }
30
- let proto = Object.getPrototypeOf(value);
27
+ let proto = Object.getPrototypeOf(object);
31
28
  while (proto) {
32
29
  let ctor = proto.constructor;
30
+ if (!ctor) {
31
+ continue;
32
+ }
33
33
  // A Generator object's constructor is an object.
34
- if (ctor && typeof ctor === 'object') {
34
+ if (typeof ctor === 'object') {
35
35
  ctor = ctor.constructor;
36
36
  }
37
37
  if (
@@ -40,20 +40,22 @@ function getCloneableType(value) {
40
40
  ctor.name !== 'Object' &&
41
41
  NATIVE_CODE_REGEXP.test(Function.prototype.toString.call(ctor))
42
42
  ) {
43
- return 0;
43
+ return false;
44
44
  }
45
45
  proto = Object.getPrototypeOf(proto);
46
46
  }
47
- return 2;
47
+ return true;
48
48
  }
49
49
 
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
- );
50
+ function printConstructor(object) {
51
+ const ctor = object.constructor;
52
+ switch (typeof ctor) {
53
+ case 'function':
54
+ return ctor.name;
55
+ case 'object':
56
+ return Object.prototype.toString.call(ctor);
57
+ default:
58
+ return '';
57
59
  }
58
60
  }
59
61
 
@@ -68,12 +70,34 @@ function restoreDescriptors(copy, changedDescriptors) {
68
70
  }
69
71
  }
70
72
 
71
- function clone(source, callbacks) {
72
- const cloneableType = getCloneableType(source);
73
- throwIfTypeNotCloneable(cloneableType);
74
- if (cloneableType === 1) {
73
+ function clone(context) {
74
+ const source = context._getSource();
75
+ if (isPrimitive(source)) {
75
76
  return source;
76
77
  }
78
+ if (!isCloneableObject(source)) {
79
+ throw new Error(
80
+ printConstructor(source) +
81
+ ' objects are not supported for cloning.',
82
+ );
83
+ }
84
+ if (context._strict) {
85
+ return cloneObjectStrict(source, context);
86
+ }
87
+ return cloneObjectLoose(source);
88
+ }
89
+
90
+ function cloneObjectLoose(source) {
91
+ if (Array.isArray(source)) {
92
+ return source.slice();
93
+ }
94
+ return {...source};
95
+ }
96
+
97
+ function cloneObjectStrict(source, context) {
98
+ if (!context._callbacks) {
99
+ context._callbacks = [];
100
+ }
77
101
  const proto = Object.getPrototypeOf(source);
78
102
  let changedDescriptors = [];
79
103
  let copy;
@@ -101,23 +125,23 @@ function clone(source, callbacks) {
101
125
  Reflect.defineProperty(copy, key, descriptor);
102
126
  }
103
127
  if (changedDescriptors.length) {
104
- callbacks.push({
128
+ context._callbacks.push({
105
129
  func: restoreDescriptors,
106
130
  args: [copy, changedDescriptors],
107
131
  });
108
132
  }
109
133
  if (Object.isFrozen(source)) {
110
- callbacks.push({
134
+ context._callbacks.push({
111
135
  func: Object.freeze,
112
136
  args: [copy],
113
137
  });
114
138
  } else if (Object.isSealed(source)) {
115
- callbacks.push({
139
+ context._callbacks.push({
116
140
  func: Object.seal,
117
141
  args: [copy],
118
142
  });
119
143
  } else if (!Object.isExtensible(source)) {
120
- callbacks.push({
144
+ context._callbacks.push({
121
145
  func: Object.preventExtensions,
122
146
  args: [copy],
123
147
  });
@@ -126,62 +150,45 @@ function clone(source, callbacks) {
126
150
  }
127
151
 
128
152
  export class CowContext {
129
- constructor(source, prop, parent) {
153
+ constructor(source, prop, parent, strict = false) {
130
154
  this._source = source;
131
155
  this._prop = prop;
132
156
  this._parent = parent;
133
- this._callbacks = [];
157
+ this._callbacks = null;
134
158
  this._result = null;
135
159
  this._status = STATUS_NONE;
136
160
  this._children = null;
161
+ this._strict = strict;
137
162
  }
138
163
 
139
164
  _copyForWrite() {
140
165
  const status = this._status;
141
166
  if (
142
- status === STATUS_CHANGED ||
167
+ status === STATUS_MUTABLE ||
143
168
  status === STATUS_REVOKED
144
169
  ) {
145
170
  return;
146
171
  }
147
172
  const stack = [];
148
173
  let parent = this;
149
- while (parent && parent._status !== STATUS_CHANGED) {
174
+ while (parent && parent._status !== STATUS_MUTABLE) {
150
175
  stack.push(parent);
151
176
  parent = parent._parent;
152
177
  }
153
178
  for (let i = stack.length - 1; i >= 0; i--) {
154
179
  const context = stack[i];
155
180
  if (!context._result) {
156
- context._result = clone(context._getSource(), context._callbacks);
181
+ context._result = clone(context);
157
182
  }
158
183
  if (context._parent) {
159
184
  context._parent._result[context._prop] = context._result;
160
185
  }
161
- context._status = STATUS_CHANGED;
162
- }
163
- }
164
-
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;
186
+ context._status = STATUS_MUTABLE;
175
187
  }
176
188
  }
177
189
 
178
190
  _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);
191
+ return Reflect.get(this._read(), prop);
185
192
  }
186
193
 
187
194
  _getSource() {
@@ -204,9 +211,13 @@ export class CowContext {
204
211
  }
205
212
  }
206
213
 
214
+ _read() {
215
+ return this._status === STATUS_MUTABLE ? this._result : this._getSource();
216
+ }
217
+
207
218
  read() {
208
219
  this._throwIfRevoked();
209
- return this._status === STATUS_CHANGED ? this._result : this._getSource();
220
+ return this._read();
210
221
  }
211
222
 
212
223
  write() {
@@ -229,12 +240,13 @@ export class CowContext {
229
240
  return child;
230
241
  }
231
242
 
232
- child = new CowContext(value, prop, this);
243
+ child = new CowContext(value, prop, this, this._strict);
233
244
  children.set(prop, child);
234
245
  return child;
235
246
  }
236
247
 
237
248
  get(...props) {
249
+ this._throwIfRevoked();
238
250
  let ctx = this;
239
251
  for (const prop of props) {
240
252
  ctx = ctx._get(prop);
@@ -245,45 +257,68 @@ export class CowContext {
245
257
  _replace(value) {
246
258
  const parent = this._parent;
247
259
  if (parent) {
248
- parent.set(this._prop, value);
260
+ parent._setIfChanged(this._prop, value);
249
261
  } else {
250
262
  this._source = value;
263
+ this._status = STATUS_NONE;
264
+ this._callbacks = null;
265
+ this._result = null;
266
+ // Child source values must be invalidated, because they can
267
+ // reference a previous copy we made.
268
+ this._setAllChildrenAsStale();
251
269
  }
252
270
  }
253
271
 
254
272
  _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
- }
273
+ this._copyForWrite();
274
+ this._result[prop] = newValue;
275
+
276
+ // Child source values must be invalidated, because they can
277
+ // reference a previous copy we made.
278
+ const children = this._children;
279
+ if (children) {
280
+ const child = children.get(prop);
281
+ if (child) {
282
+ child._setStale();
271
283
  }
272
284
  }
285
+ }
273
286
 
274
- return this;
287
+ _setIfChanged(prop, newValue) {
288
+ if (
289
+ !Object.hasOwn(this._read(), prop) ||
290
+ !Object.is(this._getPropValue(prop), newValue)
291
+ ) {
292
+ this._set(prop, newValue);
293
+ }
294
+ }
295
+
296
+ _setStale() {
297
+ this._source = null;
298
+ this._callbacks = null;
299
+ this._result = null;
300
+ this._status = STATUS_STALE;
301
+ this._setAllChildrenAsStale();
302
+ }
303
+
304
+ _setAllChildrenAsStale() {
305
+ const children = this._children;
306
+ if (children) {
307
+ for (const child of children.values()) {
308
+ child._setStale();
309
+ }
310
+ }
275
311
  }
276
312
 
277
313
  set(...args) {
278
- this._throwIfRevoked();
279
314
  const newValue = args.pop();
280
315
  const hasProps = args.length > 0;
281
- const lastProp = hasProps ? args.pop() : undefined;
282
- const ctx = hasProps ? this.get(...args) : this;
283
316
  if (hasProps) {
284
- ctx._set(lastProp, newValue);
317
+ const lastProp = args.pop();
318
+ this.get(...args)._setIfChanged(lastProp, newValue);
285
319
  } else {
286
- ctx._replace(newValue);
320
+ this._throwIfRevoked();
321
+ this._replace(newValue);
287
322
  }
288
323
  return this;
289
324
  }
@@ -294,6 +329,22 @@ export class CowContext {
294
329
  return this;
295
330
  }
296
331
 
332
+ dangerouslySetAsMutable() {
333
+ this._throwIfRevoked();
334
+ const source = this._getSource();
335
+ // N.B. This may be (dangerously) equal to `source`.
336
+ const mutableValue = this._read();
337
+ const parent = this._parent;
338
+ if (parent) {
339
+ parent._set(this._prop, mutableValue);
340
+ }
341
+ this._source = source;
342
+ this._result = mutableValue;
343
+ this._status = STATUS_MUTABLE;
344
+ this._callbacks = null;
345
+ this._setAllChildrenAsStale();
346
+ }
347
+
297
348
  parent() {
298
349
  this._throwIfRevoked();
299
350
  return this._parent;
@@ -340,12 +391,14 @@ export class CowContext {
340
391
  child.final();
341
392
  }
342
393
  }
343
- const result = this.read();
394
+ const result = this._read();
344
395
  const callbacks = this._callbacks;
345
396
  this.revoke();
346
- for (let i = 0; i < callbacks.length; i++) {
347
- const {func, args} = callbacks[i];
348
- func(...args);
397
+ if (callbacks) {
398
+ for (let i = 0; i < callbacks.length; i++) {
399
+ const {func, args} = callbacks[i];
400
+ func(...args);
401
+ }
349
402
  }
350
403
  return result;
351
404
  }
@@ -355,7 +408,6 @@ export class CowContext {
355
408
  }
356
409
  }
357
410
 
358
- export default function mutate(source) {
359
- throwIfTypeNotCloneable(getCloneableType(source));
360
- return new CowContext(source, null, null);
411
+ export default function mutate(source, strict = false) {
412
+ return new CowContext(source, null, null, strict);
361
413
  }
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};