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:
@@ -0,0 +1,18 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from app.core.broadcaster import broadcaster
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/stream")
|
||||
async def event_stream():
|
||||
return StreamingResponse(
|
||||
broadcaster.subscribe(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
@@ -5,6 +5,7 @@ from sqlalchemy import select, text, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.core.broadcaster import broadcaster
|
||||
from app.core.database import get_session
|
||||
from app.core.redis import enqueue
|
||||
from app.models.notes import NoteItem, NoteAttachment
|
||||
@@ -74,6 +75,7 @@ async def create_note(payload: NoteCreate, session: AsyncSession = Depends(get_s
|
||||
await session.commit()
|
||||
await session.refresh(note, ["attachments"])
|
||||
await enqueue("export_note_markdown", str(note.id))
|
||||
broadcaster.broadcast("notes_changed")
|
||||
return note
|
||||
|
||||
|
||||
@@ -97,6 +99,7 @@ async def update_note(
|
||||
await session.commit()
|
||||
await session.refresh(note, ["attachments"])
|
||||
await enqueue("export_note_markdown", str(note.id))
|
||||
broadcaster.broadcast("notes_changed")
|
||||
return note
|
||||
|
||||
|
||||
@@ -108,6 +111,7 @@ async def delete_note(note_id: uuid.UUID, session: AsyncSession = Depends(get_se
|
||||
await session.delete(note)
|
||||
await session.commit()
|
||||
await enqueue("remove_note_markdown", str(note_id))
|
||||
broadcaster.broadcast("notes_changed")
|
||||
return Response(status_code=204)
|
||||
|
||||
|
||||
@@ -151,6 +155,7 @@ async def add_attachment(
|
||||
await session.commit()
|
||||
await session.refresh(note, ["attachments"])
|
||||
await enqueue("export_note_markdown", str(note_id))
|
||||
broadcaster.broadcast("notes_changed")
|
||||
return note
|
||||
|
||||
|
||||
@@ -177,4 +182,5 @@ async def delete_attachment(
|
||||
await session.delete(att)
|
||||
await session.commit()
|
||||
await enqueue("export_note_markdown", str(note_id))
|
||||
broadcaster.broadcast("notes_changed")
|
||||
return Response(status_code=204)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# backend/app/api/shopping.py
|
||||
import uuid
|
||||
from datetime import datetime, timezone, date as date_type
|
||||
from decimal import Decimal
|
||||
@@ -8,6 +7,8 @@ from sqlalchemy import select, text, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.core.broadcaster import broadcaster
|
||||
|
||||
from app.core.database import get_session
|
||||
from app.models.shopping import ShoppingList, ListItem, Product, Store
|
||||
from app.schemas.shopping import (
|
||||
@@ -172,6 +173,7 @@ async def create_shopping_list(
|
||||
session.add(lst)
|
||||
await session.commit()
|
||||
await session.refresh(lst, ["items"])
|
||||
broadcaster.broadcast("shopping_changed")
|
||||
return ShoppingListDetailResponse(
|
||||
**_list_to_response(lst).model_dump(),
|
||||
items=[],
|
||||
@@ -214,6 +216,7 @@ async def update_shopping_list(
|
||||
for field, value in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(lst, field, value)
|
||||
await session.commit()
|
||||
broadcaster.broadcast("shopping_changed")
|
||||
sorted_items = sorted(lst.items, key=lambda i: (i.sort_order or 999, str(i.id)))
|
||||
return ShoppingListDetailResponse(
|
||||
**_list_to_response(lst).model_dump(),
|
||||
@@ -228,6 +231,7 @@ async def delete_shopping_list(list_id: uuid.UUID, session: AsyncSession = Depen
|
||||
raise HTTPException(404, "Liste introuvable")
|
||||
await session.delete(lst)
|
||||
await session.commit()
|
||||
broadcaster.broadcast("shopping_changed")
|
||||
return Response(status_code=204)
|
||||
|
||||
|
||||
@@ -245,6 +249,7 @@ async def add_item(
|
||||
item = ListItem(list_id=list_id, **payload.model_dump())
|
||||
session.add(item)
|
||||
await session.commit()
|
||||
broadcaster.broadcast("shopping_changed")
|
||||
stmt = (
|
||||
select(ListItem)
|
||||
.where(ListItem.id == item.id)
|
||||
@@ -294,6 +299,7 @@ async def update_item(
|
||||
product.frequency_score += 1
|
||||
|
||||
await session.commit()
|
||||
broadcaster.broadcast("shopping_changed")
|
||||
await session.refresh(item, ["product"])
|
||||
return _item_to_response(item)
|
||||
|
||||
@@ -311,6 +317,7 @@ async def delete_item(
|
||||
raise HTTPException(404, "Article introuvable")
|
||||
await session.delete(item)
|
||||
await session.commit()
|
||||
broadcaster.broadcast("shopping_changed")
|
||||
return Response(status_code=204)
|
||||
|
||||
|
||||
@@ -386,6 +393,7 @@ async def generate_magic_list(session: AsyncSession = Depends(get_session)):
|
||||
))
|
||||
|
||||
await session.commit()
|
||||
broadcaster.broadcast("shopping_changed")
|
||||
|
||||
stmt = (
|
||||
select(ShoppingList)
|
||||
@@ -433,6 +441,7 @@ async def finish_shopping(list_id: uuid.UUID, session: AsyncSession = Depends(ge
|
||||
))
|
||||
|
||||
await session.commit()
|
||||
broadcaster.broadcast("shopping_changed")
|
||||
sorted_items = sorted(lst.items, key=lambda i: (i.sort_order or 999, str(i.id)))
|
||||
return ShoppingListDetailResponse(
|
||||
**_list_to_response(lst).model_dump(),
|
||||
|
||||
@@ -5,6 +5,7 @@ from fastapi.responses import Response
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.broadcaster import broadcaster
|
||||
from app.core.database import get_session
|
||||
from app.models.todos import TodoItem
|
||||
from app.schemas.todos import TodoCreate, TodoUpdate, PostponeRequest, TodoResponse
|
||||
@@ -55,6 +56,7 @@ async def create_todo(
|
||||
session.add(item)
|
||||
await session.commit()
|
||||
await session.refresh(item)
|
||||
broadcaster.broadcast("todos_changed")
|
||||
return item
|
||||
|
||||
|
||||
@@ -74,6 +76,7 @@ async def update_todo(
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(item)
|
||||
broadcaster.broadcast("todos_changed")
|
||||
return item
|
||||
|
||||
|
||||
@@ -87,6 +90,7 @@ async def delete_todo(
|
||||
raise HTTPException(status_code=404, detail="Tâche introuvable")
|
||||
await session.delete(item)
|
||||
await session.commit()
|
||||
broadcaster.broadcast("todos_changed")
|
||||
return Response(status_code=204)
|
||||
|
||||
|
||||
@@ -108,4 +112,5 @@ async def postpone_todo(
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(item)
|
||||
broadcaster.broadcast("todos_changed")
|
||||
return item
|
||||
|
||||
Reference in New Issue
Block a user