From 2040cd575adcb415f109ff00067bfff3db8d0d5e Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 19 Oct 2024 02:44:59 +0900 Subject: [PATCH] feat: experimental `cloudflare-durable` preset (#2801) --- playground/wrangler.toml | 10 +++ src/presets/_types.gen.ts | 4 +- src/presets/cloudflare/preset.ts | 15 ++++ .../cloudflare/runtime/cloudflare-durable.ts | 83 +++++++++++++++++++ src/presets/cloudflare/types.ts | 13 +++ test/fixture/tsconfig.json | 1 + 6 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 src/presets/cloudflare/runtime/cloudflare-durable.ts diff --git a/playground/wrangler.toml b/playground/wrangler.toml index 90d5480708..c974573ee6 100644 --- a/playground/wrangler.toml +++ b/playground/wrangler.toml @@ -1,3 +1,13 @@ name = "nitro-test" + compatibility_date = "2024-09-19" + assets = { directory = "./.output/public/", binding = "ASSETS" } + +[[durable_objects.bindings]] +name = "$DurableObject" +class_name = "$DurableObject" + +[[migrations]] +tag = "v1" +new_classes = ["$DurableObject"] diff --git a/src/presets/_types.gen.ts b/src/presets/_types.gen.ts index ba3dba7f07..22a02ea47f 100644 --- a/src/presets/_types.gen.ts +++ b/src/presets/_types.gen.ts @@ -16,6 +16,6 @@ export interface PresetOptions { vercel: VercelOptions; } -export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "aws-lambda-streaming" | "azure" | "azure-functions" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-module" | "cloudflare-module-legacy" | "cloudflare-pages" | "cloudflare-pages-static" | "cloudflare-worker" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "edgio" | "firebase" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis" | "iis-handler" | "iis-node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlify-edge" | "netlify-legacy" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-listener" | "node-server" | "platform-sh" | "render-com" | "service-worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zerops" | "zerops-static"; +export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "aws-lambda-streaming" | "azure" | "azure-functions" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-durable" | "cloudflare-module" | "cloudflare-module-legacy" | "cloudflare-pages" | "cloudflare-pages-static" | "cloudflare-worker" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "edgio" | "firebase" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis" | "iis-handler" | "iis-node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlify-edge" | "netlify-legacy" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-listener" | "node-server" | "platform-sh" | "render-com" | "service-worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zerops" | "zerops-static"; -export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "aws-lambda-streaming" | "awsLambdaStreaming" | "aws_lambda_streaming" | "azure" | "azure-functions" | "azureFunctions" | "azure_functions" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-module-legacy" | "cloudflareModuleLegacy" | "cloudflare_module_legacy" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "cloudflare-worker" | "cloudflareWorker" | "cloudflare_worker" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "edgio" | "firebase" | "flight-control" | "flightControl" | "flight_control" | "genezio" | "github-pages" | "githubPages" | "github_pages" | "gitlab-pages" | "gitlabPages" | "gitlab_pages" | "heroku" | "iis" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlifyBuilder" | "netlify_builder" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-legacy" | "netlifyLegacy" | "netlify_legacy" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-listener" | "nodeListener" | "node_listener" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "service-worker" | "serviceWorker" | "service_worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercelEdge" | "vercel_edge" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {}); +export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "aws-lambda-streaming" | "awsLambdaStreaming" | "aws_lambda_streaming" | "azure" | "azure-functions" | "azureFunctions" | "azure_functions" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-durable" | "cloudflareDurable" | "cloudflare_durable" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-module-legacy" | "cloudflareModuleLegacy" | "cloudflare_module_legacy" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "cloudflare-worker" | "cloudflareWorker" | "cloudflare_worker" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "edgio" | "firebase" | "flight-control" | "flightControl" | "flight_control" | "genezio" | "github-pages" | "githubPages" | "github_pages" | "gitlab-pages" | "gitlabPages" | "gitlab_pages" | "heroku" | "iis" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlifyBuilder" | "netlify_builder" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-legacy" | "netlifyLegacy" | "netlify_legacy" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-listener" | "nodeListener" | "node_listener" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "service-worker" | "serviceWorker" | "service_worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercelEdge" | "vercel_edge" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {}); diff --git a/src/presets/cloudflare/preset.ts b/src/presets/cloudflare/preset.ts index aec20020f4..46064a441d 100644 --- a/src/presets/cloudflare/preset.ts +++ b/src/presets/cloudflare/preset.ts @@ -187,10 +187,25 @@ const cloudflareModule = defineNitroPreset( } ); +const cloudflareDurable = defineNitroPreset( + { + extends: "cloudflare-module", + entry: "./runtime/cloudflare-durable", + rollupConfig: { + external: ["cloudflare:workers"], + }, + }, + { + name: "cloudflare-durable" as const, + url: import.meta.url, + } +); + export default [ cloudflare, cloudflareModuleLegacy, cloudflareModule, + cloudflareDurable, cloudflarePages, cloudflarePagesStatic, ]; diff --git a/src/presets/cloudflare/runtime/cloudflare-durable.ts b/src/presets/cloudflare/runtime/cloudflare-durable.ts new file mode 100644 index 0000000000..69566ba49e --- /dev/null +++ b/src/presets/cloudflare/runtime/cloudflare-durable.ts @@ -0,0 +1,83 @@ +import "#nitro-internal-pollyfills"; +import type * as CF from "@cloudflare/workers-types"; +import { DurableObject } from "cloudflare:workers"; +import wsAdapter from "crossws/adapters/cloudflare-durable"; +import { useNitroApp } from "nitropack/runtime"; +import { isPublicAssetURL } from "#nitro-internal-virtual/public-assets"; +import { createHandler } from "./_module-handler"; + +const nitroApp = useNitroApp(); + +const ws = import.meta._websocket + ? wsAdapter(nitroApp.h3App.websocket) + : undefined; + +interface Env { + ASSETS?: { fetch: typeof CF.fetch }; +} + +export default createHandler({ + fetch(request, env, context, url) { + // Static assets fallback (optional binding) + if (env.ASSETS && isPublicAssetURL(url.pathname)) { + return env.ASSETS.fetch(request); + } + + // Websocket upgrade + // https://crossws.unjs.io/adapters/cloudflare#durable-objects + if ( + import.meta._websocket && + request.headers.get("upgrade") === "websocket" + ) { + return ws!.handleUpgrade(request, env, context); + } + }, +}); + +export class $DurableObject extends DurableObject { + constructor(state: DurableObjectState, env: Record) { + super(state, env); + state.waitUntil( + nitroApp.hooks.callHook("cloudflare:durable:init", this, { + state, + env, + }) + ); + if (import.meta._websocket) { + ws!.handleDurableInit(this, state, env); + } + } + + override fetch(request: Request) { + if (import.meta._websocket) { + return ws!.handleDurableUpgrade(this, request); + } + return new Response("404", { status: 404 }); + } + + override alarm(): void | Promise { + this.ctx.waitUntil( + nitroApp.hooks.callHook("cloudflare:durable:alarm", this) + ); + } + + override async webSocketMessage( + client: WebSocket, + message: ArrayBuffer | string + ) { + if (import.meta._websocket) { + return ws!.handleDurableMessage(this, client, message); + } + } + + override async webSocketClose( + client: WebSocket, + code: number, + reason: string, + wasClean: boolean + ) { + if (import.meta._websocket) { + return ws!.handleDurableClose(this, client, code, reason, wasClean); + } + } +} diff --git a/src/presets/cloudflare/types.ts b/src/presets/cloudflare/types.ts index bec2252b4e..4888e2f14c 100644 --- a/src/presets/cloudflare/types.ts +++ b/src/presets/cloudflare/types.ts @@ -5,6 +5,7 @@ import type { ScheduledController, TraceItem, } from "@cloudflare/workers-types"; +import type { DurableObject } from "cloudflare:workers"; import type { Config as WranglerConfig } from "./types.wrangler"; /** @@ -58,6 +59,8 @@ export interface CloudflareOptions { }; } +type DurableObjectState = ConstructorParameters[0]; + declare module "nitropack/types" { export interface NitroRuntimeHooks { // https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/ @@ -93,5 +96,15 @@ declare module "nitropack/types" { env: unknown; context: ExecutionContext; }) => void; + + "cloudflare:durable:init": ( + durable: DurableObject, + _: { + state: DurableObjectState; + env: unknown; + } + ) => void; + + "cloudflare:durable:alarm": (durable: DurableObject) => void; } } diff --git a/test/fixture/tsconfig.json b/test/fixture/tsconfig.json index a54dd30b7c..dab12a8ca5 100644 --- a/test/fixture/tsconfig.json +++ b/test/fixture/tsconfig.json @@ -6,6 +6,7 @@ "skipLibCheck": true, "baseUrl": ".", "strict": false, + "types": ["@cloudflare/workers-types"], // (currently manually synced with top level tsconfig.json paths) "paths": { // CLI