CesiumJS is switching from when.js to native promises-- Which will be a breaking change in 1.92

Hi all!

In the effort to modernize the CesiumJS code base, we’re finally planning on removing our outdated copy of when.js and moving to native promises. :tada:

This will need to be a breaking change. As of release version 1.91, you will need to do the following to ensure your code is working properly.

  • Remove any references to Cesium.when in your codebase, replacing them with native Promises, (eg. Promise.resolve)
  • when.defer should be replaced by creating a new Promise and passing in a callback to the constructor.
  • when.join should be replaced by Promise.all
  • In promise chains which originate from the Cesium API, switch to using the Promise API
    • otherwise changes to catch
    • always changes to finally
    • If you’re using async/await you can continue to do so without changes, as they will work the same with native promises.

Since Cesium is currently using an older version of when.js, technically there will be a change in when promises execute. While before resolved promises would run immediately, native promises will not run synchronously and will instead run at the end of a frame once resolved. However this implementation detail should not have performance implications within Cesium and is unlikely to affect your apps.

Please leave a reply here if you have any questions, tips, or tricks around this change.

Thanks!
Gabby

7 Likes

As a vaguely related side question: Is there any particular reason why Cesium isn’t using semantic versioning? Obviously the last few years have shown that Cesium can get along fine without semver :slight_smile: But it is kinda weird as a user to read a phrase like “next minor release will have breaking changes”.

I know that Cesium has had a lot of breaking changes over the years, and under semver that would have resulted in lots (dozens?) of required major version bumps.

TypeScript would be another example of a large project that doesn’t use semver - every TS release can have breaking changes.

1 Like

Hi Mark,

While we haven’t committed one way or the other yet, semver is under consideration for the near future. It’s my understanding that historically we chose not to use it for exactly the reason you said-- Since we had breaking changes in most of our releases, it would have resulted in many major version bumps. However, the project may be at a point where we have fewer breaking changes and semver will help make upgrading clearer.

Hi all,

A quick update on this-- Since this is a fairly substantial breaking change, we’ve decided on delaying the change until the next release, 1.92. We know not everyone follows the community forum, so we’ll add a bullet to CHANGES.md to warn of the change in the subsequent release.

Thanks!

1 Like

For our own codebases using CesiumJS, we’ve been able to ban modules that import when using the following eslint rule:

    "no-restricted-imports": ["error", {
        "paths": [{
          "name": "@cesiumgs/cesium-analytics",
          "importNames": ["when"],
          "message": "Please use Promise."
        }]
    }]

Hi Gabby,
Will this change affect us if we’re not using Cesium.when directly in our code?

Hi Mark,

It will affect you if you are handling any promises returned by the Cesium API, as they will have been when-style promises and will now be native-style promises. These are common for handling errors when loading data sources and 3D Tilesets. To know if you are, you can search for .otherwise or .always and replace them with .catch and .finally as described in the top comment.

Hi Gabby,

I’m just curious, I remember discussing this with Matt in an issue several years ago, and at the time he claimed that library promises significantly outperformed native in a few key scenarios – he had benchmarked an all-native build, and performance was measurably worse. Have the facts on the ground changed? Have modern browsers fixed whatever implementation detail made them worse for your use-case? (I’m thrilled to get rid of the nonstandard library, just from a developer ergonomics perspective, but it is an about-face from the last time the idea was raised.)

ETA: this is the issue in question.

Hi James,

Matt did another round of performance in his initial PR in 2020. With native promises, Cesium is running at the same level of performance. I’m honestly not sure what changed in the implementation between a few years ago and 2020, but we’re still seeing similar performance in the latest PR.

1 Like

Awesome, sounds not like a minor version release?

Hi ks_sc, historically cesiumjs has chosen not to use semver and not bump the major version due to frequent breaking changes, especially early in development. However, we are currently considering moving to semver and to bump major version in cases like this. We’re in the process of confirming this decision since it will likely have impacts among government users. So we’re thinking about it, but I can’t promise a major version increment for this next release.

2 Likes

Hi Gabby, Thanks for the info. I cannot find any occurrence of .always or .otherwise in our app (I’m not the original developer of it), so I assume we will be ok? Anyway I recently switched to loading Cesium in our app from cdn, so I’ll be able to do a quick compatibility check by updating the urls.

Awesome! I’d expect you shouldn’t run into any issues then. But feel free to reach out if anything comes up.

To work with MSL values in Cesium we’re working with EarthGravityModel1996.js and the WW15MGH.DAC file. There is a function ( getHeightData() ) that we’re trying to convert to use a native Promise. Could you help walk us through this? Here’s the function as it is now:

function getHeightData(model) {
    if (!Cesium.defined(model.data)) {
        model.data = Cesium.Resource.fetchArrayBuffer({"url":EGM96GridFileUrl});
    }

    return Cesium.when(model.data, function(data) {
        if (!(model.data instanceof Int16Array)) {
            
            // Data file is big-endian, all relevant platforms are little endian, so swap the byte order.
            var byteView = new Uint8Array(data);
            for (var k = 0; k < byteView.length; k += 2) {
                var tmp = byteView[k];
                byteView[k] = byteView[k + 1];
                byteView[k + 1] = tmp;
            }
            
            try {
                model.data = new Int16Array(data);
            }
            catch (ex) {
                console.log("getHeightData Error: " + ex.toString());
            }

        }
        
        return model.data;
    });
}

Hi Rob,

This is how I’d rewrite the function.

function getHeightData(model) {
    if (!Cesium.defined(model.data)) {
        // Return a promise which resolves to the new model data
        return Cesium.Resource.fetchArrayBuffer({"url":EGM96GridFileUrl}).then(function (data) {
          if (!(data instanceof Int16Array)) {
           
            // Data file is big-endian, all relevant platforms are little endian, so swap the byte order.
            var byteView = new Uint8Array(data);
            for (var k = 0; k < byteView.length; k += 2) {
                var tmp = byteView[k];
                byteView[k] = byteView[k + 1];
                byteView[k + 1] = tmp;
            }
            
            try {
                model.data = new Int16Array(data);
            }
            catch (ex) {
                console.log("getHeightData Error: " + ex.toString());
            }
         } else {
           model.data = data;
         }
         
         return model.data;
        });
    }

  // Otherwise return a promise that immediately resolves to the existing data
  return Promise.resolve(model.data);
}

Thank you, Gabby!

Hey Gabby,

The getHeightData function (in above post) is called many times. The way it was previously written only downloads the file the first time the function is called. But the new approach with native promise allows the file to be downloaded many times (potentially hundreds) because model.data remains undefined until the promise resolves.

So far I haven’t come up with a clean way to write the code to prevent this from happening. How would you suggest changing this so that the file is only downloaded once?

Hi Rob,

From your previous example, it looks like its OK to set model.data to a promise or array. In my above example, I have treated model.data as an array. We can rewrite the function so that model.data is assigned a promise instead.

function getHeightData(model) {
    if (!Cesium.defined(model.data)) {
        // Assign model.data to a promise which resolves to the data
        model.data = Cesium.Resource.fetchArrayBuffer({"url":EGM96GridFileUrl}).then(function (data) {
          if (!(data instanceof Int16Array)) {
           
            // Data file is big-endian, all relevant platforms are little endian, so swap the byte order.
            var byteView = new Uint8Array(data);
            for (var k = 0; k < byteView.length; k += 2) {
                var tmp = byteView[k];
                byteView[k] = byteView[k + 1];
                byteView[k + 1] = tmp;
            }
            
            try {
                return new Int16Array(data);
            }
            catch (ex) {
                console.log("getHeightData Error: " + ex.toString());
            }
         }
         
         return  data;
        });
    }

  // Otherwise return the existing promise
  return model.data;
}

Thank you, Gabby. That seems obvious now :slight_smile: