mutate-cow 4.0.3 → 4.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 +1 -1
- package/bench.mjs +43 -0
- package/{canClone.js → canClone.mjs} +0 -0
- package/{clone.js → clone.mjs} +1 -1
- package/{constants.js → constants.mjs} +0 -0
- package/{main.js → index.mjs} +4 -4
- package/{main.js.flow → index.mjs.flow} +0 -0
- package/{isObject.js → isObject.mjs} +0 -0
- package/{makeProxy.js → makeProxy.mjs} +5 -5
- package/package.json +5 -8
- package/test.mjs +749 -0
- package/{unwrap.js → unwrap.mjs} +2 -2
- package/index.js +0 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ While this doesn't appear to have been an original idea, I believe `mutate-cow`
|
|
|
25
25
|
* Arrays, objects, and class instances are supported for mutation. Inside the callback, these have the correct identities when passed to `Array.isArray` or `instanceof`.
|
|
26
26
|
* Usable Flow types are provided. (The first type parameter must be a non-read-only variant of the input type.)
|
|
27
27
|
|
|
28
|
-
For usage, please see [the tests](test.
|
|
28
|
+
For usage, please see [the tests](test.mjs).
|
|
29
29
|
|
|
30
30
|
No cows were harmed in the making of this code.
|
|
31
31
|
|
package/bench.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2019 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 Benchmark from 'benchmark';
|
|
9
|
+
import mutate from './index.mjs';
|
|
10
|
+
|
|
11
|
+
const root = {};
|
|
12
|
+
for (let i = 65, next = root; i <= 90; i++) {
|
|
13
|
+
next = next[String.fromCharCode(i)] = {
|
|
14
|
+
prop1: 1,
|
|
15
|
+
prop2: 2,
|
|
16
|
+
prop3: 3,
|
|
17
|
+
prop4: 4,
|
|
18
|
+
prop4: 5,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const updater1 = (newRoot) => {
|
|
23
|
+
newRoot.root = true;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const updater2 = (newRoot) => {
|
|
27
|
+
newRoot.A.B.C.D.E.F.G.H.I.J.K.L.M.N.O.P.Q.R.S.T.U.V.W.X.Y.Z.leaf = true;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function test() {
|
|
31
|
+
mutate(root, updater1); // shallow update
|
|
32
|
+
mutate(root, updater2); // deep update
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
new Benchmark.Suite()
|
|
36
|
+
.add('test', test)
|
|
37
|
+
.on('cycle', function (event) {
|
|
38
|
+
console.log(String(event.target));
|
|
39
|
+
})
|
|
40
|
+
.on('complete', function () {
|
|
41
|
+
console.log('Fastest is ' + this.filter('fastest').map('name'));
|
|
42
|
+
})
|
|
43
|
+
.run({async: true});
|
|
File without changes
|
package/{clone.js → clone.mjs}
RENAMED
|
File without changes
|
package/{main.js → index.mjs}
RENAMED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
import {
|
|
9
9
|
CANNOT_CLONE_ERROR,
|
|
10
10
|
PROXY_UNWRAP_KEY,
|
|
11
|
-
} from './constants';
|
|
12
|
-
import isObject from './isObject';
|
|
13
|
-
import makeProxy, {Context} from './makeProxy';
|
|
14
|
-
import unwrap from './unwrap';
|
|
11
|
+
} from './constants.mjs';
|
|
12
|
+
import isObject from './isObject.mjs';
|
|
13
|
+
import makeProxy, {Context} from './makeProxy.mjs';
|
|
14
|
+
import unwrap from './unwrap.mjs';
|
|
15
15
|
|
|
16
16
|
export default function mutate(source, updater) {
|
|
17
17
|
if (!isObject(source)) {
|
|
File without changes
|
|
File without changes
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* in the file named "LICENSE" at the root directory of this distribution.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import canClone from './canClone';
|
|
9
|
-
import clone from './clone';
|
|
8
|
+
import canClone from './canClone.mjs';
|
|
9
|
+
import clone from './clone.mjs';
|
|
10
10
|
import {
|
|
11
11
|
CANNOT_CLONE_ERROR,
|
|
12
12
|
CONFIGURABLE_AND_WRITABLE,
|
|
@@ -14,9 +14,9 @@ import {
|
|
|
14
14
|
STATUS_CHANGED,
|
|
15
15
|
STATUS_NONE,
|
|
16
16
|
STATUS_REVOKED,
|
|
17
|
-
} from './constants';
|
|
18
|
-
import isObject from './isObject';
|
|
19
|
-
import unwrap from './unwrap';
|
|
17
|
+
} from './constants.mjs';
|
|
18
|
+
import isObject from './isObject.mjs';
|
|
19
|
+
import unwrap from './unwrap.mjs';
|
|
20
20
|
|
|
21
21
|
export function Context(root, parent, prop) {
|
|
22
22
|
this.root = root || this;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mutate-cow",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Update immutable objects as if they were mutable with copy-on-write",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"immutable",
|
|
@@ -8,21 +8,18 @@
|
|
|
8
8
|
"copy on write",
|
|
9
9
|
"state management"
|
|
10
10
|
],
|
|
11
|
-
"main": "index.
|
|
12
|
-
"module": "main.js",
|
|
11
|
+
"main": "index.mjs",
|
|
13
12
|
"license": "MIT",
|
|
14
13
|
"author": "Michael Wiencek <mwtuea@gmail.com>",
|
|
15
14
|
"repository": "github:mwiencek/mutate-cow",
|
|
16
15
|
"devDependencies": {
|
|
17
16
|
"benchmark": "2.1.4",
|
|
18
|
-
"flow-bin": "0.
|
|
17
|
+
"flow-bin": "0.182.0"
|
|
19
18
|
},
|
|
20
19
|
"sideEffects": false,
|
|
21
20
|
"scripts": {
|
|
22
|
-
"test": "node
|
|
23
|
-
},
|
|
24
|
-
"dependencies": {
|
|
25
|
-
"esm": "^3.2.25"
|
|
21
|
+
"test": "node test.mjs"
|
|
26
22
|
},
|
|
23
|
+
"dependencies": {},
|
|
27
24
|
"peerDependencies": {}
|
|
28
25
|
}
|
package/test.mjs
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @flow
|
|
3
|
+
* Copyright (c) 2019 Michael Wiencek
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license. A copy can be found
|
|
6
|
+
* in the file named "LICENSE" at the root directory of this distribution.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import mutate from './index.mjs';
|
|
10
|
+
import {PROXY_UNWRAP_KEY} from './constants.mjs';
|
|
11
|
+
|
|
12
|
+
function assert(truth, message) {
|
|
13
|
+
if (!truth) {
|
|
14
|
+
throw new Error('assertion failed');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/*::
|
|
19
|
+
type DatePeriod = {
|
|
20
|
+
year: number,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ReadOnlyDatePeriod = {
|
|
24
|
+
+year: number,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type Person = {
|
|
28
|
+
name: string,
|
|
29
|
+
birth_date: DatePeriod,
|
|
30
|
+
death_date: DatePeriod,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ReadOnlyPerson = {
|
|
34
|
+
+name: string,
|
|
35
|
+
+birth_date: ReadOnlyDatePeriod,
|
|
36
|
+
+death_date: ReadOnlyDatePeriod,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type People = Array<Person>;
|
|
40
|
+
type ReadOnlyPeople = $ReadOnlyArray<ReadOnlyPerson>;
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const aliceBirthDate = {year: 2100};
|
|
44
|
+
const aliceDeathDate = {year: 2330};
|
|
45
|
+
|
|
46
|
+
const alice /*: ReadOnlyPerson */ = {
|
|
47
|
+
name: 'Alice',
|
|
48
|
+
birth_date: aliceBirthDate,
|
|
49
|
+
death_date: aliceDeathDate,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const frozenBob /*: ReadOnlyPerson */ = Object.freeze({
|
|
53
|
+
name: 'Bob',
|
|
54
|
+
birth_date: Object.freeze({year: 2450}),
|
|
55
|
+
death_date: Object.freeze({year: 2988}),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
{ // object not copied if no writes are made
|
|
59
|
+
|
|
60
|
+
const copy = mutate/*:: <Person, _>*/(alice, (copy) => {
|
|
61
|
+
assert('name' in copy);
|
|
62
|
+
assert('birth_date' in copy);
|
|
63
|
+
assert('death_date' in copy);
|
|
64
|
+
assert('year' in copy.birth_date);
|
|
65
|
+
assert('year' in copy.death_date);
|
|
66
|
+
|
|
67
|
+
copy.name;
|
|
68
|
+
copy.birth_date.year;
|
|
69
|
+
copy.death_date.year;
|
|
70
|
+
|
|
71
|
+
const properties = Object.getOwnPropertyNames(copy).sort();
|
|
72
|
+
assert(properties.length === 3);
|
|
73
|
+
assert(properties[0] === 'birth_date');
|
|
74
|
+
assert(properties[1] === 'death_date');
|
|
75
|
+
assert(properties[2] === 'name');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
assert(alice === copy);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
const copy = mutate/*:: <Person, _>*/(alice, (copy) => {
|
|
83
|
+
assert(copy.birth_date.year === 2100);
|
|
84
|
+
copy.birth_date.year = 1988;
|
|
85
|
+
assert(copy.birth_date.year === 1988);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert(copy !== alice);
|
|
89
|
+
assert(copy.birth_date !== alice.birth_date);
|
|
90
|
+
assert(copy.birth_date.year === 1988);
|
|
91
|
+
assert(copy.death_date === aliceDeathDate);
|
|
92
|
+
|
|
93
|
+
assert(alice.name === 'Alice');
|
|
94
|
+
assert(alice.birth_date === aliceBirthDate);
|
|
95
|
+
assert(alice.birth_date.year === 2100);
|
|
96
|
+
assert(alice.death_date === aliceDeathDate);
|
|
97
|
+
assert(alice.death_date.year === 2330);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
let copy = mutate/*:: <Person, _>*/(frozenBob, (copy) => {
|
|
102
|
+
copy.name;
|
|
103
|
+
copy.birth_date.year;
|
|
104
|
+
copy.death_date.year;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
assert(copy === frozenBob);
|
|
108
|
+
|
|
109
|
+
copy = mutate/*:: <Person, _>*/(frozenBob, (copy) => {
|
|
110
|
+
assert(copy.birth_date.year === 2450);
|
|
111
|
+
copy.birth_date.year = 1988;
|
|
112
|
+
assert(copy.birth_date.year === 1988);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
assert(copy !== frozenBob);
|
|
116
|
+
// frozenness is preserved
|
|
117
|
+
assert(Object.isFrozen(copy));
|
|
118
|
+
assert(copy.birth_date !== frozenBob.birth_date);
|
|
119
|
+
assert(Object.isFrozen(copy.birth_date));
|
|
120
|
+
assert(copy.birth_date.year === 1988);
|
|
121
|
+
assert(copy.death_date === frozenBob.death_date);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const people/*: ReadOnlyPeople */ = [alice, frozenBob];
|
|
125
|
+
|
|
126
|
+
{ // object not copied if no writes are made
|
|
127
|
+
|
|
128
|
+
const copy = mutate/*:: <People, _>*/(people, (copy) => {
|
|
129
|
+
copy[0].death_date.year;
|
|
130
|
+
copy[1].death_date.year;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
assert(people === copy);
|
|
134
|
+
assert(people[0] === copy[0]);
|
|
135
|
+
assert(people[1] === copy[1]);
|
|
136
|
+
assert(people[0].birth_date === copy[0].birth_date);
|
|
137
|
+
assert(people[1].death_date === copy[1].death_date);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
{ // object not copied if assignments don't change the underlying data
|
|
141
|
+
|
|
142
|
+
const copy = mutate/*:: <People, _>*/(people, (copy) => {
|
|
143
|
+
copy[0] = copy[0];
|
|
144
|
+
copy[0].birth_date = copy[0].birth_date;
|
|
145
|
+
copy[0].birth_date.year = copy[0].birth_date.year;
|
|
146
|
+
copy[0].birth_date.year = 2100;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
assert(people === copy);
|
|
150
|
+
assert(people[0] === copy[0]);
|
|
151
|
+
assert(people[1] === copy[1]);
|
|
152
|
+
assert(people[0].birth_date === copy[0].birth_date);
|
|
153
|
+
assert(people[1].death_date === copy[1].death_date);
|
|
154
|
+
assert(copy[0].birth_date.year === 2100);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
{ // object delete
|
|
158
|
+
|
|
159
|
+
const orig = {};
|
|
160
|
+
|
|
161
|
+
Object.defineProperty(orig, 'foo', {
|
|
162
|
+
configurable: false,
|
|
163
|
+
enumerable: true,
|
|
164
|
+
writable: true,
|
|
165
|
+
value: 1,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
Object.defineProperty(orig, 'bar', {
|
|
169
|
+
configurable: false,
|
|
170
|
+
enumerable: true,
|
|
171
|
+
writable: true,
|
|
172
|
+
value: 2,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
Object.defineProperty(orig, 'baz', {
|
|
176
|
+
configurable: true,
|
|
177
|
+
enumerable: true,
|
|
178
|
+
writable: false,
|
|
179
|
+
value: 3,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const copy = mutate/*:: <{foo?: number, bar?: number, baz?: number}, _>*/(orig, (copy) => {
|
|
183
|
+
delete copy.foo;
|
|
184
|
+
delete copy.bar;
|
|
185
|
+
delete copy.baz;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
assert(orig.foo === 1);
|
|
189
|
+
assert(orig.bar === 2);
|
|
190
|
+
assert(orig.baz === 3);
|
|
191
|
+
assert(!copy.hasOwnProperty('foo'));
|
|
192
|
+
assert(!copy.hasOwnProperty('bar'));
|
|
193
|
+
assert(!copy.hasOwnProperty('baz'));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
{ // overwriting a cloned object
|
|
197
|
+
|
|
198
|
+
const orig = {foo: {bar: 1}};
|
|
199
|
+
const bar3 = {bar: 3};
|
|
200
|
+
|
|
201
|
+
const copy = mutate/*:: <{foo: {bar: number}}, _>*/(orig, (copy) => {
|
|
202
|
+
copy.foo.bar = 2;
|
|
203
|
+
assert(copy.foo.bar === 2);
|
|
204
|
+
copy.foo = bar3;
|
|
205
|
+
assert(copy.foo.bar === 3);
|
|
206
|
+
copy.foo.bar = 4;
|
|
207
|
+
assert(copy.foo.bar === 4);
|
|
208
|
+
assert(bar3.bar === 3);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
assert(orig.foo.bar === 1);
|
|
212
|
+
assert(copy.foo.bar === 4);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
{ // defineProperty
|
|
216
|
+
|
|
217
|
+
const orig/*: {+foo?: number, bar?: number} */ = {};
|
|
218
|
+
|
|
219
|
+
Object.defineProperty(orig, 'bar', {
|
|
220
|
+
configurable: false,
|
|
221
|
+
enumerable: true,
|
|
222
|
+
value: 0,
|
|
223
|
+
writable: false,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const copy = mutate/*:: <{foo?: number, bar?: number}, _>*/(orig, (copy) => {
|
|
227
|
+
let desc = Reflect.getOwnPropertyDescriptor(copy, 'bar');
|
|
228
|
+
// read-only properties are configurable and writable inside `mutate`
|
|
229
|
+
assert(desc && desc.configurable);
|
|
230
|
+
assert(desc && desc.writable);
|
|
231
|
+
copy.bar = 1;
|
|
232
|
+
|
|
233
|
+
// read-only properties defined inside `mutate` stay read-only
|
|
234
|
+
Object.defineProperty(copy, 'foo', {
|
|
235
|
+
configurable: false,
|
|
236
|
+
enumerable: true,
|
|
237
|
+
value: 1,
|
|
238
|
+
writable: false,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
desc = Reflect.getOwnPropertyDescriptor(copy, 'foo');
|
|
242
|
+
let error;
|
|
243
|
+
try {
|
|
244
|
+
copy.foo = 2;
|
|
245
|
+
} catch (e) {
|
|
246
|
+
error = e;
|
|
247
|
+
}
|
|
248
|
+
assert(error && error.message.includes('read only property'));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
assert(!orig.hasOwnProperty('foo'));
|
|
252
|
+
assert(orig.bar === 0);
|
|
253
|
+
assert(copy.bar === 1);
|
|
254
|
+
assert(Reflect.getOwnPropertyDescriptor(copy, 'bar').configurable === false);
|
|
255
|
+
assert(Reflect.getOwnPropertyDescriptor(copy, 'bar').writable === false);
|
|
256
|
+
assert(copy.foo === 1);
|
|
257
|
+
assert(Reflect.getOwnPropertyDescriptor(copy, 'foo').configurable === false);
|
|
258
|
+
assert(Reflect.getOwnPropertyDescriptor(copy, 'foo').writable === false);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
{ // array push
|
|
262
|
+
|
|
263
|
+
const copy = mutate/*:: <People, _>*/(people, (copy) => {
|
|
264
|
+
assert(Array.isArray(copy));
|
|
265
|
+
copy.push((alice/*: any */));
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
assert(copy !== people);
|
|
269
|
+
assert(people.length === 2);
|
|
270
|
+
assert(copy.length === 3);
|
|
271
|
+
assert(copy[2] === alice);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
{ // array delete
|
|
275
|
+
|
|
276
|
+
const copy = mutate/*:: <People, _>*/(people, (copy) => {
|
|
277
|
+
delete copy[1];
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
assert(copy !== people);
|
|
281
|
+
assert(copy.length === 2);
|
|
282
|
+
assert(copy[0] === alice);
|
|
283
|
+
assert(copy[1] === undefined);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
{ // splice
|
|
287
|
+
let copy = mutate/*:: <People, _>*/(people, (copy) => {
|
|
288
|
+
assert(Array.isArray(copy));
|
|
289
|
+
copy.splice(0, 1);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
assert(copy !== people);
|
|
293
|
+
assert(people.length === 2);
|
|
294
|
+
assert(copy.length === 1);
|
|
295
|
+
assert(copy[0] === frozenBob);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
{ // objects are copied only once
|
|
299
|
+
|
|
300
|
+
const copy = mutate/*:: <People, _>*/(people, (copy) => {
|
|
301
|
+
const proxy1 = copy[0].death_date;
|
|
302
|
+
proxy1.year = 5000;
|
|
303
|
+
assert(proxy1 === copy[0].death_date); // proxy is unchanged
|
|
304
|
+
const ref1 = proxy1[PROXY_UNWRAP_KEY];
|
|
305
|
+
assert(people[0].death_date !== ref1); // reference was copied
|
|
306
|
+
assert(people[0].death_date.year === 2330);
|
|
307
|
+
assert(proxy1.year === 5000);
|
|
308
|
+
proxy1.year = 4000;
|
|
309
|
+
const ref2 = copy[0].death_date[PROXY_UNWRAP_KEY];
|
|
310
|
+
assert(ref1 === ref2); // reference was copied only once
|
|
311
|
+
assert(ref1.year === 4000);
|
|
312
|
+
proxy1.year = 3000;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
assert(copy[0] !== alice);
|
|
316
|
+
assert(copy[0].birth_date === alice.birth_date);
|
|
317
|
+
assert(copy[0].death_date !== aliceDeathDate);
|
|
318
|
+
assert(copy[0].death_date.year === 3000);
|
|
319
|
+
assert(copy[1] === frozenBob);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/*::
|
|
323
|
+
type WeirdArray = {|+weird: boolean|} & $ReadOnlyArray<number>;
|
|
324
|
+
*/
|
|
325
|
+
|
|
326
|
+
{ // non-index array properties
|
|
327
|
+
|
|
328
|
+
const weird/*: WeirdArray */ = (Object.defineProperty([1, 2, 3], ('weird'/*: any */), {
|
|
329
|
+
configurable: false,
|
|
330
|
+
enumerable: true,
|
|
331
|
+
value: true,
|
|
332
|
+
writable: false,
|
|
333
|
+
})/*: any */);
|
|
334
|
+
|
|
335
|
+
const weirdCopy = mutate/*:: <{|weird: boolean|} & Array<number>, _>*/(weird, (copy) => {
|
|
336
|
+
copy.push(666);
|
|
337
|
+
assert(copy.weird === true, 'weird');
|
|
338
|
+
copy.weird = false;
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
assert(weird !== weirdCopy, 'reference is changed');
|
|
342
|
+
assert(weirdCopy.length === 4, 'item was added');
|
|
343
|
+
assert(weirdCopy[3], 'added item is 666');
|
|
344
|
+
assert(weird.length === 3, 'original reference is unchanged');
|
|
345
|
+
assert(weird.weird === true, 'original reference is unchanged');
|
|
346
|
+
assert(weirdCopy.weird === false, 'not weird');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
{ // array property descriptors
|
|
350
|
+
|
|
351
|
+
const descArray/*: {+value: Array<number>} */ = {value: [1, 2, 3]};
|
|
352
|
+
|
|
353
|
+
Object.defineProperty(descArray.value, 1, {
|
|
354
|
+
configurable: false,
|
|
355
|
+
enumerable: true,
|
|
356
|
+
value: 2,
|
|
357
|
+
writable: false,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const descArrayCopy = mutate/*:: <{value: Array<number>}, _>*/(descArray, (copy) => {
|
|
361
|
+
let desc = Object.getOwnPropertyDescriptor(copy.value, 'length');
|
|
362
|
+
assert(desc && desc.configurable === false);
|
|
363
|
+
assert(desc && desc.writable === true);
|
|
364
|
+
copy.value[1] = 0;
|
|
365
|
+
desc = Object.getOwnPropertyDescriptor(copy.value, 'length');
|
|
366
|
+
assert(desc && desc.configurable === false);
|
|
367
|
+
assert(desc && desc.writable === true);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
assert(descArray.value[1] === 2);
|
|
371
|
+
|
|
372
|
+
const newDesc1 = Object.getOwnPropertyDescriptor(descArrayCopy.value, 1);
|
|
373
|
+
assert(!(newDesc1/*: any */).configurable);
|
|
374
|
+
assert((newDesc1/*: any */).enumerable);
|
|
375
|
+
assert((newDesc1/*: any */).value === 0);
|
|
376
|
+
assert(!(newDesc1/*: any */).writable);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/*::
|
|
380
|
+
type SharedFoo = {foo: string};
|
|
381
|
+
*/
|
|
382
|
+
|
|
383
|
+
{ // shared reference within one object
|
|
384
|
+
|
|
385
|
+
const shared/*: $ReadOnly<SharedFoo> */ = {foo: ''};
|
|
386
|
+
|
|
387
|
+
const object = {
|
|
388
|
+
prop1: shared,
|
|
389
|
+
prop2: shared,
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const copy = mutate/*:: <{prop1: SharedFoo, prop2: SharedFoo}, _>*/(object, (copy) => {
|
|
393
|
+
copy.prop1.foo = 'abc';
|
|
394
|
+
assert(copy.prop1.foo === 'abc');
|
|
395
|
+
assert(copy.prop2.foo === '');
|
|
396
|
+
const ref = copy.prop1;
|
|
397
|
+
assert(shared.foo === '');
|
|
398
|
+
copy.prop2.foo = '123';
|
|
399
|
+
assert(copy.prop1.foo === 'abc');
|
|
400
|
+
assert(copy.prop2.foo === '123');
|
|
401
|
+
assert(copy.prop1[PROXY_UNWRAP_KEY] === ref[PROXY_UNWRAP_KEY]); // copied only once
|
|
402
|
+
assert(shared.foo === '');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
assert(copy.prop1.foo === 'abc');
|
|
406
|
+
assert(copy.prop2.foo === '123');
|
|
407
|
+
assert(shared.foo === '');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/*::
|
|
411
|
+
type NestedShared = {foo: {foo: Array<number>}};
|
|
412
|
+
type ReadOnlyNestedShared = {+foo: {+foo: $ReadOnlyArray<number>}};
|
|
413
|
+
*/
|
|
414
|
+
|
|
415
|
+
{ // nested calls + shared reference across two objects
|
|
416
|
+
|
|
417
|
+
const shared/*: {+foo: $ReadOnlyArray<number>} */ = {foo: [1]};
|
|
418
|
+
const objectA/*: ReadOnlyNestedShared */ = {foo: shared};
|
|
419
|
+
const objectB/*: ReadOnlyNestedShared */ = {foo: shared};
|
|
420
|
+
|
|
421
|
+
const objectACopy = mutate/*:: <NestedShared, _>*/(objectA, (copyA) => {
|
|
422
|
+
const objectBCopy = mutate/*:: <NestedShared, _>*/(objectB, (copyB) => {
|
|
423
|
+
copyA.foo.foo[0] = 2;
|
|
424
|
+
copyB.foo.foo[0] = 3;
|
|
425
|
+
assert(copyA.foo.foo[0] === 2);
|
|
426
|
+
assert(copyB.foo.foo[0] === 3);
|
|
427
|
+
});
|
|
428
|
+
assert(copyA.foo.foo[0] === 2);
|
|
429
|
+
assert(objectBCopy.foo.foo[0] === 3);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
assert(objectACopy.foo.foo[0] === 2);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
{ // nested call on proxied object
|
|
436
|
+
|
|
437
|
+
const source/*: {+foo: {+bar: number}} */ = {foo: {bar: 0}};
|
|
438
|
+
|
|
439
|
+
const copy = mutate/*:: <{foo: {bar: number}}, _>*/(source, copy => {
|
|
440
|
+
copy.foo.bar++;
|
|
441
|
+
|
|
442
|
+
copy.foo = mutate/*:: <{bar: number}, _>*/(copy.foo, fooCopy => {
|
|
443
|
+
copy.foo.bar++; // overridden
|
|
444
|
+
fooCopy.bar++;
|
|
445
|
+
copy.foo.bar++; // overridden
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
copy.foo.bar++;
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
assert(copy.foo.bar === 3);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
{ // classes
|
|
455
|
+
|
|
456
|
+
class NiceBase {}
|
|
457
|
+
|
|
458
|
+
class NiceClass extends NiceBase {
|
|
459
|
+
/*::
|
|
460
|
+
value: string;
|
|
461
|
+
*/
|
|
462
|
+
|
|
463
|
+
constructor() {
|
|
464
|
+
super();
|
|
465
|
+
this.value = 'nice';
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const instance = new NiceClass();
|
|
470
|
+
|
|
471
|
+
const copy = mutate/*:: <NiceClass, _>*/(instance, (copy) => {
|
|
472
|
+
assert(copy instanceof NiceClass);
|
|
473
|
+
copy.value = 'naughty';
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
assert(instance !== copy);
|
|
477
|
+
assert(copy instanceof NiceClass);
|
|
478
|
+
assert(copy.value === 'naughty');
|
|
479
|
+
assert(instance.value === 'nice');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
{ // getters and setters
|
|
483
|
+
|
|
484
|
+
const orig = {
|
|
485
|
+
object: null,
|
|
486
|
+
get value()/*: {foo?: number} */{
|
|
487
|
+
return (this.object = this.object || {});
|
|
488
|
+
},
|
|
489
|
+
set value(x/*: {foo?: number} */)/*: void */{
|
|
490
|
+
this.object = x;
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const copy1 = mutate/*:: <typeof orig, _>*/(orig, (copy) => {
|
|
495
|
+
copy.value.foo = 1;
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const newObject = {foo: 0};
|
|
499
|
+
|
|
500
|
+
const copy2 = mutate/*:: <typeof orig, _>*/(orig, (copy) => {
|
|
501
|
+
copy.value = newObject;
|
|
502
|
+
assert(copy.value.foo === 0);
|
|
503
|
+
// This should copy newObject; otherwise we'd be able to have
|
|
504
|
+
// side-effects across multiple objects.
|
|
505
|
+
copy.value.foo = 2;
|
|
506
|
+
assert(newObject.foo === 0);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
assert(orig.value.foo === undefined);
|
|
510
|
+
assert(orig.value === orig.object);
|
|
511
|
+
assert(copy1.value.foo === 1);
|
|
512
|
+
assert(copy1.value === copy1.object);
|
|
513
|
+
assert(copy2.value.foo === 2);
|
|
514
|
+
assert(copy2.value === copy2.object);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
{ // functions
|
|
518
|
+
|
|
519
|
+
const origFunc = function () {
|
|
520
|
+
return this.value;
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const orig/*: {|+func: () => string, +value: string|} */ = {
|
|
524
|
+
func: origFunc,
|
|
525
|
+
value: 'abc',
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const copy = mutate/*:: <{func: (() => string) & {prop?: boolean}, value: string}, _>*/(orig, (copy) => {
|
|
529
|
+
assert(typeof copy.func === 'function');
|
|
530
|
+
assert(copy.func instanceof Function);
|
|
531
|
+
assert(copy.func() === 'abc');
|
|
532
|
+
copy.value = '123';
|
|
533
|
+
assert(copy.func() === '123');
|
|
534
|
+
|
|
535
|
+
let error;
|
|
536
|
+
try {
|
|
537
|
+
copy.func.prop = true;
|
|
538
|
+
} catch (e) {
|
|
539
|
+
error = e;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Can't clone functions, so the above should error.
|
|
543
|
+
assert(error && error.message.includes('unsupported'));
|
|
544
|
+
|
|
545
|
+
copy.func = () => '';
|
|
546
|
+
assert(copy.func() === '');
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
{ // null prorotypes
|
|
551
|
+
|
|
552
|
+
const orig/*: {__proto__: null, +value: {__proto__: null, +number: number}} */ = Object.create(null, {
|
|
553
|
+
value: {
|
|
554
|
+
configurable: true,
|
|
555
|
+
enumerable: true,
|
|
556
|
+
value: Object.create(null, {
|
|
557
|
+
number: {
|
|
558
|
+
configurable: true,
|
|
559
|
+
enumerable: true,
|
|
560
|
+
value: 1,
|
|
561
|
+
writable: false,
|
|
562
|
+
},
|
|
563
|
+
}),
|
|
564
|
+
writable: false,
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
const copy = mutate/*:: <{__proto__: null, value: {__proto__: null, number: number}}, _>*/(orig, (copy) => {
|
|
569
|
+
copy.value.number = 2;
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
assert(orig.value.number === 1);
|
|
573
|
+
assert(copy.value.number === 2);
|
|
574
|
+
assert(Object.getPrototypeOf(copy) === null);
|
|
575
|
+
assert(Object.getPrototypeOf(copy.value) === null);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
{ // Number / String objects
|
|
579
|
+
|
|
580
|
+
const orig/*: {+num: Number, +str: String} */ = {
|
|
581
|
+
num: new Number(1),
|
|
582
|
+
str: new String('hello'),
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const copy = mutate/*:: <{num: Number, str: String}, _>*/(orig, (copy) => {
|
|
586
|
+
let error;
|
|
587
|
+
|
|
588
|
+
error = null;
|
|
589
|
+
try {
|
|
590
|
+
copy.num.valueOf();
|
|
591
|
+
} catch (e) {
|
|
592
|
+
error = e;
|
|
593
|
+
}
|
|
594
|
+
assert(error && error.message.includes('unsupported'));
|
|
595
|
+
|
|
596
|
+
copy.num = new Number(orig.num.valueOf() + 2);
|
|
597
|
+
|
|
598
|
+
error = null;
|
|
599
|
+
try {
|
|
600
|
+
copy.str[4] = 'p';
|
|
601
|
+
} catch (e) {
|
|
602
|
+
error = e;
|
|
603
|
+
}
|
|
604
|
+
assert(error && error.message.includes('unsupported'));
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
assert(orig.num.valueOf() === 1);
|
|
608
|
+
assert(orig.str.valueOf() === 'hello');
|
|
609
|
+
assert(copy.num.valueOf() === 3);
|
|
610
|
+
assert(copy.str === orig.str);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
{ // non-objects
|
|
614
|
+
|
|
615
|
+
let error = null;
|
|
616
|
+
try { mutate(null, () => {}) } catch (e) { error = e }
|
|
617
|
+
assert(error && error.message === 'Expected an object to mutate');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
{ // derived built-ins
|
|
621
|
+
|
|
622
|
+
class FunDate extends Date {}
|
|
623
|
+
|
|
624
|
+
let error = null;
|
|
625
|
+
try {
|
|
626
|
+
mutate(new FunDate(), (copy) => {
|
|
627
|
+
copy.setFullYear(1999);
|
|
628
|
+
});
|
|
629
|
+
} catch (e) {
|
|
630
|
+
error = e;
|
|
631
|
+
}
|
|
632
|
+
assert(error && error.message.includes('unsupported'));
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/*::
|
|
636
|
+
type Cyclic = {x: Cyclic}
|
|
637
|
+
*/
|
|
638
|
+
|
|
639
|
+
{ // cyclic references
|
|
640
|
+
|
|
641
|
+
const object/*: Cyclic */ = {};
|
|
642
|
+
object.x = object;
|
|
643
|
+
|
|
644
|
+
const newObject = mutate(object, newObject => {
|
|
645
|
+
newObject.x.x = null;
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
assert(object.x.x.x === object);
|
|
649
|
+
assert(newObject.x.x === null);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
{ // using a copy reference in an object spread
|
|
653
|
+
|
|
654
|
+
const orig/*: any */ = {
|
|
655
|
+
foo: {
|
|
656
|
+
bar: {
|
|
657
|
+
baz: 1,
|
|
658
|
+
},
|
|
659
|
+
func: function () {
|
|
660
|
+
return this;
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
let copy = mutate/*:: <any, _>*/(orig, (copy) => {
|
|
666
|
+
copy.foo = {...copy.foo};
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
let error = null;
|
|
670
|
+
try {
|
|
671
|
+
copy.foo.bar.baz;
|
|
672
|
+
} catch (e) {
|
|
673
|
+
error = e;
|
|
674
|
+
}
|
|
675
|
+
assert(error && error.message.includes('forgotten to call unwrap'));
|
|
676
|
+
|
|
677
|
+
// Check that the error can trigger twice
|
|
678
|
+
try {
|
|
679
|
+
copy.foo.bar.baz;
|
|
680
|
+
} catch (e) {
|
|
681
|
+
error = e;
|
|
682
|
+
}
|
|
683
|
+
assert(error && error.message.includes('forgotten to call unwrap'));
|
|
684
|
+
|
|
685
|
+
copy = mutate/*:: <any, _>*/(orig, (copy, unwrap) => {
|
|
686
|
+
copy.foo = {...unwrap(copy.foo)};
|
|
687
|
+
assert(copy.foo.func() === copy.foo);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
assert(copy.foo.func() === copy.foo);
|
|
691
|
+
assert(copy.foo.bar === orig.foo.bar);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
{ // construct
|
|
695
|
+
|
|
696
|
+
function Cls() {
|
|
697
|
+
this.foo = 1;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const orig = {
|
|
701
|
+
Cls,
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const copy = mutate(orig, (copy) => {
|
|
705
|
+
const c = new copy.Cls();
|
|
706
|
+
assert(c instanceof Cls);
|
|
707
|
+
assert(c.foo === 1);
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
{ // Object.freeze on a proxied object
|
|
712
|
+
|
|
713
|
+
// Arrays are somewhat special, because Object.freeze will attempt to make
|
|
714
|
+
// the `length` property non-configurable and non-writable via the
|
|
715
|
+
// defineProperty trap. However, a non-configurable property "cannot be
|
|
716
|
+
// non-writable, unless there exists a corresponding non-configurable,
|
|
717
|
+
// non-writable own property on the target object," i.e. our fake proxy
|
|
718
|
+
// target.
|
|
719
|
+
const copy = mutate([0], copy => {
|
|
720
|
+
Object.freeze(copy);
|
|
721
|
+
|
|
722
|
+
const desc = Object.getOwnPropertyDescriptor(copy, 'length');
|
|
723
|
+
assert(desc && !desc.configurable);
|
|
724
|
+
assert(desc && !desc.writable);
|
|
725
|
+
|
|
726
|
+
let error;
|
|
727
|
+
try {
|
|
728
|
+
copy.error = true;
|
|
729
|
+
} catch (e) {
|
|
730
|
+
error = e;
|
|
731
|
+
}
|
|
732
|
+
assert(error && error.message.includes('not extensible'));
|
|
733
|
+
assert(Object.isExtensible(copy) === false);
|
|
734
|
+
});
|
|
735
|
+
assert(Object.isExtensible(copy) === false);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
{ // Assigning an externally-frozen object inside the proxy
|
|
739
|
+
|
|
740
|
+
const source = {ref: null};
|
|
741
|
+
const frozenRef = {name: ''};
|
|
742
|
+
Object.freeze(frozenRef);
|
|
743
|
+
|
|
744
|
+
const copy = mutate/*:: <{ref: {name: string}}, _>*/(source, copy => {
|
|
745
|
+
copy.ref = frozenRef;
|
|
746
|
+
copy.ref.name = 'hi';
|
|
747
|
+
});
|
|
748
|
+
assert(copy.ref && copy.ref.name === 'hi');
|
|
749
|
+
}
|
package/{unwrap.js → unwrap.mjs}
RENAMED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* in the file named "LICENSE" at the root directory of this distribution.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {PROXY_UNWRAP_KEY} from './constants';
|
|
9
|
-
import isObject from './isObject';
|
|
8
|
+
import {PROXY_UNWRAP_KEY} from './constants.mjs';
|
|
9
|
+
import isObject from './isObject.mjs';
|
|
10
10
|
|
|
11
11
|
export default function unwrap(value) {
|
|
12
12
|
if (isObject(value)) {
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = require('esm')(module)('./main.js');
|