Realtime updates for projects & cards

This commit is contained in:
Brieuc Dubois 2024-01-18 04:10:28 +01:00
parent e01b93f48b
commit c271af9d93
10 changed files with 186 additions and 15 deletions

View File

@ -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, "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 { 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.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 { 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.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;
} }

View File

@ -91,11 +91,16 @@ func CreateProject(c *fiber.Ctx) error {
p.ID = id; p.ID = id;
source := c.Get("X-Request-Source");
if source == "" {
return nil;
}
publish(fiber.Map{ publish(fiber.Map{
"object": "project", "object": "project",
"action": "create", "action": "create",
"id": id, "data": p,
"value": p, "X-Request-Source": source,
}) })
return nil; return nil;
@ -130,11 +135,17 @@ func UpdateProject(c *fiber.Ctx) error {
return err; return err;
} }
source := c.Get("X-Request-Source");
if source == "" {
return nil;
}
publish(fiber.Map{ publish(fiber.Map{
"object": "project", "object": "project",
"action": "update", "action": "update",
"id": id, "id": id,
"value": p, "changes": p,
"X-Request-Source": source,
}) })
return nil; return nil;
@ -164,10 +175,16 @@ func DeleteProject(c *fiber.Ctx) error {
return err; return err;
} }
source := c.Get("X-Request-Source");
if source == "" {
return nil;
}
publish(fiber.Map{ publish(fiber.Map{
"object": "project", "object": "project",
"action": "delete", "action": "delete",
"id": id, "id": id,
"X-Request-Source": source,
}) })
return nil; return nil;

View File

@ -21,6 +21,7 @@
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tauri-apps/cli": "^1.5.9", "@tauri-apps/cli": "^1.5.9",
"@types/eslint": "8.56.0", "@types/eslint": "8.56.0",
"@types/node": "^20.11.5",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",
"@zerodevx/svelte-toast": "^0.9.5", "@zerodevx/svelte-toast": "^0.9.5",
@ -1125,6 +1126,15 @@
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
"integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==" "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": { "node_modules/@types/pug": {
"version": "2.0.10", "version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
@ -3807,6 +3817,12 @@
"node": ">=14.17" "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": { "node_modules/uri-js": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",

View File

@ -21,6 +21,7 @@
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tauri-apps/cli": "^1.5.9", "@tauri-apps/cli": "^1.5.9",
"@types/eslint": "8.56.0", "@types/eslint": "8.56.0",
"@types/node": "^20.11.5",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",
"@zerodevx/svelte-toast": "^0.9.5", "@zerodevx/svelte-toast": "^0.9.5",

View File

@ -3,7 +3,6 @@ import Project from '$lib/types/Project';
import ProjectTag from '$lib/types/ProjectTag'; import ProjectTag from '$lib/types/ProjectTag';
import View from '$lib/types/View'; import View from '$lib/types/View';
import api, { processError } from '$lib/utils/api'; import api, { processError } from '$lib/utils/api';
import { parseCards } from '$lib/utils/parser';
import status from '$lib/utils/status'; import status from '$lib/utils/status';
async function getAll(): Promise<Project[]> { async function getAll(): Promise<Project[]> {

View File

@ -152,6 +152,14 @@ export default class Card {
return true; 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): Card | null;
static parse(json: any, project: Project | null | undefined): Card | null; static parse(json: any, project: Project | null | undefined): Card | null;

View File

@ -1,7 +1,14 @@
import projectsApi from '$lib/api/projectsApi'; import projectsApi from '$lib/api/projectsApi';
import { get, writable } from 'svelte/store'; 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 { export default class Project {
private _id: number; private _id: number;
@ -35,7 +42,7 @@ export default class Project {
if (!id) return null; if (!id) return null;
const project = new Project(id, 'untitled'); const project = new Project(id, 'New project');
projects.update((projects) => [...projects, project]); projects.update((projects) => [...projects, project]);
@ -58,6 +65,10 @@ export default class Project {
return true; return true;
} }
updateFromDict(dict: any) {
if (dict.title) this._title = dict.title;
}
static parse(json: any): Project | null { static parse(json: any): Project | null {
if (!json) return null; if (!json) return null;

View File

@ -7,6 +7,9 @@ import { toastAlert } from './toasts';
let backendUrl = 'http://localhost:3000'; let backendUrl = 'http://localhost:3000';
let backendWsUrl = 'ws://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() { export function getBackendWsUrl() {
return backendWsUrl; return backendWsUrl;
} }
@ -48,9 +51,10 @@ const cachedInstance = setupCache(axiosInstance, {
modifiedSince: true modifiedSince: true
}); });
axiosInstance.interceptors.request.use( cachedInstance.interceptors.request.use(
(config) => { (config) => {
pendingRequests++; pendingRequests++;
config.headers['X-Request-Source'] = randomID;
return config; return config;
}, },
(error) => { (error) => {
@ -59,7 +63,7 @@ axiosInstance.interceptors.request.use(
} }
); );
axiosInstance.interceptors.response.use( cachedInstance.interceptors.response.use(
(response) => { (response) => {
pendingRequests--; pendingRequests--;
return response; return response;
@ -70,7 +74,7 @@ axiosInstance.interceptors.response.use(
} }
); );
export default axiosInstance; export default cachedInstance;
export function processError(response: AxiosResponse<any, any>, message: string = '') { export function processError(response: AxiosResponse<any, any>, message: string = '') {
let title = `${response.status} ${response.statusText}`; let title = `${response.status} ${response.statusText}`;

View File

@ -1,5 +1,8 @@
import Card, { cards } from '$lib/types/Card';
import Project, { projects } from '$lib/types/Project'; 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 { toastAlert, toastWarning } from '$lib/utils/toasts';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
@ -61,6 +64,11 @@ export default class WebSocketManager {
this._socket.onmessage = async (event) => { this._socket.onmessage = async (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
const source = data['X-Request-Source'];
// console.log(source);
if (!source || source === randomID) {
return;
}
while (hasPendingRequests()) { while (hasPendingRequests()) {
await new Promise((resolve) => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
} }
@ -74,6 +82,15 @@ function applyMessage(data: any) {
case 'project': case 'project':
applyProject(data); applyProject(data);
break; break;
case 'card':
applyCard(data);
break;
case 'view':
applyView(data);
break;
case 'projectTag':
applyProjectTag(data);
break;
default: default:
console.log('Unknown object:', data); console.log('Unknown object:', data);
} }
@ -82,12 +99,56 @@ function applyMessage(data: any) {
function applyProject(data: any) { function applyProject(data: any) {
switch (data.action) { switch (data.action) {
case 'create': case 'create':
if (get(projects).find((p) => p.id === data.id)) break; Project.parse(data.data);
case 'update': case 'update':
Project.parse(data.value); get(projects)
.find((p) => p.id === data.id)
?.updateFromDict(data.changes);
projects.reload();
break; break;
case 'delete': case 'delete':
projects.set(get(projects).filter((p) => p.id !== data.id)); projects.set(get(projects).filter((p) => p.id !== data.id));
break; 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;
}
}

View File

@ -7,11 +7,13 @@
import type Project from '$lib/types/Project'; import type Project from '$lib/types/Project';
import { views } from '$lib/types/View'; import { views } from '$lib/types/View';
import { checkTauriUrl } from '$lib/utils/api'; import { checkTauriUrl } from '$lib/utils/api';
import WebSocketManager from '$lib/utils/webSocketManager';
import { SvelteToast } from '@zerodevx/svelte-toast'; import { SvelteToast } from '@zerodevx/svelte-toast';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
let project: Project; let project: Project;
const wsManager = new WebSocketManager();
onMount(async () => { onMount(async () => {
await checkTauriUrl(window); await checkTauriUrl(window);
@ -29,6 +31,7 @@
if (get(views).length > 0) { if (get(views).length > 0) {
currentView.set(get(views)[0]); currentView.set(get(views)[0]);
} }
wsManager.connect();
}); });
</script> </script>