modules/core/src/lifecycle/component-state.js (178 lines of code) (raw):

// Copyright (c) 2015 - 2017 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import log from '../utils/log'; import {isAsyncIterable} from '../utils/iterable-utils'; import {PROP_SYMBOLS} from './constants'; const {ASYNC_ORIGINAL, ASYNC_RESOLVED, ASYNC_DEFAULTS} = PROP_SYMBOLS; const EMPTY_PROPS = Object.freeze({}); export default class ComponentState { constructor(component = null) { this.component = component; this.asyncProps = {}; // Prop values that the layer sees this.onAsyncPropUpdated = () => {}; this.oldProps = EMPTY_PROPS; // Last props before update this.oldAsyncProps = null; // Last props before update, with async values copied. } getOldProps() { return this.oldAsyncProps || this.oldProps; } resetOldProps() { this.oldAsyncProps = null; this.oldProps = this.component.props; } // Whenever async props are changing, we need to make a copy of oldProps // otherwise the prop rewriting will affect the value both in props and oldProps. // While the copy is relatively expensive, this only happens on load completion. freezeAsyncOldProps() { if (!this.oldAsyncProps) { // Make sure oldProps is set this.oldProps = this.oldProps || this.component.props; // 1. inherit all synchronous props from oldProps // 2. reconfigure the async prop descriptors to fixed values this.oldAsyncProps = Object.create(this.oldProps); for (const propName in this.asyncProps) { Object.defineProperty(this.oldAsyncProps, propName, { enumerable: true, value: this.oldProps[propName] }); } } } // ASYNC PROP HANDLING // // Checks if a prop is overridden hasAsyncProp(propName) { return propName in this.asyncProps; } // Returns value of an overriden prop getAsyncProp(propName) { const asyncProp = this.asyncProps[propName]; return asyncProp && asyncProp.resolvedValue; } isAsyncPropLoading(propName) { if (propName) { const asyncProp = this.asyncProps[propName]; return Boolean( asyncProp && asyncProp.pendingLoadCount > 0 && asyncProp.pendingLoadCount !== asyncProp.resolvedLoadCount ); } for (const key in this.asyncProps) { if (this.isAsyncPropLoading(key)) { return true; } } return false; } // Without changing the original prop value, swap out the data resolution under the hood reloadAsyncProp(propName, value) { this._watchPromise(propName, Promise.resolve(value)); } // Updates all async/overridden props (when new props come in) // Checks if urls have changed, starts loading, or removes override setAsyncProps(props) { // NOTE: prop param and default values are only support for testing const resolvedValues = props[ASYNC_RESOLVED] || {}; const originalValues = props[ASYNC_ORIGINAL] || props; const defaultValues = props[ASYNC_DEFAULTS] || {}; // TODO - use async props from the layer's prop types for (const propName in resolvedValues) { const value = resolvedValues[propName]; this._createAsyncPropData(propName, value, defaultValues[propName]); this._updateAsyncProp(propName, value); } for (const propName in originalValues) { const value = originalValues[propName]; // Makes sure a record exists for this prop this._createAsyncPropData(propName, value, defaultValues[propName]); this._updateAsyncProp(propName, value); } } // Intercept strings (URLs) and Promises and activates loading and prop rewriting _updateAsyncProp(propName, value) { if (!this._didAsyncInputValueChange(propName, value)) { return; } // interpret value string as url and start a new load tracked by a promise if (typeof value === 'string') { const fetch = this.layer && this.layer.props.fetch; const url = value; if (fetch) { value = fetch(url, {propName, layer: this.layer}); } } // interprets promise and track the "loading" if (value instanceof Promise) { this._watchPromise(propName, value); return; } if (isAsyncIterable(value)) { this._resolveAsyncIterable(propName, value); return; } // else, normal, non-async value. Just store value for now this._setPropValue(propName, value); } // Checks if an input value actually changed (to avoid reloading/rewatching promises/urls) _didAsyncInputValueChange(propName, value) { const asyncProp = this.asyncProps[propName]; if (value === asyncProp.lastValue) { return false; } asyncProp.lastValue = value; return true; } // Set normal, non-async value _setPropValue(propName, value) { const asyncProp = this.asyncProps[propName]; asyncProp.value = value; asyncProp.resolvedValue = value; asyncProp.pendingLoadCount++; asyncProp.resolvedLoadCount = asyncProp.pendingLoadCount; } // Set a just resolved async value, calling onAsyncPropUpdates if value changes asynchronously _setAsyncPropValue(propName, value, loadCount) { // Only update if loadCount is larger or equal to resolvedLoadCount // otherwise a more recent load has already completed const asyncProp = this.asyncProps[propName]; if (asyncProp && loadCount >= asyncProp.resolvedLoadCount && value !== undefined) { // A chance to copy old props before updating this.freezeAsyncOldProps(); asyncProp.resolvedValue = value; asyncProp.resolvedLoadCount = loadCount; // Call callback to inform listener this.onAsyncPropUpdated(propName, value); } } // Tracks a promise, sets the prop when loaded, handles load count _watchPromise(propName, promise) { const asyncProp = this.asyncProps[propName]; asyncProp.pendingLoadCount++; const loadCount = asyncProp.pendingLoadCount; promise .then(data => { data = this._postProcessValue(propName, data); this._setAsyncPropValue(propName, data, loadCount); const onDataLoad = this.layer && this.layer.props.onDataLoad; if (propName === 'data' && onDataLoad) { onDataLoad(data, {propName, layer: this.layer}); } }) .catch(error => log.error(error)()); } async _resolveAsyncIterable(propName, iterable) { if (propName !== 'data') { // we only support data as async iterable this._setPropValue(propName, iterable); } const asyncProp = this.asyncProps[propName]; asyncProp.pendingLoadCount++; const loadCount = asyncProp.pendingLoadCount; let data = []; let count = 0; for await (const chunk of iterable) { data = this._postProcessValue(propName, chunk, data); // Used by the default _dataDiff function Object.defineProperty(data, '__diff', { enumerable: false, value: [{startRow: count, endRow: data.length}] }); count = data.length; this._setAsyncPropValue(propName, data, loadCount); } const onDataLoad = this.layer && this.layer.props.onDataLoad; if (onDataLoad) { onDataLoad(data, {propName, layer: this.layer}); } } // Give the app a chance to post process the loaded data _postProcessValue(propName, value, previousValue) { const {dataTransform} = this.component ? this.component.props : {}; if (propName !== 'data') { return value; } if (dataTransform) { return dataTransform(value, previousValue); } // previousValue is assigned if loaded with async iterator return previousValue ? previousValue.concat(value) : value; } // Creating an asyncProp record if needed _createAsyncPropData(propName, value, defaultValue) { const asyncProp = this.asyncProps[propName]; if (!asyncProp) { // assert(defaultValue !== undefined); this.asyncProps[propName] = { lastValue: null, // Supplied prop value (can be url/promise, not visible to layer) resolvedValue: defaultValue, // Resolved prop value (valid data, can be "shown" to layer) pendingLoadCount: 0, // How many loads have been issued resolvedLoadCount: 0 // Latest resolved load, (earlier loads will be ignored) }; } } }