// ---------------------------------------------------------------------------
// Autumn billing service — wraps the autumn-js SDK with Effect
// ---------------------------------------------------------------------------
import * as Sentry from "@sentry/cloudflare";
import * as Cloudflare from "alchemy/Cloudflare/Workers/Runtime";
import { Autumn } from "autumn-js";
import { Context, Data, Effect, Layer } from "effect";
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
export class AutumnError extends Data.TaggedError("AutumnError")<{
message: string;
cause?: unknown;
}> {}
// ---------------------------------------------------------------------------
// Service interface
// ---------------------------------------------------------------------------
export type IAutumnService = Readonly<{
use: (fn: (client: Autumn) => Promise) => Effect.Effect;
/**
* Fire-and-forget-safe execution usage tracker. Errors are caught and
* logged; the returned Effect never fails. Callers can fork it inside the
* current Effect runtime so the billing call can't stall a user-facing
* request.
*/
trackExecution: (organizationId: string) => Effect.Effect;
}>;
// ---------------------------------------------------------------------------
// Implementation
// ---------------------------------------------------------------------------
const make = Effect.gen(function* () {
const env = yield* Cloudflare.WorkerEnvironment.typed();
const secretKey = env.AUTUMN_SECRET_KEY;
if (!secretKey) {
const notConfigured = Effect.fail(
new AutumnError({ message: "Autumn not configured: AUTUMN_SECRET_KEY is empty" }),
);
return {
use: () => notConfigured,
trackExecution: () => Effect.void,
} satisfies IAutumnService;
}
const client = new Autumn({ secretKey });
const use = (fn: (client: Autumn) => Promise) =>
Effect.tryPromise({
try: () => fn(client),
catch: (cause) => new AutumnError({ message: "Autumn SDK request failed", cause }),
}).pipe(Effect.withSpan(`autumn.${fn.name ?? "use"}`));
const trackExecution = Effect.fn("autumn.trackExecution")(function* (organizationId: string) {
yield* Effect.annotateCurrentSpan({ "autumn.customer.id": organizationId });
yield* use((c) =>
c.track({ customerId: organizationId, featureId: "executions", value: 1 }),
).pipe(
Effect.catchTag("AutumnError", (error) =>
Effect.gen(function* () {
// Silent billing data loss is worth paging on — autumn.trackExecution
// is fire-and-forget so the caller doesn't handle it themselves.
yield* Effect.sync(() => {
console.error("[billing] track failed:", error);
Sentry.captureException(error);
});
yield* Effect.annotateCurrentSpan({ "autumn.track.failed": true });
}),
),
);
});
return { use, trackExecution } satisfies IAutumnService;
});
export class AutumnService extends Context.Service()(
"@executor-js/cloud/AutumnService",
) {
static Default = Layer.effect(this)(make);
}