feat: live messages via SSE + real client IP
- backend: SSE endpoint /api/messages/stream backed by Redis pub/sub - backend: read real client IP via getConnInfo (fallback for x-forwarded-for) - backend: CORS allow any origin (dev: LAN access from phone) - frontend: useMessages subscribes via EventSource, auto-reconnect, merges new messages/replies live - frontend: vite host:true to expose dev server on LAN Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
export interface Reply {
|
||||
id: string;
|
||||
content: string;
|
||||
authorIp: string;
|
||||
createdAt: string;
|
||||
parentId?: string | null;
|
||||
}
|
||||
|
||||
export interface Message extends Reply {
|
||||
@@ -18,13 +19,16 @@ export function useMessages() {
|
||||
const messages = ref<Message[]>([]);
|
||||
const loading = ref(false);
|
||||
const sending = ref(false);
|
||||
const connected = ref(false);
|
||||
|
||||
let source: EventSource | null = null;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
async function fetchMessages(): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/messages`);
|
||||
if (res.ok) {
|
||||
// L'API renvoie du plus récent au plus ancien ; on inverse pour affichage chronologique
|
||||
messages.value = ((await res.json()) as Message[]).reverse();
|
||||
}
|
||||
} finally {
|
||||
@@ -32,24 +36,67 @@ export function useMessages() {
|
||||
}
|
||||
}
|
||||
|
||||
async function postMessage(content: string): Promise<boolean> {
|
||||
function applyIncoming(payload: Reply & { parentId: string | null }): void {
|
||||
if (payload.parentId) {
|
||||
const parent = messages.value.find((m) => m.id === payload.parentId);
|
||||
if (!parent) return;
|
||||
if (parent.replies.some((r) => r.id === payload.id)) return;
|
||||
parent.replies.push(payload);
|
||||
} else {
|
||||
if (messages.value.some((m) => m.id === payload.id)) return;
|
||||
messages.value.push({ ...payload, replies: [] });
|
||||
}
|
||||
}
|
||||
|
||||
function connect(): void {
|
||||
if (source) source.close();
|
||||
source = new EventSource(`${API_URL}/api/messages/stream`);
|
||||
|
||||
source.addEventListener('ready', () => {
|
||||
connected.value = true;
|
||||
});
|
||||
|
||||
source.addEventListener('message', (e) => {
|
||||
try {
|
||||
applyIncoming(JSON.parse((e as MessageEvent).data));
|
||||
} catch {
|
||||
/* ignore malformed payload */
|
||||
}
|
||||
});
|
||||
|
||||
source.onerror = () => {
|
||||
connected.value = false;
|
||||
source?.close();
|
||||
source = null;
|
||||
reconnectTimer = setTimeout(connect, 2000);
|
||||
};
|
||||
}
|
||||
|
||||
async function postMessage(content: string, parentId?: string): Promise<boolean> {
|
||||
if (!content.trim()) return false;
|
||||
sending.value = true;
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/messages`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content: content.trim() }),
|
||||
body: JSON.stringify({ content: content.trim(), parentId }),
|
||||
});
|
||||
if (!res.ok) return false;
|
||||
await fetchMessages();
|
||||
return true;
|
||||
return res.ok;
|
||||
} finally {
|
||||
sending.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchMessages);
|
||||
onMounted(async () => {
|
||||
await fetchMessages();
|
||||
connect();
|
||||
});
|
||||
|
||||
return { messages, loading, sending, postMessage };
|
||||
onBeforeUnmount(() => {
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
source?.close();
|
||||
source = null;
|
||||
});
|
||||
|
||||
return { messages, loading, sending, connected, postMessage, fetchMessages };
|
||||
}
|
||||
|
||||
@@ -11,5 +11,6 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
host: true,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user