| ... | ... | @@ -160,16 +160,30 @@ export function validateServerUpdateReadyEvent( |
| 160 | 160 | * each nudge is the pacer: a connection that fails instantly re-enters backoff |
| 161 | 161 | * immediately and would otherwise spin a tight retry loop. |
| 162 | 162 | * |
| 163 | * A newly restarted server can also reject the first environment credential. |
| 164 | * Authentication blocks need the same paced retry during this known restart; |
| 165 | * permission and configuration failures remain blocked. |
| 166 | * |
| 163 | 167 | * Callers fork this as a child of the update command so it is interrupted as |
| 164 | 168 | * soon as the update settles, whether it succeeds, fails, or times out. |
| 165 | 169 | */ |
| 166 | 170 | export function nudgeReconnectDuringUpdateRestart(input: { |
| 167 | | readonly stateChanges: Stream.Stream<{ readonly phase: string }, unknown>; |
| 171 | readonly stateChanges: Stream.Stream< |
| 172 | { |
| 173 | readonly phase: string; |
| 174 | readonly lastFailure?: { readonly reason: string } | null; |
| 175 | }, |
| 176 | unknown |
| 177 | >; |
| 168 | 178 | readonly retryNow: Effect.Effect<void>; |
| 169 | 179 | readonly interval?: Duration.Duration; |
| 170 | 180 | }): Effect.Effect<void> { |
| 171 | 181 | return input.stateChanges.pipe( |
| 172 | | Stream.filter((state) => state.phase === "backoff"), |
| 182 | Stream.filter( |
| 183 | (state) => |
| 184 | state.phase === "backoff" || |
| 185 | (state.phase === "blocked" && state.lastFailure?.reason === "authentication"), |
| 186 | ), |
| 173 | 187 | Stream.runForEach(() => |
| 174 | 188 | Effect.sleep(input.interval ?? Duration.seconds(1)).pipe(Effect.andThen(input.retryNow)), |
| 175 | 189 | ), |