sveltejs / kit

web development, streamlined
https://kit.svelte.dev
MIT License
18.45k stars 1.89k forks source link

Handling scheduled events with adapter-cloudflare-workers #4841

Closed evrys closed 2 years ago

evrys commented 2 years ago

Describe the problem

I notice the CF Workers adapter now uses the module worker format for output. That's great! But now I'm wondering, how can I have sveltekit output the default export needed to handle scheduled events on CF? I need it for sending scheduled emails. https://developers.cloudflare.com/workers/runtime-apis/scheduled-event/#syntax-module-worker

Describe the proposed solution

Documentation on how to use the Cloudflare Workers scheduling feature with a SvelteKit project

Alternatives considered

In the version before the adapter used module workers, it was possible to just put this in hooks.ts:

if (typeof addEventListener !== 'undefined') {
  addEventListener('scheduled', (event: ScheduledEvent) => {
    event.waitUntil(...)
  })
}

However this is no longer the syntax for the module format.

Importance

nice to have

Additional Information

No response

Rich-Harris commented 2 years ago

Is there a reason sending emails needs to be the responsibility of your SvelteKit app, rather than a separate dedicated worker?

evrys commented 2 years ago

It uses a bunch of code and environment from the svelte-kit app to send the emails, since it needs to iterate over the users in the database and determine if they're ready to receive a reminder email.

But you're right-- the Cloudflare Discord helped me work out a solution using a second worker for the Cron Trigger part that calls a /heartbeat endpoint on the svelte-kit app through Service Bindings. It's a little janky because Service Bindings were just released the other day, but it works!

Rich-Harris commented 2 years ago

Excellent — will close this as it's not expected that addEventListener('scheduled', ...) will work, and if we wanted it to then we'd need to explicitly design support for it

robertaboukhalil commented 2 years ago

This is a bit hacky, but here's what I did to get a cron working within the same Svelte Kit + Cloudflare Workers adapter app. (mostly because it's really convenient to keep everything as a single worker 🙂)

wrangler.toml

[...]
main = "./.cloudflare/worker.mjs"
[...]

package.json

[...]
    "scripts": {
        [...]
        "postbuild": "cat src/lib/cron.js >> .cloudflare/worker.mjs"
    }
[...]

src/lib/cron.js

entry_default.scheduled = async (event, env, ctx) => {
    ctx.waitUntil(cron(env));
};

// Cron logic
async function cron(env) {
    console.log("Starting cron");
    // ...
}

(of course this only works if cron.js doesn't need to import anything, otherwise it would need its own build step)

colecrouter commented 2 months ago

Here's my revised solution based on @robertaboukhalil's

// $lib/cron.js
/**
 * @param {import("@cloudflare/workers-types").ScheduledEvent} event 
 * @param {Env} env 
 * @param {import('@cloudflare/workers-types').EventContext<Env, "", {}>} ctx 
 */
entry_default.scheduled = async (event, env, ctx) => {
    // ... do stuff here
};
// append.js
import { appendFile, readFile } from 'fs/promises';

const file = await readFile('src/lib/cron.js', 'utf8');
await appendFile('.cloudflare/worker.js', file, 'utf8')
    "build": "vite build && node append.js",

Note that if you're planning on fetch-ing existing endpoints in your app, you don't have SvelteKit's fetch function, so you'll get a 1042 error. You can get around this by adding a service binding that points to itself:

# wrangler.toml
[[services]]
binding = "SELF"
service = "worker_name_here"
entry_default.scheduled = async (event, env, ctx) => {
    await env.SELF.fetch('...');
};