mutate-cow 4.1.0 → 5.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/CowContext.mjs +238 -0
- package/LICENSE +1 -1
- package/README.md +110 -59
- package/bench.mjs +39 -8
- package/canClone.mjs +10 -6
- package/clone.mjs +49 -14
- package/constants.mjs +17 -18
- package/index.d.ts +51 -0
- package/index.mjs +6 -36
- package/index.mjs.flow +138 -6
- package/index.test-d.ts +60 -0
- package/package.json +10 -7
- package/test.mjs +602 -636
- package/index.js.flow +0 -8
- package/isObject.mjs +0 -11
- package/makeProxy.mjs +0 -170
- package/unwrap.mjs +0 -19
package/CowContext.mjs
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023 Michael Wiencek
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license. A copy can be found
|
|
5
|
+
* in the file named "LICENSE" at the root directory of this distribution.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import clone from './clone.mjs';
|
|
9
|
+
import {
|
|
10
|
+
STATUS_CHANGED,
|
|
11
|
+
STATUS_NONE,
|
|
12
|
+
STATUS_REVOKED,
|
|
13
|
+
} from './constants.mjs';
|
|
14
|
+
|
|
15
|
+
const STALE_VALUE = Object.freeze(Object.create(null));
|
|
16
|
+
|
|
17
|
+
export default class CowContext {
|
|
18
|
+
constructor(source, prop, root, parent) {
|
|
19
|
+
this._source = source;
|
|
20
|
+
this._prop = prop;
|
|
21
|
+
this._root = root || this;
|
|
22
|
+
this._parent = parent;
|
|
23
|
+
this._callbacks = [];
|
|
24
|
+
this._result = null;
|
|
25
|
+
this._status = STATUS_NONE;
|
|
26
|
+
this._children = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_copyForWrite() {
|
|
30
|
+
const status = this._status;
|
|
31
|
+
if (
|
|
32
|
+
status === STATUS_CHANGED ||
|
|
33
|
+
status === STATUS_REVOKED
|
|
34
|
+
) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const stack = [];
|
|
38
|
+
let parent = this;
|
|
39
|
+
while (parent && parent._status !== STATUS_CHANGED) {
|
|
40
|
+
stack.push(parent);
|
|
41
|
+
parent = parent._parent;
|
|
42
|
+
}
|
|
43
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
44
|
+
const context = stack[i];
|
|
45
|
+
if (!context._result) {
|
|
46
|
+
context._result = clone(context._getSource(), context._callbacks);
|
|
47
|
+
}
|
|
48
|
+
if (context._parent) {
|
|
49
|
+
context._parent._result[context._prop] = context._result;
|
|
50
|
+
}
|
|
51
|
+
context._status = STATUS_CHANGED;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_getPropValue(prop) {
|
|
56
|
+
const target = this.read();
|
|
57
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
58
|
+
if (descriptor) {
|
|
59
|
+
if (descriptor.get) {
|
|
60
|
+
throw new Error('Getters are unsupported.');
|
|
61
|
+
}
|
|
62
|
+
if (descriptor.set) {
|
|
63
|
+
throw new Error('Setters are unsupported.');
|
|
64
|
+
}
|
|
65
|
+
return descriptor.value;
|
|
66
|
+
}
|
|
67
|
+
return Reflect.get(target, prop);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_getSource() {
|
|
71
|
+
let source = this._source;
|
|
72
|
+
if (source === STALE_VALUE) {
|
|
73
|
+
/*
|
|
74
|
+
* `this._parent` should always be defined here, because we only
|
|
75
|
+
* ever assign `STALE_VALUE` onto child contexts.
|
|
76
|
+
*/
|
|
77
|
+
source = this._parent._getPropValue(this._prop);
|
|
78
|
+
this._source = source;
|
|
79
|
+
}
|
|
80
|
+
return source;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_throwIfRevoked() {
|
|
84
|
+
if (this.isRevoked()) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
'This context has been revoked and can no longer be used.',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
read() {
|
|
92
|
+
this._throwIfRevoked();
|
|
93
|
+
return this._status === STATUS_CHANGED ? this._result : this._getSource();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
write() {
|
|
97
|
+
this._throwIfRevoked();
|
|
98
|
+
this._copyForWrite();
|
|
99
|
+
return this._result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_get(prop) {
|
|
103
|
+
const value = this._getPropValue(prop);
|
|
104
|
+
|
|
105
|
+
let children = this._children;
|
|
106
|
+
if (!children) {
|
|
107
|
+
children = new Map();
|
|
108
|
+
this._children = children;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let child = children.get(prop);
|
|
112
|
+
if (child) {
|
|
113
|
+
return child;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
child = new CowContext(value, prop, this._root, this);
|
|
117
|
+
children.set(prop, child);
|
|
118
|
+
return child;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get(...props) {
|
|
122
|
+
let ctx = this;
|
|
123
|
+
for (const prop of props) {
|
|
124
|
+
ctx = ctx._get(prop);
|
|
125
|
+
}
|
|
126
|
+
return ctx;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_replace(value) {
|
|
130
|
+
const parent = this._parent;
|
|
131
|
+
if (parent) {
|
|
132
|
+
parent.set(this._prop, value);
|
|
133
|
+
} else {
|
|
134
|
+
this._source = value;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_set(prop, newValue) {
|
|
139
|
+
const origValue = this._getPropValue(prop);
|
|
140
|
+
|
|
141
|
+
if (!Object.is(origValue, newValue)) {
|
|
142
|
+
this._copyForWrite();
|
|
143
|
+
this._result[prop] = newValue;
|
|
144
|
+
|
|
145
|
+
// Child source values must be invalidated, because they can
|
|
146
|
+
// reference a previous copy we made.
|
|
147
|
+
const children = this._children;
|
|
148
|
+
if (children) {
|
|
149
|
+
const child = children.get(prop);
|
|
150
|
+
if (child) {
|
|
151
|
+
child._source = STALE_VALUE;
|
|
152
|
+
child._callbacks = [];
|
|
153
|
+
child._result = null;
|
|
154
|
+
child._status = STATUS_NONE;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
set(...args) {
|
|
163
|
+
this._throwIfRevoked();
|
|
164
|
+
const newValue = args.pop();
|
|
165
|
+
const hasProps = args.length > 0;
|
|
166
|
+
const lastProp = hasProps ? args.pop() : undefined;
|
|
167
|
+
const ctx = hasProps ? this.get(...args) : this;
|
|
168
|
+
if (hasProps) {
|
|
169
|
+
ctx._set(lastProp, newValue);
|
|
170
|
+
} else {
|
|
171
|
+
ctx._replace(newValue);
|
|
172
|
+
}
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
update(...args) {
|
|
177
|
+
const updater = args.pop();
|
|
178
|
+
updater(this.get(...args));
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
parent() {
|
|
183
|
+
this._throwIfRevoked();
|
|
184
|
+
return this._parent;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
root() {
|
|
188
|
+
this._throwIfRevoked();
|
|
189
|
+
return this._root;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
revoke() {
|
|
193
|
+
if (this.isRevoked()) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (this._parent) {
|
|
197
|
+
this._parent._children.delete(this._prop);
|
|
198
|
+
}
|
|
199
|
+
if (this._children) {
|
|
200
|
+
for (const child of this._children.values()) {
|
|
201
|
+
child.revoke();
|
|
202
|
+
}
|
|
203
|
+
this._children = null;
|
|
204
|
+
}
|
|
205
|
+
this._source = null;
|
|
206
|
+
this._prop = null;
|
|
207
|
+
this._root = null;
|
|
208
|
+
this._parent = null;
|
|
209
|
+
this._callbacks = null;
|
|
210
|
+
this._result = null;
|
|
211
|
+
this._status = STATUS_REVOKED;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
isRevoked() {
|
|
215
|
+
return this._status === STATUS_REVOKED;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
final() {
|
|
219
|
+
this._throwIfRevoked();
|
|
220
|
+
if (this._children) {
|
|
221
|
+
for (const child of this._children.values()) {
|
|
222
|
+
child.final();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const result = this.read();
|
|
226
|
+
const callbacks = this._callbacks;
|
|
227
|
+
this.revoke();
|
|
228
|
+
for (let i = 0; i < callbacks.length; i++) {
|
|
229
|
+
const {func, args} = callbacks[i];
|
|
230
|
+
func(...args);
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
finalRoot() {
|
|
236
|
+
return this.root().final();
|
|
237
|
+
}
|
|
238
|
+
}
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -3,90 +3,141 @@
|
|
|
3
3
|
```JavaScript
|
|
4
4
|
import mutate from 'mutate-cow';
|
|
5
5
|
|
|
6
|
-
const animals =
|
|
7
|
-
cats:
|
|
6
|
+
const animals = deepFreeze({
|
|
7
|
+
cats: ['ragamuffin', 'shorthair', 'maine coon'],
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
const newAnimals = mutate(animals
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const newAnimals = mutate(animals)
|
|
11
|
+
.set('dogs', ['hound'])
|
|
12
|
+
.update('cats', (ctx) => {
|
|
13
|
+
ctx.write().push('bobtail');
|
|
14
|
+
})
|
|
15
|
+
.final();
|
|
14
16
|
```
|
|
15
17
|
|
|
16
|
-
This module allows you to update an immutable object as if it were mutable
|
|
17
|
-
|
|
18
|
-
It's implemented using [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) objects, so browser support for that is required.
|
|
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
|
-
|
|
20
|
+
`mutate-cow` provides useful features that other packages don't:
|
|
21
21
|
|
|
22
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* Usable Flow types are provided. (The first type parameter must be a non-read-only variant of the input type.)
|
|
27
|
-
|
|
28
|
-
For usage, please see [the tests](test.mjs).
|
|
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.
|
|
29
26
|
|
|
30
27
|
No cows were harmed in the making of this code.
|
|
31
28
|
|
|
32
|
-
##
|
|
29
|
+
## API
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
### const ctx = mutate(source)
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
Returns a "context" object which can modify a copy of `source`.
|
|
37
34
|
|
|
38
|
-
```
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const newDate = new Date(orig.date.valueOf());
|
|
52
|
-
newDate.setFullYear(1999);
|
|
53
|
-
newDate.customProp = 'y';
|
|
54
|
-
copy.date = newDate;
|
|
55
|
-
copy.string = new String('yello');
|
|
56
|
-
});
|
|
35
|
+
```js
|
|
36
|
+
const foo = deepFreeze({bar: {baz: []}});
|
|
37
|
+
const ctx = mutate(foo);
|
|
38
|
+
````
|
|
39
|
+
|
|
40
|
+
### ctx.read()
|
|
41
|
+
|
|
42
|
+
Returns the current working copy of the context's `source` object, or just `source` if no changes were made.
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
ctx.read() === foo; // no changes
|
|
46
|
+
ctx.set('bar', 'baz', ['qux']);
|
|
47
|
+
ctx.read().bar.baz[0] === 'qux'; // changes
|
|
57
48
|
```
|
|
58
49
|
|
|
59
|
-
###
|
|
50
|
+
### ctx.write()
|
|
60
51
|
|
|
61
|
-
|
|
52
|
+
Returns the current working copy of the context's `source` object. Makes a shallow copy of `source` first if no changes were made.
|
|
62
53
|
|
|
63
|
-
|
|
64
|
-
const orig = {foo: {value: 1}, bar: {value: 2}};
|
|
54
|
+
You normally don't need to call `write`. It's mainly useful for accessing methods on copied objects (e.g., array methods).
|
|
65
55
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
});
|
|
56
|
+
```js
|
|
57
|
+
ctx.get('bar', 'baz').write().push('qux');
|
|
58
|
+
ctx.read().bar.baz[0] === 'qux';
|
|
70
59
|
```
|
|
71
60
|
|
|
72
|
-
|
|
61
|
+
### ctx.get(...path: [prop1, ...])
|
|
73
62
|
|
|
74
|
-
|
|
75
|
-
const orig = {foo: {value: 1}, bar: {value: 2}};
|
|
63
|
+
Returns a child context object for the given `path`.
|
|
76
64
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
65
|
+
Passing zero arguments returns `ctx`.
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
ctx.get() === ctx;
|
|
69
|
+
ctx.get('bar').read() === foo.bar;
|
|
70
|
+
ctx.get('bar', 'baz').read() === '';
|
|
80
71
|
```
|
|
81
72
|
|
|
82
|
-
|
|
73
|
+
### ctx.set(...path: [prop1, ...], value)
|
|
83
74
|
|
|
84
|
-
|
|
85
|
-
const orig = {foo: {value: 1}, bar: {value: 2}};
|
|
75
|
+
Sets the given `path` to `value` on the current working copy. Returns `ctx`.
|
|
86
76
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
77
|
+
Passing zero property names (i.e., only a value) sets the current context's value.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
// these all do the same thing
|
|
81
|
+
ctx.set({bar: {baz: 2}});
|
|
82
|
+
ctx.set('bar', {baz: 2});
|
|
83
|
+
ctx.set('bar', 'baz', 2);
|
|
84
|
+
ctx.get('bar').set({baz: 2});
|
|
85
|
+
ctx.get('bar').set('baz', 2);
|
|
86
|
+
ctx.get('bar', 'baz').set(2);
|
|
90
87
|
```
|
|
91
88
|
|
|
92
|
-
|
|
89
|
+
### ctx.update(...path: [prop1, ...], updater)
|
|
90
|
+
|
|
91
|
+
Calls `updater(ctx.get(...path))` and returns `ctx`.
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
const copy = ctx
|
|
95
|
+
.update('bar', 'baz', (bazCtx) => {
|
|
96
|
+
bazCtx.write().push('qux');
|
|
97
|
+
})
|
|
98
|
+
.final();
|
|
99
|
+
copy.bar.baz[0] === 'qux';
|
|
100
|
+
````
|
|
101
|
+
|
|
102
|
+
### ctx.parent()
|
|
103
|
+
|
|
104
|
+
Returns the parent context of `ctx`.
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
ctx.parent() === null;
|
|
108
|
+
ctx.get('bar').parent() === ctx;
|
|
109
|
+
ctx.get('bar', 'baz').parent() === ctx.get('bar');
|
|
110
|
+
````
|
|
111
|
+
|
|
112
|
+
### ctx.root()
|
|
113
|
+
|
|
114
|
+
Returns the root context of `ctx`.
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
ctx.root() === ctx;
|
|
118
|
+
ctx.get('bar').root() === ctx;
|
|
119
|
+
ctx.get('bar', 'baz').root() === ctx;
|
|
120
|
+
````
|
|
121
|
+
|
|
122
|
+
### ctx.revoke()
|
|
123
|
+
|
|
124
|
+
Revokes `ctx` so that it can no longer be used. Returns `undefined`.
|
|
125
|
+
|
|
126
|
+
Attempting to use any method other than `isRevoked` on a revoked context will throw an error. This sets all internal properties to `null` so that there's no longer any reference to the `source` object or copy.
|
|
127
|
+
|
|
128
|
+
### ctx.isRevoked()
|
|
129
|
+
|
|
130
|
+
Returns a boolean indicating whether `ctx` has been revoked.
|
|
131
|
+
|
|
132
|
+
### ctx.final()
|
|
133
|
+
|
|
134
|
+
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.
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
const copy = mutate(foo).set('bar', 'baz', 'qux').final();
|
|
138
|
+
Object.isFrozen(copy) === true; // since `foo` was frozen, `copy` will be too
|
|
139
|
+
````
|
|
140
|
+
|
|
141
|
+
### ctx.finalRoot()
|
|
142
|
+
|
|
143
|
+
Returns `ctx.root().final()`.
|
package/bench.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright (c)
|
|
2
|
+
* Copyright (c) 2023 Michael Wiencek
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the MIT license. A copy can be found
|
|
5
5
|
* in the file named "LICENSE" at the root directory of this distribution.
|
|
@@ -15,21 +15,52 @@ for (let i = 65, next = root; i <= 90; i++) {
|
|
|
15
15
|
prop2: 2,
|
|
16
16
|
prop3: 3,
|
|
17
17
|
prop4: 4,
|
|
18
|
-
|
|
18
|
+
prop5: 5,
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const updater1 = (
|
|
23
|
-
|
|
22
|
+
const updater1 = () => {
|
|
23
|
+
mutate(root).set('root', true).final();
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
const updater2 = (
|
|
27
|
-
|
|
26
|
+
const updater2 = () => {
|
|
27
|
+
mutate(root)
|
|
28
|
+
.set(
|
|
29
|
+
'A',
|
|
30
|
+
'B',
|
|
31
|
+
'C',
|
|
32
|
+
'D',
|
|
33
|
+
'E',
|
|
34
|
+
'F',
|
|
35
|
+
'G',
|
|
36
|
+
'H',
|
|
37
|
+
'I',
|
|
38
|
+
'J',
|
|
39
|
+
'K',
|
|
40
|
+
'L',
|
|
41
|
+
'M',
|
|
42
|
+
'N',
|
|
43
|
+
'O',
|
|
44
|
+
'P',
|
|
45
|
+
'Q',
|
|
46
|
+
'R',
|
|
47
|
+
'S',
|
|
48
|
+
'T',
|
|
49
|
+
'U',
|
|
50
|
+
'V',
|
|
51
|
+
'W',
|
|
52
|
+
'X',
|
|
53
|
+
'Y',
|
|
54
|
+
'Z',
|
|
55
|
+
'leaf',
|
|
56
|
+
true,
|
|
57
|
+
)
|
|
58
|
+
.final();
|
|
28
59
|
};
|
|
29
60
|
|
|
30
61
|
function test() {
|
|
31
|
-
|
|
32
|
-
|
|
62
|
+
updater1(); // shallow update
|
|
63
|
+
updater2(); // deep update
|
|
33
64
|
}
|
|
34
65
|
|
|
35
66
|
new Benchmark.Suite()
|
package/canClone.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright (c)
|
|
2
|
+
* Copyright (c) 2023 Michael Wiencek
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the MIT license. A copy can be found
|
|
5
5
|
* in the file named "LICENSE" at the root directory of this distribution.
|
|
@@ -9,14 +9,20 @@ const funcToString = Function.prototype.toString;
|
|
|
9
9
|
|
|
10
10
|
const nativeCodeRegExp = /^function \w*\(\) \{\s*\[native code\]\s*\}$/m;
|
|
11
11
|
|
|
12
|
+
export const CANNOT_CLONE_ERROR =
|
|
13
|
+
'Only plain objects, arrays, and class instances ' +
|
|
14
|
+
'can be cloned. Primitives, functions, and built-ins ' +
|
|
15
|
+
'are unsupported.';
|
|
16
|
+
|
|
12
17
|
export default function canClone(object) {
|
|
13
18
|
if (!object || typeof object !== 'object') {
|
|
14
|
-
|
|
19
|
+
throw new Error(CANNOT_CLONE_ERROR);
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
let proto = Reflect.getPrototypeOf(object);
|
|
23
|
+
|
|
18
24
|
while (proto) {
|
|
19
|
-
|
|
25
|
+
let ctor = proto.constructor;
|
|
20
26
|
// A Generator object's constructor is an object.
|
|
21
27
|
if (ctor && typeof ctor === 'object') {
|
|
22
28
|
ctor = ctor.constructor;
|
|
@@ -26,11 +32,9 @@ export default function canClone(object) {
|
|
|
26
32
|
ctor.name !== 'Array' &&
|
|
27
33
|
ctor.name !== 'Object' &&
|
|
28
34
|
nativeCodeRegExp.test(funcToString.call(ctor))) {
|
|
29
|
-
|
|
35
|
+
throw new Error(CANNOT_CLONE_ERROR);
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
proto = Reflect.getPrototypeOf(proto);
|
|
33
39
|
}
|
|
34
|
-
|
|
35
|
-
return true;
|
|
36
40
|
}
|
package/clone.mjs
CHANGED
|
@@ -1,28 +1,47 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright (c)
|
|
2
|
+
* Copyright (c) 2023 Michael Wiencek
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the MIT license. A copy can be found
|
|
5
5
|
* in the file named "LICENSE" at the root directory of this distribution.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import canClone from './canClone.mjs';
|
|
8
9
|
import {
|
|
9
10
|
NON_CONFIGURABLE,
|
|
10
11
|
NON_CONFIGURABLE_AND_WRITABLE,
|
|
11
12
|
NON_WRITABLE,
|
|
12
13
|
} from './constants.mjs';
|
|
13
14
|
|
|
15
|
+
function isPrimitive(value) {
|
|
16
|
+
switch (typeof value) {
|
|
17
|
+
case 'bigint':
|
|
18
|
+
case 'boolean':
|
|
19
|
+
case 'number':
|
|
20
|
+
case 'string':
|
|
21
|
+
case 'symbol':
|
|
22
|
+
case 'undefined':
|
|
23
|
+
return true;
|
|
24
|
+
default:
|
|
25
|
+
return value === null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
function restoreDescriptors(copy, changedDescriptors) {
|
|
15
30
|
for (let i = 0; i < changedDescriptors.length; i++) {
|
|
16
31
|
const [name, origDesc] = changedDescriptors[i];
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
19
|
-
Object.assign(
|
|
20
|
-
Reflect.defineProperty(copy, name,
|
|
32
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(copy, name);
|
|
33
|
+
if (descriptor) {
|
|
34
|
+
Object.assign(descriptor, origDesc);
|
|
35
|
+
Reflect.defineProperty(copy, name, descriptor);
|
|
21
36
|
}
|
|
22
37
|
}
|
|
23
38
|
}
|
|
24
39
|
|
|
25
40
|
export default function clone(source, callbacks) {
|
|
41
|
+
if (isPrimitive(source)) {
|
|
42
|
+
return source;
|
|
43
|
+
}
|
|
44
|
+
canClone(source);
|
|
26
45
|
let changedDescriptors;
|
|
27
46
|
let copy;
|
|
28
47
|
if (Array.isArray(source)) {
|
|
@@ -33,15 +52,15 @@ export default function clone(source, callbacks) {
|
|
|
33
52
|
const ownNames = Object.getOwnPropertyNames(source);
|
|
34
53
|
for (let i = 0; i < ownNames.length; i++) {
|
|
35
54
|
const name = ownNames[i];
|
|
36
|
-
const
|
|
37
|
-
const nonConfigurable =
|
|
55
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(source, name);
|
|
56
|
+
const nonConfigurable = descriptor.configurable === false;
|
|
38
57
|
let origDesc;
|
|
39
58
|
if (nonConfigurable) {
|
|
40
|
-
|
|
59
|
+
descriptor.configurable = true;
|
|
41
60
|
origDesc = NON_CONFIGURABLE;
|
|
42
61
|
}
|
|
43
|
-
if (
|
|
44
|
-
|
|
62
|
+
if (descriptor.writable === false) {
|
|
63
|
+
descriptor.writable = true;
|
|
45
64
|
origDesc = nonConfigurable
|
|
46
65
|
? NON_CONFIGURABLE_AND_WRITABLE
|
|
47
66
|
: NON_WRITABLE;
|
|
@@ -52,13 +71,29 @@ export default function clone(source, callbacks) {
|
|
|
52
71
|
}
|
|
53
72
|
changedDescriptors.push([name, origDesc]);
|
|
54
73
|
}
|
|
55
|
-
Reflect.defineProperty(copy, name,
|
|
74
|
+
Reflect.defineProperty(copy, name, descriptor);
|
|
56
75
|
}
|
|
57
76
|
if (changedDescriptors) {
|
|
58
|
-
callbacks.push(
|
|
77
|
+
callbacks.push({
|
|
78
|
+
func: restoreDescriptors,
|
|
79
|
+
args: [copy, changedDescriptors],
|
|
80
|
+
});
|
|
59
81
|
}
|
|
60
|
-
if (
|
|
61
|
-
callbacks.push(
|
|
82
|
+
if (Object.isFrozen(source)) {
|
|
83
|
+
callbacks.push({
|
|
84
|
+
func: Object.freeze,
|
|
85
|
+
args: [copy],
|
|
86
|
+
});
|
|
87
|
+
} else if (Object.isSealed(source)) {
|
|
88
|
+
callbacks.push({
|
|
89
|
+
func: Object.seal,
|
|
90
|
+
args: [copy],
|
|
91
|
+
});
|
|
92
|
+
} else if (!Reflect.isExtensible(source)) {
|
|
93
|
+
callbacks.push({
|
|
94
|
+
func: Reflect.preventExtensions,
|
|
95
|
+
args: [copy],
|
|
96
|
+
});
|
|
62
97
|
}
|
|
63
98
|
return copy;
|
|
64
99
|
}
|