Files
30-seconds-of-code/node_modules/relay-runtime/lib/RelayObservable.js
2019-08-20 15:52:05 +02:00

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;