sveltejs / kit

web development, streamlined
https://svelte.dev/docs/kit
MIT License
18.75k stars 1.95k forks source link

Cloudflare Worker Adapter - Durable Objects #1712

Open DrewRidley opened 3 years ago

DrewRidley commented 3 years ago

Is your feature request related to a problem? Please describe. Cloudflare recently introduced the ability to maintain websocket connections with workers, in addition to a stateful storage solution called 'Durable Objects'. For workers, these objects are accessible through a env argument passed in during the fetch invocation. More information about durable objects and their utilization can be found here. https://developers.cloudflare.com/workers/learning/using-durable-objects

Describe the solution you'd like In my opinion, the cloudflare worker adapter should expose a way to mutate and interact with this env object passed in by cloudflare during a function invocation.

Describe alternatives you've considered I have not found any alternate ways of using durable objects without modifying the adapter.

How important is this feature to you? This feature is fairly important to me personally, since it would dramatically simplify my stack if my state coordination can be done in the same worker.

DrewRidley commented 3 years ago

I can look into this feature in the coming days to see how feasible it would be to introduce it as a PR, however, if the issue requires more significant changes to the adapter I might want to wait and consult some others to learn more about the impact this feature request might have.

DrewRidley commented 3 years ago

I have done some more research and found a few areas the adapter can be improved. Namely, the documentation needs a significant upgrade. The documentation is confusing for new users and does not explain what the toml file should look like. Secondly, the adapter does not currently use kv-asset-handler to its full potential and does not set any browser cache headers on the static files served. At the very least, the adapter needs to include some way for users to be able to specify their cache preferences for static files. In light of all of these changes, I have determined that its best to refactor the module to output code in the esm format, rather than the node format previously used. This will have numerous advantages, but the primary motivation for implementation was that it is ESM modules are a prerequisite to using Durable Objects.

Unfortunately, I am struggling with getting ESBuild to bundle the dependencies of a sample project in when using format: 'esm'. format: node works perfectly, but when changing the format, the dependency resolution strategy is somehow altered.

Any help or clarification would be much appreciated!

DrewRidley commented 3 years ago

As a short update;

I managed to get the module syntax fully compiled. I am just waiting for cloudflare to merge my PR of kv-asset-handler which introduces support for the module syntax there. The adapter also has some new configuration options so my PR will include some changes to the documentation.

Most notably however, my PR will introduce a breaking change. This will likely need to be discussed further with a larger group of the community. The reason I would like to introduce the breaking change is that eventually cloudflare plans to deprecate the old syntax, and force usage of their new modular syntax. For this reason, I think it makes sense for the modular syntax to be the default for all workers created using this adapter.

Luckily, the breaking change in question can easily be mitigated with a clear and concise message informing the users of how to adapt their project to compile, and it only requires a minor two line change to their wrangler.toml. In all, I think this breaking change will serve to better the adapters position in providing users with another deployment option for their sveltekit sites.

DrewRidley commented 3 years ago

I have the code ready for a PR, I just need to wait until this PR is merged first. https://github.com/cloudflare/kv-asset-handler/pull/200 So I will be following this PR closely and will submit mine here on sveltekit once it has been merged and pushed to NPM

lukeed commented 3 years ago

So, few things going on here:

(@DrewRidley and I also spoke in Discord a little bit, so this won't be anything new)

Durable Object (DO) development is heavily tied to the ESM format for Workers. So this means that while DOs are in beta (they still are), the ESM format is in beta too, by extension.

The adapter should 100% be outputting the ESM variant code only – so that the developer has the freedom to opt into DOs if/when they see fit. However, SvelteKit shouldn't be doing this until DO/ESM development is out of beta. Personally, I have no idea if there are other breaking changes on the docket for either of these, but DO is still under "beta" label intentionally, reserving that right for "last-second" changes.

The kv-asset-handler package will surely be updated once ESM/DOs are out of beta – maybe before (I don't know). I'm sure DO going into GA will be the signal that a lot of projects were waiting for before making compatibility changes, my personal projects included.


@DrewRidley Changing the platform option for esbuild changes its resolution paths; see here. I use none & control the resolution via hardcoded mainFields and conditions values.

drozycki commented 2 years ago

@lukeed @DrewRidley Durable Objects are now GA. Do you have the PR?

DrewRidley commented 2 years ago

Sorry for the delayed response! It looks like the best way to support durable objects is to have a folder of durables that is detected and injected by the adapter. I can have a PR done within the next few days. As for websockets it's probably best that we transparently pass them through in the adapter, and let the user implement them using hooks.

tdreyno commented 2 years ago

Any update on this @DrewRidley?

Rich-Harris commented 2 years ago

adapter-cloudflare-workers is now ESM-based, and provides access to env and context (via event.platform). Aside from inconsistency between dev and build, which is covered by #3535 and #4292, is there anything more to do here?

tdreyno commented 2 years ago

I'm interest in this part. A home for the DO classes and getting them compiled correctly. Seemingly need to be concatenated?

It looks like the best way to support durable objects is to have a folder of durables that is detected and injected by the adapter.

tylerbrostrom commented 2 years ago

EDIT: The below DOES NOT work, as is.

Problem

Durable Object classes have to be defined as named exports on the main module (i.e. the Worker module). Users don’t have a way to edit the main module provided by the adapter (short of creating a custom adapter).

With Wrangler 2, this is no longer an issue.

Solution

Unlike Wrangler 1, Wrangler 2 can resolve es modules. Simply re-export default from the output main module like so…

// @file ./custom-worker-entry.js

export { default } from "./.cloudflare/worker.js";

export class MyDurableObject {
  // …
}

…and update wrangler.toml.

- main = "./.cloudflare/worker.js"
+ main = "./custom-worker-entry.js"
  site.bucket = "./.cloudflare/public"

+ [durable_objects]
+   bindings = [{
+     name = "MY_DURABLE_OBJECT",
+     class_name = "MyDurableObject",
+   }]
Rich-Harris commented 2 years ago

If you change main to ./custom-worker-entry.js, then ./.cloudflare/worker.js will no longer be created, so I don't think this will work?

export { default } from "./.cloudflare/worker.js";

It seems like we would probably need a way to tell the adapter where our DOs are defined, so that it can then inject something like

export * from '../src/my-durable-objects.js';`

(which I assume works? will confess only passing familiarity with DOs)

lukeed commented 2 years ago

The main file needs to export the DO class definitions directly, and the names of the classes must align with bindings. class_name defined in the TOML config (user responsibility)

Rich-Harris commented 2 years ago

Gotcha. So we need to be able to specify an entry point that I guess just gets concatenated to main? (Though we'll also need to think about how this would work in dev mode post-#3535/#4292)

tylerbrostrom commented 2 years ago

Doh… didn’t test my assumption. Sorry, folks.

However, I think enabling this pattern—importing a built module into a “facade” module—makes a lot of sense.

Perhaps the esbuild output ought to be a fixed path (i.e. instead of the path defined in wrangler.toml#main.

wrangler.toml#main should instead point to a “facade” module (provided by the adapter) that re-exports the built module, per my previous comment.

This would enable the Durable Objects use-case, as well as wrangler init and create-cloudflare workflows.

I can take a crack at a PR, pending a maintainer’s approval of the described implementation. Gimme a 👍 if so.

lukeed commented 2 years ago

I'd suggest having the adapter(s) expect a special bindings.[tj]s root-level file, and if present, it's injected into the final build via kit.vite.esbuild.inject options (whatever the key path is). Something like:

// src/bindings.ts
// or
// bindings.ts
export { Counter } from './other/counter.ts';

so that the final Worker output is something like:

// path/to/build/worker.mjs
class C$1 { 
  // user source from other/counter.ts
}
export { C$1 as Counter };

const worker$1 = { 
  // generated (current) worker code
}

export default worker$1;
mglikesbikes commented 2 years ago

What about the 1mb script limit tho? Seems like what we really need is guidance from Cloudflare on how this should work locally with wrangler2 and and Service Bindings (which is surely their preferred way forward, though that’s a guess on my part). Basically, all these options to me look like a surefire way to blow past the 1mb limit in a project of sufficient size, or any project given sufficient time + features. Maybe that’s not such a real concern but still. Since svelte is compiling everything down to a single worker script that’s going to bite someone at some point.

note: I don’t have any insight myself, and am actively working with my account manager at Cloudflare to get guidance; the “Cloudflare way” doesn’t seem well-articulated yet. (Would love to be wrong about that tho.)

lukeed commented 2 years ago

Sorry but this concern is irrelevant because it's also applicable to every Worker authored. The point of SK here is to automate/produce a Worker file without the user having to write actual the Worker code from scratch. All Workers users – regardless of framework choice, if at all – will still be deploying the same end-results abiding by the same end-user limits.

Every account exec can have the script size limits raise from 1MB, if necessary, although I strongly suspect the majority of SK apps won't need to do this.

If, for whatever reason, you are running into the limit and your limit(s) can't be raised, then you should use a service binding, where Worker1 (the SK app) is bound to Worker2 (the Worker + Durable Objects) and requests can be passed off safely. Alternatively, you may skip the bindings & use Custom Domains for Workers to send fetch() requests to Worker2, which you still author & deploy separately.


Edit: Custom Domains for Workers would be my suggested & preferred route, where api.foo.com is the endpoint that I author & deploy separately and my SK app (powering foo.com) talks to it if/when necessary.

mglikesbikes commented 2 years ago

I brought up the 1mb limit as a discussion point, mostly — bundling every script and 3rd-party library together into a single worker file seems philosophically opposed to where Cloudflare is headed, as evidenced by your comment showing two similar but slightly-different approaches they've released. The trade-offs are nuanced, and are totally a broader point than "how do you bind a DO to SK with wrangler?"

I was speaking up as an advocate to make this decision thoughtfully, on behalf of DX, and with architectural flexibility in mind.

Edit: Cloudflare published a blog post today that backs up with Luke's saying and points to a comment of his in February explaining the division of labor between Svelte, SvelteKit, and something like worktop or workers itself. Hope it helps someone else have an a-ha moment.

kalepail commented 2 years ago

Are there any working solutions to this problem "in the wild" yet? Just ran into it today myself and it seems like such a simple problem, just export the DO classes along with the main handler, yet I have yet to see a working solution.

denizkenan commented 2 years ago

@tyvdh , you can deploy DOs seperately and bind to svelte-kit app. This remedies the problem a bit.

kalepail commented 2 years ago

Yeah you can and I have but that places part of the code external to the main repo and breaks a unified deployment which is a bummer. It also introduces an extra complexity around development which I'm actually more interested in, running the entire development experience under a single repo and pnpm start command.

Which Miniflare has a concept for mounting external workers so there may still be something here I'm missing around development unification while having production separation.

This would actually potentially be a preferred method as this seems to be the way Cloudflare is heading anyway. Many individual functions / services tied together via bindings during deployment. It's really just a question of maintaining a good development experience and a scaleable production environment.

kalepail commented 2 years ago

fwiw I'm currently maintaining a merge file which is responsible for bundling the durable object classes with the adapter's worker output.

Here's what mine looks like:

import fs from 'fs'
import path from 'path'

const workerFile = path.join(process.cwd(), '/worker-site/worker.mjs')
const workerCode = fs.readFileSync(workerFile).toString('utf8')

const doFile = path.join(process.cwd(), '/src/helpers/do.js')
const doCode = fs.readFileSync(doFile).toString('utf8')

fs.writeFileSync(workerFile, `
  ${workerCode}
  ${doCode}
`)

I then call vite build && node merge.js to build my project. Works fine. If you had imports in the do.js file (I don't) you'd need to bundle that file with esbuild or rollup first vs just importing it as a raw string.

kalepail commented 2 years ago

I've got a fully functioning repo here: https://github.com/tyvdh/starlite-sveltekit

Please note it's not well documented in the README but the main files are:

It's a tricky problem between the dev and production environments and trying not to duplicate configurations but I'm pretty happy with this while we wait for something simpler. Ultimately I think the answer likely lies somewhere between marrying the vite server and miniflare for development and allowing websocket endpoints to pass through the cloudflare worker adapter on production.

This works though and it actually works fine for both the cloudflare worker and pages adapters. For pages you'll just have to deploy the durable objects separately and then link them manually in the dashboard as there isn't currently any way to both deploy and link when uploading a page project with durable object bindings. You could easily configure an empty worker with the durable objects to deploy as part of some separate deployment command from the git commit process that pages use for deployments. I do see eventually pages getting some sort of a configuration file though where you can both attach and deploy bindings during the automated build process.

rohanbam commented 1 year ago

Hello guys, I was wondering if the cloudflare adapter supports this yet. If not, what are the recommended ways to define durable objects within a sveltekit project and have it deploy along with the built worker script?

Klowner commented 1 year ago

Until a robust solution is implemented, I think I've managed to find a way to shim in my durable objects alongside the worker script without too much fuss using @sveltejs/adapter-cloudflare-workers. I'm using wrangler3 which includes basic module support.

/wrangler.toml (the adapter uses these as output paths)

main = "./.cloudflare/worker.js"
site.bucket = "./.cloudflare/public"

/worker.js

export { default } from './.cloudflare/worker.js';
export { MyDurableObject } from './src/mydo.ts';

Then vite build to generate the worker and public assets, followed by wrangler deploy worker.js to override the main defined in the wrangler.toml and instead use the worker.js wrapper.

Hopefully this saves someone else a few hours of cussing :wink:

205g0 commented 11 months ago

@Klowner but do you still have Cloudflare's CI/CD on git push? So to clarify, you are not using @sveltejs/adapter-cloudflare at all? But still using SvelteKit? Moreover, how would you get TS/typings right when using the DO class? Edit: I guesss importing the type and declaring it to platform.env.YOUR_DO, right?

Klowner commented 11 months ago

@205g0 correct, I'm only using @sveltejs/adapter-cloudflare-workers, no @sveltejs/adapter-cloudflare. For CI/CD I'm just using the Github Action.

In my individual DO files I include:

type Bindings = Required<App.Platform>['env']

export class MyDurableObject implements DurableObject {
   constructor (readonly state: DurableObjectState, readonly env: Bindings) { }
   // ...
}
aslakhellesoy commented 8 months ago

I managed to get this working with cloudflare-adapter and a shell script:

In ./scripts/include-durable-objects.sh:

#!/usr/bin/env bash
#
# This script replaces the default _worker.js file with a new one that includes
# durable objects exported from src/lib/durable-objects/index.ts

dir=.svelte-kit/cloudflare

./node_modules/.bin/esbuild src/lib/durable-objects/index.ts \
  --bundle \
  --format=esm \
  --sourcemap \
  --target=esnext \
  --outfile=$dir/_durable-objects.js \

mv $dir/_worker.js     $dir/_sveltekit_worker.js
mv $dir/_worker.js.map $dir/_sveltekit_worker.js.map

echo "\
export { default } from './_sveltekit_worker.js';
export * from './_durable-objects.js';
" > $dir/_worker.js

In ./src/lib/durable-objects/index.ts:

export { MyDurable } from './MyDurable';
// Add more here

In package.json:

{
  "scripts": {
    "dev": "wrangler pages dev .svelte-kit/cloudflare",
    "build": "vite build && ./scripts/include-durable-objects.sh"
  }
}

in wrangler.toml:

build.command = "npm run build"

[durable_objects]
bindings = [
  { name = "mydurable", class_name = "MyDurable },
]

[[migrations]]
tag = "v1"
new_classes = ["MyDurable"]

It works with cloudflare-adapter-workers too, but you have to change the dir variable and use worker.js instead of _worker.js.

sundaycrafts commented 8 months ago

I've written the simple solution (or workaround) here. I hope it will be helpful to you.

https://github.com/cloudflare/cloudflare-docs/issues/13062#issuecomment-1986928294

jameswoodley commented 1 month ago

Sorry if this is a necro, but are we saying to use DO in SvelteKit we need to deploy the worker separately and then bind through SvelteKit? There's currently no "native" way to do this within SvelteKit as such?

sundaycrafts commented 4 weeks ago

@jameswoodley Yes for now, unfortunally.