1import * as Effect from "effect/Effect";
2import * as Schedule from "effect/Schedule";
3import * as Semaphore from "effect/Semaphore";
4
5const projectAndEnvironmentCreateGate = Semaphore.makeUnsafe(1);
6
7/**
8 * Railway rejects project/environment creation within its 30-second creation
9 * window. Serialize these calls and retry a typed rejection once after 31s.
10 * Other mutation failures propagate so an ambiguous create is never replayed.
11 */
12export const waitOutCreateRateLimit = <
13 A,
14 E extends { readonly _tag: string },
15 R,
16>(
17 effect: Effect.Effect<A, E, R>,
18): Effect.Effect<A, E, R> =>
19 Semaphore.withPermits(
20 projectAndEnvironmentCreateGate,
21 1,
22 )(
23 effect.pipe(
24 Effect.retry({
25 while: (error) => error._tag === "RailwayRateLimited",
26 times: 1,
27 schedule: Schedule.spaced("31 seconds"),
28 }),
29 ),
30 );
31
32/**
33 * One in-flight environment-config mutation per environment. Railway's
34 * own IaC apply is a single `environmentPatchCommit` of the whole desired
35 * state. Alchemy splits that across Group / Bucket / ServiceDomain, and
36 * `serviceDomainCreate` races those patches — the API answers
37 * `Failed to create service domain, please try again`.
38 */
39const environmentConfigGates = new Map<string, Semaphore.Semaphore>();
40
41export const withEnvironmentConfigLock = <A, E, R>(
42 environmentId: string,
43 effect: Effect.Effect<A, E, R>,
44): Effect.Effect<A, E, R> => {
45 let gate = environmentConfigGates.get(environmentId);
46 if (gate === undefined) {
47 gate = Semaphore.makeUnsafe(1);
48 environmentConfigGates.set(environmentId, gate);
49 }
50 return Semaphore.withPermits(gate, 1)(effect);
51};