فهرست منبع

feat: Add useModels composable

Owen Diffey 1 سال پیش
والد
کامیت
96189c57e5

+ 20 - 10
frontend/src/components/modals/EditNews.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { defineAsyncComponent, ref, onMounted, onBeforeUnmount } from "vue";
+import { defineAsyncComponent, ref, onMounted } from "vue";
 import { marked } from "marked";
 import DOMPurify from "dompurify";
 import Toast from "toasters";
@@ -8,6 +8,7 @@ import { useModalsStore } from "@/stores/modals";
 import { useNewsModelStore } from "@/stores/models/news";
 import { useForm } from "@/composables/useForm";
 import { useEvents } from "@/composables/useEvents";
+import { useModels } from "@/composables/useModels";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
 const SaveButton = defineAsyncComponent(
@@ -28,8 +29,10 @@ const { onReady } = useEvents();
 
 const { closeCurrentModal } = useModalsStore();
 
-const { create, findById, updateById, hasPermission, unregisterModels } =
-	useNewsModelStore();
+const { registerModels, onDeleted } = useModels();
+
+const newsStore = useNewsModelStore();
+const { create, findById, updateById, hasPermission } = newsStore;
 
 const createdBy = ref();
 const createdAt = ref(0);
@@ -116,10 +119,21 @@ onMounted(async () => {
 				closeCurrentModal();
 			});
 
-			setModelValues(data, ["markdown", "status", "showToNewUsers"]);
+			if (!data) return;
+
+			const [model] = await registerModels(newsStore, [data]);
+
+			setModelValues(model, ["markdown", "status", "showToNewUsers"]);
+
+			createdBy.value = model.createdBy;
+			createdAt.value = model.createdAt;
+
+			await onDeleted(newsStore, ({ oldDoc }) => {
+				if (oldDoc._id !== props.newsId) return;
 
-			createdBy.value = data.createdBy;
-			createdAt.value = data.createdAt;
+				new Toast("News item has been deleted.");
+				closeCurrentModal();
+			});
 		}
 
 		console.log(
@@ -128,10 +142,6 @@ onMounted(async () => {
 		);
 	});
 });
-
-onBeforeUnmount(async () => {
-	if (props.newsId && !props.createNews) await unregisterModels(props.newsId);
-});
 </script>
 
 <template>

+ 145 - 0
frontend/src/composables/useModels.ts

@@ -0,0 +1,145 @@
+import { onBeforeUnmount, ref } from "vue";
+
+export const useModels = () => {
+	const models = ref([]);
+	const subscriptions = ref({
+		created: [],
+		updated: [],
+		deleted: []
+	});
+
+	const onCreated = async (store, callback: (data?: any) => any) => {
+		const uuid = await store.onCreated(callback);
+
+		subscriptions.value.created.push({
+			store,
+			uuid
+		});
+	};
+
+	const onUpdated = async (store, callback: (data?: any) => any) => {
+		const uuid = await store.onUpdated(callback);
+
+		subscriptions.value.updated.push({
+			store,
+			uuid
+		});
+	};
+
+	const onDeleted = async (store, callback: (data?: any) => any) => {
+		const uuid = await store.onDeleted(callback);
+
+		subscriptions.value.deleted.push({
+			store,
+			uuid
+		});
+	};
+
+	const removeCallback = async (
+		store,
+		type: "created" | "updated" | "deleted",
+		uuid: string
+	) => {
+		if (
+			!subscriptions.value[type].find(
+				subscription =>
+					subscription.store === store && subscription.uuid === uuid
+			)
+		)
+			return;
+
+		await store.removeCallback(type, uuid);
+
+		delete subscriptions.value[type][uuid];
+	};
+
+	const registerModels = async (store, storeModels: any[]) => {
+		let storeIndex = models.value.findIndex(model => model.store === store);
+
+		const registeredModels = await store.registerModels(storeModels);
+
+		if (storeIndex < 0) {
+			models.value.push({
+				store,
+				models: registeredModels
+			});
+
+			await onDeleted(store, ({ oldDoc }) => {
+				storeIndex = models.value.findIndex(
+					model => model.store === store
+				);
+
+				if (storeIndex < 0) return;
+
+				const modelIndex = models.value[storeIndex].models.findIndex(
+					model => model._id === oldDoc._id
+				);
+
+				if (modelIndex < 0) return;
+
+				delete models.value[storeIndex].models[modelIndex];
+			});
+
+			return registeredModels;
+		}
+
+		models.value[storeIndex].models = [
+			...models.value[storeIndex].models,
+			registeredModels
+		];
+
+		return registeredModels;
+	};
+
+	const unregisterModels = async (store, modelIds: string[]) => {
+		const storeIndex = models.value.findIndex(
+			model => model.store === store
+		);
+
+		if (storeIndex < 0) return;
+
+		const storeModels = models.value[storeIndex].models;
+
+		await store.unregisterModels(
+			storeModels
+				.filter(model => modelIds.includes(model._id))
+				.map(model => model._id)
+		);
+
+		models.value[storeIndex].modelIds = storeModels.filter(
+			model => !modelIds.includes(model._id)
+		);
+	};
+
+	onBeforeUnmount(async () => {
+		await Promise.all(
+			Object.entries(subscriptions.value).map(
+				async ([type, _subscriptions]) =>
+					Promise.all(
+						_subscriptions.map(({ store, uuid }) =>
+							removeCallback(store, type, uuid)
+						)
+					)
+			)
+		);
+		await Promise.all(
+			models.value.map(({ store, models: storeModels }) =>
+				unregisterModels(
+					store,
+					storeModels.map(model => model._id)
+				)
+			)
+		);
+	});
+
+	return {
+		models,
+		subscriptions,
+		onCreated,
+		onUpdated,
+		onDeleted,
+		removeCallback,
+		registerModels,
+		unregisterModels
+	};
+};

+ 15 - 38
frontend/src/pages/News.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { defineAsyncComponent, ref, onMounted, onBeforeUnmount } from "vue";
+import { defineAsyncComponent, ref, onMounted } from "vue";
 
 import { formatDistance } from "date-fns";
 import { marked } from "marked";
@@ -11,9 +11,9 @@ import {
 	NewsRemovedResponse
 } from "@musare_types/events/NewsEvents";
 import { GetPublishedNewsResponse } from "@musare_types/actions/NewsActions";
-import { useWebsocketStore } from "@/stores/websocket";
 import { useNewsModelStore } from "@/stores/models/news";
 import { useEvents } from "@/composables/useEvents";
+import { useModels } from "@/composables/useModels";
 
 const MainHeader = defineAsyncComponent(
 	() => import("@/components/MainHeader.vue")
@@ -25,20 +25,14 @@ const UserLink = defineAsyncComponent(
 	() => import("@/components/UserLink.vue")
 );
 
-const { onReady, subscribe } = useEvents();
-const { newest, registerModels, unregisterModels } = useNewsModelStore();
+const { onReady } = useEvents();
+const { registerModels, onCreated, onDeleted } = useModels();
+const newsStore = useNewsModelStore();
 
 const news = ref<NewsModel[]>([]);
 
 const { sanitize } = DOMPurify;
 
-const onDeleted = async ({ oldDoc }) => {
-	news.value.splice(
-		news.value.findIndex(doc => doc._id === oldDoc._id),
-		1
-	);
-};
-
 onMounted(async () => {
 	marked.use({
 		renderer: {
@@ -52,38 +46,21 @@ onMounted(async () => {
 	});
 
 	await onReady(async () => {
-		news.value = await newest();
+		news.value = await registerModels(newsStore, await newsStore.newest());
 	});
 
-	await subscribe("model.news.created", async ({ doc }) => {
-		news.value.unshift(...(await registerModels(doc)));
+	await onCreated(newsStore, async ({ doc }) => {
+		const [newDoc] = await registerModels(newsStore, [doc]);
+		news.value.unshift(newDoc);
 	});
 
-	// TODO: Subscribe to loaded model updated/deleted events
-	// socket.on("event:news.updated", (res: NewsUpdatedResponse) => {
-	// 	if (res.data.news.status === "draft") {
-	// 		news.value = news.value.filter(
-	// 			item => item._id !== res.data.news._id
-	// 		);
-	// 		return;
-	// 	}
-
-	// 	for (let n = 0; n < news.value.length; n += 1) {
-	// 		if (news.value[n]._id === res.data.news._id)
-	// 			news.value[n] = {
-	// 				...news.value[n],
-	// 				...res.data.news
-	// 			};
-	// 	}
-	// });
-
-	// socket.on("event:news.deleted", (res: NewsRemovedResponse) => {
-	// 	news.value = news.value.filter(item => item._id !== res.data.newsId);
-	// });
-});
+	await onDeleted(newsStore, async ({ oldDoc }) => {
+		const index = news.value.findIndex(doc => doc._id === oldDoc._id);
 
-onBeforeUnmount(async () => {
-	await unregisterModels(news.value.map(model => model._id));
+		if (index < 0) return;
+
+		news.value.splice(index, 1);
+	});
 });
 </script>
 

+ 103 - 10
frontend/src/stores/models/model.ts

@@ -1,5 +1,6 @@
 import { reactive, ref } from "vue";
 import { useWebsocketStore } from "../websocket";
+import utils from "@/utils";
 
 export const createModelStore = modelName => {
 	const { runJob, subscribe, unsubscribe } = useWebsocketStore();
@@ -7,7 +8,13 @@ export const createModelStore = modelName => {
 	const models = ref([]);
 	const permissions = ref(null);
 	const modelPermissions = ref({});
-	const subscriptions = ref({});
+	const createdSubcription = ref(null);
+	const subscriptions = ref({
+		models: {},
+		created: {},
+		updated: {},
+		deleted: {}
+	});
 
 	const fetchUserModelPermissions = async (_id?: string) => {
 		const data = await runJob("api.getUserModelPermissions", {
@@ -41,15 +48,67 @@ export const createModelStore = modelName => {
 		return !!data[permission];
 	};
 
-	const onUpdated = async ({ doc }) => {
+	const onCreatedCallback = async data => {
+		await Promise.all(
+			Object.values(subscriptions.value.created).map(
+				async subscription => subscription(data) // TODO: Error handling
+			)
+		);
+	};
+
+	const onCreated = async (callback: (data?: any) => any) => {
+		if (!createdSubcription.value)
+			createdSubcription.value = await subscribe(
+				`model.${modelName}.created`,
+				onCreatedCallback
+			);
+
+		const uuid = utils.guid();
+
+		subscriptions.value.created[uuid] = callback;
+
+		return uuid;
+	};
+
+	const onUpdated = async (callback: (data?: any) => any) => {
+		const uuid = utils.guid();
+
+		subscriptions.value.updated[uuid] = callback;
+
+		return uuid;
+	};
+
+	const onUpdatedCallback = async ({ doc }) => {
 		const index = models.value.findIndex(model => model._id === doc._id);
 		if (index > -1) Object.assign(models.value[index], doc);
 
 		if (modelPermissions.value[doc._id])
 			await fetchUserModelPermissions(doc._id);
+
+		await Promise.all(
+			Object.values(subscriptions.value.updated).map(
+				async subscription => subscription(data) // TODO: Error handling
+			)
+		);
 	};
 
-	const onDeleted = async ({ oldDoc }) => {
+	const onDeleted = async (callback: (data?: any) => any) => {
+		const uuid = utils.guid();
+
+		subscriptions.value.deleted[uuid] = callback;
+
+		return uuid;
+	};
+
+	const onDeletedCallback = async data => {
+		const { oldDoc } = data;
+
+		await Promise.all(
+			Object.values(subscriptions.value.deleted).map(
+				async subscription => subscription(data) // TODO: Error handling
+			)
+		);
+
 		const index = models.value.findIndex(model => model._id === oldDoc._id);
 		if (index > -1) await unregisterModels(oldDoc._id);
 
@@ -57,6 +116,27 @@ export const createModelStore = modelName => {
 			delete modelPermissions.value[oldDoc._id];
 	};
 
+	const removeCallback = async (
+		type: "created" | "updated" | "deleted",
+		uuid: string
+	) => {
+		if (!subscriptions.value[type][uuid]) return;
+
+		delete subscriptions.value[type][uuid];
+
+		if (
+			type === "created" &&
+			Object.keys(subscriptions.value.created).length === 0
+		) {
+			await unsubscribe(
+				`model.${modelName}.created`,
+				createdSubcription.value
+			);
+
+			createdSubcription.value = null;
+		}
+	};
+
 	const registerModels = async docs =>
 		Promise.all(
 			(Array.isArray(docs) ? docs : [docs]).map(async _doc => {
@@ -70,25 +150,31 @@ export const createModelStore = modelName => {
 					models.value.push(docRef);
 				}
 
-				if (subscriptions.value[_doc._id]) return docRef;
+				if (subscriptions.value.models[_doc._id]) return docRef;
 
 				const updatedChannel = `model.${modelName}.updated.${_doc._id}`;
-				const updatedUuid = await subscribe(updatedChannel, onUpdated);
+				const updatedUuid = await subscribe(
+					updatedChannel,
+					onUpdatedCallback
+				);
 				const updated = {
 					channel: updatedChannel,
-					callback: onUpdated,
+					callback: onUpdatedCallback,
 					uuid: updatedUuid
 				};
 
 				const deletedChannel = `model.${modelName}.deleted.${_doc._id}`;
-				const deletedUuid = await subscribe(deletedChannel, onDeleted);
+				const deletedUuid = await subscribe(
+					deletedChannel,
+					onDeletedCallback
+				);
 				const deleted = {
 					channel: deletedChannel,
-					callback: onDeleted,
+					callback: onDeletedCallback,
 					uuid: deletedUuid
 				};
 
-				subscriptions.value[_doc._id] = {
+				subscriptions.value.models[_doc._id] = {
 					updated,
 					deleted
 				};
@@ -108,12 +194,15 @@ export const createModelStore = modelName => {
 					)
 						return;
 
-					const { updated, deleted } = subscriptions.value[modelId];
+					const { updated, deleted } =
+						subscriptions.value.models[modelId];
 
 					await unsubscribe(updated.channel, updated.uuid);
 
 					await unsubscribe(deleted.channel, deleted.uuid);
 
+					delete subscriptions.value.models[modelId];
+
 					models.value.splice(
 						models.value.findIndex(model => model._id === modelId),
 						1
@@ -150,6 +239,10 @@ export const createModelStore = modelName => {
 		permissions,
 		modelPermissions,
 		subscriptions,
+		onCreated,
+		onUpdated,
+		onDeleted,
+		removeCallback,
 		registerModels,
 		unregisterModels,
 		fetchUserModelPermissions,

+ 2 - 0
frontend/src/stores/websocket.ts

@@ -118,6 +118,8 @@ export const useWebsocketStore = defineStore("websocket", () => {
 		);
 
 		pendingJobs.value.forEach(message => socket.value.send(message));
+
+		pendingJobs.value = [];
 	});
 
 	const init = () => {