From c271af9d930e2a05825721aba0477968f06cfb43 Mon Sep 17 00:00:00 2001 From: Brieuc Dubois Date: Thu, 18 Jan 2024 04:10:28 +0100 Subject: [PATCH] Realtime updates for projects & cards --- backend/handlers/cards.go | 57 +++++++++++++++++- backend/handlers/projects.go | 23 +++++++- frontend/package-lock.json | 16 ++++++ frontend/package.json | 1 + frontend/src/lib/api/projectsApi.ts | 1 - frontend/src/lib/types/Card.ts | 8 +++ frontend/src/lib/types/Project.ts | 15 ++++- frontend/src/lib/utils/api.ts | 10 +++- frontend/src/lib/utils/webSocketManager.ts | 67 +++++++++++++++++++++- frontend/src/routes/project/+page.svelte | 3 + 10 files changed, 186 insertions(+), 15 deletions(-) diff --git a/backend/handlers/cards.go b/backend/handlers/cards.go index 8618231..d0316d3 100644 --- a/backend/handlers/cards.go +++ b/backend/handlers/cards.go @@ -34,9 +34,27 @@ func CreateCard(c *fiber.Ctx) error { }) } - return c.Status(fiber.StatusCreated).JSON(fiber.Map{ + if err := c.Status(fiber.StatusCreated).JSON(fiber.Map{ "id": id, + }); err != nil { + return err + } + + card.ID = id; + + source := c.Get("X-Request-Source"); + if source == "" { + return nil; + } + + publish(fiber.Map{ + "object": "card", + "action": "create", + "data": card, + "X-Request-Source": source, }) + + return nil; } func GetCard(c *fiber.Ctx) error { @@ -77,7 +95,23 @@ func DeleteCard(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNotFound) } - return c.SendStatus(fiber.StatusNoContent) + if err := c.SendStatus(fiber.StatusNoContent); err != nil { + return err + } + + source := c.Get("X-Request-Source"); + if source == "" { + return nil; + } + + publish(fiber.Map{ + "object": "card", + "action": "delete", + "id": id, + "X-Request-Source": source, + }); + + return nil; } func UpdateCard(c *fiber.Ctx) error { @@ -103,5 +137,22 @@ func UpdateCard(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNotFound) } - return c.SendStatus(fiber.StatusNoContent) + if err := c.SendStatus(fiber.StatusNoContent); err != nil { + return err + } + + source := c.Get("X-Request-Source"); + if source == "" { + return nil; + } + + publish(fiber.Map{ + "object": "card", + "action": "update", + "id": id, + "changes": card, + "X-Request-Source": source, + }) + + return nil; } diff --git a/backend/handlers/projects.go b/backend/handlers/projects.go index 1a49505..adf714e 100644 --- a/backend/handlers/projects.go +++ b/backend/handlers/projects.go @@ -91,11 +91,16 @@ func CreateProject(c *fiber.Ctx) error { p.ID = id; + source := c.Get("X-Request-Source"); + if source == "" { + return nil; + } + publish(fiber.Map{ "object": "project", "action": "create", - "id": id, - "value": p, + "data": p, + "X-Request-Source": source, }) return nil; @@ -130,11 +135,17 @@ func UpdateProject(c *fiber.Ctx) error { return err; } + source := c.Get("X-Request-Source"); + if source == "" { + return nil; + } + publish(fiber.Map{ "object": "project", "action": "update", "id": id, - "value": p, + "changes": p, + "X-Request-Source": source, }) return nil; @@ -164,10 +175,16 @@ func DeleteProject(c *fiber.Ctx) error { return err; } + source := c.Get("X-Request-Source"); + if source == "" { + return nil; + } + publish(fiber.Map{ "object": "project", "action": "delete", "id": id, + "X-Request-Source": source, }) return nil; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bc82877..3b445b0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,7 @@ "@sveltejs/vite-plugin-svelte": "^3.0.0", "@tauri-apps/cli": "^1.5.9", "@types/eslint": "8.56.0", + "@types/node": "^20.11.5", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@zerodevx/svelte-toast": "^0.9.5", @@ -1125,6 +1126,15 @@ "resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz", "integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==" }, + "node_modules/@types/node": { + "version": "20.11.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", + "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/pug": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", @@ -3807,6 +3817,12 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 95b7ffe..8970fa0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "@sveltejs/vite-plugin-svelte": "^3.0.0", "@tauri-apps/cli": "^1.5.9", "@types/eslint": "8.56.0", + "@types/node": "^20.11.5", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@zerodevx/svelte-toast": "^0.9.5", diff --git a/frontend/src/lib/api/projectsApi.ts b/frontend/src/lib/api/projectsApi.ts index 9747486..4648cda 100644 --- a/frontend/src/lib/api/projectsApi.ts +++ b/frontend/src/lib/api/projectsApi.ts @@ -3,7 +3,6 @@ import Project from '$lib/types/Project'; import ProjectTag from '$lib/types/ProjectTag'; import View from '$lib/types/View'; import api, { processError } from '$lib/utils/api'; -import { parseCards } from '$lib/utils/parser'; import status from '$lib/utils/status'; async function getAll(): Promise { diff --git a/frontend/src/lib/types/Card.ts b/frontend/src/lib/types/Card.ts index 75deed9..8608e7b 100644 --- a/frontend/src/lib/types/Card.ts +++ b/frontend/src/lib/types/Card.ts @@ -152,6 +152,14 @@ export default class Card { return true; } + updateFromDict(dict: any) { + if (dict.project_id && dict.project_id !== this._project.id) { + this._project = Project.fromId(dict.project_id) as Project; + } + if (dict.title) this._title = dict.title; + if (dict.content) this._content = dict.content; + } + static parse(json: any): Card | null; static parse(json: any, project: Project | null | undefined): Card | null; diff --git a/frontend/src/lib/types/Project.ts b/frontend/src/lib/types/Project.ts index 7ad275c..0068997 100644 --- a/frontend/src/lib/types/Project.ts +++ b/frontend/src/lib/types/Project.ts @@ -1,7 +1,14 @@ import projectsApi from '$lib/api/projectsApi'; import { get, writable } from 'svelte/store'; -export const projects = writable([] as Project[]); +const { subscribe, set, update } = writable([] as Project[]); + +export const projects = { + subscribe, + set, + update, + reload: () => update((projects) => projects) +}; export default class Project { private _id: number; @@ -35,7 +42,7 @@ export default class Project { if (!id) return null; - const project = new Project(id, 'untitled'); + const project = new Project(id, 'New project'); projects.update((projects) => [...projects, project]); @@ -58,6 +65,10 @@ export default class Project { return true; } + updateFromDict(dict: any) { + if (dict.title) this._title = dict.title; + } + static parse(json: any): Project | null { if (!json) return null; diff --git a/frontend/src/lib/utils/api.ts b/frontend/src/lib/utils/api.ts index 9fe4091..a99cef0 100644 --- a/frontend/src/lib/utils/api.ts +++ b/frontend/src/lib/utils/api.ts @@ -7,6 +7,9 @@ import { toastAlert } from './toasts'; let backendUrl = 'http://localhost:3000'; let backendWsUrl = 'ws://localhost:3000'; +export let randomID = + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + export function getBackendWsUrl() { return backendWsUrl; } @@ -48,9 +51,10 @@ const cachedInstance = setupCache(axiosInstance, { modifiedSince: true }); -axiosInstance.interceptors.request.use( +cachedInstance.interceptors.request.use( (config) => { pendingRequests++; + config.headers['X-Request-Source'] = randomID; return config; }, (error) => { @@ -59,7 +63,7 @@ axiosInstance.interceptors.request.use( } ); -axiosInstance.interceptors.response.use( +cachedInstance.interceptors.response.use( (response) => { pendingRequests--; return response; @@ -70,7 +74,7 @@ axiosInstance.interceptors.response.use( } ); -export default axiosInstance; +export default cachedInstance; export function processError(response: AxiosResponse, message: string = '') { let title = `${response.status} ${response.statusText}`; diff --git a/frontend/src/lib/utils/webSocketManager.ts b/frontend/src/lib/utils/webSocketManager.ts index 6c9742e..96d81ea 100644 --- a/frontend/src/lib/utils/webSocketManager.ts +++ b/frontend/src/lib/utils/webSocketManager.ts @@ -1,5 +1,8 @@ +import Card, { cards } from '$lib/types/Card'; import Project, { projects } from '$lib/types/Project'; -import { getBackendWsUrl, hasPendingRequests } from '$lib/utils/api'; +import ProjectTag, { projectTags } from '$lib/types/ProjectTag'; +import View, { views } from '$lib/types/View'; +import { getBackendWsUrl, hasPendingRequests, randomID } from '$lib/utils/api'; import { toastAlert, toastWarning } from '$lib/utils/toasts'; import { get } from 'svelte/store'; @@ -61,6 +64,11 @@ export default class WebSocketManager { this._socket.onmessage = async (event) => { const data = JSON.parse(event.data); + const source = data['X-Request-Source']; + // console.log(source); + if (!source || source === randomID) { + return; + } while (hasPendingRequests()) { await new Promise((resolve) => setTimeout(resolve, 100)); } @@ -74,6 +82,15 @@ function applyMessage(data: any) { case 'project': applyProject(data); break; + case 'card': + applyCard(data); + break; + case 'view': + applyView(data); + break; + case 'projectTag': + applyProjectTag(data); + break; default: console.log('Unknown object:', data); } @@ -82,12 +99,56 @@ function applyMessage(data: any) { function applyProject(data: any) { switch (data.action) { case 'create': - if (get(projects).find((p) => p.id === data.id)) break; + Project.parse(data.data); case 'update': - Project.parse(data.value); + get(projects) + .find((p) => p.id === data.id) + ?.updateFromDict(data.changes); + projects.reload(); break; case 'delete': projects.set(get(projects).filter((p) => p.id !== data.id)); break; } } + +function applyCard(data: any) { + switch (data.action) { + case 'create': + Card.parse(data.data); + break; + case 'update': + get(cards) + .find((c) => c.id === data.id) + ?.updateFromDict(data.changes); + cards.reload(); + break; + case 'delete': + cards.set(get(cards).filter((c) => c.id !== data.id)); + break; + } +} + +function applyView(data: any) { + switch (data.action) { + case 'create': + case 'update': + View.parse(data.value); + break; + case 'delete': + views.set(get(views).filter((v) => v.id !== data.id)); + break; + } +} + +function applyProjectTag(data: any) { + switch (data.action) { + case 'create': + case 'update': + ProjectTag.parse(data.value); + break; + case 'delete': + projectTags.set(get(projectTags).filter((t) => t.id !== data.id)); + break; + } +} diff --git a/frontend/src/routes/project/+page.svelte b/frontend/src/routes/project/+page.svelte index 30ea771..8591dc3 100644 --- a/frontend/src/routes/project/+page.svelte +++ b/frontend/src/routes/project/+page.svelte @@ -7,11 +7,13 @@ import type Project from '$lib/types/Project'; import { views } from '$lib/types/View'; import { checkTauriUrl } from '$lib/utils/api'; + import WebSocketManager from '$lib/utils/webSocketManager'; import { SvelteToast } from '@zerodevx/svelte-toast'; import { onMount } from 'svelte'; import { get } from 'svelte/store'; let project: Project; + const wsManager = new WebSocketManager(); onMount(async () => { await checkTauriUrl(window); @@ -29,6 +31,7 @@ if (get(views).length > 0) { currentView.set(get(views)[0]); } + wsManager.connect(); });