diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7bd31d34f..5043420d1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -75,6 +75,8 @@ jobs: project-id: "prj_p0GIEsfl53L7IwVbosPvi9rPSOYW" - name: "express" project-id: "prj_cCZjpBy92VRbKHHbarDMhOHtkuIr" + - name: "fastify" + project-id: "prj_5Yap0VDQ633v998iqQ3L3aQ25Cck" env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} diff --git a/docs/app/(home)/components/frameworks.tsx b/docs/app/(home)/components/frameworks.tsx index 1d21d9755..f24283b6a 100644 --- a/docs/app/(home)/components/frameworks.tsx +++ b/docs/app/(home)/components/frameworks.tsx @@ -1,12 +1,12 @@ -"use client"; +'use client'; -import { track } from "@vercel/analytics"; -import Link from "next/link"; -import type { ComponentProps } from "react"; -import { toast } from "sonner"; -import { Badge } from "@/components/ui/badge"; +import { track } from '@vercel/analytics'; +import Link from 'next/link'; +import type { ComponentProps } from 'react'; +import { toast } from 'sonner'; +import { Badge } from '@/components/ui/badge'; -export const Express = (props: ComponentProps<"svg">) => ( +export const Express = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const AstroDark = (props: ComponentProps<"svg">) => ( +export const Fastify = (props: ComponentProps<'svg'>) => ( + + Fastify + + +); + +export const AstroDark = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const AstroLight = (props: ComponentProps<"svg">) => ( +export const AstroLight = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const AstroGray = (props: ComponentProps<"svg">) => ( +export const AstroGray = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const TanStack = (props: ComponentProps<"svg">) => ( +export const TanStack = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const TanStackGray = (props: ComponentProps<"svg">) => ( +export const TanStackGray = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const Vite = (props: ComponentProps<"svg">) => ( +export const Vite = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const Nitro = (props: ComponentProps<"svg">) => ( +export const Nitro = (props: ComponentProps<'svg'>) => ( ) => ( /> ) => ( ); -export const SvelteKit = (props: ComponentProps<"svg">) => ( +export const SvelteKit = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const SvelteKitGray = (props: ComponentProps<"svg">) => ( +export const SvelteKitGray = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const Nuxt = (props: ComponentProps<"svg">) => ( +export const Nuxt = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const NuxtGray = (props: ComponentProps<"svg">) => ( +export const NuxtGray = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const Hono = (props: ComponentProps<"svg">) => ( +export const Hono = (props: ComponentProps<'svg'>) => ( Hono ) => ( ); -export const HonoGray = (props: ComponentProps<"svg">) => ( +export const HonoGray = (props: ComponentProps<'svg'>) => ( Hono ) => ( ); -export const Bun = (props: ComponentProps<"svg">) => ( +export const Bun = (props: ComponentProps<'svg'>) => ( ) => ( id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" fill="#ccbea7" - style={{ fillRule: "evenodd" }} + style={{ fillRule: 'evenodd' }} /> ) => ( ); -export const BunGray = (props: ComponentProps<"svg">) => ( +export const BunGray = (props: ComponentProps<'svg'>) => ( ) => ( id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" fill="var(--color-background)" - style={{ fillRule: "evenodd" }} + style={{ fillRule: 'evenodd' }} /> ) => ( ); -export const Nest = (props: ComponentProps<"svg">) => ( +export const Nest = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const NestGray = (props: ComponentProps<"svg">) => ( +export const NestGray = (props: ComponentProps<'svg'>) => ( ) => ( ); -export const Next = (props: ComponentProps<"svg">) => ( +export const Next = (props: ComponentProps<'svg'>) => ( Next.js @@ -681,8 +696,8 @@ export const Next = (props: ComponentProps<"svg">) => ( export const Frameworks = () => { const handleRequest = (framework: string) => { - track("Framework requested", { framework: framework.toLowerCase() }); - toast.success("Request received", { + track('Framework requested', { framework: framework.toLowerCase() }); + toast.success('Request received', { description: `Thanks for expressing interest in ${framework}. We will be adding support for it soon.`, }); }; @@ -723,6 +738,9 @@ export const Frameworks = () => { + + +
@@ -742,21 +760,21 @@ export const Frameworks = () => {
handleRequest("NestJS")} + onClick={() => handleRequest('NestJS')} >
handleRequest("TanStack")} + onClick={() => handleRequest('TanStack')} >
handleRequest("Astro")} + onClick={() => handleRequest('Astro')} > diff --git a/docs/content/docs/getting-started/fastify.mdx b/docs/content/docs/getting-started/fastify.mdx new file mode 100644 index 000000000..53a7f5c6e --- /dev/null +++ b/docs/content/docs/getting-started/fastify.mdx @@ -0,0 +1,247 @@ +--- +title: Fastify +--- + +This guide will walk through setting up your first workflow in a Fastify app. Along the way, you'll learn more about the concepts that are fundamental to using the development kit in your own projects. + +--- + + + + +## Create Your Fastify Project + +Start by creating a new Fastify project. + +```bash +mkdir my-workflow-app +``` + +Enter the newly made directory: + +```bash +cd my-workflow-app +``` + +Initialize the project: + +```bash +npm init --y +``` + +### Install `workflow`, `fastify` and `nitro` + +```package-install +npm i workflow fastify nitro rollup +``` + + +By default, Fastify doesn't include a build system. Nitro adds one which enables compiling workflows, runs, and deploys for development and production. Learn more about [Nitro](https://v3.nitro.build). + + +If using TypeScript, you need to install the `@types/node` and `typescript` packages + +```bash +npm i -D @types/node typescript +``` + +### Configure Nitro + +Create a new file `nitro.config.ts` for your Nitro configuration with module `workflow/nitro`. This enables usage of the `"use workflow"` and `"use step"` directives + +```typescript title="nitro.config.ts" lineNumbers +import { defineNitroConfig } from "nitro/config"; + +export default defineNitroConfig({ + modules: ["workflow/nitro"], + vercel: { entryFormat: "node" }, + routes: { + "/**": { handler: "./src/index.ts", format: "node" }, + }, +}); +``` + + + + + Setup IntelliSense for TypeScript (Optional) + + +To enable helpful hints in your IDE, set up the workflow plugin in `tsconfig.json`: + +```json title="tsconfig.json" lineNumbers +{ + "compilerOptions": { + // ... rest of your TypeScript config + "plugins": [ + { + "name": "workflow" // [!code highlight] + } + ] + } +} +``` + + + + + +### Update `package.json` + +To use the Nitro builder, update your `package.json` to include the following scripts: + +```json title="package.json" lineNumbers +{ + // ... + "scripts": { + "dev": "nitro dev", + "build": "nitro build" + }, + // ... +} +``` + + + + + +## Create Your First Workflow + +Create a new file for our first workflow: + +```typescript title="workflows/user-signup.ts" lineNumbers +import { sleep } from "workflow"; + +export async function handleUserSignup(email: string) { + "use workflow"; // [!code highlight] + + const user = await createUser(email); + await sendWelcomeEmail(user); + + await sleep("5s"); // Pause for 5s - doesn't consume any resources + await sendOnboardingEmail(user); + + return { userId: user.id, status: "onboarded" }; +} +``` + +We'll fill in those functions next, but let's take a look at this code: + +- We define a **workflow** function with the directive `"use workflow"`. Think of the workflow function as the _orchestrator_ of individual **steps**. +- The Workflow DevKit's `sleep` function allows us to suspend execution of the workflow without using up any resources. A sleep can be a few seconds, hours, days, or even months long. +## Create Your Workflow Steps + +Let's now define those missing functions: +```typescript title="workflows/user-signup.ts" lineNumbers +import { FatalError } from "workflow"; + +// Our workflow function defined earlier + +async function createUser(email: string) { + "use step"; // [!code highlight] + console.log(`Creating user with email: ${email}`); + return { id: crypto.randomUUID(), email }; +} + +async function sendWelcomeEmail(user: { id: string; email: string }) { + "use step"; // [!code highlight] + console.log(`Sending welcome email to user: ${user.id}`); + if (Math.random() < 0.3) { + // Steps retry on unhandled errors + throw new Error("Retryable!"); + } +} + +async function sendOnboardingEmail(user: { id: string; email: string }) { + "use step"; // [!code highlight] + if (!user.email.includes("@")) { + // FatalError skips retries + throw new FatalError("Invalid Email"); + } + console.log(`Sending onboarding email to user: ${user.id}`); +} +``` +Taking a look at this code: + +- Business logic lives inside **steps**. When a step is invoked inside a **workflow**, it gets enqueued to run on a separate request while the workflow is suspended, just like `sleep`. +- If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count). +- Steps can throw a `FatalError` if an error is intentional and should not be retried. + + + We'll dive deeper into workflows, steps, and other ways to suspend or handle + events in [Foundations](/docs/foundations). + + + + + +## Create Your Route Handler + +To invoke your new workflow, we'll create both the Fastify app and a new API route handler at `src/index.ts` with the following code: + +```typescript title="src/index.ts" +import Fastify from "fastify"; +import { start } from "workflow/api"; +import { handleUserSignup } from "../workflows/user-signup.js"; + +const app = Fastify({ logger: true }); +app.post("/api/signup", async (req, reply) => { + const { email } = req.body as { email: string }; + await start(handleUserSignup, [email]); + return reply.send({ message: "User signup workflow started" }); +}); + +// Wait for Fastify to be ready before handling requests +await app.ready(); + + +export default (req: any, res: any) => { + app.server.emit("request", req, res); +}; +``` +This route handler creates a `POST` request endpoint at `/api/signup` that will trigger your workflow. + + + + +## Run in development + +To start your development server, run the following command in your terminal in the Fastify root directory: + +```bash +npm run dev +``` + +Once your development server is running, you can trigger your workflow by running this command in the terminal: + +```bash +curl -X POST --json '{"email":"hello@example.com"}' http://localhost:3000/api/signup +``` + +Check the Fastify development server logs to see your workflow execute as well as the steps that are being processed. + +Additionally, you can use the [Workflow DevKit CLI or Web UI](/docs/observability) to inspect your workflow runs and steps in detail. + +```bash +npx workflow inspect runs # add '--web' for an interactive Web based UI +``` + +Workflow DevKit Web UI + + + + + +--- + +## Deploying to production + +Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration. + +Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere. + +## Next Steps + +- Learn more about the [Foundations](/docs/foundations). +- Check [Errors](/docs/errors) if you encounter issues. +- Explore the [API Reference](/docs/api-reference). diff --git a/docs/content/docs/getting-started/index.mdx b/docs/content/docs/getting-started/index.mdx index 11a399aea..327aa25ce 100644 --- a/docs/content/docs/getting-started/index.mdx +++ b/docs/content/docs/getting-started/index.mdx @@ -3,7 +3,7 @@ title: Getting Started description: Start by choosing your framework. Each guide will walk you through the steps to install the dependencies and start running your first workflow. --- -import { Next, Nitro, SvelteKit, Nuxt, Hono, Bun, AstroDark, AstroLight, TanStack, Vite, Express } from '@/app/(home)/components/frameworks'; +import { Next, Nitro, SvelteKit, Nuxt, Hono, Bun, AstroDark, AstroLight, TanStack, Vite, Express, Fastify } from '@/app/(home)/components/frameworks'; @@ -24,6 +24,12 @@ import { Next, Nitro, SvelteKit, Nuxt, Hono, Bun, AstroDark, AstroLight, TanStac Express
+ +
+ + Fastify +
+
diff --git a/packages/core/e2e/local-build.test.ts b/packages/core/e2e/local-build.test.ts index 4daca2b01..0eb264c9b 100644 --- a/packages/core/e2e/local-build.test.ts +++ b/packages/core/e2e/local-build.test.ts @@ -14,6 +14,7 @@ describe.each([ 'nuxt', 'hono', 'express', + 'fastify', ])('e2e', (project) => { test('builds without errors', { timeout: 180_000 }, async () => { // skip if we're targeting specific app to test diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 797a82b2f..764d53ec7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1220,6 +1220,40 @@ importers: specifier: 'catalog:' version: 4.1.11 + workbench/fastify: + dependencies: + fastify: + specifier: ^5.6.2 + version: 5.6.2 + nitro: + specifier: 'catalog:' + version: 3.0.1-alpha.1(@netlify/blobs@9.1.2)(@vercel/functions@3.1.4(@aws-sdk/credential-provider-web-identity@3.844.0))(better-sqlite3@11.10.0)(chokidar@4.0.3)(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.7))(ioredis@5.8.2)(lru-cache@11.2.2)(rollup@4.53.2)(vite@7.1.12(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 22.19.0 + '@workflow/world-postgres': + specifier: workspace:* + version: link:../../packages/world-postgres + ai: + specifier: 'catalog:' + version: 5.0.76(zod@4.1.11) + lodash.chunk: + specifier: ^4.2.0 + version: 4.2.0 + openai: + specifier: ^6.6.0 + version: 6.6.0(ws@8.18.3)(zod@4.1.11) + typescript: + specifier: 'catalog:' + version: 5.9.3 + workflow: + specifier: workspace:* + version: link:../../packages/workflow + zod: + specifier: 'catalog:' + version: 4.1.11 + workbench/hono: devDependencies: '@workflow/world-postgres': @@ -2614,9 +2648,27 @@ packages: resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + '@fastify/busboy@3.2.0': resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -4308,6 +4360,9 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -6507,6 +6562,9 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -6536,9 +6594,20 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + alien-signals@3.0.3: resolution: {integrity: sha512-2JXjom6R7ZwrISpUphLhf4htUq1aKRCennTJ6u9kFfr3sLmC9+I4CxxVi+McoFnIg+p1HnVrfLT/iCt4Dlz//Q==} @@ -6649,6 +6718,10 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + autoprefixer@10.4.21: resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} engines: {node: ^10 || ^12 || >=14} @@ -6656,6 +6729,9 @@ packages: peerDependencies: postcss: ^8.1.0 + avvio@9.1.0: + resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -8077,6 +8153,9 @@ packages: fast-content-type-parse@3.0.0: resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -8094,16 +8173,28 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@6.1.1: + resolution: {integrity: sha512-DbgptncYEXZqDUOEl4krff4mUiVrTZZVI7BBrQR/T3BqMj/eM1flTC1Uk2uUoLcWCxjT95xKulV/Lc6hhOZsBQ==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} fast-npm-meta@0.4.7: resolution: {integrity: sha512-aZU3i3eRcSb2NCq8i6N6IlyiTyF6vqAqzBGl2NBF6ngNx/GIqfYbkLDIKZ4z4P0o/RmtsFnVqHwdrSm13o4tnQ==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-xml-parser@5.2.5: resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} hasBin: true + fastify@5.6.2: + resolution: {integrity: sha512-dPugdGnsvYkBlENLhCgX8yhyGCsCPrpA8lFWbTNU428l+YOnLgYHR69hzV8HWPC79n536EqzqQtvhtdaCE0dKg==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -8156,6 +8247,10 @@ packages: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} + find-my-way@9.3.0: + resolution: {integrity: sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==} + engines: {node: '>=20'} + find-package@1.0.0: resolution: {integrity: sha512-yVn71XCCaNgxz58ERTl8nA/8YYtIQDY9mHSrgFBfiFtdNNfY0h183Vh8BRkKxD8x9TUw3ec290uJKhDVxqGZBw==} @@ -8740,6 +8835,10 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -8984,9 +9083,15 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -9079,6 +9184,9 @@ packages: lie@3.1.1: resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + lightningcss-darwin-arm64@1.30.1: resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} engines: {node: '>= 12.0.0'} @@ -9966,6 +10074,10 @@ packages: resolution: {integrity: sha512-J7kocOS+ZNyjmW6tUUTtA7jLt8GjQlrOdz9z3yLNTvdsswO+b5lYSdMVzDczWnooyFAkkQiKyap5g/Zba+cFRA==} engines: {node: '>=20'} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -10305,6 +10417,16 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@10.1.0: + resolution: {integrity: sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==} + hasBin: true + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -10563,6 +10685,12 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -10618,6 +10746,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + quote-unquote@1.0.0: resolution: {integrity: sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==} @@ -10797,6 +10928,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + recharts-scale@0.4.5: resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} @@ -10886,6 +11021,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + require-in-the-middle@7.5.2: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} @@ -10917,6 +11056,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} @@ -10986,6 +11129,9 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex2@5.0.0: + resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -11015,6 +11161,9 @@ packages: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + seedrandom@3.0.5: resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} @@ -11142,6 +11291,9 @@ packages: smob@1.5.0: resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: @@ -11457,6 +11609,9 @@ packages: text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + throttleit@2.1.0: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} @@ -13683,8 +13838,31 @@ snapshots: levn: 0.4.1 optional: true + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + '@fastify/busboy@3.2.0': {} + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.1.1 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.2.0 + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -15391,6 +15569,8 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1 + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -18132,6 +18312,8 @@ snapshots: dependencies: event-target-shim: 5.0.1 + abstract-logging@2.0.1: {} + accepts@2.0.0: dependencies: mime-types: 3.0.1 @@ -18157,6 +18339,10 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.1.11 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -18165,6 +18351,13 @@ snapshots: uri-js: 4.4.1 optional: true + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + alien-signals@3.0.3: {} ansi-align@3.0.1: @@ -18271,6 +18464,8 @@ snapshots: async@3.2.6: {} + atomic-sleep@1.0.0: {} + autoprefixer@10.4.21(postcss@8.5.6): dependencies: browserslist: 4.27.0 @@ -18281,6 +18476,11 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 + avvio@9.1.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.19.1 + axobject-query@4.1.0: {} b4a@1.7.3: {} @@ -19791,6 +19991,8 @@ snapshots: fast-content-type-parse@3.0.0: {} + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-equals@5.3.2: {} @@ -19808,15 +20010,48 @@ snapshots: fast-json-stable-stringify@2.1.0: optional: true + fast-json-stringify@6.1.1: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: optional: true fast-npm-meta@0.4.7: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-uri@3.1.0: {} + fast-xml-parser@5.2.5: dependencies: strnum: 2.1.1 + fastify@5.6.2: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.1.0 + fast-json-stringify: 6.1.1 + find-my-way: 9.3.0 + light-my-request: 6.6.0 + pino: 10.1.0 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.3 + toad-cache: 3.7.0 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -19872,6 +20107,12 @@ snapshots: transitivePeerDependencies: - supports-color + find-my-way@9.3.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.0.0 + find-package@1.0.0: dependencies: parents: 1.0.1 @@ -20576,6 +20817,8 @@ snapshots: ipaddr.js@1.9.1: {} + ipaddr.js@2.2.0: {} + iron-webcrypto@1.2.1: {} is-alphabetical@2.0.1: {} @@ -20761,9 +21004,15 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + json-schema-traverse@0.4.1: optional: true + json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} json-stable-stringify-without-jsonify@1.0.1: @@ -20849,6 +21098,12 @@ snapshots: dependencies: immediate: 3.0.6 + light-my-request@6.6.0: + dependencies: + cookie: 1.0.2 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + lightningcss-darwin-arm64@1.30.1: optional: true @@ -22530,6 +22785,8 @@ snapshots: on-change@6.0.0: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -22964,6 +23221,26 @@ snapshots: pify@4.0.1: {} + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@10.1.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -23232,6 +23509,10 @@ snapshots: process-nextick-args@2.0.1: {} + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + process@0.11.10: {} prompts@2.4.2: @@ -23299,6 +23580,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + quote-unquote@1.0.0: {} radix-ui@1.4.3(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): @@ -23596,6 +23879,8 @@ snapshots: readdirp@4.1.2: {} + real-require@0.2.0: {} + recharts-scale@0.4.5: dependencies: decimal.js-light: 2.5.1 @@ -23751,6 +24036,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + require-in-the-middle@7.5.2: dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -23784,6 +24071,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + ret@0.5.0: {} + retry@0.12.0: {} reusify@1.1.0: {} @@ -23872,6 +24161,10 @@ snapshots: safe-buffer@5.2.1: {} + safe-regex2@5.0.0: + dependencies: + ret: 0.5.0 + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -23895,6 +24188,8 @@ snapshots: extend-shallow: 2.0.1 kind-of: 6.0.3 + secure-json-parse@4.1.0: {} + seedrandom@3.0.5: {} semver@6.3.1: {} @@ -24082,6 +24377,10 @@ snapshots: smob@1.5.0: {} + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 @@ -24480,6 +24779,10 @@ snapshots: text-hex@1.0.0: {} + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + throttleit@2.1.0: {} tiktok-video-element@0.1.1: {} diff --git a/scripts/create-test-matrix.mjs b/scripts/create-test-matrix.mjs index 369ec344d..d7cc3c18f 100644 --- a/scripts/create-test-matrix.mjs +++ b/scripts/create-test-matrix.mjs @@ -49,6 +49,12 @@ const DEV_TEST_CONFIGS = { apiFilePath: './src/index.ts', apiFileImportPath: '..', }, + fastify: { + generatedStepPath: 'node_modules/.nitro/workflow/steps.mjs', + generatedWorkflowPath: 'node_modules/.nitro/workflow/workflows.mjs', + apiFilePath: './src/index.ts', + apiFileImportPath: '..', + }, }; const matrix = { @@ -112,4 +118,10 @@ matrix.app.push({ ...DEV_TEST_CONFIGS.express, }); +matrix.app.push({ + name: 'fastify', + project: 'workbench-fastify-workflow', + ...DEV_TEST_CONFIGS.fastify, +}); + console.log(JSON.stringify(matrix)); diff --git a/workbench/fastify/.gitignore b/workbench/fastify/.gitignore new file mode 100644 index 000000000..f1af274f2 --- /dev/null +++ b/workbench/fastify/.gitignore @@ -0,0 +1,26 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +.well-known/ +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# Workflows +_workflows.ts \ No newline at end of file diff --git a/workbench/fastify/README.md b/workbench/fastify/README.md new file mode 100644 index 000000000..f68da1fba --- /dev/null +++ b/workbench/fastify/README.md @@ -0,0 +1,26 @@ +# Workflows with Fastify (Nitro v3) + +- Learn more about Fastify: https://fastify.dev +- Learn more about Nitro: https://v3.nitro.build/ + +## Commands + +**Local development:** + +```sh +npm run dev +``` + +**Production build (Vercel):** + +```sh +NITRO_PRESET=vercel npm run build +npx vercel --prebuilt +``` + +**Production build (Node.js):** + +```sh +npm run build +node .output/server/index.mjs +``` diff --git a/workbench/fastify/index.html b/workbench/fastify/index.html new file mode 120000 index 000000000..62524c9be --- /dev/null +++ b/workbench/fastify/index.html @@ -0,0 +1 @@ +../nitro-v3/index.html \ No newline at end of file diff --git a/workbench/fastify/nitro.config.ts b/workbench/fastify/nitro.config.ts new file mode 100644 index 000000000..175d1ace4 --- /dev/null +++ b/workbench/fastify/nitro.config.ts @@ -0,0 +1,10 @@ +import { defineNitroConfig } from 'nitro/config'; + +export default defineNitroConfig({ + modules: ['workflow/nitro'], + vercel: { entryFormat: 'node' }, + routes: { + '/**': { handler: './src/index.ts', format: 'node' }, + }, + plugins: ['plugins/start-pg-world.ts'], +}); diff --git a/workbench/fastify/package.json b/workbench/fastify/package.json new file mode 100644 index 000000000..3148bda5b --- /dev/null +++ b/workbench/fastify/package.json @@ -0,0 +1,32 @@ +{ + "name": "@workflow/example-fastify", + "version": "0.0.0", + "private": true, + "description": "", + "type": "module", + "main": "index.js", + "scripts": { + "generate:workflows": "node ../scripts/generate-workflows-registry.js", + "predev": "pnpm generate:workflows", + "copy:index": "mkdir -p public && cp index.html public/index.html", + "prebuild": "pnpm generate:workflows && pnpm copy:index", + "postbuild": "rm -f public/index.html", + "build": "nitro build", + "dev": "nitro dev", + "start": "node .output/server/index.mjs" + }, + "dependencies": { + "fastify": "^5.6.2", + "nitro": "catalog:" + }, + "devDependencies": { + "@types/node": "catalog:", + "@workflow/world-postgres": "workspace:*", + "ai": "catalog:", + "lodash.chunk": "^4.2.0", + "openai": "^6.6.0", + "typescript": "catalog:", + "workflow": "workspace:*", + "zod": "catalog:" + } +} diff --git a/workbench/fastify/plugins b/workbench/fastify/plugins new file mode 120000 index 000000000..2c8250f9c --- /dev/null +++ b/workbench/fastify/plugins @@ -0,0 +1 @@ +../nitro-v3/plugins \ No newline at end of file diff --git a/workbench/fastify/src/index.ts b/workbench/fastify/src/index.ts new file mode 100644 index 000000000..7c354822c --- /dev/null +++ b/workbench/fastify/src/index.ts @@ -0,0 +1,273 @@ +import Fastify from 'fastify'; +import { getHookByToken, getRun, resumeHook, start } from 'workflow/api'; +import { hydrateWorkflowArguments } from 'workflow/internal/serialization'; +import { + WorkflowRunFailedError, + WorkflowRunNotCompletedError, +} from 'workflow/internal/errors'; +import { allWorkflows } from '../_workflows.js'; +import { resolve } from 'node:path'; +import { readFile } from 'node:fs/promises'; + +type JsonResult = { ok: true; value: any } | { ok: false; error: Error }; +const parseJson = (text: string): JsonResult => { + try { + return { ok: true, value: JSON.parse(text) }; + } catch (error) { + return { ok: false, error: error as Error }; + } +}; + +const server = Fastify({ + logger: true, +}); + +server.addContentTypeParser( + 'text/*', + { parseAs: 'string' }, + server.getDefaultJsonParser('ignore', 'ignore') +); + +// allow fastify to parse empty json requests +server.addContentTypeParser( + 'application/json', + { parseAs: 'string' }, + (req, body, done) => { + const text = typeof body === 'string' ? body : body.toString(); + if (!text) return done(null, {}); + const parsed = parseJson(text); + return parsed.ok ? done(null, parsed.value) : done(parsed.error); + } +); + +server.get('/', async (req, reply) => { + const html = await readFile(resolve('./index.html'), 'utf-8'); + return reply.type('text/html').send(html); +}); + +server.post('/api/hook', async (req: any, reply) => { + const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body; + const { token, data } = body; + + let hook: Awaited>; + try { + hook = await getHookByToken(token); + console.log('hook', hook); + } catch (error) { + console.log('error during getHookByToken', error); + return reply.code(422).send(null); + } + + await resumeHook(hook.token, { + ...data, + // @ts-expect-error metadata is not typed + customData: hook.metadata?.customData, + }); + + return hook; +}); + +server.post('/api/trigger', async (req: any, reply) => { + const workflowFile = + (req.query.workflowFile as string) || 'workflows/99_e2e.ts'; + if (!workflowFile) { + return reply.code(400).send('No workflowFile query parameter provided'); + } + const workflows = allWorkflows[workflowFile as keyof typeof allWorkflows]; + if (!workflows) { + return reply.code(400).send(`Workflow file "${workflowFile}" not found`); + } + + const workflowFn = (req.query.workflowFn as string) || 'simple'; + if (!workflowFn) { + return reply.code(400).send('No workflow query parameter provided'); + } + const workflow = workflows[workflowFn as keyof typeof workflows]; + if (!workflow) { + return reply.code(400).send('Workflow not found'); + } + + let args: any[] = []; + + // Args from query string + const argsParam = req.query.args as string; + if (argsParam) { + args = argsParam.split(',').map((arg) => { + const num = parseFloat(arg); + return Number.isNaN(num) ? arg.trim() : num; + }); + } else { + // Args from body + const body = req.body; + if (body && typeof body === 'string') { + args = hydrateWorkflowArguments(JSON.parse(body), globalThis); + } else if (body && typeof body === 'object') { + args = hydrateWorkflowArguments(body, globalThis); + } else { + args = [42]; + } + } + console.log(`Starting "${workflowFn}" workflow with args: ${args}`); + + try { + const run = await start(workflow as any, args as any); + console.log('Run:', run); + return run; + } catch (err) { + console.error(`Failed to start!!`, err); + throw err; + } +}); + +server.get('/api/trigger', async (req: any, reply) => { + const runId = req.query.runId as string | undefined; + if (!runId) { + return reply.code(400).send('No runId provided'); + } + + const outputStreamParam = req.query['output-stream'] as string | undefined; + + try { + const run = getRun(runId); + + if (outputStreamParam) { + const namespace = + outputStreamParam === '1' ? undefined : outputStreamParam; + const stream = run.getReadable({ namespace }); + const reader = stream.getReader(); + + const toFramedChunk = (value: unknown) => { + if (typeof value === 'string') { + return { data: Buffer.from(value).toString('base64') }; + } + if (value instanceof ArrayBuffer) { + return { data: Buffer.from(value).toString('base64') }; + } + if (ArrayBuffer.isView(value)) { + const view = value as ArrayBufferView; + const buf = Buffer.from( + view.buffer, + view.byteOffset, + view.byteLength + ); + return { data: buf.toString('base64') }; + } + return value; + }; + + reply.type('application/octet-stream'); + // Fastify runs on Node and doesn’t send Web ReadableStreams directly + // read from the Web reader and write framed chunks to the raw response + try { + let chunkCount = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + chunkCount += 1; + + const framed = toFramedChunk(value); + reply.raw.write(`${JSON.stringify(framed)}\n`); + } + reply.raw.end(); + } catch (error) { + console.error('Error streaming data:', error); + reply.raw.end(); + } finally { + reader.releaseLock(); + } + return; + } + + const returnValue = await run.returnValue; + console.log('Return value:', returnValue); + + if (returnValue instanceof ReadableStream) { + const reader = returnValue.getReader(); + // reply.type() doesn't apply when we write directly to reply.raw + reply.raw.setHeader('Content-Type', 'application/octet-stream'); + + // Workflow returns a Web ReadableStream; stream it by pulling from + // its reader and writing to reply.raw so Fastify can flush it to the client + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + reply.raw.write(value); + } + reply.raw.end(); + } catch (streamError) { + console.error('Error streaming return value:', streamError); + reply.raw.end(); + } finally { + reader.releaseLock(); + } + return; + } + + // Fastify sends strings as text/plain by default + const payload = + typeof returnValue === 'string' || + typeof returnValue === 'number' || + typeof returnValue === 'boolean' + ? JSON.stringify(returnValue) + : returnValue; + return reply.type('application/json').send(payload); + } catch (error) { + if (error instanceof Error) { + if (WorkflowRunNotCompletedError.is(error)) { + return reply.code(202).send({ + ...error, + name: error.name, + message: error.message, + }); + } + + if (WorkflowRunFailedError.is(error)) { + const cause = error.cause; + return reply.code(400).send({ + ...error, + name: error.name, + message: error.message, + cause: { + message: cause.message, + stack: cause.stack, + code: cause.code, + }, + }); + } + } + + console.error( + 'Unexpected error while getting workflow return value:', + error + ); + + return reply.code(500).send({ + error: 'Internal server error', + }); + } +}); + +server.post('/api/test-direct-step-call', async (req: any, reply) => { + // This route tests calling step functions directly outside of any workflow context + // After the SWC compiler changes, step functions in client mode have their directive removed + // and keep their original implementation, allowing them to be called as regular async functions + const { add } = await import('../workflows/99_e2e.js'); + + const { x, y } = req.body; + + console.log(`Calling step function directly with x=${x}, y=${y}`); + + // Call step function directly as a regular async function (no workflow context) + const result = await add(x, y); + console.log(`add(${x}, ${y}) = ${result}`); + + return reply.send({ result }); +}); + +await server.ready(); + +export default (req: any, res: any) => { + server.server.emit('request', req, res); +}; diff --git a/workbench/fastify/workflows b/workbench/fastify/workflows new file mode 120000 index 000000000..876d7a80c --- /dev/null +++ b/workbench/fastify/workflows @@ -0,0 +1 @@ +../nitro-v3/workflows \ No newline at end of file