1 | import { action, extendObservable } from "mobx";
|
2 | import { invariant } from "./utils";
|
3 | export var PENDING = "pending";
|
4 | export var FULFILLED = "fulfilled";
|
5 | export var REJECTED = "rejected";
|
6 | function caseImpl(handlers) {
|
7 | switch (this.state) {
|
8 | case PENDING:
|
9 | return handlers.pending && handlers.pending(this.value);
|
10 | case REJECTED:
|
11 | return handlers.rejected && handlers.rejected(this.value);
|
12 | case FULFILLED:
|
13 | return handlers.fulfilled ? handlers.fulfilled(this.value) : this.value;
|
14 | }
|
15 | }
|
16 | /**
|
17 | * `fromPromise` takes a Promise, extends it with 2 observable properties that track
|
18 | * the status of the promise and returns it. The returned object has the following observable properties:
|
19 | * - `value`: either the initial value, the value the Promise resolved to, or the value the Promise was rejected with. use `.state` if you need to be able to tell the difference.
|
20 | * - `state`: one of `"pending"`, `"fulfilled"` or `"rejected"`
|
21 | *
|
22 | * And the following methods:
|
23 | * - `case({fulfilled, rejected, pending})`: maps over the result using the provided handlers, or returns `undefined` if a handler isn't available for the current promise state.
|
24 | *
|
25 | * The returned object implements `PromiseLike<TValue>`, so you can chain additional `Promise` handlers using `then`. You may also use it with `await` in `async` functions.
|
26 | *
|
27 | * Note that the status strings are available as constants:
|
28 | * `mobxUtils.PENDING`, `mobxUtils.REJECTED`, `mobxUtil.FULFILLED`
|
29 | *
|
30 | * fromPromise takes an optional second argument, a previously created `fromPromise` based observable.
|
31 | * This is useful to replace one promise based observable with another, without going back to an intermediate
|
32 | * "pending" promise state while fetching data. For example:
|
33 | *
|
34 | * @example
|
35 | * \@observer
|
36 | * class SearchResults extends React.Component {
|
37 | * \@observable.ref searchResults
|
38 | *
|
39 | * componentDidUpdate(nextProps) {
|
40 | * if (nextProps.query !== this.props.query)
|
41 | * this.searchResults = fromPromise(
|
42 | * window.fetch("/search?q=" + nextProps.query),
|
43 | * // by passing, we won't render a pending state if we had a successful search query before
|
44 | * // rather, we will keep showing the previous search results, until the new promise resolves (or rejects)
|
45 | * this.searchResults
|
46 | * )
|
47 | * }
|
48 | *
|
49 | * render() {
|
50 | * return this.searchResults.case({
|
51 | * pending: (staleValue) => {
|
52 | * return staleValue || "searching" // <- value might set to previous results while the promise is still pending
|
53 | * },
|
54 | * fulfilled: (value) => {
|
55 | * return value // the fresh results
|
56 | * },
|
57 | * rejected: (error) => {
|
58 | * return "Oops: " + error
|
59 | * }
|
60 | * })
|
61 | * }
|
62 | * }
|
63 | *
|
64 | * Observable promises can be created immediately in a certain state using
|
65 | * `fromPromise.reject(reason)` or `fromPromise.resolve(value?)`.
|
66 | * The main advantage of `fromPromise.resolve(value)` over `fromPromise(Promise.resolve(value))` is that the first _synchronously_ starts in the desired state.
|
67 | *
|
68 | * It is possible to directly create a promise using a resolve, reject function:
|
69 | * `fromPromise((resolve, reject) => setTimeout(() => resolve(true), 1000))`
|
70 | *
|
71 | * @example
|
72 | * const fetchResult = fromPromise(fetch("http://someurl"))
|
73 | *
|
74 | * // combine with when..
|
75 | * when(
|
76 | * () => fetchResult.state !== "pending",
|
77 | * () => {
|
78 | * console.log("Got ", fetchResult.value)
|
79 | * }
|
80 | * )
|
81 | *
|
82 | * // or a mobx-react component..
|
83 | * const myComponent = observer(({ fetchResult }) => {
|
84 | * switch(fetchResult.state) {
|
85 | * case "pending": return <div>Loading...</div>
|
86 | * case "rejected": return <div>Ooops... {fetchResult.value}</div>
|
87 | * case "fulfilled": return <div>Gotcha: {fetchResult.value}</div>
|
88 | * }
|
89 | * })
|
90 | *
|
91 | * // or using the case method instead of switch:
|
92 | *
|
93 | * const myComponent = observer(({ fetchResult }) =>
|
94 | * fetchResult.case({
|
95 | * pending: () => <div>Loading...</div>,
|
96 | * rejected: error => <div>Ooops.. {error}</div>,
|
97 | * fulfilled: value => <div>Gotcha: {value}</div>,
|
98 | * }))
|
99 | *
|
100 | * // chain additional handler(s) to the resolve/reject:
|
101 | *
|
102 | * fetchResult.then(
|
103 | * (result) => doSomeTransformation(result),
|
104 | * (rejectReason) => console.error('fetchResult was rejected, reason: ' + rejectReason)
|
105 | * ).then(
|
106 | * (transformedResult) => console.log('transformed fetchResult: ' + transformedResult)
|
107 | * )
|
108 | *
|
109 | * @param origPromise The promise which will be observed
|
110 | * @param oldPromise The previously observed promise
|
111 | * @returns origPromise with added properties and methods described above.
|
112 | */
|
113 | export function fromPromise(origPromise, oldPromise) {
|
114 | invariant(arguments.length <= 2, "fromPromise expects up to two arguments");
|
115 | invariant(typeof origPromise === "function" ||
|
116 | (typeof origPromise === "object" &&
|
117 | origPromise &&
|
118 | typeof origPromise.then === "function"), "Please pass a promise or function to fromPromise");
|
119 | if (origPromise.isPromiseBasedObservable === true)
|
120 | return origPromise;
|
121 | if (typeof origPromise === "function") {
|
122 | // If it is a (reject, resolve function, wrap it)
|
123 | origPromise = new Promise(origPromise);
|
124 | }
|
125 | var promise = origPromise;
|
126 | origPromise.then(action("observableFromPromise-resolve", function (value) {
|
127 | promise.value = value;
|
128 | promise.state = FULFILLED;
|
129 | }), action("observableFromPromise-reject", function (reason) {
|
130 | promise.value = reason;
|
131 | promise.state = REJECTED;
|
132 | }));
|
133 | promise.isPromiseBasedObservable = true;
|
134 | promise.case = caseImpl;
|
135 | var oldData = oldPromise && (oldPromise.state === FULFILLED || oldPromise.state === PENDING)
|
136 | ? oldPromise.value
|
137 | : undefined;
|
138 | extendObservable(promise, {
|
139 | value: oldData,
|
140 | state: PENDING,
|
141 | }, {}, { deep: false });
|
142 | return promise;
|
143 | }
|
144 | (function (fromPromise) {
|
145 | fromPromise.reject = action("fromPromise.reject", function (reason) {
|
146 | var p = fromPromise(Promise.reject(reason));
|
147 | p.state = REJECTED;
|
148 | p.value = reason;
|
149 | return p;
|
150 | });
|
151 | function resolveBase(value) {
|
152 | if (value === void 0) { value = undefined; }
|
153 | var p = fromPromise(Promise.resolve(value));
|
154 | p.state = FULFILLED;
|
155 | p.value = value;
|
156 | return p;
|
157 | }
|
158 | fromPromise.resolve = action("fromPromise.resolve", resolveBase);
|
159 | })(fromPromise || (fromPromise = {}));
|
160 | /**
|
161 | * Returns true if the provided value is a promise-based observable.
|
162 | * @param value any
|
163 | * @returns {boolean}
|
164 | */
|
165 | export function isPromiseBasedObservable(value) {
|
166 | return value && value.isPromiseBasedObservable === true;
|
167 | }
|