@deck.gl/core
Version:
deck.gl core library
251 lines • 10.5 kB
JavaScript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { isAsyncIterable } from "../utils/iterable-utils.js";
import { COMPONENT_SYMBOL, PROP_TYPES_SYMBOL, ASYNC_ORIGINAL_SYMBOL, ASYNC_RESOLVED_SYMBOL, ASYNC_DEFAULTS_SYMBOL } from "./constants.js";
const EMPTY_PROPS = Object.freeze({});
export default class ComponentState {
constructor(component) {
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);
}
}
this.asyncProps = {};
this.component = null;
this.resetOldProps();
}
/* Layer-facing props API */
getOldProps() {
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) {
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) {
this.component = props[COMPONENT_SYMBOL] || 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 */
_fetch(propName, url) {
return null;
}
_onResolve(propName, value) { } // eslint-disable-line @typescript-eslint/no-empty-function
_onError(propName, error) { } // eslint-disable-line @typescript-eslint/no-empty-function
// 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') {
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.
_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)
_didAsyncInputValueChange(propName, value) {
// @ts-ignore
const asyncProp = this.asyncProps[propName];
if (value === asyncProp.resolvedValue || value === asyncProp.lastValue) {
return false;
}
asyncProp.lastValue = value;
return true;
}
// Set normal, non-async value
_setPropValue(propName, value) {
// 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
_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) {
// 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
_watchPromise(propName, promise) {
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);
});
}
}
async _resolveAsyncIterable(propName, iterable) {
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 = [];
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);
}
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
_postProcessValue(asyncProp, value) {
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
_createAsyncPropData(propName, defaultValue) {
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
};
}
}
}
//# sourceMappingURL=component-state.js.map