1// @effect-diagnostics nodeBuiltinImport:off - Credential leases coordinate Node server processes.
2import * as NodePath from "node:path";
3import { lock } from "proper-lockfile";
4import { ProviderSetupError, type ProviderInstanceId } from "@t3tools/contracts";
5import * as Deferred from "effect/Deferred";
6import * as Effect from "effect/Effect";
7
8/** Keep rotating tokens and their active profile atomic across servers sharing a secret store. */
9export const withChatGptSessionLock = <A, E, R>(
10 directory: string | undefined,
11 key: string,
12 instanceId: ProviderInstanceId,
13 task: Effect.Effect<A, E, R>,
14): Effect.Effect<A, E | ProviderSetupError, R> => {
15 // In-memory stores have no shared filesystem; the auth controller still serializes its callers.
16 if (!directory) return task;
17 const failure = () =>
18 new ProviderSetupError({
19 instanceId,
20 operation: "credential-lock",
21 detail: "Could not lock the ChatGPT connection for an update. Try again.",
22 });
23 return Effect.scoped(
24 Effect.gen(function* () {
25 const compromised = yield* Deferred.make<never, ProviderSetupError>();
26 const services = yield* Effect.context<never>();
27 yield* Effect.acquireRelease(
28 Effect.tryPromise({
29 try: () =>
30 lock(NodePath.join(directory, `${key}.bin`), {
31 realpath: false,
32 stale: 120_000,
33 update: 10_000,
34 retries: { retries: 80, factor: 1, minTimeout: 500, maxTimeout: 500 },
35 onCompromised: () =>
36 Effect.runSyncWith(services)(Deferred.fail(compromised, failure())),
37 }),
38 catch: failure,
39 }),
40 (release) => Effect.promise(() => release()).pipe(Effect.ignore),
41 );
42 return yield* Effect.raceFirst(task, Deferred.await(compromised));
43 }),
44 );
45};