Websocket for real-time updates of projects
This commit is contained in:
parent
fbff4a2466
commit
f61b30e3cc
|
@ -6,6 +6,8 @@ require github.com/gofiber/fiber/v2 v2.51.0
|
|||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/fasthttp/websocket v1.5.3 // indirect
|
||||
github.com/gofiber/websocket/v2 v2.2.1 // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
|
@ -13,6 +15,7 @@ require (
|
|||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/tinylib/msgp v1.1.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.50.0 // indirect
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
|
||||
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
|
||||
github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ=
|
||||
github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U=
|
||||
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
|
||||
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
|
@ -19,6 +23,8 @@ github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
|||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
|
|
|
@ -12,6 +12,7 @@ func v1Router(router fiber.Router) error {
|
|||
tagsRouter(router.Group("/tags"))
|
||||
viewsRouter(router.Group("/views"))
|
||||
filtersRouter(router.Group("/filters"))
|
||||
wsRouter(router.Group("/ws"))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -83,9 +83,22 @@ func CreateProject(c *fiber.Ctx) error {
|
|||
|
||||
projectsLastEdit = time.Now().Truncate(time.Second);
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||
if err = c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||
"id": id,
|
||||
}); err != nil {
|
||||
return err;
|
||||
}
|
||||
|
||||
p.ID = id;
|
||||
|
||||
publish(fiber.Map{
|
||||
"object": "project",
|
||||
"action": "create",
|
||||
"id": id,
|
||||
"value": p,
|
||||
})
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
func UpdateProject(c *fiber.Ctx) error {
|
||||
|
@ -113,7 +126,18 @@ func UpdateProject(c *fiber.Ctx) error {
|
|||
|
||||
projectsLastEdit = time.Now().Truncate(time.Second);
|
||||
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
if err = c.SendStatus(fiber.StatusNoContent); err != nil {
|
||||
return err;
|
||||
}
|
||||
|
||||
publish(fiber.Map{
|
||||
"object": "project",
|
||||
"action": "update",
|
||||
"id": id,
|
||||
"value": p,
|
||||
})
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
func DeleteProject(c *fiber.Ctx) error {
|
||||
|
@ -136,7 +160,17 @@ func DeleteProject(c *fiber.Ctx) error {
|
|||
|
||||
projectsLastEdit = time.Now().Truncate(time.Second);
|
||||
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
if err = c.SendStatus(fiber.StatusNoContent); err != nil {
|
||||
return err;
|
||||
}
|
||||
|
||||
publish(fiber.Map{
|
||||
"object": "project",
|
||||
"action": "delete",
|
||||
"id": id,
|
||||
})
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
func GetProjectCards(c *fiber.Ctx) error {
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
subscribers = make(map[*websocket.Conn]bool)
|
||||
)
|
||||
|
||||
func wsRouter(router fiber.Router) error {
|
||||
router.Use("/", upgradeWebsocket)
|
||||
router.Get("/", websocket.New(handleWebsocket))
|
||||
return nil;
|
||||
}
|
||||
|
||||
func upgradeWebsocket(c *fiber.Ctx) error {
|
||||
if websocket.IsWebSocketUpgrade(c) {
|
||||
c.Locals("allowed", true)
|
||||
return c.Next()
|
||||
}
|
||||
return fiber.ErrUpgradeRequired
|
||||
}
|
||||
|
||||
func handleWebsocket(c *websocket.Conn) {
|
||||
subscribers[c] = true
|
||||
defer func() {
|
||||
delete(subscribers, c)
|
||||
c.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
_, _, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func publish(content fiber.Map) {
|
||||
jsonMessage, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
log.Println("Error marshalling JSON:", err)
|
||||
return
|
||||
}
|
||||
|
||||
for s := range subscribers {
|
||||
if err := s.WriteMessage(websocket.TextMessage, jsonMessage); err != nil {
|
||||
log.Println("Error writing to websocket:", err)
|
||||
s.Close()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import Project, { projects } from '$lib/types/Project';
|
||||
import { hasPendingRequests } from '$lib/utils/api';
|
||||
import { toastWarning } from '$lib/utils/toasts';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
let socket: WebSocket;
|
||||
|
||||
export function connectWebSocket() {
|
||||
socket = new WebSocket('ws://localhost:3000/api/v1/ws');
|
||||
|
||||
socket.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
toastWarning(
|
||||
'WebSocket disconnected',
|
||||
'You may experience sync issues. You can try to reload the page.'
|
||||
);
|
||||
|
||||
const reconnectTimer = setTimeout(() => {
|
||||
connectWebSocket();
|
||||
clearTimeout(reconnectTimer);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
socket.onerror = (err) => {
|
||||
console.error('WebSocket error:', err);
|
||||
};
|
||||
|
||||
socket.onmessage = async (event) => {
|
||||
const parsed = JSON.parse(event.data);
|
||||
while (hasPendingRequests()) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
applyMessage(parsed);
|
||||
};
|
||||
}
|
||||
|
||||
function applyMessage(data: any) {
|
||||
switch (data.object) {
|
||||
case 'project':
|
||||
applyProject(data);
|
||||
break;
|
||||
default:
|
||||
console.log('Unknown object:', data);
|
||||
}
|
||||
}
|
||||
|
||||
function applyProject(data: any) {
|
||||
switch (data.action) {
|
||||
case 'create':
|
||||
if (get(projects).find((p) => p.id === data.id)) break;
|
||||
case 'update':
|
||||
Project.parse(data.value);
|
||||
break;
|
||||
case 'delete':
|
||||
projects.set(get(projects).filter((p) => p.id !== data.id));
|
||||
break;
|
||||
}
|
||||
}
|
|
@ -1,26 +1,55 @@
|
|||
import axios, { Axios, type AxiosResponse } from 'axios';
|
||||
import { toastAlert } from './toasts';
|
||||
import { setupCache } from 'axios-cache-interceptor';
|
||||
import { toastAlert } from './toasts';
|
||||
// import { env } from '$env/dynamic/public';
|
||||
|
||||
// const backend = env.PUBLIC_BACKEND_URL || 'http://localhost:3000';
|
||||
const backend = 'http://localhost:3000';
|
||||
const backendUrl = 'http://localhost:3000';
|
||||
|
||||
export default setupCache(
|
||||
new Axios({
|
||||
...axios.defaults,
|
||||
baseURL: backend + '/api',
|
||||
validateStatus: () => true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}),
|
||||
{
|
||||
interpretHeader: true,
|
||||
modifiedSince: true
|
||||
let pendingRequests = 0;
|
||||
|
||||
export function hasPendingRequests() {
|
||||
return pendingRequests > 0;
|
||||
}
|
||||
|
||||
const axiosInstance = new Axios({
|
||||
...axios.defaults,
|
||||
baseURL: backendUrl + '/api',
|
||||
validateStatus: () => true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const cachedInstance = setupCache(axiosInstance, {
|
||||
interpretHeader: true,
|
||||
modifiedSince: true
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
pendingRequests++;
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
pendingRequests--;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => {
|
||||
pendingRequests--;
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
pendingRequests--;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default axiosInstance;
|
||||
|
||||
export function processError(response: AxiosResponse<any, any>, message: string = '') {
|
||||
let title = `${response.status} ${response.statusText}`;
|
||||
let subtitle = message;
|
||||
|
|
|
@ -1,18 +1,29 @@
|
|||
import { toast } from '@zerodevx/svelte-toast';
|
||||
|
||||
export function toastAlert(title: string, subtitle: string = '', persistant: boolean = false) {
|
||||
toast.push(
|
||||
`<strong>${title}</strong><br>${subtitle}`,
|
||||
{
|
||||
theme: {
|
||||
'--toastBackground': '#ff4d4f',
|
||||
'--toastBarBackground': '#d32f2f',
|
||||
'--toastColor': '#fff',
|
||||
},
|
||||
initial: persistant ? 0 : 1,
|
||||
next: 0,
|
||||
duration: 10000,
|
||||
pausable: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
toast.push(`<strong>${title}</strong><br>${subtitle}`, {
|
||||
theme: {
|
||||
'--toastBackground': '#ff4d4f',
|
||||
'--toastBarBackground': '#d32f2f',
|
||||
'--toastColor': '#fff'
|
||||
},
|
||||
initial: persistant ? 0 : 1,
|
||||
next: 0,
|
||||
duration: 10000,
|
||||
pausable: true
|
||||
});
|
||||
}
|
||||
|
||||
export function toastWarning(title: string, subtitle: string = '', persistant: boolean = false) {
|
||||
toast.push(`<strong>${title}</strong><br>${subtitle}`, {
|
||||
theme: {
|
||||
'--toastBackground': '#faad14',
|
||||
'--toastBarBackground': '#d48806',
|
||||
'--toastColor': '#fff'
|
||||
},
|
||||
initial: persistant ? 0 : 1,
|
||||
next: 0,
|
||||
duration: 5000,
|
||||
pausable: true
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import projectsApi from '$lib/api/projectsApi';
|
||||
import { connectWebSocket } from '$lib/api/websocket';
|
||||
import Project, { projects } from '$lib/types/Project';
|
||||
import { SvelteToast } from '@zerodevx/svelte-toast';
|
||||
import { onMount } from 'svelte';
|
||||
|
@ -8,6 +9,8 @@
|
|||
onMount(async () => {
|
||||
await projectsApi.getAll();
|
||||
});
|
||||
|
||||
connectWebSocket();
|
||||
</script>
|
||||
|
||||
<section>
|
||||
|
|
Loading…
Reference in New Issue