Last active
March 18, 2026 20:47
-
-
Save sergiocampama/77b559523f403cbc700c71f330b3cccc to your computer and use it in GitHub Desktop.
SvelteKit with Queue and Schedule request in Cloudflare Workers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| declare global { | |
| namespace App { | |
| ... | |
| interface Platform { | |
| batch?: MessageBatch<SomeMessageType> | |
| cron?: string | |
| ... | |
| } | |
| } | |
| } | |
| export {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ... | |
| const scheduleHandle: Handle = async ({ event, resolve }) => { | |
| if (event.request.headers.get("x-sveltekit-cron") !== null && !!(event.platform?.cron)) { | |
| // Process request reading the cron from event.platform.cron | |
| return new Response("ok", { | |
| status: 200 | |
| }) | |
| } else { | |
| return resolve(event) | |
| } | |
| } | |
| const queueHandle: Handle = async ({ event, resolve }) => { | |
| if (event.request.headers.get("x-sveltekit-queue") !== null && !!(event.platform?.batch)) { | |
| // Process request reading the batch messages from event.platform.batch | |
| return new Response("ok", { | |
| status: 200 | |
| }) | |
| } else { | |
| return resolve(event) | |
| } | |
| } | |
| ... | |
| export const handle = sequence( | |
| ... | |
| scheduleHandle, | |
| queueHandle, | |
| ... | |
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // src/lib/adapter/index.ts | |
| import adapter, { type AdapterOptions } from "@sveltejs/adapter-cloudflare"; | |
| import type { Builder } from "@sveltejs/kit"; | |
| import path from "node:path"; | |
| import { fileURLToPath } from "node:url"; | |
| function posixify(str: string) { | |
| return str.replace(/\\/g, '/'); | |
| } | |
| export default function (options: AdapterOptions | undefined = {}) { | |
| const realAdapter = adapter(options) | |
| return { | |
| ...realAdapter, | |
| async adapt(builder: Builder) { | |
| const realAdapterResult = realAdapter.adapt(builder) | |
| const dest = builder.getBuildDirectory("cloudflare") | |
| const worker_dest = `${dest}/_worker.js` | |
| const worker_dest_dir = path.dirname(worker_dest) | |
| const tmp = builder.getBuildDirectory("cloudflare-tmp") | |
| const files = fileURLToPath(new URL(".", import.meta.url).href) | |
| builder.rimraf(worker_dest) | |
| builder.copy(`${files}/worker.js`, worker_dest, { | |
| replace: { | |
| SERVER: `./${posixify(path.relative(worker_dest_dir, builder.getServerDirectory()))}/index.js`, | |
| MANIFEST: `./${posixify(path.relative(worker_dest_dir, tmp))}/manifest.js`, | |
| } | |
| }) | |
| return realAdapterResult | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // import adapter from "@sveltejs/adapter-cloudflare" | |
| import { vitePreprocess } from "@sveltejs/vite-plugin-svelte" | |
| import customAdapter from "./src/lib/adapter/index.ts" | |
| /** @type {import('@sveltejs/kit').Config} */ | |
| const config = { | |
| ... | |
| kit: { | |
| adapter: customAdapter(), | |
| ... | |
| }, | |
| extensions: [".svelte", ".svx"], | |
| } | |
| export default config |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // src/lib/adapter/worker.js | |
| // A copy of the worker.js file in the adapter-cloudflare package, extended. | |
| import { base_path, manifest, prerendered } from 'MANIFEST'; | |
| import { Server } from 'SERVER'; | |
| import { env } from 'cloudflare:workers'; | |
| import * as Cache from 'worktop/cfw.cache'; | |
| const server = new Server(manifest); | |
| const app_path = `/${manifest.appPath}`; | |
| const immutable = `${app_path}/immutable/`; | |
| const version_file = `${app_path}/version.json`; | |
| let origin; | |
| const initialized = server.init({ | |
| env, | |
| read: async (file) => { | |
| const url = `${origin}/${file}`; | |
| const response = await /** @type {{ ASSETS: { fetch: typeof fetch } }} */ (env).ASSETS.fetch( | |
| url | |
| ); | |
| if (!response.ok) { | |
| throw new Error( | |
| `read(...) failed: could not fetch ${url} (${response.status} ${response.statusText})` | |
| ); | |
| } | |
| return response.body; | |
| } | |
| }); | |
| export default { | |
| /** | |
| * @param {Request} req | |
| * @param {{ ASSETS: { fetch: typeof fetch } }} env | |
| * @param {import('@cloudflare/workers-types').ExecutionContext} ctx | |
| * @returns {Promise<Response>} | |
| */ | |
| async fetch(req, env, ctx) { | |
| if (!origin) { | |
| origin = new URL(req.url).origin; | |
| } | |
| // always await initialization to prevent race condition with concurrent requests | |
| await initialized; | |
| // skip cache if "cache-control: no-cache" in request | |
| let pragma = req.headers.get('cache-control') || ''; | |
| let res = !pragma.includes('no-cache') && (await Cache.lookup(req)); | |
| if (res) return res; | |
| let { pathname, search } = new URL(req.url); | |
| try { | |
| pathname = decodeURIComponent(pathname); | |
| } catch { | |
| // ignore invalid URI | |
| } | |
| const stripped_pathname = pathname.replace(/\/$/, ''); | |
| // files in /static, the service worker, and Vite imported server assets | |
| let is_static_asset = false; | |
| const filename = stripped_pathname.slice(base_path.length + 1); | |
| if (filename) { | |
| is_static_asset = | |
| manifest.assets.has(filename) || | |
| manifest.assets.has(filename + '/index.html') || | |
| filename in manifest._.server_assets || | |
| filename + '/index.html' in manifest._.server_assets; | |
| } | |
| let location = pathname.at(-1) === '/' ? stripped_pathname : pathname + '/'; | |
| if ( | |
| is_static_asset || | |
| prerendered.has(pathname) || | |
| pathname === version_file || | |
| pathname.startsWith(immutable) | |
| ) { | |
| res = await env.ASSETS.fetch(req); | |
| } else if (location && prerendered.has(location)) { | |
| // trailing slash redirect for prerendered pages | |
| if (search) location += search; | |
| res = new Response('', { | |
| status: 308, | |
| headers: { | |
| location | |
| } | |
| }); | |
| } else { | |
| // dynamically-generated pages | |
| res = await server.respond(req, { | |
| platform: { | |
| env, | |
| ctx, | |
| context: ctx, // deprecated in favor of ctx | |
| caches, | |
| cf: req.cf | |
| }, | |
| getClientAddress() { | |
| return /** @type {string} */ (req.headers.get('cf-connecting-ip')); | |
| } | |
| }); | |
| } | |
| // write to `Cache` only if response is not an error, | |
| // let `Cache.save` handle the Cache-Control and Vary headers | |
| pragma = res.headers.get('cache-control') || ''; | |
| return pragma && res.status < 400 ? Cache.save(req, res, ctx) : res; | |
| }, | |
| /** | |
| * @param {import('@cloudflare/workers-types').MessageBatch} batch | |
| * @param {any} env | |
| * @param {import('@cloudflare/workers-types').ExecutionContext} ctx | |
| * @returns {Promise<void>} | |
| */ | |
| async queue(batch, env, ctx) { | |
| // Just used the domain where the worker lives, maybe there's a better approach here to make SK not reject it. | |
| const req = new Request("MY_DOMAIN", { | |
| headers: { | |
| "cf-connecting-ip": "0.0.0.0", | |
| "x-sveltekit-queue": batch.queue | |
| } | |
| }) | |
| const res = await server.respond(req, { | |
| platform: { | |
| batch, | |
| env, | |
| ctx, | |
| context: ctx, // deprecated in favor of ctx | |
| caches, | |
| cf: req.cf | |
| }, | |
| getClientAddress() { | |
| return /** @type {string} */ (req.headers.get('cf-connecting-ip')); | |
| } | |
| }) | |
| }, | |
| /** | |
| * @param {import('@cloudflare/workers-types').ScheduledController} controller | |
| * @param {any} env | |
| * @param {import('@cloudflare/workers-types').ExecutionContext} ctx | |
| * @returns {Promise<void>} | |
| */ | |
| async scheduled(controller, env, ctx) { | |
| // Just used the domain where the worker lives, maybe there's a better approach here to make SK not reject it. | |
| const req = new Request("MY_DOMAIN", { | |
| headers: { | |
| "cf-connecting-ip": "0.0.0.0", | |
| "x-sveltekit-cron": controller.cron | |
| } | |
| }) | |
| const res = await server.respond(req, { | |
| platform: { | |
| cron: controller.cron, | |
| env, | |
| ctx, | |
| context: ctx, // deprecated in favor of ctx | |
| caches, | |
| cf: req.cf | |
| }, | |
| getClientAddress() { | |
| return /** @type {string} */ (req.headers.get('cf-connecting-ip')); | |
| } | |
| }) | |
| } | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment