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 if (!directory) return task;
16 const failure = () =>
17 new ProviderSetupError({
18 instanceId,
19 operation: "credential-lock",
20 detail: "Could not lock the ChatGPT connection for an update. Try again.",
21 });
22 return Effect.scoped(
23 Effect.gen(function* () {
24 const compromised = yield* Deferred.make<never, ProviderSetupError>();
25 const services = yield* Effect.context<never>();
26 yield* Effect.acquireRelease(
27 Effect.tryPromise({
28 try: () =>
29 lock(NodePath.join(directory, `${key}.bin`), {
30 realpath: false,
31 stale: 120_000,
32 update: 10_000,
33 retries: { retries: 80, factor: 1, minTimeout: 500, maxTimeout: 500 },
34 onCompromised: () =>
35 Effect.runSyncWith(services)(Deferred.fail(compromised, failure())),
36 }),
37 catch: failure,
38 }),
39 (release) => Effect.promise(() => release()).pipe(Effect.ignore),
40 );
41 return yield* Effect.raceFirst(task, Deferred.await(compromised));
42 }),
43 );
44};