From c62378c20a4b9fd10c36891d12ac1b4094ba6a63 Mon Sep 17 00:00:00 2001 From: John Costa Date: Wed, 2 Jul 2025 13:28:28 +0100 Subject: [PATCH] feat: updating notifications system based on incoming request This is making the frontend data logic a little complex --- frontend/src/contexts/SearchImageContext.tsx | 155 ++++++++++--------- frontend/src/network/index.ts | 6 +- frontend/src/notifications/index.ts | 149 ++++++++++-------- 3 files changed, 174 insertions(+), 136 deletions(-) diff --git a/frontend/src/contexts/SearchImageContext.tsx b/frontend/src/contexts/SearchImageContext.tsx index 63dcc8b..1829549 100644 --- a/frontend/src/contexts/SearchImageContext.tsx +++ b/frontend/src/contexts/SearchImageContext.tsx @@ -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 + ReturnType >["ImageProperties"][number] & { - rawData: string[]; + rawData: string[]; }; type SearchImageStore = { - images: Accessor; + images: Accessor; - imagesWithProperties: Accessor>; - onRefetchImages: () => void; + imagesWithProperties: Accessor>; + processingImages: Accessor< + Awaited>["ProcessingImages"] | undefined + >; + + onRefetchImages: () => void; }; // How wonderfully functional const getAllValues = (object: object): Array => { - const loop = (acc: Array, next: object): Array => { - for (const [key, _value] of Object.entries(next)) { - if (key === "ID") { - continue; - } + const loop = (acc: Array, next: object): Array => { + 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(); export const SearchImageContextProvider: Component = (props) => { - const [data, { refetch }] = createResource(getUserImages); + const [data, { refetch }] = createResource(getUserImages); - const imageData = createMemo(() => { - const d = data(); - if (d == null) { - return []; - } + const imageData = createMemo(() => { + 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 - >(() => { - const d = data(); - if (d == null) { - return {}; - } + const processingImages = () => data()?.ProcessingImages ?? []; - return groupPropertiesWithImage(d); - }); + const imagesWithProperties = createMemo< + ReturnType + >(() => { + const d = data(); + if (d == null) { + return {}; + } - return ( - - {props.children} - - ); + return groupPropertiesWithImage(d); + }); + + return ( + + {props.children} + + ); }; 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; }; diff --git a/frontend/src/network/index.ts b/frontend/src/network/index.ts index 17ffa6e..396b35f 100644 --- a/frontend/src/network/index.ts +++ b/frontend/src/network/index.ts @@ -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; diff --git a/frontend/src/notifications/index.ts b/frontend/src/notifications/index.ts index 493a87d..8b02ac6 100644 --- a/frontend/src/notifications/index.ts +++ b/frontend/src/notifications/index.ts @@ -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["Status"] | undefined - >; + ProcessingImages: Record< + string, + InferOutput["Status"] | undefined + >; }; export const Notifications = (onCompleteImage: () => void) => { - const [state, setState] = createStore({ - ProcessingImages: {}, - }); + const [state, setState] = createStore({ + ProcessingImages: {}, + }); - const access = localStorage.getItem("access"); - if (access == null) { - throw new Error("Access token not defined"); - } + const { processingImages } = useSearchImageContext(); - const dataEventListener = (e: MessageEvent) => { - 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) => { + 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>(); + createContext>();