diff --git a/packages/platform-fastify/LICENSE b/packages/platform-fastify/LICENSE new file mode 100644 index 00000000000..be1f5c14c7b --- /dev/null +++ b/packages/platform-fastify/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Effectful Technologies Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/platform-fastify/README.md b/packages/platform-fastify/README.md new file mode 100644 index 00000000000..6d245021550 --- /dev/null +++ b/packages/platform-fastify/README.md @@ -0,0 +1,595 @@ +# @effect/platform-fastify + +Fastify integration for Effect's HttpApp and @effect/rpc + +## Installation + +```bash +npm install @effect/platform-fastify fastify +# or +pnpm add @effect/platform-fastify fastify +# or +yarn add @effect/platform-fastify fastify +``` + +## Features + +- **Native Fastify Support**: Direct integration with Fastify's request/reply system +- **Type-Safe**: Full TypeScript support with end-to-end type safety +- **Streaming**: Support for streaming RPC responses with backpressure handling +- **Serialization**: Built-in support for ndjson serialization (recommended for streaming) +- **Client Disconnect Detection**: Automatic fiber interruption when clients disconnect + +## Quick Start + +```typescript +import { FastifyRpcServer } from "@effect/platform-fastify" +import { Rpc, RpcGroup, RpcSerialization } from "@effect/rpc" +import { Effect, Layer, Schema } from "effect" +import Fastify from "fastify" + +// Define your RPC schema +class User extends Schema.Class("User")({ + id: Schema.String, + name: Schema.String +}) {} + +class UserRpcs extends RpcGroup.make( + Rpc.make("GetUser", { + success: User, + payload: { id: Schema.String } + }), + Rpc.make("CreateUser", { + success: User, + payload: { name: Schema.String } + }) +) {} + +// Implement RPC handlers +const UsersLive = UserRpcs.toLayer( + Effect.gen(function* () { + return { + GetUser: ({ id }) => Effect.succeed(new User({ id, name: "John Doe" })), + CreateUser: ({ name }) => + Effect.succeed(new User({ id: crypto.randomUUID(), name })) + } + }) +) + +// Setup Fastify server +const fastify = Fastify({ logger: true }) + +// Register RPC handler +const { dispose } = FastifyRpcServer.register(fastify, UserRpcs, { + path: "/rpc", + layer: Layer.mergeAll(UsersLive, RpcSerialization.layerNdjson) +}) + +// Other routes work normally +fastify.get("/health", async () => ({ status: "ok" })) + +// Start the server +await fastify.listen({ port: 3000 }) +console.log("Server listening on http://localhost:3000") + +// Cleanup on exit +process.on("SIGTERM", async () => { + await dispose() + await fastify.close() +}) +``` + +## API Reference + +### `register` + +Registers an RPC handler as a Fastify plugin with automatic content type parser configuration. +This is the **recommended** way to add RPC routes to a Fastify server as it properly +encapsulates the content type parser configuration to avoid affecting other routes. + +**Signature:** + +```typescript +declare const register: ( + fastify: FastifyInstance, + group: RpcGroup, + options: { + readonly path: string + readonly layer: Layer< + Rpc.ToHandler | Rpc.Middleware | RpcSerialization, + LE + > + readonly disableTracing?: boolean + readonly spanPrefix?: string + readonly spanAttributes?: Record + readonly disableFatalDefects?: boolean + readonly middleware?: ( + httpApp: HttpApp.Default + ) => HttpApp.Default + readonly memoMap?: Layer.MemoMap + } +) => { + readonly dispose: () => Promise +} +``` + +**Parameters:** + +| Parameter | Description | +| ----------------------------- | -------------------------------------------------------------------- | +| `fastify` | The Fastify instance to register the handler on | +| `group` | The RPC group containing your RPC definitions | +| `options.path` | The URL path for the RPC endpoint (e.g., `"/rpc"`) | +| `options.layer` | Effect layer providing RPC handlers and serialization | +| `options.disableTracing` | Disable tracing for RPC calls | +| `options.spanPrefix` | Prefix for tracing spans | +| `options.spanAttributes` | Additional attributes for tracing spans | +| `options.disableFatalDefects` | Don't treat defects as fatal | +| `options.middleware` | HTTP middleware function to transform the HTTP app | +| `options.memoMap` | Layer memoization map to share layers across multiple instantiations | + +**Returns:** + +| Property | Description | +| --------- | --------------------------------------------------------------------------------- | +| `dispose` | Cleanup function to release resources. Call this before shutting down the server. | + +**Example:** + +```typescript +import { FastifyRpcServer } from "@effect/platform-fastify" +import { RpcSerialization } from "@effect/rpc" +import { Layer } from "effect" +import Fastify from "fastify" + +const fastify = Fastify({ logger: true }) + +const { dispose } = FastifyRpcServer.register(fastify, UserRpcs, { + path: "/rpc", + layer: Layer.mergeAll(UsersLive, RpcSerialization.layerNdjson) +}) + +await fastify.listen({ port: 3000 }) + +// Cleanup +await dispose() +await fastify.close() +``` + +--- + +### `registerEffect` + +Registers an RPC handler as a Fastify plugin, returning an Effect that extracts context +from the environment. This is useful when you want to integrate the RPC handler into an +existing Effect application and manage the context yourself, while still benefiting from +automatic content type parser configuration. + +**Signature:** + +```typescript +declare const registerEffect: ( + fastify: FastifyInstance, + group: RpcGroup, + options: { + readonly path: string + readonly disableTracing?: boolean + readonly spanPrefix?: string + readonly spanAttributes?: Record + readonly disableFatalDefects?: boolean + readonly middleware?: ( + httpApp: HttpApp.Default + ) => HttpApp.Default + } +) => Effect< + void, + never, + | Scope + | RpcSerialization + | Rpc.ToHandler + | Rpc.Context + | Rpc.Middleware +> +``` + +**Parameters:** + +| Parameter | Description | +| ----------------------------- | -------------------------------------------------- | +| `fastify` | The Fastify instance to register the handler on | +| `group` | The RPC group containing your RPC definitions | +| `options.path` | The URL path for the RPC endpoint (e.g., `"/rpc"`) | +| `options.disableTracing` | Disable tracing for RPC calls | +| `options.spanPrefix` | Prefix for tracing spans | +| `options.spanAttributes` | Additional attributes for tracing spans | +| `options.disableFatalDefects` | Don't treat defects as fatal | +| `options.middleware` | HTTP middleware function to transform the HTTP app | + +**Returns:** + +An `Effect` that registers the RPC handler. The Effect requires: + +- `Scope` - For resource management +- `RpcSerialization` - Serialization format (e.g., `RpcSerialization.layerNdjson`) +- `Rpc.ToHandler` - The RPC handler implementations +- `Rpc.Context` - Any additional context required by the RPCs +- `Rpc.Middleware` - Any RPC middleware + +**Example:** + +```typescript +import { FastifyRpcServer } from "@effect/platform-fastify" +import { RpcSerialization } from "@effect/rpc" +import { Effect, Layer } from "effect" +import Fastify from "fastify" + +const program = Effect.gen(function* () { + const fastify = Fastify() + + yield* FastifyRpcServer.registerEffect(fastify, UserRpcs, { + path: "/rpc" + }) + + yield* Effect.acquireRelease( + Effect.promise(() => fastify.listen({ port: 3000 })), + () => Effect.promise(() => fastify.close()) + ) +}) + +const MainLive = Layer.mergeAll(UsersLive, RpcSerialization.layerNdjson) + +program.pipe(Effect.provide(MainLive), Effect.scoped, Effect.runPromise) +``` + +--- + +### `toFastifyHandler` + +Creates a Fastify handler function from an RPC group. Use this when you need more control +over route registration or when integrating with existing Fastify plugins. + +**Important:** When using `toFastifyHandler` directly, you must configure Fastify to not +parse request bodies for the RPC route. + +**Signature:** + +```typescript +declare const toFastifyHandler: ( + group: RpcGroup, + options: { + readonly layer: Layer< + Rpc.ToHandler | Rpc.Middleware | RpcSerialization, + LE + > + readonly disableTracing?: boolean + readonly spanPrefix?: string + readonly spanAttributes?: Record + readonly disableFatalDefects?: boolean + readonly middleware?: ( + httpApp: HttpApp.Default + ) => HttpApp.Default + readonly memoMap?: Layer.MemoMap + } +) => { + readonly handler: ( + request: FastifyRequest, + reply: FastifyReply + ) => Promise + readonly dispose: () => Promise +} +``` + +**Parameters:** + +| Parameter | Description | +| ----------------------------- | -------------------------------------------------------------------- | +| `group` | The RPC group containing your RPC definitions | +| `options.layer` | Effect layer providing RPC handlers and serialization | +| `options.disableTracing` | Disable tracing for RPC calls | +| `options.spanPrefix` | Prefix for tracing spans | +| `options.spanAttributes` | Additional attributes for tracing spans | +| `options.disableFatalDefects` | Don't treat defects as fatal | +| `options.middleware` | HTTP middleware function to transform the HTTP app | +| `options.memoMap` | Layer memoization map to share layers across multiple instantiations | + +**Returns:** + +| Property | Description | +| --------- | ------------------------------------- | +| `handler` | The Fastify route handler function | +| `dispose` | Cleanup function to release resources | + +**Example:** + +```typescript +import { FastifyRpcServer } from "@effect/platform-fastify" +import { RpcSerialization } from "@effect/rpc" +import { Layer } from "effect" +import Fastify from "fastify" + +const { handler, dispose } = FastifyRpcServer.toFastifyHandler(UserRpcs, { + layer: Layer.mergeAll(UsersLive, RpcSerialization.layerNdjson) +}) + +const fastify = Fastify() + +// Required: disable body parsing for RPC routes +fastify.removeAllContentTypeParsers() +fastify.addContentTypeParser("*", (_req, _payload, done) => done(null)) + +fastify.post("/rpc", handler) + +await fastify.listen({ port: 3000 }) +``` + +**Scoping content type parser (when you have other routes):** + +```typescript +const { handler, dispose } = FastifyRpcServer.toFastifyHandler(UserRpcs, { + layer: Layer.mergeAll(UsersLive, RpcSerialization.layerNdjson) +}) + +const fastify = Fastify() + +// RPC route with custom body parsing (scoped to this plugin) +fastify.register((instance, _opts, done) => { + instance.removeAllContentTypeParsers() + instance.addContentTypeParser("*", (_req, _payload, done) => done(null)) + instance.post("/rpc", handler) + done() +}) + +// Other routes use normal body parsing +fastify.post("/api/users", async (req) => { + // req.body is parsed normally +}) +``` + +--- + +### `toFastifyHandlerEffect` + +Creates a Fastify handler as an Effect, allowing the context to be provided externally. +This is useful when you want to integrate the RPC handler into an existing Effect application +and manage the context yourself. + +**Important:** When using this function, you must configure Fastify to not parse request bodies +for the RPC route. + +**Signature:** + +```typescript +declare const toFastifyHandlerEffect: ( + group: RpcGroup, + options?: { + readonly disableTracing?: boolean + readonly spanPrefix?: string + readonly spanAttributes?: Record + readonly disableFatalDefects?: boolean + readonly middleware?: ( + httpApp: HttpApp.Default + ) => HttpApp.Default + } +) => Effect< + (request: FastifyRequest, reply: FastifyReply) => Promise, + never, + | Scope + | RpcSerialization + | Rpc.ToHandler + | Rpc.Context + | Rpc.Middleware +> +``` + +**Parameters:** + +| Parameter | Description | +| ----------------------------- | -------------------------------------------------- | +| `group` | The RPC group containing your RPC definitions | +| `options.disableTracing` | Disable tracing for RPC calls | +| `options.spanPrefix` | Prefix for tracing spans | +| `options.spanAttributes` | Additional attributes for tracing spans | +| `options.disableFatalDefects` | Don't treat defects as fatal | +| `options.middleware` | HTTP middleware function to transform the HTTP app | + +**Returns:** + +An `Effect` that produces the Fastify handler function. The Effect requires: + +- `Scope` - For resource management +- `RpcSerialization` - Serialization format (e.g., `RpcSerialization.layerNdjson`) +- `Rpc.ToHandler` - The RPC handler implementations +- `Rpc.Context` - Any additional context required by the RPCs +- `Rpc.Middleware` - Any RPC middleware + +**Example:** + +```typescript +import { FastifyRpcServer } from "@effect/platform-fastify" +import { RpcSerialization } from "@effect/rpc" +import { Effect, Layer } from "effect" +import Fastify from "fastify" + +const program = Effect.gen(function* () { + const handler = yield* FastifyRpcServer.toFastifyHandlerEffect(UserRpcs) + + const fastify = Fastify() + fastify.removeAllContentTypeParsers() + fastify.addContentTypeParser("*", (_req, _payload, done) => done(null)) + fastify.post("/rpc", handler) + + yield* Effect.acquireRelease( + Effect.promise(() => fastify.listen({ port: 3000 })), + () => Effect.promise(() => fastify.close()) + ) +}) + +const MainLive = Layer.mergeAll(UsersLive, RpcSerialization.layerNdjson) + +program.pipe(Effect.provide(MainLive), Effect.scoped, Effect.runPromise) +``` + +--- + +## FastifyHttpAppServer + +The package also exports `FastifyHttpAppServer` for attaching any `HttpApp` to Fastify routes, +not just RPC handlers. This is the lower-level API that `FastifyRpcServer` is built on. + +### `FastifyHttpAppServer.toHandlerEffect` + +Create a Fastify handler from an `HttpApp` as an Effect, allowing the context to be provided externally. + +**Signature:** + +```typescript +declare const toHandlerEffect: ( + httpApp: HttpApp.Default +) => Effect< + (request: FastifyRequest, reply: FastifyReply) => Promise, + never, + Exclude | Scope +> +``` + +**Example:** + +```typescript +import { FastifyHttpAppServer } from "@effect/platform-fastify" +import { HttpServerResponse } from "@effect/platform" +import { Effect } from "effect" +import Fastify from "fastify" + +const httpApp = Effect.succeed(HttpServerResponse.text("Hello, World!")) + +const program = Effect.gen(function* () { + const handler = yield* FastifyHttpAppServer.toHandlerEffect(httpApp) + + const fastify = Fastify() + fastify.get("/hello", handler) + + yield* Effect.acquireRelease( + Effect.promise(() => fastify.listen({ port: 3000 })), + () => Effect.promise(() => fastify.close()) + ) +}) + +program.pipe(Effect.scoped, Effect.runPromise) +``` + +--- + +### `FastifyHttpAppServer.toHandler` + +Create a Fastify handler from an `HttpApp` with a layer that provides the required context. + +**Signature:** + +```typescript +declare const toHandler: ( + httpApp: HttpApp.Default, + layer: Layer, LE>, + options?: { + readonly memoMap?: Layer.MemoMap + } +) => { + readonly handler: ( + request: FastifyRequest, + reply: FastifyReply + ) => Promise + readonly dispose: () => Promise +} +``` + +**Example:** + +```typescript +import { FastifyHttpAppServer } from "@effect/platform-fastify" +import { HttpServerResponse } from "@effect/platform" +import { Effect, Layer, Context } from "effect" +import Fastify from "fastify" + +class Greeter extends Context.Tag("Greeter")< + Greeter, + { greet: (name: string) => string } +>() {} + +const httpApp = Effect.gen(function* () { + const greeter = yield* Greeter + return HttpServerResponse.text(greeter.greet("World")) +}) + +const GreeterLive = Layer.succeed(Greeter, { + greet: (name) => `Hello, ${name}!` +}) + +const { handler, dispose } = FastifyHttpAppServer.toHandler( + httpApp, + GreeterLive +) + +const fastify = Fastify() +fastify.get("/hello", handler) + +await fastify.listen({ port: 3000 }) + +// Cleanup +await dispose() +await fastify.close() +``` + +--- + +## Serialization + +The RPC layer requires a serialization format. Use `RpcSerialization.layerNdjson` for +newline-delimited JSON, which is recommended for streaming responses: + +```typescript +import { RpcSerialization } from "@effect/rpc" + +FastifyRpcServer.register(fastify, UserRpcs, { + path: "/rpc", + layer: Layer.mergeAll(UsersLive, RpcSerialization.layerNdjson) +}) +``` + +## Streaming RPCs + +For streaming responses, define your RPC with `RpcSchema.Stream`: + +```typescript +import { Rpc, RpcGroup, RpcSchema, RpcSerialization } from "@effect/rpc" +import { Effect, Schema, Stream } from "effect" + +class StreamingRpcs extends RpcGroup.make( + Rpc.make("StreamUsers", { + success: RpcSchema.Stream({ + success: User, + failure: Schema.Never + }), + payload: { count: Schema.Number } + }) +) {} + +const StreamingLive = StreamingRpcs.toLayer( + Effect.sync(() => ({ + StreamUsers: ({ count }) => + Stream.fromIterable( + Array.from( + { length: count }, + (_, i) => new User({ id: String(i), name: `User ${i}` }) + ) + ) + })) +) + +FastifyRpcServer.register(fastify, StreamingRpcs, { + path: "/rpc", + layer: Layer.mergeAll(StreamingLive, RpcSerialization.layerNdjson) +}) +``` + +## License + +MIT diff --git a/packages/platform-fastify/docgen.json b/packages/platform-fastify/docgen.json new file mode 100644 index 00000000000..83a34fd7f08 --- /dev/null +++ b/packages/platform-fastify/docgen.json @@ -0,0 +1,24 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/platform-fastify/src/", + "exclude": ["src/internal/**/*.ts"], + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"], + "@effect/platform": ["../../../platform/src/index.js"], + "@effect/platform/*": ["../../../platform/src/*.js"], + "@effect/rpc": ["../../../rpc/src/index.js"], + "@effect/rpc/*": ["../../../rpc/src/*.js"], + "@effect/platform-fastify": ["../../../platform-fastify/src/index.js"], + "@effect/platform-fastify/*": ["../../../platform-fastify/src/*.js"] + } + } +} diff --git a/packages/platform-fastify/examples/basic.ts b/packages/platform-fastify/examples/basic.ts new file mode 100644 index 00000000000..6a0dd257392 --- /dev/null +++ b/packages/platform-fastify/examples/basic.ts @@ -0,0 +1,96 @@ +import { FastifyRpcServer } from "@effect/platform-fastify" +import { Rpc, RpcGroup, RpcSerialization } from "@effect/rpc" +import { Effect, Layer, Schema } from "effect" +import Fastify from "fastify" + +// Define your RPC schema +class User extends Schema.Class("User")({ + id: Schema.String, + name: Schema.String, + email: Schema.String +}) {} + +class UserRpcs extends RpcGroup.make( + Rpc.make("GetUser", { + success: User, + payload: { id: Schema.String } + }), + Rpc.make("CreateUser", { + success: User, + payload: { name: Schema.String, email: Schema.String } + }), + Rpc.make("ListUsers", { + success: Schema.Array(User) + }) +) {} + +// Implement RPC handlers +const UsersLive = UserRpcs.toLayer( + Effect.gen(function*() { + // Simulated in-memory database + const users = new Map() + + return { + GetUser: ({ id }) => + Effect.sync(() => { + const user = users.get(id) + if (!user) { + throw new Error(`User ${id} not found`) + } + return user + }), + + CreateUser: ({ email, name }) => + Effect.sync(() => { + const id = crypto.randomUUID() + const user = new User({ id, name, email }) + users.set(id, user) + return user + }), + + ListUsers: () => Effect.sync(() => Array.from(users.values())) + } + }) +) + +// Setup Fastify server +const fastify = Fastify({ logger: true }) + +// Register RPC handler (automatically configures content type parser for this route) +const { dispose } = FastifyRpcServer.register(fastify, UserRpcs, { + path: "/rpc", + layer: Layer.mergeAll(UsersLive, RpcSerialization.layerNdjson) +}) + +// Add health check (uses normal Fastify body parsing) +fastify.get("/health", async () => { + return { status: "ok" } +}) + +// Start the server +const start = async () => { + try { + await fastify.listen({ port: 3000 }) + console.log("Server listening on http://localhost:3000") + console.log("RPC endpoint: POST http://localhost:3000/rpc") + } catch (err) { + fastify.log.error(err) + process.exit(1) + } +} + +start() + +// Cleanup on exit +process.on("SIGTERM", async () => { + console.log("SIGTERM received, shutting down gracefully...") + await dispose() + await fastify.close() +}) + +process.on("SIGINT", async () => { + console.log("SIGINT received, shutting down gracefully...") + await dispose() + await fastify.close() + process.exit(0) +}) diff --git a/packages/platform-fastify/package.json b/packages/platform-fastify/package.json new file mode 100644 index 00000000000..24114464ddc --- /dev/null +++ b/packages/platform-fastify/package.json @@ -0,0 +1,65 @@ +{ + "name": "@effect/platform-fastify", + "type": "module", + "version": "0.1.0", + "license": "MIT", + "description": "Fastify integration for Effect's HttpApp and @effect/rpc", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/platform-fastify" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "fastify", + "platform", + "rpc", + "effect" + ], + "keywords": [ + "typescript", + "fastify", + "platform", + "rpc", + "effect" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "directory": "dist", + "linkDirectory": false + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null + }, + "scripts": { + "codegen": "build-utils prepare-v3", + "build": "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v3", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "peerDependencies": { + "@effect/platform": "workspace:^", + "@effect/rpc": "workspace:^", + "effect": "workspace:^", + "fastify": "^5.0.0" + }, + "devDependencies": { + "@effect/platform": "workspace:^", + "@effect/rpc": "workspace:^", + "@types/node": "^22.16.4", + "effect": "workspace:^", + "fastify": "^5.6.2" + } +} diff --git a/packages/platform-fastify/src/FastifyHttpAppServer.ts b/packages/platform-fastify/src/FastifyHttpAppServer.ts new file mode 100644 index 00000000000..b18963194a5 --- /dev/null +++ b/packages/platform-fastify/src/FastifyHttpAppServer.ts @@ -0,0 +1,104 @@ +/** + * @since 1.0.0 + */ +import type * as HttpApp from "@effect/platform/HttpApp" +import type * as HttpServerRequest from "@effect/platform/HttpServerRequest" +import type * as Effect from "effect/Effect" +import type * as Layer from "effect/Layer" +import type * as Scope from "effect/Scope" +import type * as FastifyTypes from "fastify" +import * as internal from "./internal/fastifyHttpAppServer.js" + +/** + * Create a Fastify handler from an `HttpApp` as an Effect, allowing the context + * to be provided externally. + * + * This is useful when you want to integrate an HttpApp into an existing Effect + * application and manage the context yourself. + * + * Note: When using this function, you need to configure Fastify's content type + * parser to not parse the request body if your HttpApp needs to read the raw body. + * + * @since 1.0.0 + * @category constructors + * @example + * import { FastifyHttpAppServer } from "@effect/platform-fastify" + * import { HttpServerResponse } from "@effect/platform" + * import { Effect } from "effect" + * import Fastify from "fastify" + * + * const httpApp = Effect.succeed(HttpServerResponse.text("Hello, World!")) + * + * const program = Effect.gen(function* () { + * const handler = yield* FastifyHttpAppServer.toHandlerEffect(httpApp) + * + * const fastify = Fastify() + * fastify.get("/hello", handler) + * + * yield* Effect.acquireRelease( + * Effect.promise(() => fastify.listen({ port: 3000 })), + * () => Effect.promise(() => fastify.close()) + * ) + * }) + * + * program.pipe(Effect.scoped, Effect.runPromise) + */ +export const toHandlerEffect: ( + httpApp: HttpApp.Default +) => Effect.Effect< + (request: FastifyTypes.FastifyRequest, reply: FastifyTypes.FastifyReply) => Promise, + never, + Exclude | Scope.Scope +> = internal.toHandlerEffect + +/** + * Create a Fastify handler from an `HttpApp` with a layer that provides the + * required context. + * + * This function returns a handler that can be registered with Fastify routes, + * along with a dispose function to clean up resources. + * + * Note: When using this function, you need to configure Fastify's content type + * parser to not parse the request body if your HttpApp needs to read the raw body. + * + * @since 1.0.0 + * @category constructors + * @example + * import { FastifyHttpAppServer } from "@effect/platform-fastify" + * import { HttpServerResponse } from "@effect/platform" + * import { Effect, Layer, Context } from "effect" + * import Fastify from "fastify" + * + * class Greeter extends Context.Tag("Greeter") string }>() {} + * + * const httpApp = Effect.gen(function* () { + * const greeter = yield* Greeter + * return HttpServerResponse.text(greeter.greet("World")) + * }) + * + * const GreeterLive = Layer.succeed(Greeter, { greet: (name) => `Hello, ${name}!` }) + * + * const { handler, dispose } = FastifyHttpAppServer.toHandler(httpApp, GreeterLive) + * + * const fastify = Fastify() + * fastify.get("/hello", handler) + * + * await fastify.listen({ port: 3000 }) + * + * // Cleanup + * await dispose() + * await fastify.close() + */ +export const toHandler: ( + httpApp: HttpApp.Default, + layer: Layer.Layer, LE>, + options?: { + readonly memoMap?: Layer.MemoMap + } +) => { + readonly handler: ( + request: FastifyTypes.FastifyRequest, + reply: FastifyTypes.FastifyReply + ) => Promise + readonly dispose: () => Promise +} = internal.toHandler diff --git a/packages/platform-fastify/src/FastifyRpcServer.ts b/packages/platform-fastify/src/FastifyRpcServer.ts new file mode 100644 index 00000000000..7446cd0f144 --- /dev/null +++ b/packages/platform-fastify/src/FastifyRpcServer.ts @@ -0,0 +1,222 @@ +/** + * @since 1.0.0 + */ +import type * as HttpApp from "@effect/platform/HttpApp" +import type * as Rpc from "@effect/rpc/Rpc" +import type * as RpcGroup from "@effect/rpc/RpcGroup" +import type * as RpcSerialization from "@effect/rpc/RpcSerialization" +import type * as Effect from "effect/Effect" +import type * as Layer from "effect/Layer" +import type * as Scope from "effect/Scope" +import type * as FastifyTypes from "fastify" +import * as internal from "./internal/fastifyRpcServer.js" + +/** + * Register an RPC handler as a Fastify plugin with proper content type parser configuration. + * + * This is the recommended way to add RPC routes to a Fastify server as it properly + * encapsulates the content type parser configuration to avoid affecting other routes. + * + * @since 1.0.0 + * @category constructors + * @example + * import { FastifyRpcServer } from "@effect/platform-fastify" + * import { RpcSerialization } from "@effect/rpc" + * import { Layer } from "effect" + * import Fastify from "fastify" + * + * const fastify = Fastify({ logger: true }) + * + * const { dispose } = FastifyRpcServer.register(fastify, UserRpcs, { + * path: "/rpc", + * layer: Layer.mergeAll(UsersLive, RpcSerialization.layerNdjson) + * }) + * + * await fastify.listen({ port: 3000 }) + */ +export const register: ( + fastify: FastifyTypes.FastifyInstance, + group: RpcGroup.RpcGroup, + options: { + readonly path: string + readonly layer: Layer.Layer< + | Rpc.ToHandler + | Rpc.Middleware + | RpcSerialization.RpcSerialization, + LE + > + readonly disableTracing?: boolean | undefined + readonly spanPrefix?: string | undefined + readonly spanAttributes?: Record | undefined + readonly disableFatalDefects?: boolean | undefined + readonly middleware?: ( + httpApp: HttpApp.Default + ) => HttpApp.Default + readonly memoMap?: Layer.MemoMap + } +) => { + readonly dispose: () => Promise +} = internal.register + +/** + * Register an RPC handler as a Fastify plugin, returning an Effect that extracts + * context from the environment. + * + * This is useful when you want to integrate the RPC handler into an existing Effect + * application and manage the context yourself, while still benefiting from automatic + * content type parser configuration. + * + * @since 1.0.0 + * @category constructors + * @example + * import { FastifyRpcServer } from "@effect/platform-fastify" + * import { RpcSerialization } from "@effect/rpc" + * import { Effect, Layer } from "effect" + * import Fastify from "fastify" + * + * const program = Effect.gen(function* () { + * const fastify = Fastify() + * + * yield* FastifyRpcServer.registerEffect(fastify, UserRpcs, { + * path: "/rpc" + * }) + * + * yield* Effect.acquireRelease( + * Effect.promise(() => fastify.listen({ port: 3000 })), + * () => Effect.promise(() => fastify.close()) + * ) + * }) + * + * const MainLive = Layer.mergeAll(UsersLive, RpcSerialization.layerNdjson) + * + * program.pipe(Effect.provide(MainLive), Effect.runPromise) + */ +export const registerEffect: ( + fastify: FastifyTypes.FastifyInstance, + group: RpcGroup.RpcGroup, + options: { + readonly path: string + readonly disableTracing?: boolean | undefined + readonly spanPrefix?: string | undefined + readonly spanAttributes?: Record | undefined + readonly disableFatalDefects?: boolean | undefined + readonly middleware?: ( + httpApp: HttpApp.Default + ) => HttpApp.Default + } +) => Effect.Effect< + void, + never, + | Scope.Scope + | RpcSerialization.RpcSerialization + | Rpc.ToHandler + | Rpc.Context + | Rpc.Middleware +> = internal.registerEffect + +/** + * Construct a Fastify handler from an `RpcGroup`. + * + * Note: When using this function directly, you need to configure Fastify's content type + * parser to not parse the request body. Consider using `register` instead for a simpler setup. + * + * @since 1.0.0 + * @category constructors + * @example + * import { FastifyRpcServer } from "@effect/platform-fastify" + * import { RpcSerialization } from "@effect/rpc" + * import { Layer } from "effect" + * import Fastify from "fastify" + * + * const { handler, dispose } = FastifyRpcServer.toFastifyHandler(UserRpcs, { + * layer: Layer.mergeAll(UsersLive, RpcSerialization.layerNdjson) + * }) + * + * const fastify = Fastify({ logger: true }) + * // Required: disable body parsing for RPC routes + * fastify.removeAllContentTypeParsers() + * fastify.addContentTypeParser("*", (_req, _payload, done) => done(null)) + * fastify.post('/rpc', handler) + * await fastify.listen({ port: 3000 }) + */ +export const toFastifyHandler: ( + group: RpcGroup.RpcGroup, + options: { + readonly layer: Layer.Layer< + | Rpc.ToHandler + | Rpc.Middleware + | RpcSerialization.RpcSerialization, + LE + > + readonly disableTracing?: boolean | undefined + readonly spanPrefix?: string | undefined + readonly spanAttributes?: Record | undefined + readonly disableFatalDefects?: boolean | undefined + readonly middleware?: ( + httpApp: HttpApp.Default + ) => HttpApp.Default + readonly memoMap?: Layer.MemoMap + } +) => { + readonly handler: ( + request: FastifyTypes.FastifyRequest, + reply: FastifyTypes.FastifyReply + ) => Promise + readonly dispose: () => Promise +} = internal.toFastifyHandler + +/** + * Create a Fastify handler as an Effect, allowing the context to be provided externally. + * + * This is useful when you want to integrate the RPC handler into an existing Effect application + * and manage the context yourself. + * + * Note: When using this function, you need to configure Fastify's content type + * parser to not parse the request body. + * + * @since 1.0.0 + * @category constructors + * @example + * import { FastifyRpcServer } from "@effect/platform-fastify" + * import { RpcSerialization } from "@effect/rpc" + * import { Effect, Layer } from "effect" + * import Fastify from "fastify" + * + * const program = Effect.gen(function* () { + * const handler = yield* FastifyRpcServer.toFastifyHandlerEffect(UserRpcs) + * + * const fastify = Fastify() + * fastify.removeAllContentTypeParsers() + * fastify.addContentTypeParser("*", (_req, _payload, done) => done(null)) + * fastify.post("/rpc", handler) + * + * yield* Effect.acquireRelease( + * Effect.promise(() => fastify.listen({ port: 3000 })), + * () => Effect.promise(() => fastify.close()) + * ) + * }) + * + * const MainLive = Layer.mergeAll(UsersLive, RpcSerialization.layerNdjson) + * + * program.pipe(Effect.provide(MainLive), Effect.runPromise) + */ +export const toFastifyHandlerEffect: ( + group: RpcGroup.RpcGroup, + options?: { + readonly disableTracing?: boolean | undefined + readonly spanPrefix?: string | undefined + readonly spanAttributes?: Record | undefined + readonly disableFatalDefects?: boolean | undefined + readonly middleware?: ( + httpApp: HttpApp.Default + ) => HttpApp.Default + } +) => Effect.Effect< + (request: FastifyTypes.FastifyRequest, reply: FastifyTypes.FastifyReply) => Promise, + never, + | Scope.Scope + | RpcSerialization.RpcSerialization + | Rpc.ToHandler + | Rpc.Context + | Rpc.Middleware +> = internal.toFastifyHandlerEffect diff --git a/packages/platform-fastify/src/index.ts b/packages/platform-fastify/src/index.ts new file mode 100644 index 00000000000..d91a0a8d670 --- /dev/null +++ b/packages/platform-fastify/src/index.ts @@ -0,0 +1,9 @@ +/** + * @since 1.0.0 + */ +export * as FastifyHttpAppServer from "./FastifyHttpAppServer.js" + +/** + * @since 1.0.0 + */ +export * as FastifyRpcServer from "./FastifyRpcServer.js" diff --git a/packages/platform-fastify/src/internal/fastifyHttpAppServer.ts b/packages/platform-fastify/src/internal/fastifyHttpAppServer.ts new file mode 100644 index 00000000000..7bed363024c --- /dev/null +++ b/packages/platform-fastify/src/internal/fastifyHttpAppServer.ts @@ -0,0 +1,503 @@ +import * as Cookies from "@effect/platform/Cookies" +import type * as Headers from "@effect/platform/Headers" +import * as App from "@effect/platform/HttpApp" +import * as IncomingMessage from "@effect/platform/HttpIncomingMessage" +import type { HttpMethod } from "@effect/platform/HttpMethod" +import * as Error from "@effect/platform/HttpServerError" +import * as ServerRequest from "@effect/platform/HttpServerRequest" +import type * as ServerResponse from "@effect/platform/HttpServerResponse" +import * as Multipart from "@effect/platform/Multipart" +import type * as UrlParams from "@effect/platform/UrlParams" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Inspectable from "effect/Inspectable" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Runtime from "effect/Runtime" +import * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import type * as FastifyTypes from "fastify" +import type * as Http from "node:http" +import { Readable } from "node:stream" +import { pipeline } from "node:stream/promises" + +const resolveSymbol = Symbol.for("@effect/platform-fastify/resolve") + +/** @internal */ +export const toHandlerEffect = ( + httpApp: App.Default +): Effect.Effect< + (request: FastifyTypes.FastifyRequest, reply: FastifyTypes.FastifyReply) => Promise, + never, + Exclude | Scope.Scope +> => + Effect.gen(function*() { + const handledApp: Effect.Effect = App.toHandled( + httpApp as App.Default, + handleResponse + ) as any + const runtime = yield* Effect.runtime() + const runFork = Runtime.runFork(runtime) + + return ( + req: FastifyTypes.FastifyRequest, + rep: FastifyTypes.FastifyReply + ): Promise => + new Promise((resolve) => { + const serverRequest = new FastifyServerRequest(req, rep, resolve) + + const fiber = runFork( + Effect.scoped( + Effect.provideService( + handledApp, + ServerRequest.HttpServerRequest, + serverRequest + ) + ) + ) + + // Handle client disconnection + rep.raw.on("close", () => { + if (!rep.raw.writableEnded) { + fiber.unsafeInterruptAsFork(Error.clientAbortFiberId) + } + }) + }) + }) as any + +/** @internal */ +export const toHandler = ( + httpApp: App.Default, + layer: Layer.Layer, LE>, + options?: { + readonly memoMap?: Layer.MemoMap + } +): { + readonly handler: ( + request: FastifyTypes.FastifyRequest, + reply: FastifyTypes.FastifyReply + ) => Promise + readonly dispose: () => Promise +} => { + const scope = Effect.runSync(Scope.make()) + const dispose = () => Effect.runPromise(Scope.close(scope, Exit.void)) + + // Include Layer.scope so that scoped resources in the layer are properly managed + const fullLayer = Layer.mergeAll(layer, Layer.scope) + + type Handler = (request: FastifyTypes.FastifyRequest, reply: FastifyTypes.FastifyReply) => Promise + + let handlerCache: Handler | undefined + let handlerPromise: Promise | undefined + + function handler( + request: FastifyTypes.FastifyRequest, + reply: FastifyTypes.FastifyReply + ): Promise { + if (handlerCache) { + return handlerCache(request, reply) + } + if (!handlerPromise) { + // Build the handler by: + // 1. Building a runtime from the layer (with memoMap if provided) + // 2. Using that runtime to provide services to toHandlerEffect + // 3. Extending the scope we created so resources are tied to our dispose() + const buildEffect = Effect.gen(function*() { + const runtime = yield* (options?.memoMap + ? Layer.toRuntimeWithMemoMap(fullLayer, options.memoMap) + : Layer.toRuntime(fullLayer)) + return yield* (toHandlerEffect(httpApp) as Effect.Effect).pipe( + Effect.provide(runtime) + ) + }).pipe( + Effect.tap((h) => + Effect.sync(() => { + handlerCache = h + }) + ), + Scope.extend(scope) + ) + handlerPromise = Effect.runPromise(buildEffect) + } + return handlerPromise.then((f) => f(request, reply)) + } + + return { handler, dispose } as const +} + +/** @internal */ +export const handleResponse = ( + request: ServerRequest.HttpServerRequest, + response: ServerResponse.HttpServerResponse +): Effect.Effect => + Effect.suspend(() => { + const req = request as FastifyServerRequest + const fastifyReply = req.reply + const resolve = (req as any)[resolveSymbol] as () => void + + if (fastifyReply.sent) { + resolve() + return Effect.void + } + + // Set headers + let headers: Record> = response.headers + if (!Cookies.isEmpty(response.cookies)) { + headers = { ...headers } + const toSet = Cookies.toSetCookieHeaders(response.cookies) + if (headers["set-cookie"] !== undefined) { + toSet.push(headers["set-cookie"] as string) + } + headers["set-cookie"] = toSet + } + + // Set status and headers + fastifyReply.status(response.status) + for (const [key, value] of Object.entries(headers)) { + fastifyReply.header(key, value) + } + + // Handle HEAD requests + if (request.method === "HEAD") { + fastifyReply.send() + resolve() + return Effect.void + } + + const body = response.body + + switch (body._tag) { + case "Empty": { + fastifyReply.send() + resolve() + return Effect.void + } + case "Raw": { + // Handle Node.js streams + if ( + typeof body.body === "object" && body.body !== null && "pipe" in body.body && + typeof body.body.pipe === "function" + ) { + return Effect.tryPromise({ + try: (signal) => pipeline(body.body as Readable, fastifyReply.raw, { signal, end: true }), + catch: (cause) => + new Error.ResponseError({ + request, + response, + reason: "Decode", + cause + }) + }).pipe( + Effect.tap(() => Effect.sync(() => resolve())), + Effect.interruptible + ) + } + fastifyReply.send(body.body) + resolve() + return Effect.void + } + case "Uint8Array": { + fastifyReply.send(Buffer.from(body.body)) + resolve() + return Effect.void + } + case "FormData": { + return Effect.suspend(() => { + const r = new Response(body.formData) + for (const [key, value] of r.headers) { + fastifyReply.header(key, value) + } + return Effect.async((resume, signal) => { + const nodeStream = Readable.fromWeb(r.body as any, { signal }) + nodeStream.pipe(fastifyReply.raw) + nodeStream.on("error", (cause) => { + resume(Effect.fail( + new Error.ResponseError({ + request, + response, + reason: "Decode", + cause + }) + )) + }) + nodeStream.on("end", () => { + resolve() + resume(Effect.void) + }) + }).pipe(Effect.interruptible) + }) + } + case "Stream": { + const drainLatch = Effect.unsafeMakeLatch() + fastifyReply.raw.on("drain", () => drainLatch.unsafeOpen()) + return body.stream.pipe( + Stream.orDie, + Stream.runForEachChunk((chunk) => { + const array = Chunk.toReadonlyArray(chunk) + if (array.length === 0) return Effect.void + let needDrain = false + for (let i = 0; i < array.length; i++) { + const written = fastifyReply.raw.write(array[i]) + if (!written && !needDrain) { + needDrain = true + } + } + return needDrain ? Effect.suspend(() => drainLatch.await) : Effect.void + }), + Effect.ensuring( + Effect.sync(() => { + fastifyReply.raw.end() + resolve() + }) + ), + Effect.interruptible + ) + } + } + }) + +/** @internal */ +export class FastifyServerRequest extends Inspectable.Class implements ServerRequest.HttpServerRequest { + readonly [ServerRequest.TypeId]: ServerRequest.TypeId = ServerRequest.TypeId + readonly [IncomingMessage.TypeId]: IncomingMessage.TypeId = IncomingMessage.TypeId + readonly [resolveSymbol]: () => void + + constructor( + readonly request: FastifyTypes.FastifyRequest, + readonly reply: FastifyTypes.FastifyReply, + resolve: () => void + ) { + super() + this[resolveSymbol] = resolve + } + + get source(): Http.IncomingMessage { + return this.request.raw + } + + get url(): string { + return this.request.url + } + + get originalUrl(): string { + return this.request.url + } + + get method(): HttpMethod { + return this.request.method.toUpperCase() as HttpMethod + } + + private headersCache: Headers.Headers | undefined + get headers(): Headers.Headers { + if (this.headersCache) { + return this.headersCache + } + return this.headersCache = this.request.headers as Headers.Headers + } + + get remoteAddress(): Option.Option { + return Option.fromNullable(this.request.ip) + } + + private cookiesCache: Record | undefined + get cookies(): Record { + if (this.cookiesCache) { + return this.cookiesCache + } + return this.cookiesCache = Cookies.parseHeader(this.headers.cookie ?? "") + } + + private onError = (cause: unknown): Error.RequestError => + new Error.RequestError({ + request: this, + reason: "Decode", + cause + }) + + private bodyData: ArrayBuffer | undefined + private bodyError: unknown | undefined + private bodyReading = false + private bodyWaiters: Array<{ + resolve: (value: ArrayBuffer) => void + reject: (error: unknown) => void + }> = [] + + private readBody(): void { + if (this.bodyData !== undefined || this.bodyError !== undefined || this.bodyReading) { + return + } + + this.bodyReading = true + const body = this.request.body + + // If Fastify parsed the body, use it immediately + if (body !== undefined && body !== null) { + if (body instanceof ArrayBuffer) { + this.bodyData = body + } else if (Buffer.isBuffer(body)) { + this.bodyData = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) + } else { + const str = typeof body === "string" ? body : JSON.stringify(body) + this.bodyData = new TextEncoder().encode(str).buffer + } + // Resolve all waiters + for (const waiter of this.bodyWaiters) { + waiter.resolve(this.bodyData) + } + this.bodyWaiters = [] + return + } + + // Otherwise, read from raw stream + const chunks: Array = [] + const req = this.request.raw + + req.on("data", (chunk: Buffer) => { + chunks.push(chunk) + }) + + req.on("end", () => { + const result = Buffer.concat(chunks) + this.bodyData = result.buffer.slice(result.byteOffset, result.byteOffset + result.byteLength) + // Resolve all waiters + for (const waiter of this.bodyWaiters) { + waiter.resolve(this.bodyData) + } + this.bodyWaiters = [] + }) + + req.on("error", (error) => { + this.bodyError = error + // Reject all waiters + for (const waiter of this.bodyWaiters) { + waiter.reject(error) + } + this.bodyWaiters = [] + }) + } + + get arrayBuffer(): Effect.Effect { + const onError = this.onError + return Effect.async((resume) => { + // If we already have the data, return it + if (this.bodyData !== undefined) { + resume(Effect.succeed(this.bodyData)) + return + } + + // If we already have an error, return it + if (this.bodyError !== undefined) { + resume(Effect.fail(onError(this.bodyError))) + return + } + + // Otherwise, queue up and start reading if needed + this.bodyWaiters.push({ + resolve: (value) => resume(Effect.succeed(value)), + reject: (error) => resume(Effect.fail(onError(error))) + }) + this.readBody() + }) + } + + get text(): Effect.Effect { + const body = this.request.body + // If Fastify parsed the body and it's a string, return directly + if (body !== undefined && body !== null && typeof body === "string") { + return Effect.succeed(body) + } + + // Otherwise, get arrayBuffer and decode it + const onError = this.onError + return Effect.async((resume) => { + // If we already have the data, return it + if (this.bodyData !== undefined) { + const result = new TextDecoder().decode(this.bodyData) + resume(Effect.succeed(result)) + return + } + + // If we already have an error, return it + if (this.bodyError !== undefined) { + resume(Effect.fail(onError(this.bodyError))) + return + } + + // Otherwise, queue up and start reading if needed + this.bodyWaiters.push({ + resolve: (arrayBuffer) => { + const result = new TextDecoder().decode(arrayBuffer) + resume(Effect.succeed(result)) + }, + reject: (error) => resume(Effect.fail(onError(error))) + }) + this.readBody() + }) + } + + get json(): Effect.Effect { + return Effect.sync(() => this.request.body ?? null) + } + + get urlParamsBody(): Effect.Effect { + return Effect.sync(() => this.request.query as UrlParams.UrlParams) + } + + get stream(): Stream.Stream { + // Use arrayBuffer as the source since we can't read the raw stream twice + return Stream.fromEffect( + Effect.map(this.arrayBuffer, (buffer) => new Uint8Array(buffer)) + ) + } + + get multipart(): ServerRequest.HttpServerRequest["multipart"] { + return Effect.fail( + new Multipart.MultipartError({ + reason: "InternalError", + cause: "Multipart not implemented in @effect/platform-fastify" + }) + ) + } + + get multipartStream(): ServerRequest.HttpServerRequest["multipartStream"] { + return Stream.fail( + new Multipart.MultipartError({ + reason: "InternalError", + cause: "Multipart stream not implemented in @effect/platform-fastify" + }) + ) + } + + get upgrade(): ServerRequest.HttpServerRequest["upgrade"] { + return Effect.fail( + new Error.RequestError({ + request: this, + reason: "Decode", + description: "Upgrade not implemented in @effect/platform-fastify" + }) + ) + } + + modify( + _options: { + readonly url?: string + readonly headers?: Headers.Headers + readonly remoteAddress?: string + } + ): ServerRequest.HttpServerRequest { + return this + } + + toString(): string { + return `FastifyServerRequest(${this.method} ${this.url})` + } + + toJSON(): unknown { + return IncomingMessage.inspect(this, { + _id: "@effect/platform-fastify/FastifyServerRequest", + method: this.method, + url: this.originalUrl + }) + } +} diff --git a/packages/platform-fastify/src/internal/fastifyRpcServer.ts b/packages/platform-fastify/src/internal/fastifyRpcServer.ts new file mode 100644 index 00000000000..be20cf8dc22 --- /dev/null +++ b/packages/platform-fastify/src/internal/fastifyRpcServer.ts @@ -0,0 +1,180 @@ +import type * as App from "@effect/platform/HttpApp" +import type * as Rpc from "@effect/rpc/Rpc" +import type * as RpcGroup from "@effect/rpc/RpcGroup" +import type * as RpcSerialization from "@effect/rpc/RpcSerialization" +import * as RpcServer from "@effect/rpc/RpcServer" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import type * as FastifyTypes from "fastify" +import * as FastifyHttpAppServer from "./fastifyHttpAppServer.js" + +/** @internal */ +export const toFastifyHandlerEffect = ( + group: RpcGroup.RpcGroup, + options?: { + readonly disableTracing?: boolean | undefined + readonly spanPrefix?: string | undefined + readonly spanAttributes?: Record | undefined + readonly disableFatalDefects?: boolean | undefined + readonly middleware?: ( + httpApp: App.Default + ) => App.Default + } +): Effect.Effect< + (request: FastifyTypes.FastifyRequest, reply: FastifyTypes.FastifyReply) => Promise, + never, + | Scope.Scope + | RpcSerialization.RpcSerialization + | Rpc.ToHandler + | Rpc.Context + | Rpc.Middleware +> => + Effect.gen(function*() { + const httpApp = yield* RpcServer.toHttpApp(group, options) + const finalApp = options?.middleware ? options.middleware(httpApp) : httpApp + return yield* FastifyHttpAppServer.toHandlerEffect(finalApp) + }) + +/** @internal */ +export const registerEffect = ( + fastify: FastifyTypes.FastifyInstance, + group: RpcGroup.RpcGroup, + options: { + readonly path: string + readonly disableTracing?: boolean | undefined + readonly spanPrefix?: string | undefined + readonly spanAttributes?: Record | undefined + readonly disableFatalDefects?: boolean | undefined + readonly middleware?: ( + httpApp: App.Default + ) => App.Default + } +): Effect.Effect< + void, + never, + | Scope.Scope + | RpcSerialization.RpcSerialization + | Rpc.ToHandler + | Rpc.Context + | Rpc.Middleware +> => + Effect.gen(function*() { + const handler = yield* toFastifyHandlerEffect(group, options) + + fastify.register((instance, _opts, done) => { + instance.removeAllContentTypeParsers() + instance.addContentTypeParser("*", (_req, _payload, parserDone) => { + parserDone(null) + }) + instance.post(options.path, handler) + done() + }) + }) + +/** @internal */ +export const register = ( + fastify: FastifyTypes.FastifyInstance, + group: RpcGroup.RpcGroup, + options: { + readonly path: string + readonly layer: Layer.Layer< + | Rpc.ToHandler + | Rpc.Middleware + | RpcSerialization.RpcSerialization, + LE + > + readonly disableTracing?: boolean | undefined + readonly spanPrefix?: string | undefined + readonly spanAttributes?: Record | undefined + readonly disableFatalDefects?: boolean | undefined + readonly middleware?: ( + httpApp: App.Default + ) => App.Default + readonly memoMap?: Layer.MemoMap + } +): { readonly dispose: () => Promise } => { + const { dispose, handler } = toFastifyHandler(group, options) + + fastify.register((instance, _opts, done) => { + instance.removeAllContentTypeParsers() + instance.addContentTypeParser("*", (_req, _payload, parserDone) => { + parserDone(null) + }) + instance.post(options.path, handler) + done() + }) + + return { dispose } +} + +/** @internal */ +export const toFastifyHandler = ( + group: RpcGroup.RpcGroup, + options: { + readonly layer: Layer.Layer< + | Rpc.ToHandler + | Rpc.Middleware + | RpcSerialization.RpcSerialization, + LE + > + readonly disableTracing?: boolean | undefined + readonly spanPrefix?: string | undefined + readonly spanAttributes?: Record | undefined + readonly disableFatalDefects?: boolean | undefined + readonly middleware?: ( + httpApp: App.Default + ) => App.Default + readonly memoMap?: Layer.MemoMap + } +): { + readonly handler: ( + request: FastifyTypes.FastifyRequest, + reply: FastifyTypes.FastifyReply + ) => Promise + readonly dispose: () => Promise +} => { + const scope = Effect.runSync(Scope.make()) + const dispose = () => Effect.runPromise(Scope.close(scope, Exit.void)) + + // Include Layer.scope so that scoped resources in the layer are properly managed + const fullLayer = Layer.mergeAll(options.layer, Layer.scope) + + type Handler = (request: FastifyTypes.FastifyRequest, reply: FastifyTypes.FastifyReply) => Promise + + let handlerCache: Handler | undefined + let handlerPromise: Promise | undefined + + function handler( + request: FastifyTypes.FastifyRequest, + reply: FastifyTypes.FastifyReply + ): Promise { + if (handlerCache) { + return handlerCache(request, reply) + } + if (!handlerPromise) { + // Build the handler by: + // 1. Building a runtime from the layer (with memoMap if provided) + // 2. Using that runtime to provide services to toFastifyHandlerEffect + // 3. Extending the scope we created so resources are tied to our dispose() + const buildEffect = Effect.gen(function*() { + const runtime = yield* (options.memoMap + ? Layer.toRuntimeWithMemoMap(fullLayer, options.memoMap) + : Layer.toRuntime(fullLayer)) + return yield* Effect.provide(toFastifyHandlerEffect(group, options), runtime) + }).pipe( + Effect.tap((h) => + Effect.sync(() => { + handlerCache = h + }) + ), + Scope.extend(scope) + ) + handlerPromise = Effect.runPromise(buildEffect) + } + return handlerPromise.then((f) => f(request, reply)) + } + + return { handler, dispose } as const +} diff --git a/packages/platform-fastify/test/FastifyRpcServer.test.ts b/packages/platform-fastify/test/FastifyRpcServer.test.ts new file mode 100644 index 00000000000..584634decbb --- /dev/null +++ b/packages/platform-fastify/test/FastifyRpcServer.test.ts @@ -0,0 +1,285 @@ +import { FetchHttpClient } from "@effect/platform" +import { FastifyRpcServer } from "@effect/platform-fastify" +import { Rpc, RpcClient, RpcGroup, RpcSchema, RpcSerialization } from "@effect/rpc" +import { assert, describe, it } from "@effect/vitest" +import { Cause, Chunk, Effect, Layer, Schema, Stream } from "effect" +import Fastify from "fastify" + +// Define test schema +class TestUser extends Schema.Class("TestUser")({ + id: Schema.String, + name: Schema.String +}) {} + +class TestRpcs extends RpcGroup.make( + Rpc.make("GetUser", { + success: TestUser, + payload: { id: Schema.String } + }), + Rpc.make("CreateUser", { + success: TestUser, + payload: { name: Schema.String } + }) +) {} + +// Streaming RPC schema +class StreamingRpcs extends RpcGroup.make( + Rpc.make("StreamUsers", { + success: RpcSchema.Stream({ + success: TestUser, + failure: Schema.Never + }), + payload: { count: Schema.Number } + }) +) {} + +// Helper to create a test layer with server +const makeTestLayer = ( + group: RpcGroup.RpcGroup, + handlersLayer: Layer.Layer> +) => + Layer.unwrapScoped( + Effect.gen(function*() { + const fastify = Fastify() + + const { dispose } = FastifyRpcServer.register(fastify, group, { + path: "/rpc", + layer: Layer.mergeAll(handlersLayer, RpcSerialization.layerNdjson) + }) + + yield* Effect.acquireRelease( + Effect.promise(() => fastify.listen({ port: 0 })), + () => + Effect.promise(async () => { + await dispose() + await fastify.close() + }) + ) + + const address = fastify.server.address() + const port = typeof address === "object" && address ? address.port : 0 + + return RpcClient.layerProtocolHttp({ + url: `http://localhost:${port}/rpc` + }).pipe( + Layer.provide(FetchHttpClient.layer), + Layer.provide(RpcSerialization.layerNdjson) + ) + }) + ) + +describe("FastifyRpcServer", { sequential: true }, () => { + it.scoped("should handle RPC requests", () => { + const users = new Map() + + const layer = makeTestLayer( + TestRpcs, + TestRpcs.toLayer( + Effect.sync(() => ({ + GetUser: ({ id }) => + Effect.sync(() => { + const user = users.get(id) + if (!user) throw new Error("User not found") + return user + }), + CreateUser: ({ name }) => + Effect.sync(() => { + const id = crypto.randomUUID() + const user = new TestUser({ id, name }) + users.set(id, user) + return user + }) + })) + ) + ) + + return Effect.gen(function*() { + const client = yield* RpcClient.make(TestRpcs) + + // Create a user + const created = yield* client.CreateUser({ name: "Alice" }) + assert.strictEqual(created.name, "Alice") + + // Get the user + const retrieved = yield* client.GetUser({ id: created.id }) + assert.strictEqual(retrieved.id, created.id) + assert.strictEqual(retrieved.name, "Alice") + }).pipe(Effect.provide(layer)) + }) + + it.scoped("should handle defects gracefully", () => { + const layer = makeTestLayer( + TestRpcs, + TestRpcs.toLayer( + Effect.sync(() => ({ + GetUser: () => Effect.die(new Error("User service unavailable")), + CreateUser: () => Effect.die(new Error("Create not allowed")) + })) + ) + ) + + return Effect.gen(function*() { + const client = yield* RpcClient.make(TestRpcs) + + // This should fail with a defect + const cause = yield* client.GetUser({ id: "123" }).pipe( + Effect.sandbox, + Effect.flip + ) + + // The cause should be a Die with the error + assert.isTrue(Cause.isDie(cause)) + }).pipe(Effect.provide(layer)) + }) + + it.scoped("should handle streaming RPC", () => { + const layer = makeTestLayer( + StreamingRpcs, + StreamingRpcs.toLayer( + Effect.sync(() => ({ + StreamUsers: ({ count }) => + Stream.fromIterable( + Array.from({ length: count }, (_, i) => new TestUser({ id: String(i + 1), name: `User ${i + 1}` })) + ) + })) + ) + ) + + return Effect.gen(function*() { + const client = yield* RpcClient.make(StreamingRpcs) + + // Stream 5 users + const usersChunk = yield* client.StreamUsers({ count: 5 }).pipe( + Stream.runCollect + ) + const users = Chunk.toReadonlyArray(usersChunk) + + assert.strictEqual(users.length, 5) + assert.strictEqual(users[0].name, "User 1") + assert.strictEqual(users[4].name, "User 5") + }).pipe(Effect.provide(layer)) + }) + + it.scoped("should work with toFastifyHandlerEffect", () => { + const users = new Map() + + const handlersLayer = TestRpcs.toLayer( + Effect.sync(() => ({ + GetUser: ({ id }) => + Effect.sync(() => { + const user = users.get(id) + if (!user) throw new Error("User not found") + return user + }), + CreateUser: ({ name }) => + Effect.sync(() => { + const id = crypto.randomUUID() + const user = new TestUser({ id, name }) + users.set(id, user) + return user + }) + })) + ) + + const serverLayer = Layer.unwrapScoped( + Effect.gen(function*() { + const handler = yield* FastifyRpcServer.toFastifyHandlerEffect(TestRpcs) + + const fastify = Fastify() + fastify.removeAllContentTypeParsers() + fastify.addContentTypeParser("*", (_req, _payload, done) => { + done(null) + }) + fastify.post("/rpc", handler) + + yield* Effect.acquireRelease( + Effect.promise(() => fastify.listen({ port: 0 })), + () => Effect.promise(() => fastify.close()) + ) + + const address = fastify.server.address() + const port = typeof address === "object" && address ? address.port : 0 + + return RpcClient.layerProtocolHttp({ + url: `http://localhost:${port}/rpc` + }).pipe( + Layer.provide(FetchHttpClient.layer), + Layer.provide(RpcSerialization.layerNdjson) + ) + }).pipe(Effect.provide(Layer.mergeAll(handlersLayer, RpcSerialization.layerNdjson))) + ) + + return Effect.gen(function*() { + const client = yield* RpcClient.make(TestRpcs) + + // Create a user + const created = yield* client.CreateUser({ name: "Bob" }) + assert.strictEqual(created.name, "Bob") + + // Get the user + const retrieved = yield* client.GetUser({ id: created.id }) + assert.strictEqual(retrieved.id, created.id) + assert.strictEqual(retrieved.name, "Bob") + }).pipe(Effect.provide(serverLayer)) + }) + + it.scoped("should work with registerEffect", () => { + const users = new Map() + + const handlersLayer = TestRpcs.toLayer( + Effect.sync(() => ({ + GetUser: ({ id }) => + Effect.sync(() => { + const user = users.get(id) + if (!user) throw new Error("User not found") + return user + }), + CreateUser: ({ name }) => + Effect.sync(() => { + const id = crypto.randomUUID() + const user = new TestUser({ id, name }) + users.set(id, user) + return user + }) + })) + ) + + const serverLayer = Layer.unwrapScoped( + Effect.gen(function*() { + const fastify = Fastify() + + yield* FastifyRpcServer.registerEffect(fastify, TestRpcs, { + path: "/rpc" + }) + + yield* Effect.acquireRelease( + Effect.promise(() => fastify.listen({ port: 0 })), + () => Effect.promise(() => fastify.close()) + ) + + const address = fastify.server.address() + const port = typeof address === "object" && address ? address.port : 0 + + return RpcClient.layerProtocolHttp({ + url: `http://localhost:${port}/rpc` + }).pipe( + Layer.provide(FetchHttpClient.layer), + Layer.provide(RpcSerialization.layerNdjson) + ) + }).pipe(Effect.provide(Layer.mergeAll(handlersLayer, RpcSerialization.layerNdjson))) + ) + + return Effect.gen(function*() { + const client = yield* RpcClient.make(TestRpcs) + + // Create a user + const created = yield* client.CreateUser({ name: "Charlie" }) + assert.strictEqual(created.name, "Charlie") + + // Get the user + const retrieved = yield* client.GetUser({ id: created.id }) + assert.strictEqual(retrieved.id, created.id) + assert.strictEqual(retrieved.name, "Charlie") + }).pipe(Effect.provide(serverLayer)) + }) +}) diff --git a/packages/platform-fastify/tsconfig.build.json b/packages/platform-fastify/tsconfig.build.json new file mode 100644 index 00000000000..36e343e155f --- /dev/null +++ b/packages/platform-fastify/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "build/esm", + "declarationDir": "build/dts" + }, + "include": ["src"], + "references": [{ "path": "../platform" }, { "path": "../rpc" }] +} diff --git a/packages/platform-fastify/tsconfig.examples.json b/packages/platform-fastify/tsconfig.examples.json new file mode 100644 index 00000000000..32873ed2cec --- /dev/null +++ b/packages/platform-fastify/tsconfig.examples.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.src.json", + "include": ["examples"], + "references": [{ "path": "./tsconfig.src.json" }] +} diff --git a/packages/platform-fastify/tsconfig.json b/packages/platform-fastify/tsconfig.json new file mode 100644 index 00000000000..a4c89654fef --- /dev/null +++ b/packages/platform-fastify/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "./tsconfig.src.json" }, + { "path": "./tsconfig.test.json" }, + { "path": "./tsconfig.examples.json" } + ] +} diff --git a/packages/platform-fastify/tsconfig.src.json b/packages/platform-fastify/tsconfig.src.json new file mode 100644 index 00000000000..c15327dd15e --- /dev/null +++ b/packages/platform-fastify/tsconfig.src.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "build/dts", + "declarationDir": "build/dts" + }, + "include": ["src"], + "references": [{ "path": "../platform" }, { "path": "../rpc" }] +} diff --git a/packages/platform-fastify/tsconfig.test.json b/packages/platform-fastify/tsconfig.test.json new file mode 100644 index 00000000000..c8dbcca5345 --- /dev/null +++ b/packages/platform-fastify/tsconfig.test.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.src.json", + "include": ["test"], + "references": [{ "path": "./tsconfig.src.json" }] +} diff --git a/packages/platform-fastify/vitest.config.ts b/packages/platform-fastify/vitest.config.ts new file mode 100644 index 00000000000..1d58cfef6fb --- /dev/null +++ b/packages/platform-fastify/vitest.config.ts @@ -0,0 +1,7 @@ +import { mergeConfig } from "vitest/config" +import shared from "../../vitest.shared.js" + +export default mergeConfig( + shared, + {} +) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1cb9486883f..edac1e11654 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -559,6 +559,25 @@ importers: version: link:../effect publishDirectory: dist + packages/platform-fastify: + devDependencies: + '@effect/platform': + specifier: workspace:^ + version: link:../platform + '@effect/rpc': + specifier: workspace:^ + version: link:../rpc + '@types/node': + specifier: ^22.16.4 + version: 22.16.4 + effect: + specifier: workspace:^ + version: link:../effect + fastify: + specifier: ^5.6.2 + version: 5.6.2 + publishDirectory: dist + packages/platform-node: dependencies: '@effect/platform-node-shared': @@ -1900,10 +1919,28 @@ packages: resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==} 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@2.1.1': resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@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==} + '@grpc/grpc-js@1.13.4': resolution: {integrity: sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==} engines: {node: '>=12.10.0'} @@ -2439,6 +2476,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'} @@ -3127,6 +3167,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@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -3154,6 +3197,14 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + 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==} @@ -3280,6 +3331,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'} + autolinker@0.28.1: resolution: {integrity: sha512-zQAFO1Dlsn69eXaO6+7YZc+v84aquQKbwpzCE3L0stj56ERn9hutFxPopViLjo9G+rWwjozRhgS5KJ25Xy19cQ==} @@ -3287,6 +3342,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + avvio@9.1.0: + resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} + aws-ssl-profiles@1.1.2: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} @@ -4277,6 +4335,9 @@ packages: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} engines: {node: '>=8.0.0'} + 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==} @@ -4293,12 +4354,21 @@ 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-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + fastify@5.6.2: + resolution: {integrity: sha512-dPugdGnsvYkBlENLhCgX8yhyGCsCPrpA8lFWbTNU428l+YOnLgYHR69hzV8HWPC79n536EqzqQtvhtdaCE0dKg==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -4348,6 +4418,10 @@ packages: find-my-way-ts@0.1.6: resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} + find-my-way@9.3.0: + resolution: {integrity: sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==} + engines: {node: '>=20'} + find-up@3.0.0: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} @@ -4681,6 +4755,10 @@ packages: resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==} engines: {node: '>=12.22.0'} + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + is-alphabetical@1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} @@ -5041,6 +5119,9 @@ 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==} @@ -5119,6 +5200,9 @@ packages: cpu: [x64, arm64, wasm32] os: [darwin, linux, win32] + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + lighthouse-logger@1.4.2: resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} @@ -5608,6 +5692,10 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -5823,6 +5911,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 + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -5944,6 +6042,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'} @@ -5984,6 +6088,9 @@ packages: queue@6.0.2: resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + 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==} @@ -6057,6 +6164,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + recast@0.23.11: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} @@ -6138,6 +6249,10 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + 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'} @@ -6146,6 +6261,9 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -6186,6 +6304,13 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + 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'} + safe-stringify@1.2.0: resolution: {integrity: sha512-C+LbapLbyGhP/WeMTrnYhIPjUoNTXZ/A3Znli8D5iF+IZXrDlgvfruykOq/bZ/5ncGy/K6RsavHlkirgWDFNdA==} engines: {node: '>=16'} @@ -6201,6 +6326,9 @@ packages: scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -6229,6 +6357,9 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -6315,6 +6446,9 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -6554,6 +6688,9 @@ packages: text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + throat@5.0.0: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} @@ -6615,6 +6752,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -7971,8 +8112,31 @@ snapshots: '@eslint/core': 0.15.1 levn: 0.4.1 + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.0.6 + '@fastify/busboy@2.1.1': {} + '@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.3.0 + '@grpc/grpc-js@1.13.4': dependencies: '@grpc/proto-loader': 0.7.15 @@ -8485,6 +8649,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 @@ -8542,7 +8708,7 @@ snapshots: dependencies: '@react-native/dev-middleware': 0.80.1 chalk: 4.1.2 - debug: 4.4.1 + debug: 4.4.3 invariant: 2.2.4 metro: 0.82.5 metro-config: 0.82.5 @@ -8562,7 +8728,7 @@ snapshots: chrome-launcher: 0.15.2 chromium-edge-launcher: 0.2.0 connect: 3.7.0 - debug: 4.4.1 + debug: 4.4.3 invariant: 2.2.4 nullthrows: 1.1.1 open: 7.4.2 @@ -9263,6 +9429,8 @@ snapshots: dependencies: event-target-shim: 5.0.1 + abstract-logging@2.0.1: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -9280,6 +9448,10 @@ snapshots: agent-base@7.1.4: {} + 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 @@ -9433,6 +9605,8 @@ snapshots: async@3.2.6: {} + atomic-sleep@1.0.0: {} + autolinker@0.28.1: dependencies: gulp-header: 1.8.12 @@ -9441,6 +9615,11 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + avvio@9.1.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.19.1 + aws-ssl-profiles@1.1.2: {} aws4fetch@1.0.20: {} @@ -10532,6 +10711,8 @@ snapshots: dependencies: pure-rand: 6.1.0 + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -10548,10 +10729,41 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + 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.0.6 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + fast-uri@3.0.6: {} + 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.2 + toad-cache: 3.7.0 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -10622,6 +10834,12 @@ snapshots: find-my-way-ts@0.1.6: {} + find-my-way@9.3.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.0.0 + find-up@3.0.0: dependencies: locate-path: 3.0.0 @@ -10982,6 +11200,8 @@ snapshots: transitivePeerDependencies: - supports-color + ipaddr.js@2.3.0: {} + is-alphabetical@1.0.4: {} is-alphanumerical@1.0.4: @@ -11366,6 +11586,10 @@ 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: {} json-schema-traverse@1.0.0: {} @@ -11452,6 +11676,12 @@ snapshots: '@libsql/linux-x64-musl': 0.4.7 '@libsql/win32-x64-msvc': 0.4.7 + light-my-request@6.6.0: + dependencies: + cookie: 1.0.2 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + lighthouse-logger@1.4.2: dependencies: debug: 2.6.9 @@ -11676,7 +11906,7 @@ snapshots: metro-file-map@0.82.5: dependencies: - debug: 4.4.1 + debug: 4.4.3 fb-watchman: 2.0.2 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -11772,7 +12002,7 @@ snapshots: chalk: 4.1.2 ci-info: 2.0.0 connect: 3.7.0 - debug: 4.4.1 + debug: 4.4.3 error-stack-parser: 2.1.4 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -12059,6 +12289,8 @@ snapshots: obuf@1.1.2: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.3.0: dependencies: ee-first: 1.1.1 @@ -12281,6 +12513,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 + pirates@4.0.7: {} pkg-dir@3.0.0: @@ -12399,6 +12651,10 @@ snapshots: process-nextick-args@2.0.1: {} + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + process@0.11.10: {} promise-limit@2.7.0: {} @@ -12449,6 +12705,8 @@ snapshots: dependencies: inherits: 2.0.4 + quick-format-unescaped@4.0.4: {} + quote-unquote@1.0.0: {} randomatic@3.1.1: @@ -12582,6 +12840,8 @@ snapshots: picomatch: 2.3.1 optional: true + real-require@0.2.0: {} + recast@0.23.11: dependencies: ast-types: 0.16.1 @@ -12659,10 +12919,14 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + ret@0.5.0: {} + retry@0.12.0: {} reusify@1.1.0: {} + rfdc@1.4.1: {} + rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -12727,6 +12991,12 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-regex2@5.0.0: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + safe-stringify@1.2.0: {} safer-buffer@2.1.2: {} @@ -12737,6 +13007,8 @@ snapshots: scheduler@0.26.0: {} + secure-json-parse@4.1.0: {} + semver@5.7.2: {} semver@6.3.1: {} @@ -12774,6 +13046,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -12896,6 +13170,10 @@ snapshots: slash@3.0.0: {} + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -13217,6 +13495,10 @@ snapshots: dependencies: b4a: 1.6.7 + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + throat@5.0.0: {} through2@2.0.5: @@ -13263,6 +13545,8 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toidentifier@1.0.1: {} toml@3.0.0: {}