Skip to content

Commit 0f76f98

Browse files
authored
Async recipes (#307)
* test: recursive produce call in async recipe * fix: recursive produce call in async recipe * feat: support async recipes Drafts won't be revoked until the returned promise is fulfilled or rejected. * test: avoid async/await for node 6 * Some additional comments, improvement in error handling * feat: createDraft/finishDraft functions Closes #302 * types: createDraft/finishDraft functions * refactor: rename createPublicDraft/finishPublicDraft for consistency * test: use snapshots in createDraft tests * types: async produce * chores: added additional invariants * docs: reordered readme a bit to a more logical order * docs: async and createDraft / finishDraft * docs: fixes * BREAKING CHANGE: added migration guide: Promises returned from producer will be evaluated
1 parent eabe9db commit 0f76f98

File tree

12 files changed

+706
-188
lines changed

12 files changed

+706
-188
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`manual - es5 cannot modify after finish 1`] = `"Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {\\"a\\":2}"`;
4+
5+
exports[`manual - es5 should check arguments 1`] = `"First argument to createDraft should be a plain object, an array, or an immerable object."`;
6+
7+
exports[`manual - es5 should check arguments 2`] = `"First argument to createDraft should be a plain object, an array, or an immerable object."`;
8+
9+
exports[`manual - es5 should check arguments 3`] = `"First argument to finishDraft should be an object from createDraft."`;
10+
11+
exports[`manual - es5 should not finish drafts from produce 1`] = `"The draft provided was not created using \`createDraft\`"`;
12+
13+
exports[`manual - es5 should not finish twice 1`] = `"The draft provided was has already been finished"`;
14+
15+
exports[`manual - es5 should support patches drafts 1`] = `
16+
Array [
17+
Array [
18+
Array [
19+
Object {
20+
"op": "replace",
21+
"path": Array [
22+
"a",
23+
],
24+
"value": 2,
25+
},
26+
Object {
27+
"op": "add",
28+
"path": Array [
29+
"b",
30+
],
31+
"value": 3,
32+
},
33+
],
34+
Array [
35+
Object {
36+
"op": "replace",
37+
"path": Array [
38+
"a",
39+
],
40+
"value": 1,
41+
},
42+
Object {
43+
"op": "remove",
44+
"path": Array [
45+
"b",
46+
],
47+
},
48+
],
49+
],
50+
]
51+
`;
52+
53+
exports[`manual - proxy cannot modify after finish 1`] = `"Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {\\"a\\":2}"`;
54+
55+
exports[`manual - proxy should check arguments 1`] = `"First argument to createDraft should be a plain object, an array, or an immerable object."`;
56+
57+
exports[`manual - proxy should check arguments 2`] = `"First argument to createDraft should be a plain object, an array, or an immerable object."`;
58+
59+
exports[`manual - proxy should check arguments 3`] = `"First argument to finishDraft should be an object from createDraft."`;
60+
61+
exports[`manual - proxy should not finish drafts from produce 1`] = `"The draft provided was not created using \`createDraft\`"`;
62+
63+
exports[`manual - proxy should not finish twice 1`] = `"The draft provided was has already been finished"`;
64+
65+
exports[`manual - proxy should support patches drafts 1`] = `
66+
Array [
67+
Array [
68+
Array [
69+
Object {
70+
"op": "replace",
71+
"path": Array [
72+
"a",
73+
],
74+
"value": 2,
75+
},
76+
Object {
77+
"op": "add",
78+
"path": Array [
79+
"b",
80+
],
81+
"value": 3,
82+
},
83+
],
84+
Array [
85+
Object {
86+
"op": "replace",
87+
"path": Array [
88+
"a",
89+
],
90+
"value": 1,
91+
},
92+
Object {
93+
"op": "remove",
94+
"path": Array [
95+
"b",
96+
],
97+
},
98+
],
99+
],
100+
]
101+
`;

__tests__/base.js

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,7 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
826826
expect(next.obj).toBe(next.arr[0])
827827
})
828828

829-
it("can return an object with two references to any pristine draft", () => {
829+
it("can return an object with two references to an unmodified draft", () => {
830830
const base = {a: {}}
831831
const next = produce(base, d => {
832832
return [d.a, d.a]
@@ -844,6 +844,65 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
844844
})
845845
})
846846

847+
// TODO: rewrite tests with async/await once node 6 support is dropped
848+
describe("async recipe function", () => {
849+
it("can modify the draft", () => {
850+
const base = {a: 0, b: 0}
851+
return produce(base, d => {
852+
d.a = 1
853+
return Promise.resolve().then(() => {
854+
d.b = 1
855+
})
856+
}).then(res => {
857+
expect(res).not.toBe(base)
858+
expect(res).toEqual({a: 1, b: 1})
859+
})
860+
})
861+
862+
it("works with rejected promises", () => {
863+
let draft
864+
const base = {a: 0, b: 0}
865+
const err = new Error("passed")
866+
return produce(base, d => {
867+
draft = d
868+
draft.b = 1
869+
return Promise.reject(err)
870+
}).then(
871+
() => {
872+
throw "failed"
873+
},
874+
e => {
875+
expect(e).toBe(err)
876+
expect(() => draft.a).toThrowError(/revoked/)
877+
}
878+
)
879+
})
880+
881+
it("supports recursive produce calls after await", () => {
882+
const base = {obj: {k: 1}}
883+
return produce(base, d => {
884+
const obj = d.obj
885+
delete d.obj
886+
return Promise.resolve().then(() => {
887+
d.a = produce({}, d => {
888+
d.b = obj // Assign a draft owned by the parent scope.
889+
})
890+
891+
// Auto-freezing is prevented when an unowned draft exists.
892+
expect(Object.isFrozen(d.a)).toBeFalsy()
893+
894+
// Ensure `obj` is not revoked.
895+
obj.c = 1
896+
})
897+
}).then(res => {
898+
expect(res).not.toBe(base)
899+
expect(res).toEqual({
900+
a: {b: {k: 1, c: 1}}
901+
})
902+
})
903+
})
904+
})
905+
847906
it("throws when the draft is modified and another object is returned", () => {
848907
const base = {x: 3}
849908
expect(() => {

__tests__/manual.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"use strict"
2+
import {
3+
setUseProxies,
4+
createDraft,
5+
finishDraft,
6+
produce,
7+
isDraft
8+
} from "../src/index"
9+
10+
runTests("proxy", true)
11+
runTests("es5", false)
12+
13+
function runTests(name, useProxies) {
14+
describe("manual - " + name, () => {
15+
setUseProxies(useProxies)
16+
17+
it("should check arguments", () => {
18+
expect(() => createDraft(3)).toThrowErrorMatchingSnapshot()
19+
const buf = new Buffer([])
20+
expect(() => createDraft(buf)).toThrowErrorMatchingSnapshot()
21+
expect(() => finishDraft({})).toThrowErrorMatchingSnapshot()
22+
})
23+
24+
it("should support manual drafts", () => {
25+
const state = [{}, {}, {}]
26+
27+
const draft = createDraft(state)
28+
draft.forEach((item, index) => {
29+
item.index = index
30+
})
31+
32+
const result = finishDraft(draft)
33+
34+
expect(result).not.toBe(state)
35+
expect(result).toEqual([{index: 0}, {index: 1}, {index: 2}])
36+
expect(state).toEqual([{}, {}, {}])
37+
})
38+
39+
it("cannot modify after finish", () => {
40+
const state = {a: 1}
41+
42+
const draft = createDraft(state)
43+
draft.a = 2
44+
expect(finishDraft(draft)).toEqual({a: 2})
45+
expect(() => {
46+
draft.a = 3
47+
}).toThrowErrorMatchingSnapshot()
48+
})
49+
50+
it("should support patches drafts", () => {
51+
const state = {a: 1}
52+
53+
const draft = createDraft(state)
54+
draft.a = 2
55+
draft.b = 3
56+
57+
const listener = jest.fn()
58+
const result = finishDraft(draft, listener)
59+
60+
expect(result).not.toBe(state)
61+
expect(result).toEqual({a: 2, b: 3})
62+
expect(listener.mock.calls).toMatchSnapshot()
63+
})
64+
65+
it("should handle multiple create draft calls", () => {
66+
const state = {a: 1}
67+
68+
const draft = createDraft(state)
69+
draft.a = 2
70+
71+
const draft2 = createDraft(state)
72+
draft2.b = 3
73+
74+
const result = finishDraft(draft)
75+
76+
expect(result).not.toBe(state)
77+
expect(result).toEqual({a: 2})
78+
79+
draft2.a = 4
80+
const result2 = finishDraft(draft2)
81+
expect(result2).not.toBe(result)
82+
expect(result2).toEqual({a: 4, b: 3})
83+
})
84+
85+
it("combines with produce - 1", () => {
86+
const state = {a: 1}
87+
88+
const draft = createDraft(state)
89+
draft.a = 2
90+
const res1 = produce(draft, d => {
91+
d.b = 3
92+
})
93+
draft.b = 4
94+
const res2 = finishDraft(draft)
95+
expect(res1).toEqual({a: 2, b: 3})
96+
expect(res2).toEqual({a: 2, b: 4})
97+
})
98+
99+
it("combines with produce - 2", () => {
100+
const state = {a: 1}
101+
102+
const res1 = produce(state, draft => {
103+
draft.b = 3
104+
const draft2 = createDraft(draft)
105+
draft.c = 4
106+
draft2.d = 5
107+
const res2 = finishDraft(draft2)
108+
expect(res2).toEqual({
109+
a: 1,
110+
b: 3,
111+
d: 5
112+
})
113+
draft.d = 2
114+
})
115+
expect(res1).toEqual({
116+
a: 1,
117+
b: 3,
118+
c: 4,
119+
d: 2
120+
})
121+
})
122+
123+
it("should not finish drafts from produce", () => {
124+
produce({x: 1}, draft => {
125+
expect(() => finishDraft(draft)).toThrowErrorMatchingSnapshot()
126+
})
127+
})
128+
129+
it("should not finish twice", () => {
130+
const draft = createDraft({a: 1})
131+
draft.a++
132+
finishDraft(draft)
133+
expect(() => finishDraft(draft)).toThrowErrorMatchingSnapshot()
134+
})
135+
})
136+
}

__tests__/produce.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,15 +196,52 @@ it("does not enforce immutability at the type level", () => {
196196
exactType(result, {} as any[])
197197
})
198198

199-
it("can produce nothing", () => {
200-
let result = produce({}, _ => nothing)
199+
it("can produce an undefined value", () => {
200+
let base = {} as {readonly a: number}
201+
202+
// Return only nothing.
203+
let result = produce(base, _ => nothing)
201204
exactType(result, undefined)
205+
206+
// Return maybe nothing.
207+
let result2 = produce(base, draft => {
208+
if (draft.a > 0) return nothing
209+
})
210+
exactType(result2, {} as typeof base | undefined)
211+
})
212+
213+
it("can return the draft itself", () => {
214+
let base = {} as {readonly a: number}
215+
let result = produce(base, draft => draft)
216+
217+
// Currently, the `readonly` modifier is lost.
218+
exactType(result, {} as {a: number} | undefined)
219+
})
220+
221+
it("can return a promise", () => {
222+
let base = {} as {readonly a: number}
223+
224+
// Return a promise only.
225+
exactType(
226+
produce(base, draft => {
227+
return Promise.resolve(draft.a > 0 ? null : undefined)
228+
}),
229+
{} as Promise<{readonly a: number} | null>
230+
)
231+
232+
// Return a promise or undefined.
233+
exactType(
234+
produce(base, draft => {
235+
if (draft.a > 0) return Promise.resolve()
236+
}),
237+
{} as (Promise<{readonly a: number}> | {readonly a: number})
238+
)
202239
})
203240

204241
it("works with `void` hack", () => {
205-
let obj = {} as {readonly a: number}
206-
let res = produce(obj, s => void s.a++)
207-
exactType(res, obj)
242+
let base = {} as {readonly a: number}
243+
let copy = produce(base, s => void s.a++)
244+
exactType(copy, base)
208245
})
209246

210247
it("works with generic parameters", () => {

0 commit comments

Comments
 (0)