| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Schedule from "effect/Schedule"; |
| 3 | import * as Semaphore from "effect/Semaphore"; |
| 4 | |
| 5 | const 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 | */ |
| 12 | export 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 | */ |
| 39 | const environmentConfigGates = new Map<string, Semaphore.Semaphore>(); |
| 40 | |
| 41 | export 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 | }; |