diff options
author | Stefan Malzner <stefan@adlk.io> | 2017-10-13 12:29:40 +0200 |
---|---|---|
committer | Stefan Malzner <stefan@adlk.io> | 2017-10-13 12:29:40 +0200 |
commit | 58cda9cc7fb79ca9df6746de7f9662bc08dc156a (patch) | |
tree | 1211600c2a5d3b5f81c435c6896618111a611720 /src/stores/lib | |
download | ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.tar.gz ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.tar.zst ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.zip |
initial commit
Diffstat (limited to 'src/stores/lib')
-rw-r--r-- | src/stores/lib/CachedRequest.js | 106 | ||||
-rw-r--r-- | src/stores/lib/Reaction.js | 22 | ||||
-rw-r--r-- | src/stores/lib/Request.js | 112 | ||||
-rw-r--r-- | src/stores/lib/Store.js | 44 |
4 files changed, 284 insertions, 0 deletions
diff --git a/src/stores/lib/CachedRequest.js b/src/stores/lib/CachedRequest.js new file mode 100644 index 000000000..c0c3d40a1 --- /dev/null +++ b/src/stores/lib/CachedRequest.js | |||
@@ -0,0 +1,106 @@ | |||
1 | // @flow | ||
2 | import { action } from 'mobx'; | ||
3 | import { isEqual, remove } from 'lodash'; | ||
4 | import Request from './Request'; | ||
5 | |||
6 | export default class CachedRequest extends Request { | ||
7 | _apiCalls = []; | ||
8 | _isInvalidated = true; | ||
9 | |||
10 | execute(...callArgs) { | ||
11 | // Do not continue if this request is already loading | ||
12 | if (this._isWaitingForResponse) return this; | ||
13 | |||
14 | // Very simple caching strategy -> only continue if the call / args changed | ||
15 | // or the request was invalidated manually from outside | ||
16 | const existingApiCall = this._findApiCall(callArgs); | ||
17 | |||
18 | // Invalidate if new or different api call will be done | ||
19 | if (existingApiCall && existingApiCall !== this._currentApiCall) { | ||
20 | this._isInvalidated = true; | ||
21 | this._currentApiCall = existingApiCall; | ||
22 | } else if (!existingApiCall) { | ||
23 | this._isInvalidated = true; | ||
24 | this._currentApiCall = this._addApiCall(callArgs); | ||
25 | } | ||
26 | |||
27 | // Do not continue if this request is not invalidated (see above) | ||
28 | if (!this._isInvalidated) return this; | ||
29 | |||
30 | // This timeout is necessary to avoid warnings from mobx | ||
31 | // regarding triggering actions as side-effect of getters | ||
32 | setTimeout(action(() => { | ||
33 | this.isExecuting = true; | ||
34 | // Apply the previous result from this call immediately (cached) | ||
35 | if (existingApiCall) { | ||
36 | this.result = existingApiCall.result; | ||
37 | } | ||
38 | }), 0); | ||
39 | |||
40 | // Issue api call & save it as promise that is handled to update the results of the operation | ||
41 | this._promise = new Promise((resolve, reject) => { | ||
42 | this._api[this._method](...callArgs) | ||
43 | .then((result) => { | ||
44 | setTimeout(action(() => { | ||
45 | this.result = result; | ||
46 | if (this._currentApiCall) this._currentApiCall.result = result; | ||
47 | this.isExecuting = false; | ||
48 | this.isError = false; | ||
49 | this.wasExecuted = true; | ||
50 | this._isInvalidated = false; | ||
51 | this._isWaitingForResponse = false; | ||
52 | this._triggerHooks(); | ||
53 | resolve(result); | ||
54 | }), 1); | ||
55 | return result; | ||
56 | }) | ||
57 | .catch(action((error) => { | ||
58 | setTimeout(action(() => { | ||
59 | this.error = error; | ||
60 | this.isExecuting = false; | ||
61 | this.isError = true; | ||
62 | this.wasExecuted = true; | ||
63 | this._isWaitingForResponse = false; | ||
64 | this._triggerHooks(); | ||
65 | reject(error); | ||
66 | }), 1); | ||
67 | })); | ||
68 | }); | ||
69 | |||
70 | this._isWaitingForResponse = true; | ||
71 | return this; | ||
72 | } | ||
73 | |||
74 | invalidate(options = { immediately: false }) { | ||
75 | this._isInvalidated = true; | ||
76 | if (options.immediately && this._currentApiCall) { | ||
77 | return this.execute(...this._currentApiCall.args); | ||
78 | } | ||
79 | return this; | ||
80 | } | ||
81 | |||
82 | patch(modify) { | ||
83 | return new Promise((resolve) => { | ||
84 | setTimeout(action(() => { | ||
85 | const override = modify(this.result); | ||
86 | if (override !== undefined) this.result = override; | ||
87 | if (this._currentApiCall) this._currentApiCall.result = this.result; | ||
88 | resolve(this); | ||
89 | }), 0); | ||
90 | }); | ||
91 | } | ||
92 | |||
93 | removeCacheForCallWith(...args) { | ||
94 | remove(this._apiCalls, c => isEqual(c.args, args)); | ||
95 | } | ||
96 | |||
97 | _addApiCall(args) { | ||
98 | const newCall = { args, result: null }; | ||
99 | this._apiCalls.push(newCall); | ||
100 | return newCall; | ||
101 | } | ||
102 | |||
103 | _findApiCall(args) { | ||
104 | return this._apiCalls.find(c => isEqual(c.args, args)); | ||
105 | } | ||
106 | } | ||
diff --git a/src/stores/lib/Reaction.js b/src/stores/lib/Reaction.js new file mode 100644 index 000000000..e9bc26d81 --- /dev/null +++ b/src/stores/lib/Reaction.js | |||
@@ -0,0 +1,22 @@ | |||
1 | // @flow | ||
2 | import { autorun } from 'mobx'; | ||
3 | |||
4 | export default class Reaction { | ||
5 | reaction; | ||
6 | hasBeenStarted; | ||
7 | dispose; | ||
8 | |||
9 | constructor(reaction) { | ||
10 | this.reaction = reaction; | ||
11 | this.hasBeenStarted = false; | ||
12 | } | ||
13 | |||
14 | start() { | ||
15 | this.dispose = autorun(() => this.reaction()); | ||
16 | this.hasBeenStarted = true; | ||
17 | } | ||
18 | |||
19 | stop() { | ||
20 | if (this.hasBeenStarted) this.dispose(); | ||
21 | } | ||
22 | } | ||
diff --git a/src/stores/lib/Request.js b/src/stores/lib/Request.js new file mode 100644 index 000000000..4a6925cc5 --- /dev/null +++ b/src/stores/lib/Request.js | |||
@@ -0,0 +1,112 @@ | |||
1 | import { observable, action, computed } from 'mobx'; | ||
2 | import { isEqual } from 'lodash/fp'; | ||
3 | |||
4 | export default class Request { | ||
5 | static _hooks = []; | ||
6 | |||
7 | static registerHook(hook) { | ||
8 | Request._hooks.push(hook); | ||
9 | } | ||
10 | |||
11 | @observable result = null; | ||
12 | @observable error = null; | ||
13 | @observable isExecuting = false; | ||
14 | @observable isError = false; | ||
15 | @observable wasExecuted = false; | ||
16 | |||
17 | _promise = Promise; | ||
18 | _api = {}; | ||
19 | _method = ''; | ||
20 | _isWaitingForResponse = false; | ||
21 | _currentApiCall = null; | ||
22 | |||
23 | constructor(api, method) { | ||
24 | this._api = api; | ||
25 | this._method = method; | ||
26 | } | ||
27 | |||
28 | execute(...callArgs) { | ||
29 | // Do not continue if this request is already loading | ||
30 | if (this._isWaitingForResponse) return this; | ||
31 | |||
32 | if (!this._api[this._method]) { | ||
33 | throw new Error(`Missing method <${this._method}> on api object:`, this._api); | ||
34 | } | ||
35 | |||
36 | // This timeout is necessary to avoid warnings from mobx | ||
37 | // regarding triggering actions as side-effect of getters | ||
38 | setTimeout(action(() => { | ||
39 | this.isExecuting = true; | ||
40 | }), 0); | ||
41 | |||
42 | // Issue api call & save it as promise that is handled to update the results of the operation | ||
43 | this._promise = new Promise((resolve, reject) => { | ||
44 | this._api[this._method](...callArgs) | ||
45 | .then((result) => { | ||
46 | setTimeout(action(() => { | ||
47 | this.result = result; | ||
48 | if (this._currentApiCall) this._currentApiCall.result = result; | ||
49 | this.isExecuting = false; | ||
50 | this.isError = false; | ||
51 | this.wasExecuted = true; | ||
52 | this._isWaitingForResponse = false; | ||
53 | this._triggerHooks(); | ||
54 | resolve(result); | ||
55 | }), 1); | ||
56 | return result; | ||
57 | }) | ||
58 | .catch(action((error) => { | ||
59 | setTimeout(action(() => { | ||
60 | this.error = error; | ||
61 | this.isExecuting = false; | ||
62 | this.isError = true; | ||
63 | this.wasExecuted = true; | ||
64 | this._isWaitingForResponse = false; | ||
65 | this._triggerHooks(); | ||
66 | reject(error); | ||
67 | }), 1); | ||
68 | })); | ||
69 | }); | ||
70 | |||
71 | this._isWaitingForResponse = true; | ||
72 | this._currentApiCall = { args: callArgs, result: null }; | ||
73 | return this; | ||
74 | } | ||
75 | |||
76 | reload() { | ||
77 | return this.execute(...this._currentApiCall.args); | ||
78 | } | ||
79 | |||
80 | isExecutingWithArgs(...args) { | ||
81 | return this.isExecuting && this._currentApiCall && isEqual(this._currentApiCall.args, args); | ||
82 | } | ||
83 | |||
84 | @computed get isExecutingFirstTime() { | ||
85 | return !this.wasExecuted && this.isExecuting; | ||
86 | } | ||
87 | |||
88 | then(...args) { | ||
89 | if (!this._promise) throw new Error('You have to call Request::execute before you can access it as promise'); | ||
90 | return this._promise.then(...args); | ||
91 | } | ||
92 | |||
93 | catch(...args) { | ||
94 | if (!this._promise) throw new Error('You have to call Request::execute before you can access it as promise'); | ||
95 | return this._promise.catch(...args); | ||
96 | } | ||
97 | |||
98 | _triggerHooks() { | ||
99 | Request._hooks.forEach(hook => hook(this)); | ||
100 | } | ||
101 | |||
102 | reset() { | ||
103 | this.result = null; | ||
104 | this.isExecuting = false; | ||
105 | this.isError = false; | ||
106 | this.wasExecuted = false; | ||
107 | this._isWaitingForResponse = false; | ||
108 | this._promise = Promise; | ||
109 | |||
110 | return this; | ||
111 | } | ||
112 | } | ||
diff --git a/src/stores/lib/Store.js b/src/stores/lib/Store.js new file mode 100644 index 000000000..873da7b37 --- /dev/null +++ b/src/stores/lib/Store.js | |||
@@ -0,0 +1,44 @@ | |||
1 | import { computed, observable } from 'mobx'; | ||
2 | import Reaction from './Reaction'; | ||
3 | |||
4 | export default class Store { | ||
5 | stores = {}; | ||
6 | api = {}; | ||
7 | actions = {}; | ||
8 | |||
9 | _reactions = []; | ||
10 | |||
11 | // status implementation | ||
12 | @observable _status = null; | ||
13 | @computed get actionStatus() { | ||
14 | return this._status || []; | ||
15 | } | ||
16 | set actionStatus(status) { | ||
17 | this._status = status; | ||
18 | } | ||
19 | |||
20 | constructor(stores, api, actions) { | ||
21 | this.stores = stores; | ||
22 | this.api = api; | ||
23 | this.actions = actions; | ||
24 | } | ||
25 | |||
26 | registerReactions(reactions) { | ||
27 | reactions.forEach(reaction => this._reactions.push(new Reaction(reaction))); | ||
28 | } | ||
29 | |||
30 | setup() {} | ||
31 | |||
32 | initialize() { | ||
33 | this.setup(); | ||
34 | this._reactions.forEach(reaction => reaction.start()); | ||
35 | } | ||
36 | |||
37 | teardown() { | ||
38 | this._reactions.forEach(reaction => reaction.stop()); | ||
39 | } | ||
40 | |||
41 | resetStatus() { | ||
42 | this._status = null; | ||
43 | } | ||
44 | } | ||