4.17 kBJavaScriptView Raw
1import { createAtom, _allowStateChanges } from "mobx";
2import { NOOP, invariant } from "./utils";
3/**
4 * `fromResource` creates an observable whose current state can be inspected using `.current()`,
5 * and which can be kept in sync with some external datasource that can be subscribed to.
6 *
7 * The created observable will only subscribe to the datasource if it is in use somewhere,
8 * (un)subscribing when needed. To enable `fromResource` to do that two callbacks need to be provided,
9 * one to subscribe, and one to unsubscribe. The subscribe callback itself will receive a `sink` callback, which can be used
10 * to update the current state of the observable, allowing observes to react.
11 *
12 * Whatever is passed to `sink` will be returned by `current()`. The values passed to the sink will not be converted to
13 * observables automatically, but feel free to do so.
14 * It is the `current()` call itself which is being tracked,
15 * so make sure that you don't dereference to early.
16 *
17 * For inspiration, an example integration with the apollo-client on [github](https://github.com/apollostack/apollo-client/issues/503#issuecomment-241101379),
18 * or the [implementation](https://github.com/mobxjs/mobx-utils/blob/1d17cf7f7f5200937f68cc0b5e7ec7f3f71dccba/src/now.ts#L43-L57) of `mobxUtils.now`
19 *
20 * The following example code creates an observable that connects to a `dbUserRecord`,
21 * which comes from an imaginary database and notifies when it has changed.
22 *
23 * @example
24 * function createObservableUser(dbUserRecord) {
25 * let currentSubscription;
26 * return fromResource(
27 * (sink) => {
28 * // sink the current state
29 * sink(dbUserRecord.fields)
30 * // subscribe to the record, invoke the sink callback whenever new data arrives
31 * currentSubscription = dbUserRecord.onUpdated(() => {
32 * sink(dbUserRecord.fields)
33 * })
34 * },
35 * () => {
36 * // the user observable is not in use at the moment, unsubscribe (for now)
37 * dbUserRecord.unsubscribe(currentSubscription)
38 * }
39 * )
40 * }
41 *
42 * // usage:
43 * const myUserObservable = createObservableUser(myDatabaseConnector.query("name = 'Michel'"))
44 *
45 * // use the observable in autorun
46 * autorun(() => {
47 * // printed everytime the database updates its records
48 * console.log(myUserObservable.current().displayName)
49 * })
50 *
51 * // ... or a component
52 * const userComponent = observer(({ user }) =>
53 * <div>{user.current().displayName}</div>
54 * )
55 *
56 * @export
57 * @template T
58 * @param {(sink: (newValue: T) => void) => void} subscriber
59 * @param {IDisposer} [unsubscriber=NOOP]
60 * @param {T} [initialValue=undefined] the data that will be returned by `get()` until the `sink` has emitted its first data
61 * @returns {{
62 * current(): T;
63 * dispose(): void;
64 * isAlive(): boolean;
65 * }}
66 */
67export function fromResource(subscriber, unsubscriber, initialValue) {
68 if (unsubscriber === void 0) { unsubscriber = NOOP; }
69 if (initialValue === void 0) { initialValue = undefined; }
70 var isActive = false;
71 var isDisposed = false;
72 var value = initialValue;
73 var suspender = function () {
74 if (isActive) {
75 isActive = false;
76 unsubscriber();
77 }
78 };
79 var atom = createAtom("ResourceBasedObservable", function () {
80 invariant(!isActive && !isDisposed);
81 isActive = true;
82 subscriber(function (newValue) {
83 _allowStateChanges(true, function () {
84 value = newValue;
85 atom.reportChanged();
86 });
87 });
88 }, suspender);
89 return {
90 current: function () {
91 invariant(!isDisposed, "subscribingObservable has already been disposed");
92 var isBeingTracked = atom.reportObserved();
93 if (!isBeingTracked && !isActive)
94 console.warn("Called `get` of a subscribingObservable outside a reaction. Current value will be returned but no new subscription has started");
95 return value;
96 },
97 dispose: function () {
98 isDisposed = true;
99 suspender();
100 },
101 isAlive: function () { return isActive; },
102 };
103}