(organizationId: string)
| 127 | const now = options?.now ?? Date.now; |
| 128 | |
| 129 | const decide = (organizationId: string): Effect.Effect<GateDecision> => |
| 130 | Effect.suspend(() => { |
| 131 | const windowId = Math.floor(now() / windowMs); |
| 132 | return increment(organizationId, windowId).pipe( |
| 133 | Effect.timeoutOrElse({ |
| 134 | duration: `${timeoutMs} millis`, |
| 135 | orElse: () => Effect.fail(new RateLimitCheckTimeoutError({ timeoutMs })), |
| 136 | }), |
| 137 | Effect.map( |
| 138 | (count): GateDecision => |
| 139 | count > limit |
| 140 | ? { |
| 141 | blocked: true, |
| 142 | error: new ExecutionRateLimitExceededError({ |
| 143 | organizationId, |
| 144 | message: RATE_LIMIT_BLOCKED_MESSAGE, |
| 145 | }), |
| 146 | } |
| 147 | : { blocked: false }, |
| 148 | ), |
| 149 | // FAIL OPEN: the backstop must never block executions because its |
| 150 | // counter is unreachable or slow. |
| 151 | Effect.catch((error: unknown) => |
| 152 | Effect.gen(function* () { |
| 153 | yield* Effect.sync(() => { |
| 154 | console.warn("[rate-limit] execution rate limit check failed open:", error); |
| 155 | }); |
| 156 | yield* captureCauseEffect(error); |
| 157 | return { blocked: false } as const satisfies GateDecision; |
| 158 | }), |
| 159 | ), |
| 160 | ); |
| 161 | }); |
| 162 | |
| 163 | return { |
| 164 | decorate: (organizationId, engine) => withPreExecutionGate(engine, decide(organizationId)), |
no test coverage detected