-
Notifications
You must be signed in to change notification settings - Fork 640
/
flow.ts
199 lines (185 loc) · 6.08 KB
/
flow.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import {
getCurrentActionContext,
getNextActionId,
getParentActionContext,
IMiddlewareEventType,
runWithActionContext
} from "./action"
import { argsToArray, setImmediateWithFallback, fail } from "../utils"
/**
* @hidden
*/
export type FlowReturn<R> = R extends Promise<infer T> ? T : R
/**
* See [asynchronous actions](concepts/async-actions.md).
*
* @returns The flow as a promise.
*/
export function flow<R, Args extends any[]>(
generator: (...args: Args) => Generator<Promise<any>, R, any>
): (...args: Args) => Promise<FlowReturn<R>> {
return createFlowSpawner(generator.name, generator) as any
}
/**
* @deprecated Not needed since TS3.6.
* Used for TypeScript to make flows that return a promise return the actual promise result.
*
* @param val
* @returns
*/
export function castFlowReturn<T>(val: T): T {
return val as any
}
/**
* @experimental
* experimental api - might change on minor/patch releases
*
* Convert a promise-returning function to a generator-returning one.
* This is intended to allow for usage of `yield*` in async actions to
* retain the promise return type.
*
* Example:
* ```ts
* function getDataAsync(input: string): Promise<number> { ... }
* const getDataGen = toGeneratorFunction(getDataAsync);
*
* const someModel.actions(self => ({
* someAction: flow(function*() {
* // value is typed as number
* const value = yield* getDataGen("input value");
* ...
* })
* }))
* ```
*/
export function toGeneratorFunction<R, Args extends any[]>(p: (...args: Args) => Promise<R>) {
return function* (...args: Args) {
return (yield p(...args)) as R
}
}
/**
* @experimental
* experimental api - might change on minor/patch releases
*
* Convert a promise to a generator yielding that promise
* This is intended to allow for usage of `yield*` in async actions to
* retain the promise return type.
*
* Example:
* ```ts
* function getDataAsync(input: string): Promise<number> { ... }
*
* const someModel.actions(self => ({
* someAction: flow(function*() {
* // value is typed as number
* const value = yield* toGenerator(getDataAsync("input value"));
* ...
* })
* }))
* ```
*/
export function* toGenerator<R>(p: Promise<R>) {
return (yield p) as R
}
/**
* @internal
* @hidden
*/
export function createFlowSpawner(name: string, generator: Function) {
const spawner = function flowSpawner(this: any) {
// Implementation based on https://github.com/tj/co/blob/master/index.js
const runId = getNextActionId()
const parentContext = getCurrentActionContext()!
if (!parentContext) {
throw fail("a mst flow must always have a parent context")
}
const parentActionContext = getParentActionContext(parentContext)
if (!parentActionContext) {
throw fail("a mst flow must always have a parent action context")
}
const contextBase = {
name,
id: runId,
tree: parentContext.tree,
context: parentContext.context,
parentId: parentContext.id,
allParentIds: [...parentContext.allParentIds, parentContext.id],
rootId: parentContext.rootId,
parentEvent: parentContext,
parentActionEvent: parentActionContext
}
const args = arguments
function wrap(fn: any, type: IMiddlewareEventType, arg: any) {
fn.$mst_middleware = (spawner as any).$mst_middleware // pick up any middleware attached to the flow
runWithActionContext(
{
...contextBase,
type,
args: [arg]
},
fn
)
}
return new Promise(function (resolve, reject) {
let gen: any
const init = function asyncActionInit() {
gen = generator.apply(null, arguments)
onFulfilled(undefined) // kick off the flow
}
;(init as any).$mst_middleware = (spawner as any).$mst_middleware
runWithActionContext(
{
...contextBase,
type: "flow_spawn",
args: argsToArray(args)
},
init
)
function onFulfilled(res: any) {
let ret
try {
// prettier-ignore
wrap((r: any) => { ret = gen.next(r) }, "flow_resume", res)
} catch (e) {
// prettier-ignore
setImmediateWithFallback(() => {
wrap((r: any) => { reject(e) }, "flow_throw", e)
})
return
}
next(ret)
return
}
function onRejected(err: any) {
let ret
try {
// prettier-ignore
wrap((r: any) => { ret = gen.throw(r) }, "flow_resume_error", err) // or yieldError?
} catch (e) {
// prettier-ignore
setImmediateWithFallback(() => {
wrap((r: any) => { reject(e) }, "flow_throw", e)
})
return
}
next(ret)
}
function next(ret: any) {
if (ret.done) {
// prettier-ignore
setImmediateWithFallback(() => {
wrap((r: any) => { resolve(r) }, "flow_return", ret.value)
})
return
}
// TODO: support more type of values? See https://github.com/tj/co/blob/249bbdc72da24ae44076afd716349d2089b31c4c/index.js#L100
if (!ret.value || typeof ret.value.then !== "function") {
// istanbul ignore next
throw fail("Only promises can be yielded to `async`, got: " + ret)
}
return ret.value.then(onFulfilled, onRejected)
}
})
}
return spawner
}