625 lines
17 KiB
JavaScript
625 lines
17 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
*
|
|
* @format
|
|
*/
|
|
'use strict';
|
|
|
|
var hostReportError = swallowError;
|
|
/**
|
|
* Limited implementation of ESObservable, providing the limited set of behavior
|
|
* Relay networking requires.
|
|
*
|
|
* Observables retain the benefit of callbacks which can be called
|
|
* synchronously, avoiding any UI jitter, while providing a compositional API,
|
|
* which simplifies logic and prevents mishandling of errors compared to
|
|
* the direct use of callback functions.
|
|
*
|
|
* ESObservable: https://github.com/tc39/proposal-observable
|
|
*/
|
|
|
|
var RelayObservable =
|
|
/*#__PURE__*/
|
|
function () {
|
|
RelayObservable.create = function create(source) {
|
|
return new RelayObservable(source);
|
|
}; // Use RelayObservable.create()
|
|
|
|
|
|
function RelayObservable(source) {
|
|
if (process.env.NODE_ENV !== "production") {
|
|
// Early runtime errors for ill-formed sources.
|
|
if (!source || typeof source !== 'function') {
|
|
throw new Error('Source must be a Function: ' + String(source));
|
|
}
|
|
}
|
|
|
|
this._source = source;
|
|
}
|
|
/**
|
|
* When an emitted error event is not handled by an Observer, it is reported
|
|
* to the host environment (what the ESObservable spec refers to as
|
|
* "HostReportErrors()").
|
|
*
|
|
* The default implementation in development rethrows thrown errors, and
|
|
* logs emitted error events to the console, while in production does nothing
|
|
* (swallowing unhandled errors).
|
|
*
|
|
* Called during application initialization, this method allows
|
|
* application-specific handling of unhandled errors. Allowing, for example,
|
|
* integration with error logging or developer tools.
|
|
*
|
|
* A second parameter `isUncaughtThrownError` is true when the unhandled error
|
|
* was thrown within an Observer handler, and false when the unhandled error
|
|
* was an unhandled emitted event.
|
|
*
|
|
* - Uncaught thrown errors typically represent avoidable errors thrown from
|
|
* application code, which should be handled with a try/catch block, and
|
|
* usually have useful stack traces.
|
|
*
|
|
* - Unhandled emitted event errors typically represent unavoidable events in
|
|
* application flow such as network failure, and may not have useful
|
|
* stack traces.
|
|
*/
|
|
|
|
|
|
RelayObservable.onUnhandledError = function onUnhandledError(callback) {
|
|
hostReportError = callback;
|
|
};
|
|
/**
|
|
* Accepts various kinds of data sources, and always returns a RelayObservable
|
|
* useful for accepting the result of a user-provided FetchFunction.
|
|
*/
|
|
|
|
|
|
RelayObservable.from = function from(obj) {
|
|
return isObservable(obj) ? fromObservable(obj) : require("./isPromise")(obj) ? fromPromise(obj) : fromValue(obj);
|
|
};
|
|
/**
|
|
* Creates a RelayObservable, given a function which expects a legacy
|
|
* Relay Observer as the last argument and which returns a Disposable.
|
|
*
|
|
* To support migration to Observable, the function may ignore the
|
|
* legacy Relay observer and directly return an Observable instead.
|
|
*/
|
|
|
|
|
|
RelayObservable.fromLegacy = function fromLegacy(callback) {
|
|
return RelayObservable.create(function (sink) {
|
|
var result = callback({
|
|
onNext: sink.next,
|
|
onError: sink.error,
|
|
onCompleted: sink.complete
|
|
});
|
|
return isObservable(result) ? result.subscribe(sink) : function () {
|
|
return result.dispose();
|
|
};
|
|
});
|
|
};
|
|
/**
|
|
* Similar to promise.catch(), observable.catch() handles error events, and
|
|
* provides an alternative observable to use in it's place.
|
|
*
|
|
* If the catch handler throws a new error, it will appear as an error event
|
|
* on the resulting Observable.
|
|
*/
|
|
|
|
|
|
var _proto = RelayObservable.prototype;
|
|
|
|
_proto["catch"] = function _catch(fn) {
|
|
var _this = this;
|
|
|
|
return RelayObservable.create(function (sink) {
|
|
var subscription;
|
|
|
|
_this.subscribe({
|
|
start: function start(sub) {
|
|
subscription = sub;
|
|
},
|
|
next: sink.next,
|
|
complete: sink.complete,
|
|
error: function error(_error2) {
|
|
try {
|
|
fn(_error2).subscribe({
|
|
start: function start(sub) {
|
|
subscription = sub;
|
|
},
|
|
next: sink.next,
|
|
complete: sink.complete,
|
|
error: sink.error
|
|
});
|
|
} catch (error2) {
|
|
sink.error(error2, true
|
|
/* isUncaughtThrownError */
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
return function () {
|
|
return subscription.unsubscribe();
|
|
};
|
|
});
|
|
};
|
|
/**
|
|
* Returns a new Observable which returns the same values as this one, but
|
|
* modified so that the provided Observer is called to perform a side-effects
|
|
* for all events emitted by the source.
|
|
*
|
|
* Any errors that are thrown in the side-effect Observer are unhandled, and
|
|
* do not affect the source Observable or its Observer.
|
|
*
|
|
* This is useful for when debugging your Observables or performing other
|
|
* side-effects such as logging or performance monitoring.
|
|
*/
|
|
|
|
|
|
_proto["do"] = function _do(observer) {
|
|
var _this2 = this;
|
|
|
|
return RelayObservable.create(function (sink) {
|
|
var both = function both(action) {
|
|
return function () {
|
|
try {
|
|
observer[action] && observer[action].apply(observer, arguments);
|
|
} catch (error) {
|
|
hostReportError(error, true
|
|
/* isUncaughtThrownError */
|
|
);
|
|
}
|
|
|
|
sink[action] && sink[action].apply(sink, arguments);
|
|
};
|
|
};
|
|
|
|
return _this2.subscribe({
|
|
start: both('start'),
|
|
next: both('next'),
|
|
error: both('error'),
|
|
complete: both('complete'),
|
|
unsubscribe: both('unsubscribe')
|
|
});
|
|
});
|
|
};
|
|
/**
|
|
* Returns a new Observable which returns the same values as this one, but
|
|
* modified so that the finally callback is performed after completion,
|
|
* whether normal or due to error or unsubscription.
|
|
*
|
|
* This is useful for cleanup such as resource finalization.
|
|
*/
|
|
|
|
|
|
_proto["finally"] = function _finally(fn) {
|
|
var _this3 = this;
|
|
|
|
return RelayObservable.create(function (sink) {
|
|
var subscription = _this3.subscribe(sink);
|
|
|
|
return function () {
|
|
subscription.unsubscribe();
|
|
fn();
|
|
};
|
|
});
|
|
};
|
|
/**
|
|
* Returns a new Observable which is identical to this one, unless this
|
|
* Observable completes before yielding any values, in which case the new
|
|
* Observable will yield the values from the alternate Observable.
|
|
*
|
|
* If this Observable does yield values, the alternate is never subscribed to.
|
|
*
|
|
* This is useful for scenarios where values may come from multiple sources
|
|
* which should be tried in order, i.e. from a cache before a network.
|
|
*/
|
|
|
|
|
|
_proto.ifEmpty = function ifEmpty(alternate) {
|
|
var _this4 = this;
|
|
|
|
return RelayObservable.create(function (sink) {
|
|
var hasValue = false;
|
|
|
|
var current = _this4.subscribe({
|
|
next: function next(value) {
|
|
hasValue = true;
|
|
sink.next(value);
|
|
},
|
|
error: sink.error,
|
|
complete: function complete() {
|
|
if (hasValue) {
|
|
sink.complete();
|
|
} else {
|
|
current = alternate.subscribe(sink);
|
|
}
|
|
}
|
|
});
|
|
|
|
return function () {
|
|
current.unsubscribe();
|
|
};
|
|
});
|
|
};
|
|
/**
|
|
* Observable's primary API: returns an unsubscribable Subscription to the
|
|
* source of this Observable.
|
|
*
|
|
* Note: A sink may be passed directly to .subscribe() as its observer,
|
|
* allowing for easily composing Observables.
|
|
*/
|
|
|
|
|
|
_proto.subscribe = function subscribe(observer) {
|
|
if (process.env.NODE_ENV !== "production") {
|
|
// Early runtime errors for ill-formed observers.
|
|
if (!observer || typeof observer !== 'object') {
|
|
throw new Error('Observer must be an Object with callbacks: ' + String(observer));
|
|
}
|
|
}
|
|
|
|
return _subscribe(this._source, observer);
|
|
};
|
|
/**
|
|
* Supports subscription of a legacy Relay Observer, returning a Disposable.
|
|
*/
|
|
|
|
|
|
_proto.subscribeLegacy = function subscribeLegacy(legacyObserver) {
|
|
var subscription = this.subscribe({
|
|
next: legacyObserver.onNext,
|
|
error: legacyObserver.onError,
|
|
complete: legacyObserver.onCompleted
|
|
});
|
|
return {
|
|
dispose: subscription.unsubscribe
|
|
};
|
|
};
|
|
/**
|
|
* Returns a new Observerable where each value has been transformed by
|
|
* the mapping function.
|
|
*/
|
|
|
|
|
|
_proto.map = function map(fn) {
|
|
var _this5 = this;
|
|
|
|
return RelayObservable.create(function (sink) {
|
|
var subscription = _this5.subscribe({
|
|
complete: sink.complete,
|
|
error: sink.error,
|
|
next: function next(value) {
|
|
try {
|
|
var mapValue = fn(value);
|
|
sink.next(mapValue);
|
|
} catch (error) {
|
|
sink.error(error, true
|
|
/* isUncaughtThrownError */
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
return function () {
|
|
subscription.unsubscribe();
|
|
};
|
|
});
|
|
};
|
|
/**
|
|
* Returns a new Observable where each value is replaced with a new Observable
|
|
* by the mapping function, the results of which returned as a single
|
|
* merged Observable.
|
|
*/
|
|
|
|
|
|
_proto.mergeMap = function mergeMap(fn) {
|
|
var _this6 = this;
|
|
|
|
return RelayObservable.create(function (sink) {
|
|
var subscriptions = [];
|
|
|
|
function start(subscription) {
|
|
this._sub = subscription;
|
|
subscriptions.push(subscription);
|
|
}
|
|
|
|
function complete() {
|
|
subscriptions.splice(subscriptions.indexOf(this._sub), 1);
|
|
|
|
if (subscriptions.length === 0) {
|
|
sink.complete();
|
|
}
|
|
}
|
|
|
|
_this6.subscribe({
|
|
start: start,
|
|
next: function next(value) {
|
|
try {
|
|
if (!sink.closed) {
|
|
RelayObservable.from(fn(value)).subscribe({
|
|
start: start,
|
|
next: sink.next,
|
|
error: sink.error,
|
|
complete: complete
|
|
});
|
|
}
|
|
} catch (error) {
|
|
sink.error(error, true
|
|
/* isUncaughtThrownError */
|
|
);
|
|
}
|
|
},
|
|
error: sink.error,
|
|
complete: complete
|
|
});
|
|
|
|
return function () {
|
|
subscriptions.forEach(function (sub) {
|
|
return sub.unsubscribe();
|
|
});
|
|
subscriptions.length = 0;
|
|
};
|
|
});
|
|
};
|
|
/**
|
|
* Returns a new Observable which first mirrors this Observable, then when it
|
|
* completes, waits for `pollInterval` milliseconds before re-subscribing to
|
|
* this Observable again, looping in this manner until unsubscribed.
|
|
*
|
|
* The returned Observable never completes.
|
|
*/
|
|
|
|
|
|
_proto.poll = function poll(pollInterval) {
|
|
var _this7 = this;
|
|
|
|
if (process.env.NODE_ENV !== "production") {
|
|
if (typeof pollInterval !== 'number' || pollInterval <= 0) {
|
|
throw new Error('RelayObservable: Expected pollInterval to be positive, got: ' + pollInterval);
|
|
}
|
|
}
|
|
|
|
return RelayObservable.create(function (sink) {
|
|
var subscription;
|
|
var timeout;
|
|
|
|
var poll = function poll() {
|
|
subscription = _this7.subscribe({
|
|
next: sink.next,
|
|
error: sink.error,
|
|
complete: function complete() {
|
|
timeout = setTimeout(poll, pollInterval);
|
|
}
|
|
});
|
|
};
|
|
|
|
poll();
|
|
return function () {
|
|
clearTimeout(timeout);
|
|
subscription.unsubscribe();
|
|
};
|
|
});
|
|
};
|
|
/**
|
|
* Returns a Promise which resolves when this Observable yields a first value
|
|
* or when it completes with no value.
|
|
*/
|
|
|
|
|
|
_proto.toPromise = function toPromise() {
|
|
var _this8 = this;
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
var subscription;
|
|
|
|
_this8.subscribe({
|
|
start: function start(sub) {
|
|
subscription = sub;
|
|
},
|
|
next: function next(val) {
|
|
resolve(val);
|
|
subscription.unsubscribe();
|
|
},
|
|
error: reject,
|
|
complete: resolve
|
|
});
|
|
});
|
|
};
|
|
|
|
return RelayObservable;
|
|
}(); // Use declarations to teach Flow how to check isObservable.
|
|
|
|
|
|
function isObservable(obj) {
|
|
return typeof obj === 'object' && obj !== null && typeof obj.subscribe === 'function';
|
|
}
|
|
|
|
function fromObservable(obj) {
|
|
return obj instanceof RelayObservable ? obj : RelayObservable.create(function (sink) {
|
|
return obj.subscribe(sink);
|
|
});
|
|
}
|
|
|
|
function fromPromise(promise) {
|
|
return RelayObservable.create(function (sink) {
|
|
// Since sink methods do not throw, the resulting Promise can be ignored.
|
|
promise.then(function (value) {
|
|
sink.next(value);
|
|
sink.complete();
|
|
}, sink.error);
|
|
});
|
|
}
|
|
|
|
function fromValue(value) {
|
|
return RelayObservable.create(function (sink) {
|
|
sink.next(value);
|
|
sink.complete();
|
|
});
|
|
}
|
|
|
|
function _subscribe(source, observer) {
|
|
var closed = false;
|
|
var cleanup; // Ideally we would simply describe a `get closed()` method on the Sink and
|
|
// Subscription objects below, however not all flow environments we expect
|
|
// Relay to be used within will support property getters, and many minifier
|
|
// tools still do not support ES5 syntax. Instead, we can use defineProperty.
|
|
|
|
var withClosed = function withClosed(obj) {
|
|
return Object.defineProperty(obj, 'closed', {
|
|
get: function get() {
|
|
return closed;
|
|
}
|
|
});
|
|
};
|
|
|
|
function doCleanup() {
|
|
if (cleanup) {
|
|
if (cleanup.unsubscribe) {
|
|
cleanup.unsubscribe();
|
|
} else {
|
|
try {
|
|
cleanup();
|
|
} catch (error) {
|
|
hostReportError(error, true
|
|
/* isUncaughtThrownError */
|
|
);
|
|
}
|
|
}
|
|
|
|
cleanup = undefined;
|
|
}
|
|
} // Create a Subscription.
|
|
|
|
|
|
var subscription = withClosed({
|
|
unsubscribe: function unsubscribe() {
|
|
if (!closed) {
|
|
closed = true; // Tell Observer that unsubscribe was called.
|
|
|
|
try {
|
|
observer.unsubscribe && observer.unsubscribe(subscription);
|
|
} catch (error) {
|
|
hostReportError(error, true
|
|
/* isUncaughtThrownError */
|
|
);
|
|
} finally {
|
|
doCleanup();
|
|
}
|
|
}
|
|
}
|
|
}); // Tell Observer that observation is about to begin.
|
|
|
|
try {
|
|
observer.start && observer.start(subscription);
|
|
} catch (error) {
|
|
hostReportError(error, true
|
|
/* isUncaughtThrownError */
|
|
);
|
|
} // If closed already, don't bother creating a Sink.
|
|
|
|
|
|
if (closed) {
|
|
return subscription;
|
|
} // Create a Sink respecting subscription state and cleanup.
|
|
|
|
|
|
var sink = withClosed({
|
|
next: function next(value) {
|
|
if (!closed && observer.next) {
|
|
try {
|
|
observer.next(value);
|
|
} catch (error) {
|
|
hostReportError(error, true
|
|
/* isUncaughtThrownError */
|
|
);
|
|
}
|
|
}
|
|
},
|
|
error: function error(_error3, isUncaughtThrownError) {
|
|
if (closed || !observer.error) {
|
|
closed = true;
|
|
hostReportError(_error3, isUncaughtThrownError || false);
|
|
doCleanup();
|
|
} else {
|
|
closed = true;
|
|
|
|
try {
|
|
observer.error(_error3);
|
|
} catch (error2) {
|
|
hostReportError(error2, true
|
|
/* isUncaughtThrownError */
|
|
);
|
|
} finally {
|
|
doCleanup();
|
|
}
|
|
}
|
|
},
|
|
complete: function complete() {
|
|
if (!closed) {
|
|
closed = true;
|
|
|
|
try {
|
|
observer.complete && observer.complete();
|
|
} catch (error) {
|
|
hostReportError(error, true
|
|
/* isUncaughtThrownError */
|
|
);
|
|
} finally {
|
|
doCleanup();
|
|
}
|
|
}
|
|
}
|
|
}); // If anything goes wrong during observing the source, handle the error.
|
|
|
|
try {
|
|
cleanup = source(sink);
|
|
} catch (error) {
|
|
sink.error(error, true
|
|
/* isUncaughtThrownError */
|
|
);
|
|
}
|
|
|
|
if (process.env.NODE_ENV !== "production") {
|
|
// Early runtime errors for ill-formed returned cleanup.
|
|
if (cleanup !== undefined && typeof cleanup !== 'function' && (!cleanup || typeof cleanup.unsubscribe !== 'function')) {
|
|
throw new Error('Returned cleanup function which cannot be called: ' + String(cleanup));
|
|
}
|
|
} // If closed before the source function existed, cleanup now.
|
|
|
|
|
|
if (closed) {
|
|
doCleanup();
|
|
}
|
|
|
|
return subscription;
|
|
}
|
|
|
|
function swallowError(_error, _isUncaughtThrownError) {// do nothing.
|
|
}
|
|
|
|
if (process.env.NODE_ENV !== "production") {
|
|
// Default implementation of HostReportErrors() in development builds.
|
|
// Can be replaced by the host application environment.
|
|
RelayObservable.onUnhandledError(function (error, isUncaughtThrownError) {
|
|
if (typeof fail === 'function') {
|
|
// In test environments (Jest), fail() immediately fails the current test.
|
|
fail(String(error));
|
|
} else if (isUncaughtThrownError) {
|
|
// Rethrow uncaught thrown errors on the next frame to avoid breaking
|
|
// current logic.
|
|
setTimeout(function () {
|
|
throw error;
|
|
});
|
|
} else if (typeof console !== 'undefined') {
|
|
// Otherwise, log the unhandled error for visibility.
|
|
// eslint-disable-next-line no-console
|
|
console.error('RelayObservable: Unhandled Error', error);
|
|
}
|
|
});
|
|
}
|
|
|
|
module.exports = RelayObservable; |