Merge pull request #1602 from thecodingmachine/e2e_reconnect_tests

Adding a reconnect feature in case first Pusher request fails
This commit is contained in:
David Négrier 2021-12-06 18:44:18 +01:00 committed by GitHub
commit 6a8717c22f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1580 additions and 113 deletions

View file

@ -56,6 +56,7 @@
"queue-typescript": "^1.0.1",
"quill": "1.3.6",
"quill-delta-to-html": "^0.12.0",
"retry-axios": "^2.6.0",
"rxjs": "^6.6.3",
"simple-peer": "^9.11.0",
"socket.io-client": "^2.3.0",

View file

@ -1,8 +1,8 @@
<script lang="ts">
import { errorStore } from "../../Stores/ErrorStore";
import { errorStore, hasClosableMessagesInErrorStore } from "../../Stores/ErrorStore";
function close(): boolean {
errorStore.clearMessages();
errorStore.clearClosableMessages();
return false;
}
</script>
@ -11,12 +11,14 @@
<p class="nes-text is-error title">Error</p>
<div class="body">
{#each $errorStore as error}
<p>{error}</p>
<p>{error.message}</p>
{/each}
</div>
<div class="button-bar">
<button class="nes-btn is-error" on:click={close}>Close</button>
</div>
{#if $hasClosableMessagesInErrorStore}
<div class="button-bar">
<button class="nes-btn is-error" on:click={close}>Close</button>
</div>
{/if}
</div>
<style lang="scss">

View file

@ -0,0 +1,37 @@
import axios from "axios";
import * as rax from "retry-axios";
import { errorStore } from "../Stores/ErrorStore";
/**
* This instance of Axios will retry in case of an issue and display an error message as a HTML overlay.
*/
export const axiosWithRetry = axios.create();
axiosWithRetry.defaults.raxConfig = {
instance: axiosWithRetry,
retry: Infinity,
noResponseRetries: Infinity,
maxRetryAfter: 60_000,
// You can detect when a retry is happening, and figure out how many
// retry attempts have been made
onRetryAttempt: (err) => {
const cfg = rax.getConfig(err);
console.log(err);
console.log(cfg);
console.log(`Retry attempt #${cfg?.currentRetryAttempt} on URL '${err.config.url}'`);
errorStore.addErrorMessage("Unable to connect to WorkAdventure. Are you connected to internet?", {
closable: false,
id: "axios_retry",
});
},
};
axiosWithRetry.interceptors.response.use((res) => {
if (res.status < 400) {
errorStore.clearMessageById("axios_retry");
}
return res;
});
const interceptorId = rax.attach(axiosWithRetry);

View file

@ -10,6 +10,7 @@ import { _ServiceWorker } from "../Network/ServiceWorker";
import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore";
import { userIsConnected } from "../Stores/MenuStore";
import { analyticsClient } from "../Administration/AnalyticsClient";
import { axiosWithRetry } from "./AxiosUtils";
class ConnectionManager {
private localUser!: LocalUser;
@ -232,7 +233,7 @@ class ConnectionManager {
}
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
const data = await axiosWithRetry.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
this.localUser = new LocalUser(data.userUuid, [], data.email);
this.authToken = data.authToken;
if (!isBenchmark) {

View file

@ -1,7 +1,10 @@
import * as rax from "retry-axios";
import Axios from "axios";
import { CONTACT_URL, PUSHER_URL, DISABLE_ANONYMOUS, OPID_LOGIN_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable";
import type { CharacterTexture } from "./LocalUser";
import { localUserStore } from "./LocalUserStore";
import axios from "axios";
import { axiosWithRetry } from "./AxiosUtils";
export class MapDetail {
constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {}
@ -90,7 +93,7 @@ export class Room {
private async getMapDetail(): Promise<MapDetail | RoomRedirect> {
try {
const result = await Axios.get(`${PUSHER_URL}/map`, {
const result = await axiosWithRetry.get(`${PUSHER_URL}/map`, {
params: {
playUri: this.roomUrl.toString(),
authToken: localUserStore.getAuthToken(),

View file

@ -255,6 +255,9 @@ export class RoomConnection implements RoomConnection {
warningContainerStore.activateWarningContainer();
} else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed.
} else if (message.hasErrormessage()) {
const errorMessage = message.getErrormessage() as ErrorMessage;
console.error("An error occurred server side: " + errorMessage.getMessage());
} else {
throw new Error("Unknown message received");
}

View file

@ -269,7 +269,9 @@ export class GameScene extends DirtyScene {
// 127.0.0.1, localhost and *.localhost are considered secure, even on HTTP.
// So if we are in https, we can still try to load a HTTP local resource (can be useful for testing purposes)
// See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure
const url = new URL(file.src);
const base = new URL(window.location.href);
base.pathname = "";
const url = new URL(file.src, base.toString());
const host = url.host.split(":")[0];
if (
window.location.protocol === "https:" &&
@ -322,7 +324,6 @@ export class GameScene extends DirtyScene {
this.onMapLoad(data);
}
this.load.bitmapFont("main_font", "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
//eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.load as any).rexWebFont({
custom: {

View file

@ -3,6 +3,7 @@ import { Scene } from "phaser";
import { ErrorScene, ErrorSceneName } from "../Reconnecting/ErrorScene";
import { WAError } from "../Reconnecting/WAError";
import { waScaleManager } from "../Services/WaScaleManager";
import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene";
export const EntrySceneName = "EntryScene";
@ -17,6 +18,14 @@ export class EntryScene extends Scene {
});
}
// From the very start, let's preload images used in the ReconnectingScene.
preload() {
this.load.image(ReconnectingTextures.icon, "static/images/favicons/favicon-32x32.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(ReconnectingTextures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
}
create() {
gameManager
.init(this.scene)

View file

@ -4,6 +4,7 @@ import Sprite = Phaser.GameObjects.Sprite;
import Text = Phaser.GameObjects.Text;
import ScenePlugin = Phaser.Scenes.ScenePlugin;
import { WAError } from "./WAError";
import Axios from "axios";
export const ErrorSceneName = "ErrorScene";
enum Textures {
@ -36,7 +37,11 @@ export class ErrorScene extends Phaser.Scene {
preload() {
this.load.image(Textures.icon, "static/images/favicons/favicon-32x32.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(Textures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
if (!this.cache.bitmapFont.has("main_font")) {
// We put this inside a "if" because despite the cache, Phaser will make a query to the XML file. And if there is no connection (which
// is not unlikely given the fact we are in an error scene), this will cause an error.
this.load.bitmapFont(Textures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
}
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
}
@ -71,9 +76,9 @@ export class ErrorScene extends Phaser.Scene {
/**
* Displays the error page, with an error message matching the "error" parameters passed in.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public static showError(error: any, scene: ScenePlugin): void {
public static showError(error: unknown, scene: ScenePlugin): void {
console.error(error);
console.trace();
if (typeof error === "string" || error instanceof String) {
scene.start(ErrorSceneName, {
@ -86,9 +91,10 @@ export class ErrorScene extends Phaser.Scene {
subTitle: error.subTitle,
message: error.details,
});
} else if (error.response) {
} else if (Axios.isAxiosError(error) && error.response) {
// Axios HTTP error
// client received an error response (5xx, 4xx)
console.error("Axios error. Request:", error.request, " - Response: ", error.response);
scene.start(ErrorSceneName, {
title:
"HTTP " +
@ -98,9 +104,10 @@ export class ErrorScene extends Phaser.Scene {
subTitle: "An error occurred while accessing URL:",
message: error.response.config.url,
});
} else if (error.request) {
} else if (Axios.isAxiosError(error)) {
// Axios HTTP error
// client never received a response, or request never left
console.error("Axios error. No full HTTP response received. Request to URL:", error.config.url);
scene.start(ErrorSceneName, {
title: "Network error",
subTitle: error.message,

View file

@ -3,7 +3,7 @@ import Image = Phaser.GameObjects.Image;
import Sprite = Phaser.GameObjects.Sprite;
export const ReconnectingSceneName = "ReconnectingScene";
enum ReconnectingTextures {
export enum ReconnectingTextures {
icon = "icon",
mainFont = "main_font",
}
@ -41,7 +41,7 @@ export class ReconnectingScene extends Phaser.Scene {
"Connection lost. Reconnecting..."
);
const cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat");
const cat = this.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat");
this.anims.create({
key: "right",
frames: this.anims.generateFrameNumbers("cat", { start: 6, end: 8 }),

View file

@ -1,15 +1,27 @@
import { writable } from "svelte/store";
import { derived, writable } from "svelte/store";
interface ErrorMessage {
id: string | undefined;
closable: boolean; // Whether it can be closed by a user action or not
message: string | number | boolean | undefined;
}
/**
* A store that contains a list of error messages to be displayed.
*/
function createErrorStore() {
const { subscribe, set, update } = writable<string[]>([]);
const { subscribe, set, update } = writable<ErrorMessage[]>([]);
return {
subscribe,
addErrorMessage: (e: string | Error): void => {
update((messages: string[]) => {
addErrorMessage: (
e: string | Error,
options?: {
closable?: boolean;
id?: string;
}
): void => {
update((messages: ErrorMessage[]) => {
let message: string;
if (e instanceof Error) {
message = e.message;
@ -17,17 +29,35 @@ function createErrorStore() {
message = e;
}
if (!messages.includes(message)) {
messages.push(message);
if (!messages.find((errorMessage) => errorMessage.message === message)) {
messages.push({
message,
closable: options?.closable ?? true,
id: options?.id,
});
}
return messages;
});
},
clearMessages: (): void => {
set([]);
clearMessageById: (id: string): void => {
update((messages: ErrorMessage[]) => {
messages = messages.filter((message) => message.id !== id);
return messages;
});
},
clearClosableMessages: (): void => {
update((messages: ErrorMessage[]) => {
messages = messages.filter((message) => message.closable);
return messages;
});
},
};
}
export const errorStore = createErrorStore();
export const hasClosableMessagesInErrorStore = derived(errorStore, ($errorStore) => {
const closableMessage = $errorStore.find((errorMessage) => errorMessage.closable);
return !!closableMessage;
});

View file

@ -32,6 +32,7 @@ module.exports = {
rewrites: [{ from: /^_\/.*$/, to: "/index.html" }],
disableDotRule: true,
},
liveReload: process.env.LIVE_RELOAD != "0" && process.env.LIVE_RELOAD != "false",
},
module: {
rules: [

View file

@ -4872,6 +4872,11 @@ ret@~0.1.10:
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
retry-axios@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-2.6.0.tgz#d4dc5c8a8e73982e26a705e46a33df99a28723e0"
integrity sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==
retry@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"