mutate-cow 7.0.0 → 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 +9 -10
- package/package.json +1 -1
- package/src/index.js +135 -83
- package/src/index.js.flow +3 -1
- package/types/index.d.ts +2 -0
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
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
|
|
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
|
|
24
|
-
if (
|
|
25
|
-
return
|
|
23
|
+
function isCloneableObject(object) {
|
|
24
|
+
if (typeof object === 'function') {
|
|
25
|
+
return false;
|
|
26
26
|
}
|
|
27
|
-
|
|
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 (
|
|
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
|
|
43
|
+
return false;
|
|
44
44
|
}
|
|
45
45
|
proto = Object.getPrototypeOf(proto);
|
|
46
46
|
}
|
|
47
|
-
return
|
|
47
|
+
return true;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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(
|
|
72
|
-
const
|
|
73
|
-
|
|
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
|
-
|
|
128
|
+
context._callbacks.push({
|
|
105
129
|
func: restoreDescriptors,
|
|
106
130
|
args: [copy, changedDescriptors],
|
|
107
131
|
});
|
|
108
132
|
}
|
|
109
133
|
if (Object.isFrozen(source)) {
|
|
110
|
-
|
|
134
|
+
context._callbacks.push({
|
|
111
135
|
func: Object.freeze,
|
|
112
136
|
args: [copy],
|
|
113
137
|
});
|
|
114
138
|
} else if (Object.isSealed(source)) {
|
|
115
|
-
|
|
139
|
+
context._callbacks.push({
|
|
116
140
|
func: Object.seal,
|
|
117
141
|
args: [copy],
|
|
118
142
|
});
|
|
119
143
|
} else if (!Object.isExtensible(source)) {
|
|
120
|
-
|
|
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 ===
|
|
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 !==
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
if (
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317
|
+
const lastProp = args.pop();
|
|
318
|
+
this.get(...args)._setIfChanged(lastProp, newValue);
|
|
285
319
|
} else {
|
|
286
|
-
|
|
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.
|
|
394
|
+
const result = this._read();
|
|
344
395
|
const callbacks = this._callbacks;
|
|
345
396
|
this.revoke();
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
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
|
@@ -68,7 +68,7 @@ export type GetCowContextRoot<+C> =
|
|
|
68
68
|
? (P extends null ? C : GetCowContextRoot<P>)
|
|
69
69
|
: empty;
|
|
70
70
|
|
|
71
|
-
declare class CowContext<
|
|
71
|
+
declare export class CowContext<
|
|
72
72
|
+T,
|
|
73
73
|
+ParentContext: CowAnyContext | null = CowAnyContext | null,
|
|
74
74
|
>{
|
|
@@ -101,6 +101,7 @@ declare 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 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};
|