`makeTrackExecutionUsage` lived in its own `services/autumn-tracker.ts` only to dodge the `api/layers.ts` → `@tanstack/react-start` import cycle that breaks vitest-pool-workers. It baked fire-and-forget semantics + error logging into the function shape — callers got back a `(id) => void` and couldn't compose or test the billing call. Moves the tracker onto the service itself as `AutumnService.trackExecution` (Effect shape, error channel `never`), matching the existing `use` method pattern. Callers now `Effect.runFork(autumn.trackExecution(id))` at the single boundary in `execution-stack.ts`, which is honest about the fire-and-forget choice happening at that layer — not in the service. - `services/autumn.ts`: adds `trackExecution` to `IAutumnService`. Errors are caught and logged; the returned Effect never fails. Wrapped in an `autumn.trackExecution` span so traces pick it up the same way the existing `use` calls do. - `services/execution-stack.ts`: inlines the `Effect.runFork` at the sole caller; drops the `autumn-tracker` import. - `api/autumn.ts`: drops the re-export that pointed at the old file. - Deletes `services/autumn-tracker.ts`.
Unified · Split
| @@ -8,12 +8,6 @@ import { AutumnService } from "../services/autumn"; · 7 unmodified lines | ||
| 8 | 8 | import { HttpResponseError, isServerError, toErrorServerResponse } from "./error-response"; |
| 9 | 9 | import { SharedServices } from "./layers"; |
| 10 | 10 | |
| 11 | −// Re-export so existing callers don't have to change their import path. | |
| 12 | −// The actual implementation moved to `services/autumn-tracker.ts` to break | |
| 13 | −// a DO → api/* → tanstack-start import cycle that crashed module load | |
| 14 | −// under vitest-pool-workers. | |
| 15 | −export { makeTrackExecutionUsage } from "../services/autumn-tracker"; | |
| 16 | − | |
| 17 | 11 | const handleAutumnRequestEffect = Effect.gen(function* () { |
| 18 | 12 | const request = yield* HttpServerRequest.HttpServerRequest; |
| 19 | 13 | const webRequest = yield* Effect.mapError( |
| @@ -1,38 +0,0 @@ | ||
| 1 | −// --------------------------------------------------------------------------- | |
| 2 | −// Fire-and-forget execution usage tracker against Autumn. | |
| 3 | −// --------------------------------------------------------------------------- | |
| 4 | −// | |
| 5 | −// Lives in `services/` rather than `api/` because callers on the Durable | |
| 6 | −// Object path (`mcp-session.ts` → `execution-stack.ts`) transitively pull | |
| 7 | −// it in, and anything under `api/` eventually re-exports from | |
| 8 | −// `api/layers.ts`, which imports `auth/handlers.ts` → `@tanstack/react-start` | |
| 9 | −// — that pulls the `#tanstack-start-plugin-adapters` subpath specifier | |
| 10 | −// into Vite's resolver and breaks vitest-pool-workers module load. | |
| 11 | −// | |
| 12 | −// This file has exactly one responsibility, no HTTP concerns, and imports | |
| 13 | −// nothing from `api/`. | |
| 14 | −// --------------------------------------------------------------------------- | |
| 15 | − | |
| 16 | −import { Effect } from "effect"; | |
| 17 | − | |
| 18 | −import type { AutumnService } from "./autumn"; | |
| 19 | − | |
| 20 | −export const makeTrackExecutionUsage = (autumn: AutumnService["Type"]) => { | |
| 21 | − return (organizationId: string): void => { | |
| 22 | − autumn | |
| 23 | − .use((client) => | |
| 24 | − client.track({ | |
| 25 | − customerId: organizationId, | |
| 26 | − featureId: "executions", | |
| 27 | − value: 1, | |
| 28 | − }), | |
| 29 | − ) | |
| 30 | − .pipe( | |
| 31 | − Effect.catchAll((err) => { | |
| 32 | − console.error("[billing] track failed:", err); | |
| 33 | − return Effect.void; | |
| 34 | − }), | |
| 35 | − Effect.runFork, | |
| 36 | − ); | |
| 37 | − }; | |
| 38 | −}; | |
| @@ -21,6 +21,13 @@ export class AutumnError extends Data.TaggedError("AutumnError")<{ · 20 unmodified lines | ||
| 21 | 21 | |
| 22 | 22 | export type IAutumnService = Readonly<{ |
| 23 | 23 | use: <A>(fn: (client: Autumn) => Promise<A>) => Effect.Effect<A, AutumnError, never>; |
| 24 | + /** | |
| 25 | + * Fire-and-forget-safe execution usage tracker. Errors are caught and | |
| 26 | + * logged; the returned Effect never fails. Callers typically | |
| 27 | + * `Effect.runFork` it at the boundary so the billing call can't stall a | |
| 28 | + * user-facing request. | |
| 29 | + */ | |
| 30 | + trackExecution: (organizationId: string) => Effect.Effect<void, never, never>; | |
| 24 | 31 | }>; |
| 25 | 32 | |
| 26 | 33 | // --------------------------------------------------------------------------- |
| @@ -31,9 +38,13 @@ const make = Effect.sync(() => { · 4 unmodified lines | ||
| 31 | 38 | const secretKey = server.AUTUMN_SECRET_KEY; |
| 32 | 39 | |
| 33 | 40 | if (!secretKey) { |
| 41 | + const notConfigured = Effect.die( | |
| 42 | + new Error("Autumn not configured — AUTUMN_SECRET_KEY is empty"), | |
| 43 | + ); | |
| 34 | 44 | return { |
| 35 | − use: () => Effect.die(new Error("Autumn not configured — AUTUMN_SECRET_KEY is empty")), | |
| 36 | − } as IAutumnService; | |
| 45 | + use: () => notConfigured, | |
| 46 | + trackExecution: () => Effect.void, | |
| 47 | + } satisfies IAutumnService; | |
| 37 | 48 | } |
| 38 | 49 | |
| 39 | 50 | const client = new Autumn({ secretKey }); |
| @@ -44,7 +55,19 @@ const make = Effect.sync(() => { · 4 unmodified lines | ||
| 44 | 55 | catch: (cause) => new AutumnError({ cause }), |
| 45 | 56 | }).pipe(Effect.withSpan(`autumn.${fn.name ?? "use"}`)); |
| 46 | 57 | |
| 47 | − return { use } satisfies IAutumnService; | |
| 58 | + const trackExecution = (organizationId: string) => | |
| 59 | + use((c) => | |
| 60 | + c.track({ customerId: organizationId, featureId: "executions", value: 1 }), | |
| 61 | + ).pipe( | |
| 62 | + Effect.asVoid, | |
| 63 | + Effect.tapError((err) => | |
| 64 | + Effect.sync(() => console.error("[billing] track failed:", err)), | |
| 65 | + ), | |
| 66 | + Effect.orElse(() => Effect.void), | |
| 67 | + Effect.withSpan("autumn.trackExecution"), | |
| 68 | + ); | |
| 69 | + | |
| 70 | + return { use, trackExecution } satisfies IAutumnService; | |
| 48 | 71 | }); |
| 49 | 72 | |
| 50 | 73 | export class AutumnService extends Context.Tag("@executor/cloud/AutumnService")< |
| @@ -12,7 +12,6 @@ import { makeDynamicWorkerExecutor } from "@executor/runtime-dynamic-worker"; · 11 unmodified lines | ||
| 12 | 12 | |
| 13 | 13 | import { withExecutionUsageTracking } from "../api/execution-usage"; |
| 14 | 14 | import { AutumnService } from "./autumn"; |
| 15 | −import { makeTrackExecutionUsage } from "./autumn-tracker"; | |
| 16 | 15 | import { createScopedExecutor } from "./executor"; |
| 17 | 16 | |
| 18 | 17 | export const makeExecutionStack = (organizationId: string, organizationName: string) => |
| @@ -23,7 +22,7 @@ export const makeExecutionStack = (organizationId: string, organizationName: str · 4 unmodified lines | ||
| 23 | 22 | const engine = withExecutionUsageTracking( |
| 24 | 23 | organizationId, |
| 25 | 24 | createExecutionEngine({ executor, codeExecutor }), |
| 26 | − makeTrackExecutionUsage(autumn), | |
| 25 | + (orgId) => Effect.runFork(autumn.trackExecution(orgId)), | |
| 27 | 26 | ); |
| 28 | 27 | return { executor, engine }; |
| 29 | 28 | }); |