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 (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
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/google/uuid v1.4.0 // indirect
|
||||||
github.com/klauspost/compress v1.16.7 // indirect
|
github.com/klauspost/compress v1.16.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // 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/mattn/go-runewidth v0.0.15 // indirect
|
||||||
github.com/philhofer/fwd v1.1.2 // indirect
|
github.com/philhofer/fwd v1.1.2 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // 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/tinylib/msgp v1.1.8 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.50.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 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
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 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ=
|
||||||
github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U=
|
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 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
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/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 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
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 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||||
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
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=
|
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"))
|
tagsRouter(router.Group("/tags"))
|
||||||
viewsRouter(router.Group("/views"))
|
viewsRouter(router.Group("/views"))
|
||||||
filtersRouter(router.Group("/filters"))
|
filtersRouter(router.Group("/filters"))
|
||||||
|
wsRouter(router.Group("/ws"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,9 +83,22 @@ func CreateProject(c *fiber.Ctx) error {
|
||||||
|
|
||||||
projectsLastEdit = time.Now().Truncate(time.Second);
|
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,
|
"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 {
|
func UpdateProject(c *fiber.Ctx) error {
|
||||||
|
@ -113,7 +126,18 @@ func UpdateProject(c *fiber.Ctx) error {
|
||||||
|
|
||||||
projectsLastEdit = time.Now().Truncate(time.Second);
|
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 {
|
func DeleteProject(c *fiber.Ctx) error {
|
||||||
|
@ -136,7 +160,17 @@ func DeleteProject(c *fiber.Ctx) error {
|
||||||
|
|
||||||
projectsLastEdit = time.Now().Truncate(time.Second);
|
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 {
|
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 axios, { Axios, type AxiosResponse } from 'axios';
|
||||||
import { toastAlert } from './toasts';
|
|
||||||
import { setupCache } from 'axios-cache-interceptor';
|
import { setupCache } from 'axios-cache-interceptor';
|
||||||
|
import { toastAlert } from './toasts';
|
||||||
// import { env } from '$env/dynamic/public';
|
// import { env } from '$env/dynamic/public';
|
||||||
|
|
||||||
// const backend = env.PUBLIC_BACKEND_URL || 'http://localhost:3000';
|
// const backend = env.PUBLIC_BACKEND_URL || 'http://localhost:3000';
|
||||||
const backend = 'http://localhost:3000';
|
const backendUrl = 'http://localhost:3000';
|
||||||
|
|
||||||
export default setupCache(
|
let pendingRequests = 0;
|
||||||
new Axios({
|
|
||||||
|
export function hasPendingRequests() {
|
||||||
|
return pendingRequests > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const axiosInstance = new Axios({
|
||||||
...axios.defaults,
|
...axios.defaults,
|
||||||
baseURL: backend + '/api',
|
baseURL: backendUrl + '/api',
|
||||||
validateStatus: () => true,
|
validateStatus: () => true,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
}),
|
});
|
||||||
{
|
|
||||||
|
const cachedInstance = setupCache(axiosInstance, {
|
||||||
interpretHeader: true,
|
interpretHeader: true,
|
||||||
modifiedSince: 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 = '') {
|
export function processError(response: AxiosResponse<any, any>, message: string = '') {
|
||||||
let title = `${response.status} ${response.statusText}`;
|
let title = `${response.status} ${response.statusText}`;
|
||||||
let subtitle = message;
|
let subtitle = message;
|
||||||
|
|
|
@ -1,18 +1,29 @@
|
||||||
import { toast } from '@zerodevx/svelte-toast';
|
import { toast } from '@zerodevx/svelte-toast';
|
||||||
|
|
||||||
export function toastAlert(title: string, subtitle: string = '', persistant: boolean = false) {
|
export function toastAlert(title: string, subtitle: string = '', persistant: boolean = false) {
|
||||||
toast.push(
|
toast.push(`<strong>${title}</strong><br>${subtitle}`, {
|
||||||
`<strong>${title}</strong><br>${subtitle}`,
|
|
||||||
{
|
|
||||||
theme: {
|
theme: {
|
||||||
'--toastBackground': '#ff4d4f',
|
'--toastBackground': '#ff4d4f',
|
||||||
'--toastBarBackground': '#d32f2f',
|
'--toastBarBackground': '#d32f2f',
|
||||||
'--toastColor': '#fff',
|
'--toastColor': '#fff'
|
||||||
},
|
},
|
||||||
initial: persistant ? 0 : 1,
|
initial: persistant ? 0 : 1,
|
||||||
next: 0,
|
next: 0,
|
||||||
duration: 10000,
|
duration: 10000,
|
||||||
pausable: true,
|
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">
|
<script lang="ts">
|
||||||
import projectsApi from '$lib/api/projectsApi';
|
import projectsApi from '$lib/api/projectsApi';
|
||||||
|
import { connectWebSocket } from '$lib/api/websocket';
|
||||||
import Project, { projects } from '$lib/types/Project';
|
import Project, { projects } from '$lib/types/Project';
|
||||||
import { SvelteToast } from '@zerodevx/svelte-toast';
|
import { SvelteToast } from '@zerodevx/svelte-toast';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
@ -8,6 +9,8 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await projectsApi.getAll();
|
await projectsApi.getAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connectWebSocket();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
Loading…
Reference in New Issue