Skip to content

Commit bd3b47b

Browse files
authored
Feature: rewrote Map and Set implementation, code base cleanup
Fixes #472, #466, #449, #492. Also drops bundle size with ~10%.
2 parents 98794b8 + f4a4701 commit bd3b47b

File tree

20 files changed

+1627
-1091
lines changed

20 files changed

+1627
-1091
lines changed

__tests__/base.js

Lines changed: 177 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,10 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
188188
})
189189

190190
it("supports iteration", () => {
191-
const base = [{id: 1, a: 1}, {id: 2, a: 1}]
191+
const base = [
192+
{id: 1, a: 1},
193+
{id: 2, a: 1}
194+
]
192195
const findById = (collection, id) => {
193196
for (const item of collection) {
194197
if (item.id === id) return item
@@ -386,7 +389,10 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
386389
})
387390

388391
it("supports 'keys", () => {
389-
const base = new Map([["first", Symbol()], ["second", Symbol()]])
392+
const base = new Map([
393+
["first", Symbol()],
394+
["second", Symbol()]
395+
])
390396
const result = produce(base, draft => {
391397
expect([...draft.keys()]).toEqual(["first", "second"])
392398
draft.set("third", Symbol())
@@ -490,7 +496,7 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
490496
it("can use 'delete' to remove items", () => {
491497
const nextState = produce(baseState, s => {
492498
expect(s.aMap.has("jedi")).toBe(true)
493-
s.aMap.delete("jedi")
499+
expect(s.aMap.delete("jedi")).toBe(true)
494500
expect(s.aMap.has("jedi")).toBe(false)
495501
})
496502
expect(nextState.aMap).not.toBe(baseState.aMap)
@@ -535,11 +541,55 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
535541
expect(base.get("first").get("second").prop).toBe("test")
536542
expect(result.get("first").get("second").prop).toBe("test1")
537543
})
544+
545+
it("treats void deletes as no-op", () => {
546+
const base = new Map([["x", 1]])
547+
const next = produce(base, d => {
548+
expect(d.delete("y")).toBe(false)
549+
})
550+
expect(next).toBe(base)
551+
})
552+
553+
it("revokes map proxies", () => {
554+
let m
555+
produce(baseState, s => {
556+
m = s.aMap
557+
})
558+
expect(() => m.get("x")).toThrow(
559+
"Cannot use a proxy that has been revoked"
560+
)
561+
expect(() => m.set("x", 3)).toThrow(
562+
"Cannot use a proxy that has been revoked"
563+
)
564+
})
565+
566+
it("does not draft map keys", () => {
567+
// anything else would be terribly confusing
568+
const key = {a: 1}
569+
const map = new Map([[key, 2]])
570+
const next = produce(map, d => {
571+
const dKey = Array.from(d.keys())[0]
572+
expect(isDraft(dKey)).toBe(false)
573+
expect(dKey).toBe(key)
574+
dKey.a += 1
575+
d.set(dKey, d.get(dKey) + 1)
576+
d.set(key, d.get(key) + 1)
577+
expect(d.get(key)).toBe(4)
578+
expect(key.a).toBe(2)
579+
})
580+
const entries = Array.from(next.entries())
581+
expect(entries).toEqual([[key, 4]])
582+
expect(entries[0][0]).toBe(key)
583+
expect(entries[0][0].a).toBe(2)
584+
})
538585
})
539586

540587
describe("set drafts", () => {
541588
it("supports iteration", () => {
542-
const base = new Set([{id: 1, a: 1}, {id: 2, a: 1}])
589+
const base = new Set([
590+
{id: 1, a: 1},
591+
{id: 2, a: 1}
592+
])
543593
const findById = (set, id) => {
544594
for (const item of set) {
545595
if (item.id === id) return item
@@ -553,12 +603,25 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
553603
obj2.a = 2
554604
})
555605
expect(result).not.toBe(base)
556-
expect(base).toEqual(new Set([{id: 1, a: 1}, {id: 2, a: 1}]))
557-
expect(result).toEqual(new Set([{id: 1, a: 2}, {id: 2, a: 2}]))
606+
expect(base).toEqual(
607+
new Set([
608+
{id: 1, a: 1},
609+
{id: 2, a: 1}
610+
])
611+
)
612+
expect(result).toEqual(
613+
new Set([
614+
{id: 1, a: 2},
615+
{id: 2, a: 2}
616+
])
617+
)
558618
})
559619

560620
it("supports 'entries'", () => {
561-
const base = new Set([{id: 1, a: 1}, {id: 2, a: 1}])
621+
const base = new Set([
622+
{id: 1, a: 1},
623+
{id: 2, a: 1}
624+
])
562625
const findById = (set, id) => {
563626
for (const [item1, item2] of set.entries()) {
564627
expect(item1).toBe(item2)
@@ -573,12 +636,25 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
573636
obj2.a = 2
574637
})
575638
expect(result).not.toBe(base)
576-
expect(base).toEqual(new Set([{id: 1, a: 1}, {id: 2, a: 1}]))
577-
expect(result).toEqual(new Set([{id: 1, a: 2}, {id: 2, a: 2}]))
639+
expect(base).toEqual(
640+
new Set([
641+
{id: 1, a: 1},
642+
{id: 2, a: 1}
643+
])
644+
)
645+
expect(result).toEqual(
646+
new Set([
647+
{id: 1, a: 2},
648+
{id: 2, a: 2}
649+
])
650+
)
578651
})
579652

580653
it("supports 'values'", () => {
581-
const base = new Set([{id: 1, a: 1}, {id: 2, a: 1}])
654+
const base = new Set([
655+
{id: 1, a: 1},
656+
{id: 2, a: 1}
657+
])
582658
const findById = (set, id) => {
583659
for (const item of set.values()) {
584660
if (item.id === id) return item
@@ -592,12 +668,25 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
592668
obj2.a = 2
593669
})
594670
expect(result).not.toBe(base)
595-
expect(base).toEqual(new Set([{id: 1, a: 1}, {id: 2, a: 1}]))
596-
expect(result).toEqual(new Set([{id: 1, a: 2}, {id: 2, a: 2}]))
671+
expect(base).toEqual(
672+
new Set([
673+
{id: 1, a: 1},
674+
{id: 2, a: 1}
675+
])
676+
)
677+
expect(result).toEqual(
678+
new Set([
679+
{id: 1, a: 2},
680+
{id: 2, a: 2}
681+
])
682+
)
597683
})
598684

599685
it("supports 'keys'", () => {
600-
const base = new Set([{id: 1, a: 1}, {id: 2, a: 1}])
686+
const base = new Set([
687+
{id: 1, a: 1},
688+
{id: 2, a: 1}
689+
])
601690
const findById = (set, id) => {
602691
for (const item of set.keys()) {
603692
if (item.id === id) return item
@@ -611,12 +700,25 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
611700
obj2.a = 2
612701
})
613702
expect(result).not.toBe(base)
614-
expect(base).toEqual(new Set([{id: 1, a: 1}, {id: 2, a: 1}]))
615-
expect(result).toEqual(new Set([{id: 1, a: 2}, {id: 2, a: 2}]))
703+
expect(base).toEqual(
704+
new Set([
705+
{id: 1, a: 1},
706+
{id: 2, a: 1}
707+
])
708+
)
709+
expect(result).toEqual(
710+
new Set([
711+
{id: 1, a: 2},
712+
{id: 2, a: 2}
713+
])
714+
)
616715
})
617716

618717
it("supports forEach with mutation after reads", () => {
619-
const base = new Set([{id: 1, a: 1}, {id: 2, a: 2}])
718+
const base = new Set([
719+
{id: 1, a: 1},
720+
{id: 2, a: 2}
721+
])
620722
const result = produce(base, draft => {
621723
let sum1 = 0
622724
draft.forEach(({a}) => {
@@ -631,8 +733,18 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
631733
expect(sum2).toBe(23)
632734
})
633735
expect(result).not.toBe(base)
634-
expect(base).toEqual(new Set([{id: 1, a: 1}, {id: 2, a: 2}]))
635-
expect(result).toEqual(new Set([{id: 1, a: 11}, {id: 2, a: 12}]))
736+
expect(base).toEqual(
737+
new Set([
738+
{id: 1, a: 1},
739+
{id: 2, a: 2}
740+
])
741+
)
742+
expect(result).toEqual(
743+
new Set([
744+
{id: 1, a: 11},
745+
{id: 2, a: 12}
746+
])
747+
)
636748
})
637749

638750
it("state stays the same if the same item is added", () => {
@@ -669,13 +781,14 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
669781
it("can use 'delete' to remove items", () => {
670782
const nextState = produce(baseState, s => {
671783
expect(s.aSet.has("Luke")).toBe(true)
672-
s.aSet.delete("Luke")
784+
expect(s.aSet.delete("Luke")).toBe(true)
785+
expect(s.aSet.delete("Luke")).toBe(false)
673786
expect(s.aSet.has("Luke")).toBe(false)
674787
})
675788
expect(nextState.aSet).not.toBe(baseState.aSet)
676-
expect(nextState.aSet.size).toBe(baseState.aSet.size - 1)
677789
expect(baseState.aSet.has("Luke")).toBe(true)
678790
expect(nextState.aSet.has("Luke")).toBe(false)
791+
expect(nextState.aSet.size).toBe(baseState.aSet.size - 1)
679792
})
680793

681794
it("can use 'clear' to remove items", () => {
@@ -710,6 +823,32 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
710823
expect(base).toEqual(new Set([new Set(["Serenity"])]))
711824
expect(result).toEqual(new Set([new Set(["Serenity", "Firefly"])]))
712825
})
826+
827+
it("supports has / delete on elements from the original", () => {
828+
const obj = {}
829+
const set = new Set([obj])
830+
const next = produce(set, d => {
831+
expect(d.has(obj)).toBe(true)
832+
d.add(3)
833+
expect(d.has(obj)).toBe(true)
834+
d.delete(obj)
835+
expect(d.has(obj)).toBe(false)
836+
})
837+
expect(next).toEqual(new Set([3]))
838+
})
839+
840+
it("revokes sets", () => {
841+
let m
842+
produce(baseState, s => {
843+
m = s.aSet
844+
})
845+
expect(() => m.has("x")).toThrow(
846+
"Cannot use a proxy that has been revoked"
847+
)
848+
expect(() => m.add("x")).toThrow(
849+
"Cannot use a proxy that has been revoked"
850+
)
851+
})
713852
})
714853

715854
it("supports `immerable` symbol on constructor", () => {
@@ -1236,6 +1375,20 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
12361375
expect(result[0].a).toEqual(2)
12371376
})
12381377

1378+
it("does not draft external data", () => {
1379+
const externalData = {x: 3}
1380+
const base = {}
1381+
const next = produce(base, draft => {
1382+
// potentially, we *could* draft external data automatically, but only if those statements are not switched...
1383+
draft.y = externalData
1384+
draft.y.x += 1
1385+
externalData.x += 1
1386+
})
1387+
expect(next).toEqual({y: {x: 5}})
1388+
expect(externalData.x).toBe(5)
1389+
expect(next.y).toBe(externalData)
1390+
})
1391+
12391392
autoFreeze &&
12401393
test("issue #469, state not frozen", () => {
12411394
const project = produce(
@@ -1598,7 +1751,10 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
15981751
const c = {c: 3}
15991752
const set1 = new Set([a, b])
16001753
const set2 = new Set([c])
1601-
const map = new Map([["set1", set1], ["set2", set2]])
1754+
const map = new Map([
1755+
["set1", set1],
1756+
["set2", set2]
1757+
])
16021758
const base = {map}
16031759

16041760
function first(set) {

0 commit comments

Comments
 (0)