Hi all,
The CesiumJS squad is looking to deprecate readyPromise
and related patterns throughout the API, and we’d like to know if you have any feedback on these changes.
Why get rid of readyPromise
?
The readyPromise
pattern was established in many part of the cesium API, such as 3D Tiles, ImageryProvider, and TerrainProviders, to handle asynchronous operation such as making request to load metadata before all properties of an object can become available. These asynchronous operations are initiated on object creation and aggregated into a promise.
A common place this pops up is with Cesium3DTilesets
– It’s required to await the readyPromise
property before the bounding sphere can be accessed.
tileset.readyPromise
.then(function (tileset) {
viewer.zoomTo(
tileset,
new Cesium.HeadingPitchRange(
0.5,
-0.2,
tileset.boundingSphere.radius * 4.0
)
);
})
.catch(function (error) {
console.log(error);
});
However, this has been a problematic part of the CesiumJS API that does not match modern promise best practices.
- The background asynchronous nature can cause race conditions, especially if they do not properly handle destroyed objects. This has been coming up often in tests which assume these background operation can be ignored, leading to “random” CI failures.
- The existing API can cause confusion around how to handle errors.
- When
readyPromises
are unhandled, in many modern systems, a failed promise can lead to a critical error.
How will the API change?
The plan is to deprecate readyPromise
properties throughout the API starting with release 1.104. As this affects high-touch areas of the API like imagery providers, terrain providers, and 3D Tiles, we’ll remove the old code paths after three releases.
The new implementation moves asynchronous operations out of synchronous constructors and into async factory functions, which perform operations like loading necessary metadata up front, returning a promise that resolves to the created 3D tileset or provider. The created object can then be assumed fully ready, and all properties can be accessed immediately (such as Cesium3DTileset.boundingSphere
). 3D tilesets or providers will still make requests for new tiles as changes to the camera and scene occur and is handled internally, the success or failure of which are exposed via events.
So what does this look like?
A few examples of this are as follows:
3D Tiles
Before
const tileset = scene.primitives.add(
new Cesium.Cesium3DTileset({
url: Cesium.IonResource.fromAssetId(1240402),
})
);
tileset.readyPromise
.then(function (tileset) {
// Wait to reference tileset properties
viewer.zoomTo(
tileset,
new Cesium.HeadingPitchRange(
0.5,
-0.2,
tileset.boundingSphere.radius * 4.0
)
);
})
.catch(function (error) {
// Handle errors
console.log(`There was an error while creating the 3D tileset. ${error}`);
});
After
try {
const tileset = await Cesium3DTileset.fromUrl(Cesium.IonResource.fromAssetId(1240402));
scene.primitives.add(tileset);
// Immediately reference tileset properties
viewer.zoomTo(
tileset,
new Cesium.HeadingPitchRange(
0.5,
-0.2,
tileset.boundingSphere.radius * 4.0
)
);
} catch (error) {
// Handle errors
console.log(`There was an error while creating the 3D tileset. ${error}`);
}
Imagery
Before
const imageryProvider = Cesium.TileMapServiceImageryProvider({
url: Cesium.buildModuleUrl("Assets/Textures/NaturalEarthII")
});
imageryProvider.readyPromise.then(() => {
// Wait to reference provider properties
const tilingScheme = imageryProvider.tilingScheme;
}).catch(error => {
// Handle errors
console.log(`There was an error while creating the imagery layer. ${error}`);
});
After
try {
const imageryProvider = await Cesium.TileMapServiceImageryProvider.fromUrl(
Cesium.buildModuleUrl("Assets/Textures/NaturalEarthII")
);
const imageryLayer = new ImageryLayer(imageryProvider);
scene.imageryLayers.add(imageryLayer);
// Immediately access provider properties
const tilingScheme = imageryProvider.tilingScheme;
} catch (error) {
// Handle error
console.log(`There was an error while creating the imagery layer. ${error}`);
}
Alternatively, you can create an ImageryLayer
from the async factory function and deal with any errors via events. This is convenient for synchronously configuring the Cesium Viewer or CesiumWidget. By default, errors will be caught and logged to the console, but this behavior can also be overridden by subscribing to the errorEvent
.
const baseLayer = Cesium.ImageryLayer.fromProviderAsync(
Cesium.TileMapServiceImageryProvider.fromUrl(
Cesium.buildModuleUrl("Assets/Textures/NaturalEarthII")
)
);
baseLayer.errorEvent.addEventListener(error => {
console.log(`There was an error creating the base imagery layer. ${error}`);
});
const viewer = new Cesium.Viewer("cesiumContainer", {
baseLayer: baseLayer,
baseLayerPicker: false,
});
Terrain
Before
const worldTerrainProvider = Cesium.createWorldTerrain();
worldTerrainProvider.readyPromise.catch(error => {
// Handle error
console.log(`There was an error while creating terrain. ${error}`);
});
After
try {
const worldTerrainProvider = await Cesium.createWorldTerrainAsync();
viewer.terrainProvider = worldTerrainProvider;
} catch (error) {
console.log(`Failed to load terrain. ${error}`);
}
Alternatively, there is now a Terrain
class available to help handle the asynchronous operation and expose them as events. This is convenient for synchronously configuring the Cesium Viewer or CesiumWidget. By default, errors will be caught and logged to the console, but this behavior can also be overridden by subscribing to the errorEvent
.
const viewer = new Cesium.Viewer("cesiumContainer", {
terrain: Cesium.Terrain.fromWorldTerrain(),
});
Please give us your feedback!
As this will be a fairly major change to some of the core CesiumJS APIs, please let us know if you have any questions, concerns, or feedback.
Thanks!