Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

abort fetch! #38

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions verwaltung/composables/auth/sse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ const invalidationCb: Record<string, (() => void)[]> = {};

const { authToken } = useAuthData();

export function onInvalidate(key: string[], cb: () => void) {
export function onInvalidate(
key: string[],
cb: () => void,
signal?: AbortSignal
) {
if (currentSource?.readyState === 2) {
createNewEventSource();
}
Expand All @@ -18,10 +22,17 @@ export function onInvalidate(key: string[], cb: () => void) {
}
});

onScopeDispose(() => {
const cleanup = () => {
key.forEach(k => {
invalidationCb[k].splice(invalidationCb[k].indexOf(cb), 1);
});
};

signal?.addEventListener('abort', cleanup);

onScopeDispose(() => {
cleanup();
signal?.removeEventListener('abort', cleanup);
});
}
let currentSource: EventSource | null = null;
Expand Down
88 changes: 71 additions & 17 deletions verwaltung/composables/reloadableData.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,92 @@
import { ref, onMounted, Ref, watchEffect } from 'vue';
import {
ref,
Ref,
shallowRef,
shallowReadonly,
DeepReadonly,
watch,
isRef,
ShallowRef,
onScopeDispose
} from 'vue';
import { onInvalidate } from '@/composables/auth';

export function useDataReload<T>(cb: () => Promise<T>, init?: T) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: Ref<T> = ref<T>() as any;
export function useDataReload<T, O>(
cb: (options: O) => Promise<T>,
args: Ref<O> | null,
{
init,
watchArgs = true,
invalidations = []
}: {
init?: T;
watchArgs?: boolean;
invalidations?: Ref<Array<string>> | Array<string>;
} = {}
) {
// Init Refs
const loading = ref(true);
const error = ref<null | Error>(null);
const data = shallowRef<T>() as unknown as ShallowRef<T>;
const error = shallowRef<null | Error>(null);

if (init) data.value = init;

async function reload(): Promise<T> {
let fetchAbort: AbortController;

async function reload() {
if (fetchAbort) fetchAbort.abort();

fetchAbort = new AbortController();

// Stop fetch on unmounted etc.
onScopeDispose(() => fetchAbort.abort());

try {
const val = await cb();
// any wird hier benötigt da signal eigentlich typenmäßig nicht existiert aber praktisch doch.
const val = await cb({
signal: fetchAbort.signal,
...(args?.value ?? {})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
data.value = val;
error.value = null;
loading.value = false;
return val;
} catch (ex) {
loading.value = false;
error.value = ex as Error;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return null as any;
}
}

onMounted(() => watchEffect(() => reload()));
// Watch argument change?
if (watchArgs && args) watch(args, reload);

if (isRef(invalidations)) {
// watch invalidations
let invalidateAbort = new AbortController();

onInvalidate(invalidations.value, reload, invalidateAbort.signal);

onScopeDispose(() => invalidateAbort.abort());

watch(invalidations, () => {
invalidateAbort.abort();
invalidateAbort = new AbortController();
onInvalidate(invalidations.value, reload, invalidateAbort.signal);
});
} else {
// Static invalidations
onInvalidate(invalidations, reload);
}

// Load data!
reload();

return {
data,
// Typenmäßig machen wir den return nur readable um bugs zu vermeiden. Wir machen daher auch nur ein shallow da die Datenstrucktur nicht komplett proxifiziert werden muss.
// Da der .value wert readonly sein sollte nutzen wir shallowReadonly.
data: shallowReadonly(data as Ref<DeepReadonly<T>>),
reload,
loading,
error,
nav: () => {
reload();
return true;
}
error
};
}
36 changes: 15 additions & 21 deletions verwaltung/pages/user/[id].vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';

import { onInvalidate } from '@/composables/auth';
import { useDataReload } from '@/composables/reloadableData';
import { toDateFormat } from '@/composables/date';

Expand All @@ -15,20 +14,15 @@ import loadUser from '@api/admin/user/_id.get';

const route = useRoute();

const {
data: user,
reload,
loading,
error,
nav
} = useDataReload(() =>
loadUser({
params: { id: route.params.id as string }
})
);
const args = computed(() => ({
params: { id: route.params.id as string }
}));

onInvalidate([`user:${route.params.id}`], () => reload());
onBeforeRouteUpdate(nav);
const invalidations = computed(() => [`user:${route.params.id}`]);

const { data, error, reload, loading } = useDataReload(loadUser, args, {
invalidations
});

const valid_until_date = ref<string>();
function extend() {
Expand Down Expand Up @@ -79,10 +73,10 @@ v-container(fluid)
p Ein fehler beim laden ist aufgetreten: {{error}}
v-btn(@click="reload") Nochmal versuchen
template(v-else)
h2 Benutzerverwaltung {{ user.user.username }} ({{ user.user.name }})
p Benutzer freigeschaltet bis: {{ toDateFormat(user.user.valid_until) }}
p Benutzer ist {{ user.user.is_admin ? '' : 'kein' }} Administrator
p E-Mail: {{user.user.email}}
h2 Benutzerverwaltung {{ data.user.username }} ({{ data.user.name }})
p Benutzer freigeschaltet bis: {{ toDateFormat(data.user.valid_until) }}
p Benutzer ist {{ data.user.is_admin ? '' : 'kein' }} Administrator
p E-Mail: {{data.user.email}}

FormDialog(title="Benutzer hinzufügen" @save="extend")
template(v-slot:activator="{ props }")
Expand All @@ -106,7 +100,7 @@ v-container(fluid)


v-list
v-list-item(v-for="recht in user.rechte" :key="recht.user_rechte_id" @click="")
v-list-item(v-for="recht in data.rechte" :key="recht.user_rechte_id" @click="")
v-list-item-title {{ recht.recht }} für {{ recht.recht_object_name }} (ID: {{ recht.recht_object_id }})
template(v-slot:append)
v-list-item-avatar(right)
Expand Down
10 changes: 4 additions & 6 deletions verwaltung/pages/user/index.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<script setup lang="ts">
import { reactive, toRaw } from 'vue';
import { reactive, toRaw, ref } from 'vue';

import { onInvalidate } from '@/composables/auth';
import { useDataReload } from '@/composables/reloadableData';

import { required } from '@/rules/requires';

import FormDialog from '@/components/FormDialog.vue';
Expand All @@ -17,7 +15,9 @@ const {
reload,
loading,
error
} = useDataReload(() => loadUserList(), []);
} = useDataReload(loadUserList, null, {
invalidations: ['user']
});

const newUserData = reactive({
username: '',
Expand All @@ -31,8 +31,6 @@ function saveNewUser() {
body: toRaw(newUserData)
});
}

onInvalidate(['user'], reload);
</script>
<template lang="pug">
v-container(fluid)
Expand Down
3 changes: 2 additions & 1 deletion vite-plugins/server-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default () => {
import { wrapFetchOptions } from '@/composables/api'
import { handleDate } from '@/composables/apiDateHandler'

export default async ({ params, query, body } = {}) => {
export default async ({ params, query, body, signal } = {}) => {
const url = new URL(\`${pathLit}\`, __API_BASE_URL__)

if(query) {
Expand All @@ -39,6 +39,7 @@ export default () => {

const res = await fetch(url.href, wrapFetchOptions({
method: '${METHOD}',
signal,
${
METHOD !== 'get'
? `headers: {
Expand Down