91 lines · 4.5 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// Metered execution stack: cloud's billing overlay over the execution seams.//// Cloud is the only host that meters executions, and BOTH of its execution// planes do so: the HTTP `/api/*` executor plane (api/protected.ts) and the MCP// session Durable Object (mcp/session-durable-object.ts). This module composes// the four billing-free `CloudExecutionSeamsLayer` seams with an `EngineDecorator`// that calls `AutumnService.trackExecution` after each execution.//// Keeping this in the cloud APP layer (not the neutral `engine/execution-stack.ts`)// is the billing-boundary line: the seams module names no billing service; the// metered overlay, provided ONLY here, does. Both planes import THIS layer and// supply `AutumnService` from their own context (boot for the HTTP plane,// `AutumnService.Default` locally for the DO).// --------------------------------------------------------------------------- import { Effect, Layer } from "effect"; import {  CodeExecutorProvider,  DbProvider,  EngineDecorator,  HostConfig,  PluginsProvider,  type EngineStackIdentity,} from "@executor-js/api/server"; import { AutumnService } from "../extensions/billing/service";import { hasNonFreeOrganizationSubscription } from "../extensions/billing/plans";import type { DbService } from "../db/db";import { CloudExecutionSeamsLayer } from "../engine/execution-stack";import { makeExecutionLimitGate } from "./execution-gate";import { makeCloudExecutionRateLimiter } from "./execution-rate-limit";import { withExecutionUsageTracking } from "./execution-usage"; // Usage-metering decorator bound to the billing service, plus the two// pre-execution guards this layer owns, ordered cheapest first://   1. rate-limit backstop (counter DO; free-tier abuse only — any org on a//      plan other than Free is exempt via the subscription lookup below)//   2. execution balance gate (Autumn check, cached 60s, fails open)//   3. usage tracking — fire-and-forget (`Effect.runFork`) so the billing//      call can't stall a user-facing execution.// The guards wrap OUTSIDE the tracker so a blocked execution is neither run// nor tracked. One gate/limiter instance per layer build: the balance cache is// shared by every engine this decorator produces (in the MCP session DO that's// the session's engines; on the HTTP plane the boot layer builds it once).export const CloudMeteringEngineDecorator: Layer.Layer<EngineDecorator, never, AutumnService> =  Layer.effect(EngineDecorator)(    Effect.map(AutumnService.asEffect(), (autumn): EngineDecorator["Service"] => {      const balanceGate = makeExecutionLimitGate((organizationId) =>        autumn.checkExecutionBalance(organizationId),      );      // The limiter's exemption. This is the billing coupling the limiter      // module deliberately avoids owning. Any active subscription other than      // Free exempts the org: the backstop is for free-tier abuse, and the      // narrower "is on a plan we sell today" predicate the org-creation gate      // uses capped grandfathered and pay-as-you-go customers. The limiter      // calls this only for orgs already over the cap and caches the answer,      // so the extra Autumn round trip stays off the hot path.      const rateLimiter = makeCloudExecutionRateLimiter((organizationId) =>        Effect.map(          autumn.use((client) => client.customers.getOrCreate({ customerId: organizationId })),          (customer) => hasNonFreeOrganizationSubscription(customer.subscriptions),        ),      );      return {        decorate: (engine, identity: EngineStackIdentity) =>          rateLimiter.decorate(            identity.organizationId,            balanceGate.decorate(              identity.organizationId,              withExecutionUsageTracking(identity.organizationId, engine, (organizationId) =>                Effect.runFork(autumn.trackExecution(organizationId)),              ),            ),          ),      };    }),  ); /** * The metered execution stack used by BOTH cloud planes (HTTP executor plane and * MCP session DO): the four billing-free `CloudExecutionSeamsLayer` seams plus * the billing decorator. Requires `DbService` (per-request Hyperdrive db) and * `AutumnService` (usage metering) from the surrounding context. */export const CloudMeteredExecutionStackLayer: Layer.Layer<  DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider | EngineDecorator,  never,  AutumnService | DbService> = Layer.merge(CloudExecutionSeamsLayer, CloudMeteringEngineDecorator);