Skip to content

Instantly share code, notes, and snippets.

@sergiocampama
Last active March 18, 2026 20:47
Show Gist options
  • Select an option

  • Save sergiocampama/77b559523f403cbc700c71f330b3cccc to your computer and use it in GitHub Desktop.

Select an option

Save sergiocampama/77b559523f403cbc700c71f330b3cccc to your computer and use it in GitHub Desktop.
SvelteKit with Queue and Schedule request in Cloudflare Workers
declare global {
namespace App {
...
interface Platform {
batch?: MessageBatch<SomeMessageType>
cron?: string
...
}
}
}
export {}
...
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,
...
)
// 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
}
}
}
// 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
// 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