Deno 2 and Backwards Compatibility in Dropserver

Deno 2 is here. My project Dropserver, an application platform for your personal web services, uses Deno as its app sandbox.

Deno 2’s arrival forced me to break backwards compatibility in Dropserver, which is something I really don’t like doing. Here’s an explanation of why this happened, how it chafes with my vision for Dropserver, and how I plan to avoid this in the future.

Deno code in Dropserver

Dropserver is written in Go, but it runs apps written in JavaScript (or Typescript) inside Deno. Dropserver injects code in the Deno sandbox to help run the app. This code ships with Dropserver to guarantee compatibility between the Go side and the Deno side.

To make it easier to write apps I publish a dropserver_app library that serves as a bridge between app code and the Dropserver code inside Deno. App devs import this library and call its exported members to interact with the Dropserver system, such as creating routes or fetching the authenticated user, etc…

To sum up Dropserver runs JavaScript in Deno:

  • code that ships with the Dropserver executable
  • code that I publish as a library in deno.land/x/
  • code written by app devs

If the only code was the one that shipped with Dropserver, I would have no problems: I would patch that and ship it and tell people to upgrade. But that’s not the nature of an application platform, is it? The idea is to run other people’s code, so when things change, how do you allow old apps to keep working?

What changed in Deno 2

Several types of changes took place:

  • Typescript version changed
  • Global space changes
  • Deno namespace changes

See Deno’s migration guide for full details.

TypeScript changes

Deno comes with TypeScript built-in, which means your version of TS is tied to your version of Deno. And TS makes breaking changes. In particular, in TS 5.6 errors in catch blocks are type “unknown” and you have to assert them before using them. So I went over my sandbox code and asserted error types. I changed this:

//...
catch(e) {
    // do somethign with error
}

…to this:

//...
catch(e) {
    if( e instanceof Error) {
        // do somethign with error
    }
}

I even had to submit a tiny patch to the deno-sqlite library because of this TypeScript change. Yawn. It’s boring menial labor, but if you don’t do it, TypeScript complains.

Deno changes

Deno removed window from the global space. So all references to window cause an error. This is trivial to fix: change window.foo to globalThis.foo.

They also moved many APIs from the Deno.* namespace to the standard library. In all cases the change to make is trivial, but if you don’t do it, you get a fatal error.

All these changes add up to a situation where I can’t expect Dropserver apps developed under Deno 1 to work in Deno 2 unchanged. And that’s a bummer.

Dropserver Vision

My vision with Dropserver is that an app you start using should continue to run for as long as possible. I would like a 10-year shelf life for apps, even if the developer abandons it. This is hard to pull off for many reasons, but it’s not impossible. I have been writing JavaScript for web browsers for nearly 20 years, and a lot of the code I wrote back then still works, unmodified, today.

While Deno follows web standards which promise stability, they also have their own APIs which means that there are going to be changes. Given Dropserver’s deep dependence on Deno for the app runtime, that’s rough, but I know how I got here.

Software changes

Note that I don’t blame Deno here. They went 4 years on 1.x. That’s pretty calm, especially for a VC funded team. If anything, I find that they are very careful with breaking changes. The changes needed are trivial to implement. By comparison, PHP breaks things every year, and PHP 8 was awful to migrate to (ask me how I know!) Over in Python world, it sounds like Python 2 to 3 was miserable.

The reality is software changes, regardless of where its funding comes from and what guides its decision-making. So the job is to deal with the changes.

Vision violation: Deno 2 is a breaking change for Dropserver

I had initially hoped I could just cruise into Deno 2 with no breaking changes (meaning that no changes would be needed to old apps to run on Deno 2.) After all I make a big deal of all the layers I have in place to allow these kinds of transitions.

Unfortunately the changes to TypeScript mean that even if I were able to patch over all the Deno changes, any TS checking would probably raise errors in existing app code. So there is no point in trying to make this non-breaking.

So does that mean my vision is unrealistic? No. But I need to make some changes to be better prepared for future changes.

How to avoid breaking changes in a breaking changes world

Here are some things I will do to make Dropserver more resilient to changes in Deno: (note that I use Deno 1 and 2 to illustrate the ideas, it’s obviously too late to do these things for v2!)

No TypeScript checking

Right now deno runs with --check=all when installing an app. The idea is to ensure that the types in use are compatible between the sandbox code (shipped with the ds executable) and the app code (written externally.) If TS raises errors, there is a good chance something went wrong somewhere, and that things will likely error out unexpectedly.

On the flip side, TypeScript is prickly, never really stable and error-prone. It’s possible to get TS errors on perfectly OK code (see the catch blocks above).

So TS checking will be turned off. If an app misbehaves or errors out, maybe the user will be able to enable it on a one-off basis to gather errors to send to the app developer (or to me 😬).

Monkey patch old Deno features

Since Dropserver’s sandbox code runs first, before the app code, I have some freedom to monkey patch the environment that the app code will see.

I could have re-added the window global to the sandbox, and patched the Deno namespace to implement the removed features. But this can cause problems: these were removed for a reason, and forcing them to stay could cause conflicts in the other direction.

So the correct thing to do is to monkey-patch, but only if the app is designed for Deno 1. The problem is I don’t have any way to know if an app expects to run in Deno 1 or Deno 2.

Add Deno version to app manifest

I’m unsure the exact way to approach this, but the app’s manifest should probably give an indication of what runtime it definitely works with. Maybe the app packaging code could inject "deno-version":"1.46.3" to the dropapp.json to represent the version of deno in use at packaging time. If that’s what’s on the system, then that’s what the app code works with, right? Then ds-host can read that and determine what features it needs to monkey-patch.

Or perhaps I need to have automated testing of the app as part of ds-dev, and have a field called tested-with in the manifest.

Let Dropserver control the Deno version

Ultimately, I think Dropserver will download its own copy of Deno and use that to run the sandbox. There are several advantages:

  • Simpler installation of ds-host, no need to also install Deno
  • Safer. DS could check for new versions and always run the latest release

It could also mean having multiple versions installed. In the current case, we’d have 1.46.3, and 2.0.0. And if the app says it was built or compatible with 1.x, then use that.

Then, when 1.x is no longer supported, Dropserver would fall back to monkey-patching Deno 2 so that it works like Deno 1 for the apps that need it.

Back to today’s reality

I shipped updates to Leftovers and ShoppingList. The user-facing changelog says this:

This release updates the code to support future software releases,
in particular it supports "Deno" version 2.

🤢 🙈

Olivier Forget

Los Angeles, USA
RSS Email Mastodon

Aerospace Engineer turned sofware developer and bootstrappin' entrepreneur.