| ... | ... | @@ -15,10 +15,13 @@ import { |
| 15 | 15 | type PreviewAutomationStreamEvent, |
| 16 | 16 | } from "@t3tools/contracts"; |
| 17 | 17 | import * as Effect from "effect/Effect"; |
| 18 | import * as Exit from "effect/Exit"; |
| 19 | import * as Cause from "effect/Cause"; |
| 18 | 20 | import * as Deferred from "effect/Deferred"; |
| 19 | 21 | import * as Fiber from "effect/Fiber"; |
| 20 | 22 | import * as Result from "effect/Result"; |
| 21 | 23 | import * as Stream from "effect/Stream"; |
| 24 | import * as TestClock from "effect/testing/TestClock"; |
| 22 | 25 | |
| 23 | 26 | import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts"; |
| 24 | 27 | |
| ... | ... | @@ -1105,3 +1108,132 @@ it.effect("accepts responses only from the host that received the request", () = |
| 1105 | 1108 | }), |
| 1106 | 1109 | ), |
| 1107 | 1110 | ); |
| 1111 | |
| 1112 | it.effect("evicts an unanswered host and lets later calls use a healthy runtime", () => |
| 1113 | Effect.scoped( |
| 1114 | Effect.gen(function* () { |
| 1115 | const broker = yield* makeBroker; |
| 1116 | const connected = yield* Deferred.make<string>(); |
| 1117 | const received = yield* Deferred.make<RoutedRequest>(); |
| 1118 | const otherReceived = yield* Deferred.make<void>(); |
| 1119 | const otherCompleted = yield* Deferred.make<void>(); |
| 1120 | const oldTab = PreviewTabId.make("tab-on-frozen-host"); |
| 1121 | const events = yield* broker.connect(makeHost()); |
| 1122 | const consumer = yield* Stream.runForEach(events, (event) => { |
| 1123 | if (event.type === "connected") return Deferred.succeed(connected, event.connectionId); |
| 1124 | const request = { ...event.request, connectionId: event.connectionId }; |
| 1125 | if (request.operation === "open") { |
| 1126 | return broker.respond({ |
| 1127 | clientId: "client-1", |
| 1128 | connectionId: event.connectionId, |
| 1129 | requestId: request.requestId, |
| 1130 | ok: true, |
| 1131 | result: { tabId: oldTab }, |
| 1132 | }); |
| 1133 | } |
| 1134 | return request.operation === "snapshot" |
| 1135 | ? Deferred.succeed(received, request) |
| 1136 | : Deferred.succeed(otherReceived, undefined); |
| 1137 | }).pipe(Effect.forkScoped); |
| 1138 | const connectionId = yield* Deferred.await(connected); |
| 1139 | yield* broker.invoke({ scope, operation: "open", input: {} }); |
| 1140 | |
| 1141 | const healthyConnected = yield* Deferred.make<void>(); |
| 1142 | const healthyRequests: RoutedRequest[] = []; |
| 1143 | const healthy = yield* broker.connect(makeHost({ clientId: "healthy" })); |
| 1144 | yield* Stream.runForEach(healthy, (event) => { |
| 1145 | if (event.type === "connected") return Deferred.succeed(healthyConnected, undefined); |
| 1146 | healthyRequests.push({ ...event.request, connectionId: event.connectionId }); |
| 1147 | return broker.respond({ |
| 1148 | clientId: "healthy", |
| 1149 | connectionId: event.connectionId, |
| 1150 | requestId: event.request.requestId, |
| 1151 | ok: true, |
| 1152 | result: "healthy", |
| 1153 | }); |
| 1154 | }).pipe(Effect.forkScoped); |
| 1155 | yield* Deferred.await(healthyConnected); |
| 1156 | |
| 1157 | const timedOut = yield* broker |
| 1158 | .invoke<void>({ |
| 1159 | scope, |
| 1160 | operation: "snapshot", |
| 1161 | input: {}, |
| 1162 | timeoutMs: 1_000, |
| 1163 | }) |
| 1164 | .pipe(Effect.flip, Effect.forkScoped); |
| 1165 | const lateRequest = yield* Deferred.await(received); |
| 1166 | const other = yield* broker |
| 1167 | .invoke<void>({ |
| 1168 | scope, |
| 1169 | operation: "evaluate", |
| 1170 | input: {}, |
| 1171 | timeoutMs: 10_000, |
| 1172 | }) |
| 1173 | .pipe( |
| 1174 | Effect.flip, |
| 1175 | Effect.tap(() => Deferred.succeed(otherCompleted, undefined)), |
| 1176 | Effect.forkScoped, |
| 1177 | ); |
| 1178 | yield* Deferred.await(otherReceived); |
| 1179 | yield* TestClock.adjust(1_000); |
| 1180 | expect(yield* Fiber.join(timedOut)).toMatchObject({ _tag: "PreviewAutomationTimeoutError" }); |
| 1181 | expect(yield* Deferred.isDone(otherCompleted)).toBe(true); |
| 1182 | expect(yield* Fiber.join(other)).toMatchObject({ |
| 1183 | _tag: "PreviewAutomationClientDisconnectedError", |
| 1184 | }); |
| 1185 | const consumerExit = yield* Fiber.await(consumer); |
| 1186 | expect(Exit.isFailure(consumerExit)).toBe(true); |
| 1187 | if (Exit.isFailure(consumerExit)) { |
| 1188 | expect(Cause.hasInterruptsOnly(consumerExit.cause)).toBe(true); |
| 1189 | } |
| 1190 | |
| 1191 | // Late traffic from the evicted connection cannot restore its assignment. |
| 1192 | yield* broker.respond({ |
| 1193 | clientId: "client-1", |
| 1194 | connectionId, |
| 1195 | requestId: lateRequest.requestId, |
| 1196 | ok: true, |
| 1197 | result: { tabId: oldTab }, |
| 1198 | }); |
| 1199 | yield* broker.focusHost({ |
| 1200 | clientId: "client-1", |
| 1201 | connectionId, |
| 1202 | environmentId: scope.environmentId, |
| 1203 | focused: true, |
| 1204 | }); |
| 1205 | expect(yield* broker.invoke({ scope, operation: "status", input: {} })).toBe("healthy"); |
| 1206 | expect(healthyRequests).toHaveLength(1); |
| 1207 | expect(healthyRequests[0]?.tabId).toBeUndefined(); |
| 1208 | }), |
| 1209 | ), |
| 1210 | ); |
| 1211 | |
| 1212 | it.effect("keeps a host that responds with an operation timeout", () => |
| 1213 | Effect.scoped( |
| 1214 | Effect.gen(function* () { |
| 1215 | const broker = yield* makeBroker; |
| 1216 | const connected = yield* Deferred.make<void>(); |
| 1217 | const events = yield* broker.connect(makeHost()); |
| 1218 | yield* Stream.runForEach(events, (event) => { |
| 1219 | if (event.type === "connected") return Deferred.succeed(connected, undefined); |
| 1220 | return broker.respond({ |
| 1221 | clientId: "client-1", |
| 1222 | connectionId: event.connectionId, |
| 1223 | requestId: event.request.requestId, |
| 1224 | ...(event.request.operation === "waitFor" |
| 1225 | ? { |
| 1226 | ok: false, |
| 1227 | error: { _tag: "PreviewAutomationTimeoutError", message: "Selector timed out" }, |
| 1228 | } |
| 1229 | : { ok: true, result: "responsive" }), |
| 1230 | }); |
| 1231 | }).pipe(Effect.forkScoped); |
| 1232 | yield* Deferred.await(connected); |
| 1233 | expect( |
| 1234 | yield* broker.invoke<void>({ scope, operation: "waitFor", input: {} }).pipe(Effect.flip), |
| 1235 | ).toMatchObject({ _tag: "PreviewAutomationTimeoutError" }); |
| 1236 | expect(yield* broker.invoke({ scope, operation: "status", input: {} })).toBe("responsive"); |
| 1237 | }), |
| 1238 | ), |
| 1239 | ); |