/** * Redeeming a Codex reset credit is an account-level action: instances that * share the directory holding `auth.json` share the credit, so their * redemptions must serialise on that directory, not the instance. This * service keeps one lock and one pending idempotency key per account key so * overlapping confirmations from any instance queue rather than spending two * credits, and a retry after a timeout re-sends the same attempt. * * @module provider/Layers/codexResetCredit */ import type { ProviderConsumeResetCreditOutcome } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import type * as PlatformError from "effect/PlatformError"; import * as Ref from "effect/Ref"; import * as Semaphore from "effect/Semaphore"; /** * Bounded so a hung app-server cannot hold the account lock forever; the * timeout interrupts the scoped request, which kills the process, and the * kept idempotency key makes the user's retry safe. */ export const CODEX_RESET_CREDIT_TIMEOUT = Duration.seconds(20); interface AccountRedemptionState { readonly lock: Semaphore.Semaphore; readonly pendingKey: Ref.Ref; } export class CodexResetCreditCoordinator extends Context.Service< CodexResetCreditCoordinator, { /** * Run `consume` under the account's lock with a stable idempotency key. * The key is cleared only when Codex reports an outcome; a failure * (timeout included) keeps it so the next attempt is the same attempt. */ readonly redeem: ( accountKey: string, consume: (idempotencyKey: string) => Effect.Effect, ) => Effect.Effect; } >()("t3/provider/Layers/codexResetCredit/CodexResetCreditCoordinator") {} /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { const crypto = yield* Crypto.Crypto; const statesRef = yield* Ref.make>(new Map()); // Get-or-create through one Ref.modify so two first redemptions for the // same account cannot each install their own lock. const stateFor = Effect.fn("CodexResetCreditCoordinator.stateFor")(function* ( accountKey: string, ) { const existing = (yield* Ref.get(statesRef)).get(accountKey); if (existing) return existing; const candidate = { lock: yield* Semaphore.make(1), pendingKey: yield* Ref.make(null), }; return yield* Ref.modify(statesRef, (states) => { const current = states.get(accountKey); if (current) return [current, states] as const; const next = new Map(states); next.set(accountKey, candidate); return [candidate, next] as const; }); }); const redeem: CodexResetCreditCoordinator["Service"]["redeem"] = (accountKey, consume) => Effect.gen(function* () { const state = yield* stateFor(accountKey); return yield* state.lock.withPermits(1)( Effect.gen(function* () { const existing = yield* Ref.get(state.pendingKey); const idempotencyKey = existing ?? (yield* crypto.randomUUIDv4); yield* Ref.set(state.pendingKey, idempotencyKey); const outcome = yield* consume(idempotencyKey); yield* Ref.set(state.pendingKey, null); return outcome; }), ); }); return { redeem } satisfies CodexResetCreditCoordinator["Service"]; }); export const layer = Layer.effect(CodexResetCreditCoordinator, make); /** * Self-contained for tests: a counter-backed Crypto so keys are deterministic * and distinct without the platform layer. */ export const layerTest = Layer.effect( CodexResetCreditCoordinator, Effect.gen(function* () { let counter = 0; return yield* make.pipe( Effect.provideService( Crypto.Crypto, Crypto.make({ randomBytes: (size) => { counter += 1; return new Uint8Array(size).fill(counter); }, digest: (_algorithm, data) => Effect.succeed(data), }), ), ); }), );