Magnetar stream status for notification view
ci/woodpecker/push/ociImagePush Pipeline failed Details

This commit is contained in:
Natty 2024-04-07 04:12:27 +02:00
parent d12b65fff7
commit ff3f0927fb
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
7 changed files with 130 additions and 16 deletions

View File

@ -28,11 +28,13 @@ async-trait = "0.1"
async-stream = "0.3" async-stream = "0.3"
axum = "0.7" axum = "0.7"
axum-extra = "0.9" axum-extra = "0.9"
base64 = "0.22"
cached = "0.47" cached = "0.47"
cfg-if = "1" cfg-if = "1"
chrono = "0.4" chrono = "0.4"
compact_str = "0.7" compact_str = "0.7"
dotenvy = "0.15" dotenvy = "0.15"
ed25519-dalek = "2.1"
either = "1.9" either = "1.9"
emojis = "0.6" emojis = "0.6"
futures = "0.3" futures = "0.3"
@ -42,6 +44,7 @@ headers = "0.4"
http = "1.0" http = "1.0"
hyper = "1.1" hyper = "1.1"
idna = "0.5" idna = "0.5"
indexmap = "2.2"
itertools = "0.12" itertools = "0.12"
lru = "0.12" lru = "0.12"
miette = "5.9" miette = "5.9"
@ -51,12 +54,14 @@ percent-encoding = "2.2"
quick-xml = "0.31" quick-xml = "0.31"
redis = "0.24" redis = "0.24"
regex = "1.9" regex = "1.9"
reqwest = "0.11" rsa = "0.9"
reqwest = "0.12"
sea-orm = "0.12" sea-orm = "0.12"
sea-orm-migration = "0.12" sea-orm-migration = "0.12"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
sha2 = "0.10"
strum = "0.25" strum = "0.25"
tera = { version = "1", default-features = false } tera = { version = "1", default-features = false }
thiserror = "1" thiserror = "1"

View File

@ -5,6 +5,8 @@
<MkError v-else-if="error" @retry="init()" /> <MkError v-else-if="error" @retry="init()" />
<div v-else-if="empty" key="_empty_" class="empty"> <div v-else-if="empty" key="_empty_" class="empty">
<MagStreamStatus @reconnect="reload()" />
<slot name="empty"> <slot name="empty">
<div class="_fullinfo"> <div class="_fullinfo">
<img <img
@ -18,6 +20,8 @@
</div> </div>
<div v-else ref="rootEl" class="list"> <div v-else ref="rootEl" class="list">
<MagStreamStatus @reconnect="reload()" />
<XList <XList
v-slot="{ item: notificationGroup }" v-slot="{ item: notificationGroup }"
class="elsfgstc" class="elsfgstc"
@ -89,6 +93,7 @@ import XNotification from "@/components/MagNotification.vue";
import XNotificationGroup from "@/components/MagNotificationGroup.vue"; import XNotificationGroup from "@/components/MagNotificationGroup.vue";
import XList from "@/components/MkDateSeparatedList.vue"; import XList from "@/components/MkDateSeparatedList.vue";
import XNote from "@/components/MagNote.vue"; import XNote from "@/components/MagNote.vue";
import MagStreamStatus from "@/components/MagStreamStatus.vue";
import { magStream, stream } from "@/stream"; import { magStream, stream } from "@/stream";
import { $i } from "@/account"; import { $i } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
@ -153,6 +158,7 @@ const onNotification = (notification: packed.PackNotification) => {
}; };
let notifStream: ((e: ChannelEvent) => void) | undefined; let notifStream: ((e: ChannelEvent) => void) | undefined;
let connection: Connection<Channels["main"]>; let connection: Connection<Channels["main"]>;
onMounted(() => { onMounted(() => {

View File

@ -0,0 +1,78 @@
<template>
<div
class="mag-connection-status"
v-if="state !== 'connected' || showConnected"
:class="state"
>
<span v-if="state === 'connected'">
{{ i18n.t("connectionStatus.connected") }}
</span>
<span v-if="state === 'exponentialBackoff' || state === 'initial'">
{{ i18n.t("connectionStatus.reconnecting") }}
<MkEllipsis />
</span>
<span v-else-if="state === 'failed' || state === 'closed'">
{{ i18n.t("connectionStatus.disconnected") }}
</span>
</div>
</template>
<script lang="ts" setup>
import { i18n } from "@/i18n";
import { magStream } from "@/stream";
import { MagChannelState } from "magnetar-common";
import { onMounted, onUnmounted, ref } from "vue";
defineProps<{
showConnected?: boolean
}>();
const state = ref<MagChannelState>(magStream.state);
let stateListener: (e: MagChannelState) => void = (e) => {
if (
state.value !== "initial" &&
state.value !== "connected" &&
e === "connected"
) {
emit("reconnect");
}
state.value = e;
};
onMounted(() => {
magStream.on("stateChange", stateListener);
});
onUnmounted(() => {
magStream.off("stateChange", stateListener);
});
const emit = defineEmits<{
(e: "reconnect"): void;
}>();
</script>
<style lang="scss" scoped>
.mag-connection-status {
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--infoBg);
color: var(--infoFg);
// Experimental
position: sticky;
z-index: 5;
top: 0;
&.closed, &.failed {
background-color: var(--infoWarnBg);
color: var(--error);
}
&.exponentialBackoff {
background-color: var(--infoWarnBg);
color: var(--infoWarnFg);
}
}
</style>

View File

@ -2199,3 +2199,8 @@ _experiments:
_dialog: _dialog:
charactersExceeded: "Max characters exceeded! Current: {current}/Limit: {max}" charactersExceeded: "Max characters exceeded! Current: {current}/Limit: {max}"
charactersBelow: "Not enough characters! Current: {current}/Limit: {min}" charactersBelow: "Not enough characters! Current: {current}/Limit: {min}"
connectionStatus:
connected: "Connected"
reconnecting: "Reconnecting"
disconnected: "Disconnected from the server"

View File

@ -13,7 +13,7 @@ import {
FrontendApiEndpoints, FrontendApiEndpoints,
} from "./fe-api"; } from "./fe-api";
import { MagEventChannel } from "./sse-listener"; import { MagEventChannel, MagChannelState } from "./sse-listener";
import * as types from "./types"; import * as types from "./types";
import * as packed from "./packed"; import * as packed from "./packed";
@ -43,6 +43,7 @@ export {
FrontendApiEndpoint, FrontendApiEndpoint,
FrontendApiEndpoints, FrontendApiEndpoints,
MagEventChannel, MagEventChannel,
MagChannelState,
types, types,
packed, packed,
endpoints, endpoints,

View File

@ -1,7 +1,12 @@
import { EventEmitter } from "eventemitter3"; import { EventEmitter } from "eventemitter3";
import { ChannelEvent } from "./types/ChannelEvent"; import { ChannelEvent } from "./types/ChannelEvent";
export type MagChannelState = "connected" | "exponentialBackoff" | "failed"; export type MagChannelState =
| "initial"
| "connected"
| "exponentialBackoff"
| "failed"
| "closed";
export class MagEventChannel extends EventEmitter<{ export class MagEventChannel extends EventEmitter<{
stateChange: MagChannelState; stateChange: MagChannelState;
@ -15,11 +20,12 @@ export class MagEventChannel extends EventEmitter<{
private readonly backoffFactor: number; private readonly backoffFactor: number;
private readonly backoffBase: number; private readonly backoffBase: number;
private readonly closePromise: Promise<"cancelled">; private readonly closePromise: Promise<"cancelled">;
private _state: MagChannelState;
public constructor( public constructor(
baseUrl: string, baseUrl: string,
token: string | null, token: string | null,
maxReconnectAttempts: number = 12, maxReconnectAttempts: number = 15,
backoffFactor: number = 1.618, backoffFactor: number = 1.618,
backoffBase: number = 500.0 backoffBase: number = 500.0
) { ) {
@ -30,6 +36,8 @@ export class MagEventChannel extends EventEmitter<{
this.maxAttempts = maxReconnectAttempts; this.maxAttempts = maxReconnectAttempts;
this.backoffFactor = backoffFactor; this.backoffFactor = backoffFactor;
this.backoffBase = backoffBase; this.backoffBase = backoffBase;
this._state = "initial";
this.updateState("initial");
this.closePromise = new Promise((resolve) => { this.closePromise = new Promise((resolve) => {
this.on("close", resolve); this.on("close", resolve);
}); });
@ -49,9 +57,18 @@ export class MagEventChannel extends EventEmitter<{
return cb; return cb;
} }
public get state(): MagChannelState {
return this._state;
}
private updateState(state: MagChannelState) {
this._state = state;
this.emit("stateChange", state);
}
private async connect() { private async connect() {
if (this.attempts >= this.maxAttempts) { if (this.attempts >= this.maxAttempts) {
this.emit("stateChange", "failed"); this.updateState("failed");
return; return;
} }
@ -73,7 +90,7 @@ export class MagEventChannel extends EventEmitter<{
response.status >= 500 || response.status >= 500 ||
response.body === null response.body === null
) { ) {
this.emit("stateChange", "exponentialBackoff"); this.updateState("exponentialBackoff");
setTimeout( setTimeout(
() => this.connect(), () => this.connect(),
this.backoffBase * Math.pow(this.backoffFactor, this.attempts) this.backoffBase * Math.pow(this.backoffFactor, this.attempts)
@ -83,7 +100,7 @@ export class MagEventChannel extends EventEmitter<{
} }
if (response.status >= 400 && response.status < 500) { if (response.status >= 400 && response.status < 500) {
this.emit("stateChange", "failed"); this.updateState("failed");
return; return;
} }
@ -91,7 +108,7 @@ export class MagEventChannel extends EventEmitter<{
const decoderStream = new TextDecoderStream(); const decoderStream = new TextDecoderStream();
const reader = response.body.pipeThrough(decoderStream).getReader(); const reader = response.body.pipeThrough(decoderStream).getReader();
this.emit("stateChange", "connected"); this.updateState("connected");
let buf = ""; let buf = "";
@ -101,11 +118,11 @@ export class MagEventChannel extends EventEmitter<{
if (res === "cancelled") break; if (res === "cancelled") break;
if (res.done) { if (res.done) {
this.emit("stateChange", "exponentialBackoff"); this.updateState("exponentialBackoff");
setTimeout( setTimeout(
() => this.connect(), () => this.connect(),
this.backoffBase * this.backoffBase *
Math.pow(this.backoffFactor, this.attempts) Math.pow(this.backoffFactor, this.attempts)
); );
this.attempts++; this.attempts++;
break; break;
@ -134,6 +151,8 @@ export class MagEventChannel extends EventEmitter<{
const data = JSON.parse(text) as ChannelEvent; const data = JSON.parse(text) as ChannelEvent;
this.emit("message", data); this.emit("message", data);
} }
this.updateState("closed");
} }
public async close() { public async close() {

View File

@ -1438,8 +1438,8 @@ packages:
'@types/node': 20.8.10 '@types/node': 20.8.10
dev: true dev: true
/@types/yauzl@2.10.2: /@types/yauzl@2.10.3:
resolution: {integrity: sha512-Km7XAtUIduROw7QPgvcft0lIupeG8a8rdKL8RiSyKvlE7dYY31fEn41HVuQsRFDuROA8tA4K2UVL+WdfFmErBA==} resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
requiresBuild: true requiresBuild: true
dependencies: dependencies:
'@types/node': 14.18.63 '@types/node': 14.18.63
@ -2705,8 +2705,8 @@ packages:
is-plain-object: 5.0.0 is-plain-object: 5.0.0
dev: true dev: true
/core-js@3.33.2: /core-js@3.36.1:
resolution: {integrity: sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ==} resolution: {integrity: sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==}
requiresBuild: true requiresBuild: true
dev: true dev: true
@ -3428,7 +3428,7 @@ packages:
get-stream: 5.2.0 get-stream: 5.2.0
yauzl: 2.10.0 yauzl: 2.10.0
optionalDependencies: optionalDependencies:
'@types/yauzl': 2.10.2 '@types/yauzl': 2.10.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true dev: true
@ -7733,7 +7733,7 @@ packages:
name: plyr name: plyr
version: 3.7.0 version: 3.7.0
dependencies: dependencies:
core-js: 3.33.2 core-js: 3.36.1
custom-event-polyfill: 1.0.7 custom-event-polyfill: 1.0.7
loadjs: 4.2.0 loadjs: 4.2.0
rangetouch: 2.0.1 rangetouch: 2.0.1