mutate-cow 8.0.0 → 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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/index.js +161 -139
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mutate-cow",
3
- "version": "8.0.0",
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,6 +5,8 @@
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;
@@ -27,20 +29,19 @@ function isCloneableObject(object) {
27
29
  let proto = Object.getPrototypeOf(object);
28
30
  while (proto) {
29
31
  let ctor = proto.constructor;
30
- if (!ctor) {
31
- continue;
32
- }
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;
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
  }
@@ -55,7 +56,7 @@ function printConstructor(object) {
55
56
  case 'object':
56
57
  return Object.prototype.toString.call(ctor);
57
58
  default:
58
- return '';
59
+ return '[primitive constructor]';
59
60
  }
60
61
  }
61
62
 
@@ -70,125 +71,50 @@ function restoreDescriptors(copy, changedDescriptors) {
70
71
  }
71
72
  }
72
73
 
73
- function clone(context) {
74
- const source = context._getSource();
75
- if (isPrimitive(source)) {
76
- return source;
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
- }
101
- const proto = Object.getPrototypeOf(source);
102
- let changedDescriptors = [];
103
- let copy;
104
- if (Array.isArray(source)) {
105
- copy = Reflect.construct(Array, source, proto.constructor);
106
- } else {
107
- copy = Object.create(proto);
108
- }
109
- const ownKeys = Reflect.ownKeys(source);
110
- for (let i = 0; i < ownKeys.length; i++) {
111
- const key = ownKeys[i];
112
- const descriptor = Object.getOwnPropertyDescriptor(source, key);
113
- let origDesc;
114
- if (descriptor.configurable === false) {
115
- descriptor.configurable = true;
116
- origDesc = {configurable: false};
117
- }
118
- if (descriptor.writable === false) {
119
- descriptor.writable = true;
120
- origDesc = {...origDesc, writable: false};
121
- }
122
- if (origDesc) {
123
- changedDescriptors.push([key, origDesc]);
124
- }
125
- Reflect.defineProperty(copy, key, descriptor);
126
- }
127
- if (changedDescriptors.length) {
128
- context._callbacks.push({
129
- func: restoreDescriptors,
130
- args: [copy, changedDescriptors],
131
- });
132
- }
133
- if (Object.isFrozen(source)) {
134
- context._callbacks.push({
135
- func: Object.freeze,
136
- args: [copy],
137
- });
138
- } else if (Object.isSealed(source)) {
139
- context._callbacks.push({
140
- func: Object.seal,
141
- args: [copy],
142
- });
143
- } else if (!Object.isExtensible(source)) {
144
- context._callbacks.push({
145
- func: Object.preventExtensions,
146
- args: [copy],
147
- });
148
- }
149
- return copy;
150
- }
151
-
152
74
  export class CowContext {
153
- constructor(source, prop, parent, strict = false) {
75
+ constructor(source, prop, parent) {
154
76
  this._source = source;
155
77
  this._prop = prop;
156
78
  this._parent = parent;
157
- this._callbacks = null;
158
79
  this._result = null;
159
80
  this._status = STATUS_NONE;
160
81
  this._children = null;
161
- this._strict = strict;
162
82
  }
163
83
 
164
- _copyForWrite() {
165
- const status = this._status;
166
- if (
167
- status === STATUS_MUTABLE ||
168
- status === STATUS_REVOKED
169
- ) {
84
+ _copyForWrite(forceObject) {
85
+ if (this._status === STATUS_MUTABLE || this._status === STATUS_REVOKED) {
170
86
  return;
171
87
  }
172
- const stack = [];
173
- let parent = this;
174
- while (parent && parent._status !== STATUS_MUTABLE) {
175
- stack.push(parent);
176
- parent = parent._parent;
177
- }
178
- for (let i = stack.length - 1; i >= 0; i--) {
179
- const context = stack[i];
180
- if (!context._result) {
181
- context._result = clone(context);
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);
182
99
  }
183
- if (context._parent) {
184
- context._parent._result[context._prop] = context._result;
185
- }
186
- context._status = STATUS_MUTABLE;
187
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;
107
+ }
108
+
109
+ _cloneSourceObject(source) {
110
+ if (Array.isArray(source)) {
111
+ return source.slice();
112
+ }
113
+ return {...source};
188
114
  }
189
115
 
190
116
  _getPropValue(prop) {
191
- return Reflect.get(this._read(), prop);
117
+ return Reflect.get(this._read() ?? EMPTY_OBJECT, prop);
192
118
  }
193
119
 
194
120
  _getSource() {
@@ -222,7 +148,7 @@ export class CowContext {
222
148
 
223
149
  write() {
224
150
  this._throwIfRevoked();
225
- this._copyForWrite();
151
+ this._copyForWrite(/* forceObject = */ false);
226
152
  return this._result;
227
153
  }
228
154
 
@@ -240,7 +166,7 @@ export class CowContext {
240
166
  return child;
241
167
  }
242
168
 
243
- child = new CowContext(value, prop, this, this._strict);
169
+ child = new this.constructor(value, prop, this);
244
170
  children.set(prop, child);
245
171
  return child;
246
172
  }
@@ -261,7 +187,6 @@ export class CowContext {
261
187
  } else {
262
188
  this._source = value;
263
189
  this._status = STATUS_NONE;
264
- this._callbacks = null;
265
190
  this._result = null;
266
191
  // Child source values must be invalidated, because they can
267
192
  // reference a previous copy we made.
@@ -270,7 +195,7 @@ export class CowContext {
270
195
  }
271
196
 
272
197
  _set(prop, newValue) {
273
- this._copyForWrite();
198
+ this._copyForWrite(/* forceObject = */ true);
274
199
  this._result[prop] = newValue;
275
200
 
276
201
  // Child source values must be invalidated, because they can
@@ -285,9 +210,10 @@ export class CowContext {
285
210
  }
286
211
 
287
212
  _setIfChanged(prop, newValue) {
213
+ const object = this._read() ?? EMPTY_OBJECT;
288
214
  if (
289
- !Object.hasOwn(this._read(), prop) ||
290
- !Object.is(this._getPropValue(prop), newValue)
215
+ !Object.hasOwn(object, prop) ||
216
+ !Object.is(Reflect.get(object, prop), newValue)
291
217
  ) {
292
218
  this._set(prop, newValue);
293
219
  }
@@ -295,7 +221,6 @@ export class CowContext {
295
221
 
296
222
  _setStale() {
297
223
  this._source = null;
298
- this._callbacks = null;
299
224
  this._result = null;
300
225
  this._status = STATUS_STALE;
301
226
  this._setAllChildrenAsStale();
@@ -341,7 +266,6 @@ export class CowContext {
341
266
  this._source = source;
342
267
  this._result = mutableValue;
343
268
  this._status = STATUS_MUTABLE;
344
- this._callbacks = null;
345
269
  this._setAllChildrenAsStale();
346
270
  }
347
271
 
@@ -359,27 +283,31 @@ export class CowContext {
359
283
  return root;
360
284
  }
361
285
 
362
- revoke() {
286
+ _revoke(recursive) {
363
287
  if (this.isRevoked()) {
364
288
  return;
365
289
  }
366
290
  if (this._parent) {
367
291
  this._parent._children.delete(this._prop);
368
292
  }
369
- if (this._children) {
370
- for (const child of this._children.values()) {
371
- child.revoke();
293
+ if (recursive && this._children) {
294
+ const childrenToRevoke = [...this._children.values()];
295
+ for (const child of childrenToRevoke) {
296
+ child._revoke(true);
372
297
  }
373
- this._children = null;
374
298
  }
299
+ this._children = null;
375
300
  this._source = null;
376
301
  this._prop = null;
377
302
  this._parent = null;
378
- this._callbacks = null;
379
303
  this._result = null;
380
304
  this._status = STATUS_REVOKED;
381
305
  }
382
306
 
307
+ revoke() {
308
+ this._revoke(/* recursive = */ true);
309
+ }
310
+
383
311
  isRevoked() {
384
312
  return this._status === STATUS_REVOKED;
385
313
  }
@@ -387,13 +315,107 @@ export class CowContext {
387
315
  final() {
388
316
  this._throwIfRevoked();
389
317
  if (this._children) {
390
- for (const child of this._children.values()) {
318
+ const childrenToFinalize = [...this._children.values()];
319
+ for (const child of childrenToFinalize) {
391
320
  child.final();
392
321
  }
393
322
  }
394
323
  const result = this._read();
324
+ this._revoke(/* recursive = */ false);
325
+ return result;
326
+ }
327
+
328
+ finalRoot() {
329
+ return this.root().final();
330
+ }
331
+ }
332
+
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() {
395
417
  const callbacks = this._callbacks;
396
- this.revoke();
418
+ const result = super.final();
397
419
  if (callbacks) {
398
420
  for (let i = 0; i < callbacks.length; i++) {
399
421
  const {func, args} = callbacks[i];
@@ -402,12 +424,12 @@ export class CowContext {
402
424
  }
403
425
  return result;
404
426
  }
405
-
406
- finalRoot() {
407
- return this.root().final();
408
- }
409
427
  }
410
428
 
411
429
  export default function mutate(source, strict = false) {
412
- return new CowContext(source, null, null, strict);
430
+ if (strict) {
431
+ return new CowContextStrict(source, null, null);
432
+ } else {
433
+ return new CowContext(source, null, null);
434
+ }
413
435
  }