feat: updating notifications system based on incoming request
This is making the frontend data logic a little complex
This commit is contained in:
@ -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;
|
||||
};
|
||||
|
@ -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>;
|
||||
|
@ -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>>();
|
||||
|
Reference in New Issue
Block a user