UNPKG

@deck.gl/core

Version:

deck.gl core library

331 lines (285 loc) 11 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import {isAsyncIterable} from '../utils/iterable-utils'; import { COMPONENT_SYMBOL, PROP_TYPES_SYMBOL, ASYNC_ORIGINAL_SYMBOL, ASYNC_RESOLVED_SYMBOL, ASYNC_DEFAULTS_SYMBOL } from './constants'; import type Component from './component'; import {PropType} from './prop-types'; const EMPTY_PROPS = Object.freeze({}); /** Internal state of an async prop */ type AsyncPropState = { /** The prop type definition from component.defaultProps, if exists */ type: PropType | null; /** Supplied prop value (can be url/promise, not visible to the component) */ lastValue: any; /** Resolved prop value (valid data, can be "shown" to the component) */ resolvedValue: any; /** How many loads have been issued */ pendingLoadCount: number; /** Latest resolved load, (earlier loads will be ignored) */ resolvedLoadCount: number; }; export default class ComponentState<ComponentT extends Component> { /** The component that this state instance belongs to. `null` if this state has been finalized. */ component: ComponentT | null; onAsyncPropUpdated: (propName: string, value: any) => void; private asyncProps: Partial<Record<string, AsyncPropState>>; private oldProps: ComponentT['props'] | null; private oldAsyncProps: ComponentT['props'] | null; constructor(component: ComponentT) { this.component = component; this.asyncProps = {}; // Prop values that the layer sees this.onAsyncPropUpdated = () => {}; this.oldProps = null; // Last props before update this.oldAsyncProps = null; // Last props before update, with async values copied. } finalize() { for (const propName in this.asyncProps) { const asyncProp = this.asyncProps[propName]; if (asyncProp && asyncProp.type && asyncProp.type.release) { // Release any resources created by transforms asyncProp.type.release( asyncProp.resolvedValue, asyncProp.type, this.component as Component ); } } this.asyncProps = {}; this.component = null; this.resetOldProps(); } /* Layer-facing props API */ getOldProps(): ComponentT['props'] | typeof EMPTY_PROPS { return this.oldAsyncProps || this.oldProps || EMPTY_PROPS; } resetOldProps() { this.oldAsyncProps = null; this.oldProps = this.component ? this.component.props : null; } // Checks if a prop is overridden hasAsyncProp(propName: string): boolean { return propName in this.asyncProps; } // Returns value of an overriden prop getAsyncProp(propName: string): any { const asyncProp = this.asyncProps[propName]; return asyncProp && asyncProp.resolvedValue; } isAsyncPropLoading(propName?: string): boolean { 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: string, value: any) { 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: ComponentT['props']) { this.component = (props[COMPONENT_SYMBOL] as ComponentT) || this.component; // NOTE: prop param and default values are only support for testing const resolvedValues = props[ASYNC_RESOLVED_SYMBOL] || {}; const originalValues = props[ASYNC_ORIGINAL_SYMBOL] || props; const defaultValues = props[ASYNC_DEFAULTS_SYMBOL] || {}; // TODO - use async props from the layer's prop types for (const propName in resolvedValues) { const value = resolvedValues[propName]; this._createAsyncPropData(propName, defaultValues[propName]); this._updateAsyncProp(propName, value); // Use transformed value resolvedValues[propName] = this.getAsyncProp(propName); } for (const propName in originalValues) { const value = originalValues[propName]; // Makes sure a record exists for this prop this._createAsyncPropData(propName, defaultValues[propName]); this._updateAsyncProp(propName, value); } } /* Placeholder methods for subclassing */ protected _fetch(propName: string, url: string): any { return null; } protected _onResolve(propName: string, value: any) {} // eslint-disable-line @typescript-eslint/no-empty-function protected _onError(propName: string, error: Error) {} // eslint-disable-line @typescript-eslint/no-empty-function // Intercept strings (URLs) and Promises and activates loading and prop rewriting private _updateAsyncProp(propName: string, value: any) { if (!this._didAsyncInputValueChange(propName, value)) { return; } // interpret value string as url and start a new load tracked by a promise if (typeof value === 'string') { value = this._fetch(propName, value); } // interprets promise and track the "loading" if (value instanceof Promise) { this._watchPromise(propName, value); return; } if (isAsyncIterable(value)) { this._resolveAsyncIterable(propName, value); // eslint-disable-line @typescript-eslint/no-floating-promises return; } // else, normal, non-async value. Just store value for now this._setPropValue(propName, value); } // 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. private _freezeAsyncOldProps() { if (!this.oldAsyncProps && this.oldProps) { // 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] }); } } } // Checks if an input value actually changed (to avoid reloading/rewatching promises/urls) private _didAsyncInputValueChange(propName: string, value: any): boolean { // @ts-ignore const asyncProp: AsyncPropState = this.asyncProps[propName]; if (value === asyncProp.resolvedValue || value === asyncProp.lastValue) { return false; } asyncProp.lastValue = value; return true; } // Set normal, non-async value private _setPropValue(propName: string, value: any) { // Save the current value before overwriting so that diffProps can access both this._freezeAsyncOldProps(); const asyncProp = this.asyncProps[propName]; if (asyncProp) { value = this._postProcessValue(asyncProp, value); asyncProp.resolvedValue = value; asyncProp.pendingLoadCount++; asyncProp.resolvedLoadCount = asyncProp.pendingLoadCount; } } // Set a just resolved async value, calling onAsyncPropUpdates if value changes asynchronously private _setAsyncPropValue(propName: string, value: any, loadCount: number) { // 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) { // Save the current value before overwriting so that diffProps can access both 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 private _watchPromise(propName: string, promise: Promise<any>) { const asyncProp = this.asyncProps[propName]; if (asyncProp) { asyncProp.pendingLoadCount++; const loadCount = asyncProp.pendingLoadCount; promise .then(data => { if (!this.component) { // This component state has been finalized return; } data = this._postProcessValue(asyncProp, data); this._setAsyncPropValue(propName, data, loadCount); this._onResolve(propName, data); }) .catch(error => { this._onError(propName, error); }); } } private async _resolveAsyncIterable( propName: string, iterable: AsyncIterable<any> ): Promise<void> { if (propName !== 'data') { // we only support data as async iterable this._setPropValue(propName, iterable); return; } const asyncProp = this.asyncProps[propName]; if (!asyncProp) { return; } asyncProp.pendingLoadCount++; const loadCount = asyncProp.pendingLoadCount; let data: any[] = []; let count = 0; for await (const chunk of iterable) { if (!this.component) { // This component state has been finalized return; } // @ts-expect-error (2339) dataTransform is not decared in base component props const {dataTransform} = this.component.props; if (dataTransform) { data = dataTransform(chunk, data) as any[]; } else { data = data.concat(chunk); } // 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); } this._onResolve(propName, data); } // Give the app a chance to post process the loaded data private _postProcessValue(asyncProp: AsyncPropState, value: any) { const propType = asyncProp.type; if (propType && this.component) { if (propType.release) { propType.release(asyncProp.resolvedValue, propType, this.component); } if (propType.transform) { return propType.transform(value, propType, this.component); } } return value; } // Creating an asyncProp record if needed private _createAsyncPropData(propName: string, defaultValue: any) { const asyncProp = this.asyncProps[propName]; if (!asyncProp) { const propTypes = this.component && this.component.props[PROP_TYPES_SYMBOL]; // assert(defaultValue !== undefined); this.asyncProps[propName] = { type: propTypes && propTypes[propName], lastValue: null, resolvedValue: defaultValue, pendingLoadCount: 0, resolvedLoadCount: 0 }; } } }