feat(sse): sync temps réel multi-appareils via Server-Sent Events v0.5.8
- Broadcaster asyncio.Queue avec keepalive 25s (prévient timeout proxy) - Endpoint GET /api/events/stream (StreamingResponse text/event-stream) - Broadcast notes_changed / todos_changed / shopping_changed sur toutes mutations - Hook useServerEvents: EventSource avec reconnexion automatique (3s) - Pages Notes, Todos, Shopping abonnées aux événements SSE - nginx: location SSE dédiée (proxy_buffering off, timeout 24h) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,17 @@ server {
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location /api/events/stream {
|
||||
proxy_pass http://backend:8000/api/events/stream;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 86400s;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000/api/;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homehub-frontend",
|
||||
"private": true,
|
||||
"version": "0.5.7",
|
||||
"version": "0.5.8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export function useServerEvents(handlers: Record<string, () => void>) {
|
||||
const handlersRef = useRef(handlers)
|
||||
handlersRef.current = handlers
|
||||
|
||||
useEffect(() => {
|
||||
let es: EventSource
|
||||
let retryTimeout: ReturnType<typeof setTimeout>
|
||||
|
||||
function connect() {
|
||||
es = new EventSource('/api/events/stream')
|
||||
Object.keys(handlersRef.current).forEach(event => {
|
||||
es.addEventListener(event, () => handlersRef.current[event]?.())
|
||||
})
|
||||
es.onerror = () => {
|
||||
es.close()
|
||||
retryTimeout = setTimeout(connect, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
connect()
|
||||
return () => {
|
||||
es?.close()
|
||||
clearTimeout(retryTimeout)
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useServerEvents } from '../hooks/useServerEvents'
|
||||
import type { Note, NoteFilters } from '../api/notes'
|
||||
import { fetchNotes, createNote, updateNote, deleteNote, addAttachment, deleteAttachment } from '../api/notes'
|
||||
import Modal from '../components/Modal'
|
||||
@@ -354,6 +355,7 @@ export default function NotesPage() {
|
||||
}, [filters])
|
||||
|
||||
useEffect(() => { void load() }, [load])
|
||||
useServerEvents({ notes_changed: () => void load() })
|
||||
|
||||
function handleSearchChange(val: string) {
|
||||
setSearchInput(val)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// frontend/src/pages/ShoppingPage.tsx
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useServerEvents } from '../hooks/useServerEvents'
|
||||
import { matchesSearch } from '../utils/search'
|
||||
import type { ShoppingListDetail, ShoppingList, Store, Product, ShoppingItem } from '../api/shopping'
|
||||
import {
|
||||
@@ -127,6 +128,7 @@ export default function ShoppingPage() {
|
||||
}, [])
|
||||
|
||||
useEffect(() => { void loadData() }, [loadData])
|
||||
useServerEvents({ shopping_changed: () => void loadData() })
|
||||
|
||||
async function refreshProducts() {
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// frontend/src/pages/TodosPage.tsx
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useServerEvents } from '../hooks/useServerEvents'
|
||||
import type { Todo, TodoCreate, TodoFilters } from '../api/todos'
|
||||
import { fetchTodos, createTodo, updateTodo, deleteTodo, postponeTodo } from '../api/todos'
|
||||
import SwipeableRow from '../components/todos/SwipeableRow'
|
||||
@@ -73,6 +74,7 @@ export default function TodosPage() {
|
||||
}, [filters])
|
||||
|
||||
useEffect(() => { void load() }, [load])
|
||||
useServerEvents({ todos_changed: () => void load() })
|
||||
|
||||
async function handleCreate(data: TodoCreate) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user