-
Notifications
You must be signed in to change notification settings - Fork 4
/
AsyncTask.js
120 lines (99 loc) · 4.79 KB
/
AsyncTask.js
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
import { basicObject, basicFunction, promiseAlike } from 'source/common/verify'
import { catchPromise } from 'source/common/error'
// ## AsyncTask ##
// a data structure to allow saving resource heavy code to run later
// hold all of the idle/run/done phase data
// contain a promise te represent the running code
// and async query func to allow external code interact with code inside the promise
const IDLE = 'idle'
const RUN = 'run'
const DONE = 'done'
const ASYNC_TASK_PHASE_MAP = {
IDLE,
RUN,
DONE
}
const PLAN = 'plan'
const PROMISE = 'promise'
const QUERY = 'query'
const PLAN_PROMISE = 'planPromise'
const OUTPUT = 'output'
const ASYNC_TASK_KEY_MAP = {
PLAN, // idle phase data
PROMISE, QUERY, PLAN_PROMISE, // run phase data
OUTPUT // done phase data
}
// TODO: NOTE:
// if for convenience the value is directly used in code
// better add verify code like `strictEqual(ASYNC_TASK_KEY_MAP.OUTPUT, 'output')`
const KEY_RESET = {
// [ PLAN ]: undefined, // keep idle phase data
[ PLAN_PROMISE ]: undefined, [ QUERY ]: undefined, [ PROMISE ]: undefined, // drop run phase data
[ OUTPUT ]: undefined // drop done phase data
}
// TODO: NOTE: what this try to solve:
// it's convenient to using promise to hold a running `task`
// but there lack way to interact with code inside the running task
// for example:
// - promise timeout have no way to tell the inside code to exit ASAP
// - and current promise timeout code will leave the resource heavy promise running free
// - other code can follow this pattern to provide control, like `run()` from `source/node/system/Run`
__DEV__ && console.log('SAMPLE_ASYNC_TASK', {
// idle phase data
[ PLAN ]: (SAMPLE_ASYNC_TASK, ...extraOptionalArgList) => ({ // this pattern allow re-use same func with different config. better if is pure function, currently must be sync (async needed?)
// currently the task object is used to track the task change, so itself can't be immutable
// outer code should just assign the result back to the task object and re-use
// only re-create when a "new" task is needed, like copy
[ PLAN_PROMISE ]: Promise,
[ QUERY ]: async (type) => {}
}),
// run phase data
[ PLAN_PROMISE ]: Promise, // the running task, one `then` before OUTPUT is collected, will resolve and reject
[ QUERY ]: (
// required, provide an async way to interact with the code inside the running task
// this can be used as:
// - getState: async () => ({ ... })
// - cancel: async (reason) => {}
(async (type) => {}) || // default, nothing happen
(async (type, payload) => {}) || // recommended Redux dispatch pattern: `async (type, payload) => resolveToResultStateOrRejectWithError`
(async (type, ...extraOptionalArgList) => { // use as async emit, for retrieving state, or try to cancel early
if (type === 'get:state') return { state: 'still running' }
if (type === 'cancel') {} // do something to make task promise resolve/reject faster
if (type === 'get:value') return 'the value' // receive value and
if (type === 'set:value') {} // change value and alter the task behaviour?
})
),
[ PROMISE ]: Promise, // indicate the end of run phase, resolve to `{ result, error }`, no reject, after OUTPUT is set
// done phase data
[ OUTPUT ]: undefined || { // if both have value, pick error, and always check error since result can actually be undefined
error: undefined || Error,
result: undefined || 'Any'
},
// optional extra data
id: '123abcABC',
name: 'sample asyncTask',
config: { data: 1 } // can pass extra data to plan func when creating task
// readyState: 0, // optional run phase data, direct expose the running state/progress, like `XMLHttpRequest.readyState`
})
const getAsyncTaskPhase = (asyncTask) => asyncTask[ OUTPUT ] ? DONE
: asyncTask[ PROMISE ] ? RUN
: IDLE
const runAsyncTask = (asyncTask) => { // re-run will overwrite existing `promise/query`
if (__DEV__ && getAsyncTaskPhase(asyncTask) !== IDLE) throw new Error('should reset asyncTask to idle')
const planResult = asyncTask[ PLAN ](asyncTask)
if (__DEV__ && !basicObject(planResult)) throw new Error('expect asyncTask[ PLAN ] to return object')
if (planResult !== asyncTask) Object.assign(asyncTask, planResult) // merge back and re-use same task object
__DEV__ && promiseAlike(asyncTask[ PLAN_PROMISE ])
__DEV__ && basicFunction(asyncTask[ QUERY ])
asyncTask[ PROMISE ] = catchPromise(asyncTask[ PLAN_PROMISE ]) // should not reject
.then((output) => (asyncTask[ OUTPUT ] = output)) // record output as { result, error }
return asyncTask[ PROMISE ]
}
const resetAsyncTask = (asyncTask, extra) => ({ ...asyncTask, ...extra, ...KEY_RESET }) // return a new idle asyncTask
export {
ASYNC_TASK_PHASE_MAP,
ASYNC_TASK_KEY_MAP,
getAsyncTaskPhase,
runAsyncTask,
resetAsyncTask
}