cloudflare / workerd

The JavaScript / Wasm runtime that powers Cloudflare Workers
https://blog.cloudflare.com/workerd-open-source-workers-runtime/
Apache License 2.0
6.17k stars 294 forks source link

Support class exports with constructors for stateless workers #121

Open ObsidianMinor opened 1 year ago

ObsidianMinor commented 1 year ago

Durable Objects let me do this

export class Worker {
  constructor(state, env) {
    // hell yea constructors
    this.state = state
    this.env = env
  }

  async fetch(req) {
    return new Response(`Hello world`)
  }
}

and that's cool. It's especially helpful with SubtleCrypto since a lot of the API uses promises for some reason. I can then call SubtleCrypto APIs once on Worker construction to make keys and store them in the class using a call to state.blockConcurrencyWhile.

this.state.blockConcurrencyWhile((async () {
  this.key = crypto.subtle.importKey(
    'raw',
    this.env.keyData,
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['verify']
  );
})());

But what if stateless workers could do this?

export default class {
  constructor(ctx, env) {
    // hell yea constructors
    this.ctx = ctx
    this.env = env
    this.ctx.blockConcurrencyWhile((async () {
      this.key = crypto.subtle.importKey(
        'raw',
        this.env.keyData,
        { name: 'HMAC', hash: 'SHA-256' },
        false,
        ['verify']
      );
    })());
  }

  async fetch(req) {
    return new Response(`Hello world`)
  }
}

This way users can do things like SubtleCrypto key imports once instead of every request in a way that makes more obvious sense (the fact that the env object is reused and your modifications persist across requests is not obvious or pure).

kentonv commented 1 year ago

I think an early version of the modules proposal actually looked like this. I'm definitely sympathetic to the idea.

The problem is that while it's a nice fit for the model we have today, it's not really a fit for the model we want to have. Namely, we eventually want to get to a place where every event appears to run in a fresh state, as if a new isolate were created just for it. In that case, would the constructor have to re-run for every request? If so, then we've lost the advantage of this syntax.

Though, it seems likely that any isolate-per-request mechanism will be implemented by having each request start from a snapshot, so that we only have to evaluate the worker's global scope once. In that case, you could argue we should just run the constructor pre-snapshot. However, this has some weirdness. Currently, we do not allow async I/O in the global scope, meaning that global scope evaluation is completely deterministic and side-effect-free. That implies that anyone external to the system cannot tell whether snapshots are being used or not -- starting from a snapshot looks identical to starting from scratch. However, if the constructor can do I/O pre-snapshot, now the snapshotting mechanism is observable. This can lead to a lot of confusion and possibly broken cryptography. For instance, you might generate a secret key in the constructor that requires a nonce, and use a simple counter for the nonce, incremented on every use. However, the snapshotting mechanism would have the effect of resetting the nonce on every request, without resetting the key, breaking your cryptography!

So I'm nervous about introducing this kind of syntax now. Perhaps when we have a better idea of exactly how our single-use-isolates mechanism actually works, we can reconsider if there's a way to work constructors into it.

Meanwhile, we probably need to design APIs that can provide an alternative to global variable caching once single-use-isolates make global variables no longer usable. We need a caching mechanism that doesn't store the value in the isolate. (As you know, we have an internal prototype of this.) This approach has the potential to be better in the long run anyway, since the cache can potentially be shared across multiple worker instances.

jed commented 1 year ago

@ObsidianMinor, FWIW you don't need to instantiate crypto keys yourself; workerd supports them as first-class resources.