UNPKG

@nozbe/watermelondb

Version:

Build powerful React Native and React web apps that scale from hundreds to tens of thousands of records and remain fast

301 lines (292 loc) 11.3 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); exports.__esModule = true; exports.default = void 0; var _inheritsLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/inheritsLoose")); var _react = require("react"); var _hoistNonReactStatics = _interopRequireDefault(require("hoist-non-react-statics")); var _garbageCollector = _interopRequireDefault(require("./garbageCollector")); /* eslint-disable react/no-direct-mutation-state */ /* eslint-disable react/sort-comp */ function subscribe(value, onNext, onError, onComplete) { var wmelonTag = value && value.constructor && value.constructor._wmelonTag; if ('model' === wmelonTag) { onNext(value); return value.experimentalSubscribe(function (isDeleted) { if (isDeleted) { onComplete(); } else { onNext(value); } }); } else if ('query' === wmelonTag) { return value.experimentalSubscribe(onNext); } else if ('function' === typeof value.observe) { var subscription = value.observe().subscribe(onNext, onError, onComplete); return function () { return subscription.unsubscribe(); }; } else if ('function' === typeof value.subscribe) { var _subscription = value.subscribe(onNext, onError, onComplete); return function () { return _subscription.unsubscribe(); }; } // eslint-disable-next-line no-console console.error("[withObservable] Value passed to withObservables doesn't appear to be observable:", value); throw new Error("[withObservable] Value passed to withObservables doesn't appear to be observable. See console for details"); } function identicalArrays(left, right) { if (left.length !== right.length) { return false; } for (var i = 0, len = left.length; i < len; i += 1) { if (left[i] !== right[i]) { return false; } } return true; } function getTriggeringProps(props, propNames) { if (!propNames) { return []; } return propNames.map(function (name) { return props[name]; }); } var hasOwn = function (obj, key) { // $FlowFixMe return Object.prototype.hasOwnProperty.call(obj, key); }; // TODO: This is probably not going to be 100% safe to use under React async mode // Do more research var WithObservablesComponent = /*#__PURE__*/function (_Component) { function WithObservablesComponent(props, BaseComponent, getObservables, triggerProps) { var _this = _Component.call(this, props) || this; _this._unsubscribe = null; _this._prefetchTimeoutCanceled = false; _this._exitedConstructor = false; _this.BaseComponent = BaseComponent; _this.triggerProps = triggerProps; _this.getObservables = getObservables; _this.state = { isFetching: true, values: {}, error: null, triggeredFromProps: getTriggeringProps(props, triggerProps) }; // The recommended React practice is to subscribe to async sources on `didMount` // Unfortunately, that's slow, because we have an unnecessary empty render even if we // can get first values before render. // // So we're subscribing in constructor, but that's dangerous. We have no guarantee that // the component will actually be mounted (and therefore that `willUnmount` will be called // to safely unsubscribe). So we're setting a safety timeout to avoid leaking memory. // If component is not mounted before timeout, we'll unsubscribe just to be sure. // (If component is mounted after all, just super slow, we'll subscribe again on didMount) _this.subscribeWithoutSettingState(_this.props); (0, _garbageCollector.default)(function () { if (!_this._prefetchTimeoutCanceled) { // eslint-disable-next-line no-console console.warn("[withObservables] Unsubscribing from source. Leaky component!"); _this.unsubscribe(); } }); _this._exitedConstructor = true; return _this; } (0, _inheritsLoose2.default)(WithObservablesComponent, _Component); var _proto = WithObservablesComponent.prototype; _proto.componentDidMount = function () { this.cancelPrefetchTimeout(); if (!this._unsubscribe) { // eslint-disable-next-line no-console console.warn("[withObservables] Component mounted but no subscription present. Slow component (timed out) or a bug! Re-subscribing..."); var newTriggeringProps = getTriggeringProps(this.props, this.triggerProps); this.subscribe(this.props, newTriggeringProps); } } // eslint-disable-next-line ; _proto.UNSAFE_componentWillReceiveProps = function (nextProps) { var { triggeredFromProps: triggeredFromProps } = this.state; var newTriggeringProps = getTriggeringProps(nextProps, this.triggerProps); if (!identicalArrays(triggeredFromProps, newTriggeringProps)) { this.subscribe(nextProps, newTriggeringProps); } }; _proto.subscribe = function (props, triggeredFromProps) { this.setState({ isFetching: true, values: {}, triggeredFromProps: triggeredFromProps }); this.subscribeWithoutSettingState(props); } // NOTE: This is a hand-coded equivalent of Rx combineLatestObject ; _proto.subscribeWithoutSettingState = function (props) { var _this2 = this; this.unsubscribe(); var observablesObject = this.getObservables(props); var subscriptions = []; var isUnsubscribed = false; var unsubscribe = function () { isUnsubscribed = true; subscriptions.forEach(function (_unsubscribe) { return _unsubscribe(); }); subscriptions = []; }; var values = {}; var valueCount = 0; var keys = Object.keys(observablesObject); var keyCount = keys.length; keys.forEach(function (key) { if (isUnsubscribed) { return; } // $FlowFixMe var subscribable = observablesObject[key]; subscriptions.push(subscribe( // $FlowFixMe subscribable, function (value) { // console.log(`new value for ${key}, all keys: ${keys}`) // Check if we have values for all observables; if yes - we can render; otherwise - only set value var isFirstEmission = !hasOwn(values, key); if (isFirstEmission) { valueCount += 1; } values[key] = value; var hasAllValues = valueCount === keyCount; if (hasAllValues && !isUnsubscribed) { // console.log('okay, all values') _this2.withObservablesOnChange(values); } }, function (error) { // Error in one observable should cause all observables to be unsubscribed from - the component is, in effect, broken now unsubscribe(); _this2.withObservablesOnError(error); }, function () { // TODO: Should we do anything on completion? // console.log(`completed for ${key}`) })); }); if ('production' !== process.env.NODE_ENV) { var renderedTriggerProps = this.triggerProps ? this.triggerProps.join(',') : 'null'; var renderedKeys = keys.join(', '); this.constructor.displayName = "withObservables[".concat(renderedTriggerProps, "] { ").concat(renderedKeys, " }"); } this._unsubscribe = unsubscribe; } // DO NOT rename (we want on call stack as debugging help) ; _proto.withObservablesOnChange = function (values) { if (this._exitedConstructor) { this.setState({ values: values, isFetching: false }); } else { // Source has called with first values synchronously while we're still in the // constructor. Here, `this.setState` does not work and we must mutate this.state // directly this.state.values = values; this.state.isFetching = false; } } // DO NOT rename (we want on call stack as debugging help) ; _proto.withObservablesOnError = function (error) { // console.error(`[withObservables] Error in Rx composition`, error) if (this._exitedConstructor) { this.setState({ error: error, isFetching: false }); } else { this.state.error = error; this.state.isFetching = false; } }; _proto.unsubscribe = function () { this._unsubscribe && this._unsubscribe(); this.cancelPrefetchTimeout(); }; _proto.cancelPrefetchTimeout = function () { this._prefetchTimeoutCanceled = true; }; _proto.shouldComponentUpdate = function (nextProps, nextState) { // If one of the triggering props change but we don't yet have first values from the new // observable, *don't* render anything! return !nextState.isFetching; }; _proto.componentWillUnmount = function () { this.unsubscribe(); }; _proto.render = function () { var { isFetching: isFetching, values: values, error: error } = this.state; if (isFetching) { return null; } else if (error) { // rethrow error found in Rx composition as to unify withObservables errors with other React errors // the responsibility for handling errors is on the user (by using an Error Boundary) throw error; } else { return (0, _react.createElement)(this.BaseComponent, Object.assign({}, this.props, values)); } }; return WithObservablesComponent; }(_react.Component); /** * * Injects new props to a component with values from the passed Observables * * Every time one of the `triggerProps` changes, `getObservables()` is called * and the returned Observables are subscribed to. * * Every time one of the Observables emits a new value, the matching inner prop is updated. * * You can return multiple Observables in the function. You can also return arbitrary objects that have * an `observe()` function that returns an Observable. * * The inner component will not render until all supplied Observables return their first values. * If `triggerProps` change, renders will also be paused until the new Observables emit first values. * * If you only want to subscribe to Observables once (the Observables don't depend on outer props), * pass `null` to `triggerProps`. * * Errors are re-thrown in render(). Use React Error Boundary to catch them. * * Example use: * ```js * withObservables(['task'], ({ task }) => ({ * task: task, * comments: task.comments.observe() * })) * ``` */ var withObservables = function (triggerProps, getObservables) { return function (BaseComponent) { var ConcreteWithObservablesComponent = /*#__PURE__*/function (_WithObservablesCompo) { function ConcreteWithObservablesComponent(props) { return _WithObservablesCompo.call(this, props, BaseComponent, getObservables, triggerProps) || this; } (0, _inheritsLoose2.default)(ConcreteWithObservablesComponent, _WithObservablesCompo); return ConcreteWithObservablesComponent; }(WithObservablesComponent); if ('production' !== process.env.NODE_ENV) { var renderedTriggerProps = triggerProps ? triggerProps.join(',') : 'null'; ConcreteWithObservablesComponent.displayName = "withObservables[".concat(renderedTriggerProps, "]"); } return (0, _hoistNonReactStatics.default)(ConcreteWithObservablesComponent, BaseComponent); }; }; var _default = exports.default = withObservables;