feat: updating notifications system based on incoming request

This is making the frontend data logic a little complex
This commit is contained in:
2025-07-02 13:28:28 +01:00
parent 5cf0b66688
commit c62378c20a
3 changed files with 174 additions and 136 deletions

View File

@ -1,106 +1,113 @@
import {
type Accessor,
type Component,
type ParentProps,
createContext,
createMemo,
createResource,
useContext,
type Accessor,
type Component,
type ParentProps,
createContext,
createMemo,
createResource,
useContext,
} from "solid-js";
import { getUserImages } from "../network";
import { groupPropertiesWithImage } from "../utils/groupPropertiesWithImage";
export type ImageWithRawData = Awaited<
ReturnType<typeof getUserImages>
ReturnType<typeof getUserImages>
>["ImageProperties"][number] & {
rawData: string[];
rawData: string[];
};
type SearchImageStore = {
images: Accessor<ImageWithRawData[]>;
images: Accessor<ImageWithRawData[]>;
imagesWithProperties: Accessor<ReturnType<typeof groupPropertiesWithImage>>;
onRefetchImages: () => void;
imagesWithProperties: Accessor<ReturnType<typeof groupPropertiesWithImage>>;
processingImages: Accessor<
Awaited<ReturnType<typeof getUserImages>>["ProcessingImages"] | undefined
>;
onRefetchImages: () => void;
};
// How wonderfully functional
const getAllValues = (object: object): Array<string> => {
const loop = (acc: Array<string>, next: object): Array<string> => {
for (const [key, _value] of Object.entries(next)) {
if (key === "ID") {
continue;
}
const loop = (acc: Array<string>, next: object): Array<string> => {
for (const [key, _value] of Object.entries(next)) {
if (key === "ID") {
continue;
}
const value: unknown = _value;
switch (typeof value) {
case "object":
if (value != null) {
acc.push(...loop(acc, value));
}
break;
case "string":
case "number":
case "boolean":
acc.push(value.toString());
break;
default:
break;
}
}
const value: unknown = _value;
switch (typeof value) {
case "object":
if (value != null) {
acc.push(...loop(acc, value));
}
break;
case "string":
case "number":
case "boolean":
acc.push(value.toString());
break;
default:
break;
}
}
return acc;
};
return acc;
};
return loop([], object);
return loop([], object);
};
const SearchImageContext = createContext<SearchImageStore>();
export const SearchImageContextProvider: Component<ParentProps> = (props) => {
const [data, { refetch }] = createResource(getUserImages);
const [data, { refetch }] = createResource(getUserImages);
const imageData = createMemo<ImageWithRawData[]>(() => {
const d = data();
if (d == null) {
return [];
}
const imageData = createMemo<ImageWithRawData[]>(() => {
const d = data();
if (d == null) {
return [];
}
return d.ImageProperties.map((d) => ({
...d,
rawData: getAllValues(d),
}));
});
return d.ImageProperties.map((d) => ({
...d,
rawData: getAllValues(d),
}));
});
const imagesWithProperties = createMemo<
ReturnType<typeof groupPropertiesWithImage>
>(() => {
const d = data();
if (d == null) {
return {};
}
const processingImages = () => data()?.ProcessingImages ?? [];
return groupPropertiesWithImage(d);
});
const imagesWithProperties = createMemo<
ReturnType<typeof groupPropertiesWithImage>
>(() => {
const d = data();
if (d == null) {
return {};
}
return (
<SearchImageContext.Provider
value={{
images: imageData,
imagesWithProperties: imagesWithProperties,
onRefetchImages: refetch,
}}
>
{props.children}
</SearchImageContext.Provider>
);
return groupPropertiesWithImage(d);
});
return (
<SearchImageContext.Provider
value={{
images: imageData,
imagesWithProperties: imagesWithProperties,
processingImages,
onRefetchImages: refetch,
}}
>
{props.children}
</SearchImageContext.Provider>
);
};
export const useSearchImageContext = () => {
const context = useContext(SearchImageContext);
if (context == null) {
throw new Error(
"Unreachable: We should always have a mounted context and no undefined values",
);
}
const context = useContext(SearchImageContext);
if (context == null) {
throw new Error(
"Unreachable: We should always have a mounted context and no undefined values",
);
}
return context;
return context;
};

View File

@ -175,7 +175,11 @@ const userProcessingImageValidator = strictObject({
ImageName: string(),
Image: null_(),
}),
Status: union([literal("not-started"), literal("in-progress")]),
Status: union([
literal("not-started"),
literal("in-progress"),
literal("complete"),
]),
});
export type UserImage = InferOutput<typeof dataTypeValidator>;

View File

@ -1,88 +1,115 @@
import { createStore } from "solid-js/store";
import {
type InferOutput,
literal,
pipe,
safeParse,
strictObject,
string,
union,
uuid,
type InferOutput,
literal,
pipe,
safeParse,
strictObject,
string,
union,
uuid,
} from "valibot";
import { base } from "../network";
import { createContext, onCleanup } from "solid-js";
import { createContext, createEffect, onCleanup } from "solid-js";
import { useSearchImageContext } from "../contexts/SearchImageContext";
const processingImagesValidator = strictObject({
ImageID: pipe(string(), uuid()),
Status: union([literal("in-progress"), literal("complete")]),
ImageID: pipe(string(), uuid()),
Status: union([
literal("not-started"),
literal("in-progress"),
literal("complete"),
]),
});
type NotificationState = {
ProcessingImages: Record<
string,
InferOutput<typeof processingImagesValidator>["Status"] | undefined
>;
ProcessingImages: Record<
string,
InferOutput<typeof processingImagesValidator>["Status"] | undefined
>;
};
export const Notifications = (onCompleteImage: () => void) => {
const [state, setState] = createStore<NotificationState>({
ProcessingImages: {},
});
const [state, setState] = createStore<NotificationState>({
ProcessingImages: {},
});
const access = localStorage.getItem("access");
if (access == null) {
throw new Error("Access token not defined");
}
const { processingImages } = useSearchImageContext();
const dataEventListener = (e: MessageEvent<unknown>) => {
if (typeof e.data !== "string") {
console.error("Error type is not string");
return;
}
const access = localStorage.getItem("access");
if (access == null) {
throw new Error("Access token not defined");
}
let jsonData: object = {};
try {
jsonData = JSON.parse(e.data);
} catch (e) {
console.error(e);
return;
}
const dataEventListener = (e: MessageEvent<unknown>) => {
if (typeof e.data !== "string") {
console.error("Error type is not string");
return;
}
const processingImage = safeParse(processingImagesValidator, jsonData);
if (!processingImage.success) {
console.error("Processing image could not be parsed.", e.data);
return;
}
let jsonData: object = {};
try {
jsonData = JSON.parse(e.data);
} catch (e) {
console.error(e);
return;
}
console.log("SSE: ", processingImage);
const processingImage = safeParse(processingImagesValidator, jsonData);
if (!processingImage.success) {
console.error("Processing image could not be parsed.", e.data);
return;
}
const { ImageID, Status } = processingImage.output;
console.log("SSE: ", processingImage);
if (Status === "complete") {
setState("ProcessingImages", ImageID, undefined);
onCompleteImage();
} else {
setState("ProcessingImages", ImageID, Status);
}
};
const { ImageID, Status } = processingImage.output;
const events = new EventSource(`${base}/notifications?token=${access}`);
if (Status === "complete") {
setState("ProcessingImages", ImageID, undefined);
onCompleteImage();
} else {
setState("ProcessingImages", ImageID, Status);
}
};
events.addEventListener("data", dataEventListener);
const upsertImageProcessing = (
images: NotificationState["ProcessingImages"],
) => {
setState("ProcessingImages", (currentImages) => ({
...currentImages,
...images,
}));
};
events.onerror = (e) => {
console.error(e);
};
createEffect(() => {
const images = processingImages();
if (images == null) {
return;
}
onCleanup(() => {
events.removeEventListener("data", dataEventListener);
events.close();
});
upsertImageProcessing(
Object.fromEntries(images.map((i) => [i.ImageID, i.Status])),
);
});
return {
state,
};
const events = new EventSource(`${base}/notifications?token=${access}`);
events.addEventListener("data", dataEventListener);
events.onerror = (e) => {
console.error(e);
};
onCleanup(() => {
events.removeEventListener("data", dataEventListener);
events.close();
});
return {
state,
};
};
export const NotificationsContext =
createContext<ReturnType<typeof Notifications>>();
createContext<ReturnType<typeof Notifications>>();