Corrige le Realtime qui ne diffusait aucun événement à travers l'app
Build and deploy / deploy (push) Successful in 37s
Build and deploy / deploy (push) Successful in 37s
Les canaux Realtime (Char, Gardien, classement, journal, calendrier, Icare) s'abonnaient juste après createClient(), avant que la session ne soit chargée de façon asynchrone — le canal rejoignait donc Supabase en rôle "anon" au lieu de "authenticated", et les policies RLS "to authenticated" bloquaient silencieusement tous les événements (l'abonnement lui-même réussissait, ce qui masquait le problème). Ajoute waitForRealtimeAuth() dans src/lib/supabase/client.ts, qui attend la session et pousse explicitement le token avant de s'abonner. Vérifié avec deux sessions navigateur distinctes : les changements se propagent maintenant réellement sans rechargement.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { createClient } from "@/lib/supabase/client";
|
import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
|
||||||
import { DayCard, type DayRow } from "./day-card";
|
import { DayCard, type DayRow } from "./day-card";
|
||||||
import { PANTHEON_PRESETS, getGodPreset } from "@/lib/pantheon";
|
import { PANTHEON_PRESETS, getGodPreset } from "@/lib/pantheon";
|
||||||
import { IconPlus } from "@/components/icons";
|
import { IconPlus } from "@/components/icons";
|
||||||
@@ -69,15 +69,22 @@ export function CalendrierView({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
const channel = supabase
|
let channel: ReturnType<typeof supabase.channel> | null = null;
|
||||||
.channel("calendrier-changes")
|
let cancelled = false;
|
||||||
.on("postgres_changes", { event: "*", schema: "public", table: "days" }, refetch)
|
|
||||||
.on("postgres_changes", { event: "*", schema: "public", table: "events" }, refetch)
|
waitForRealtimeAuth(supabase).then(() => {
|
||||||
.on("postgres_changes", { event: "*", schema: "public", table: "settings" }, refetch)
|
if (cancelled) return;
|
||||||
.subscribe();
|
channel = supabase
|
||||||
|
.channel("calendrier-changes")
|
||||||
|
.on("postgres_changes", { event: "*", schema: "public", table: "days" }, refetch)
|
||||||
|
.on("postgres_changes", { event: "*", schema: "public", table: "events" }, refetch)
|
||||||
|
.on("postgres_changes", { event: "*", schema: "public", table: "settings" }, refetch)
|
||||||
|
.subscribe();
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
supabase.removeChannel(channel);
|
cancelled = true;
|
||||||
|
if (channel) supabase.removeChannel(channel);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { createClient } from "@/lib/supabase/client";
|
import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
|
||||||
import { EntryRanking } from "./entry-ranking";
|
import { EntryRanking } from "./entry-ranking";
|
||||||
import { GardienBar } from "./gardien-bar";
|
import { GardienBar } from "./gardien-bar";
|
||||||
import { RouletteSection } from "./roulette-section";
|
import { RouletteSection } from "./roulette-section";
|
||||||
@@ -64,16 +64,23 @@ export function CharView({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
const channel = supabase
|
let channel: ReturnType<typeof supabase.channel> | null = null;
|
||||||
.channel("char-changes")
|
let cancelled = false;
|
||||||
.on("postgres_changes", { event: "*", schema: "public", table: "chariot_slots" }, refetch)
|
|
||||||
.on("postgres_changes", { event: "*", schema: "public", table: "chariot_entries" }, refetch)
|
waitForRealtimeAuth(supabase).then(() => {
|
||||||
.on("postgres_changes", { event: "*", schema: "public", table: "chariot_questions" }, refetch)
|
if (cancelled) return;
|
||||||
.on("postgres_changes", { event: "*", schema: "public", table: "settings" }, refetch)
|
channel = supabase
|
||||||
.subscribe();
|
.channel("char-changes")
|
||||||
|
.on("postgres_changes", { event: "*", schema: "public", table: "chariot_slots" }, refetch)
|
||||||
|
.on("postgres_changes", { event: "*", schema: "public", table: "chariot_entries" }, refetch)
|
||||||
|
.on("postgres_changes", { event: "*", schema: "public", table: "chariot_questions" }, refetch)
|
||||||
|
.on("postgres_changes", { event: "*", schema: "public", table: "settings" }, refetch)
|
||||||
|
.subscribe();
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
supabase.removeChannel(channel);
|
cancelled = true;
|
||||||
|
if (channel) supabase.removeChannel(channel);
|
||||||
};
|
};
|
||||||
}, [refetch]);
|
}, [refetch]);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { IconChevronDown, IconPencil, IconPlus, IconTrash } from "@/components/icons";
|
import { IconChevronDown, IconPencil, IconPlus, IconTrash } from "@/components/icons";
|
||||||
import { createClient } from "@/lib/supabase/client";
|
import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
|
||||||
import type { QuestionRow } from "./page";
|
import type { QuestionRow } from "./page";
|
||||||
|
|
||||||
function QuestionForm({
|
function QuestionForm({
|
||||||
@@ -211,14 +211,21 @@ export function QuestionsView({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
const channel = supabase
|
let channel: ReturnType<typeof supabase.channel> | null = null;
|
||||||
.channel("char-questions-changes")
|
let cancelled = false;
|
||||||
.on("postgres_changes", { event: "*", schema: "public", table: "chariot_questions" }, refetch)
|
|
||||||
.on("postgres_changes", { event: "*", schema: "public", table: "settings" }, refetch)
|
waitForRealtimeAuth(supabase).then(() => {
|
||||||
.subscribe();
|
if (cancelled) return;
|
||||||
|
channel = supabase
|
||||||
|
.channel("char-questions-changes")
|
||||||
|
.on("postgres_changes", { event: "*", schema: "public", table: "chariot_questions" }, refetch)
|
||||||
|
.on("postgres_changes", { event: "*", schema: "public", table: "settings" }, refetch)
|
||||||
|
.subscribe();
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
supabase.removeChannel(channel);
|
cancelled = true;
|
||||||
|
if (channel) supabase.removeChannel(channel);
|
||||||
};
|
};
|
||||||
}, [refetch]);
|
}, [refetch]);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Avatar } from "@/components/avatar";
|
import { Avatar } from "@/components/avatar";
|
||||||
import { computeRanksBy } from "@/lib/ranking";
|
import { computeRanksBy } from "@/lib/ranking";
|
||||||
import { createClient } from "@/lib/supabase/client";
|
import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
|
||||||
import { IcarusGame } from "./icarus-game";
|
import { IcarusGame } from "./icarus-game";
|
||||||
import type { IcarusScoreEntry } from "./page";
|
import type { IcarusScoreEntry } from "./page";
|
||||||
|
|
||||||
@@ -45,15 +45,22 @@ export function IcareView({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
const channel = supabase
|
let channel: ReturnType<typeof supabase.channel> | null = null;
|
||||||
.channel("icarus-scores")
|
let cancelled = false;
|
||||||
.on("postgres_changes", { event: "*", schema: "public", table: "icarus_scores" }, () => {
|
|
||||||
refetch();
|
waitForRealtimeAuth(supabase).then(() => {
|
||||||
})
|
if (cancelled) return;
|
||||||
.subscribe();
|
channel = supabase
|
||||||
|
.channel("icarus-scores")
|
||||||
|
.on("postgres_changes", { event: "*", schema: "public", table: "icarus_scores" }, () => {
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.subscribe();
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
supabase.removeChannel(channel);
|
cancelled = true;
|
||||||
|
if (channel) supabase.removeChannel(channel);
|
||||||
};
|
};
|
||||||
}, [refetch]);
|
}, [refetch]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { createClient } from "@/lib/supabase/client";
|
import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
|
||||||
|
|
||||||
type Entry = {
|
type Entry = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -31,20 +31,27 @@ export function JournalView({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
const channel = supabase
|
let channel: ReturnType<typeof supabase.channel> | null = null;
|
||||||
.channel("journal-points-log")
|
let cancelled = false;
|
||||||
.on(
|
|
||||||
"postgres_changes",
|
waitForRealtimeAuth(supabase).then(() => {
|
||||||
{ event: "INSERT", schema: "public", table: "points_log" },
|
if (cancelled) return;
|
||||||
(payload) => {
|
channel = supabase
|
||||||
const row = payload.new as Entry;
|
.channel("journal-points-log")
|
||||||
setEntries((current) => [row, ...current]);
|
.on(
|
||||||
},
|
"postgres_changes",
|
||||||
)
|
{ event: "INSERT", schema: "public", table: "points_log" },
|
||||||
.subscribe();
|
(payload) => {
|
||||||
|
const row = payload.new as Entry;
|
||||||
|
setEntries((current) => [row, ...current]);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
supabase.removeChannel(channel);
|
cancelled = true;
|
||||||
|
if (channel) supabase.removeChannel(channel);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { createClient } from "@/lib/supabase/client";
|
import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
|
||||||
import { Avatar } from "@/components/avatar";
|
import { Avatar } from "@/components/avatar";
|
||||||
import { Podium } from "@/components/podium";
|
import { Podium } from "@/components/podium";
|
||||||
import { JudgePointControls } from "@/components/judge-point-controls";
|
import { JudgePointControls } from "@/components/judge-point-controls";
|
||||||
@@ -47,34 +47,41 @@ export function LeaderboardView({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
const channel = supabase
|
let channel: ReturnType<typeof supabase.channel> | null = null;
|
||||||
.channel("leaderboard-profiles")
|
let cancelled = false;
|
||||||
.on(
|
|
||||||
"postgres_changes",
|
|
||||||
{ event: "*", schema: "public", table: "profiles" },
|
|
||||||
(payload) => {
|
|
||||||
if (payload.eventType === "DELETE") return;
|
|
||||||
const row = payload.new as Profile;
|
|
||||||
|
|
||||||
setProfiles((current) => {
|
waitForRealtimeAuth(supabase).then(() => {
|
||||||
const exists = current.some((p) => p.id === row.id);
|
if (cancelled) return;
|
||||||
return exists
|
channel = supabase
|
||||||
? current.map((p) => (p.id === row.id ? { ...p, ...row } : p))
|
.channel("leaderboard-profiles")
|
||||||
: [...current, row];
|
.on(
|
||||||
});
|
"postgres_changes",
|
||||||
|
{ event: "*", schema: "public", table: "profiles" },
|
||||||
|
(payload) => {
|
||||||
|
if (payload.eventType === "DELETE") return;
|
||||||
|
const row = payload.new as Profile;
|
||||||
|
|
||||||
const previousPoints = pointsRef.current.get(row.id);
|
setProfiles((current) => {
|
||||||
if (previousPoints !== undefined && previousPoints !== row.points) {
|
const exists = current.some((p) => p.id === row.id);
|
||||||
setFlashingId(row.id);
|
return exists
|
||||||
setTimeout(() => setFlashingId((current) => (current === row.id ? null : current)), 900);
|
? current.map((p) => (p.id === row.id ? { ...p, ...row } : p))
|
||||||
}
|
: [...current, row];
|
||||||
pointsRef.current.set(row.id, row.points);
|
});
|
||||||
},
|
|
||||||
)
|
const previousPoints = pointsRef.current.get(row.id);
|
||||||
.subscribe();
|
if (previousPoints !== undefined && previousPoints !== row.points) {
|
||||||
|
setFlashingId(row.id);
|
||||||
|
setTimeout(() => setFlashingId((current) => (current === row.id ? null : current)), 900);
|
||||||
|
}
|
||||||
|
pointsRef.current.set(row.id, row.points);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
supabase.removeChannel(channel);
|
cancelled = true;
|
||||||
|
if (channel) supabase.removeChannel(channel);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createBrowserClient } from "@supabase/ssr";
|
import { createBrowserClient } from "@supabase/ssr";
|
||||||
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
export function createClient() {
|
export function createClient() {
|
||||||
return createBrowserClient(
|
return createBrowserClient(
|
||||||
@@ -6,3 +7,18 @@ export function createClient() {
|
|||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createClient() charge la session (et donc le token Realtime) de façon
|
||||||
|
// asynchrone après coup : un .channel().subscribe() lancé juste après
|
||||||
|
// rejoint le canal encore en rôle "anon", et les policies RLS
|
||||||
|
// "to authenticated" ne laissent alors jamais passer aucun événement
|
||||||
|
// (le canal s'abonne "avec succès" mais ne reçoit plus rien ensuite).
|
||||||
|
// À appeler avant tout .channel(...).subscribe().
|
||||||
|
export async function waitForRealtimeAuth(supabase: SupabaseClient) {
|
||||||
|
const {
|
||||||
|
data: { session },
|
||||||
|
} = await supabase.auth.getSession();
|
||||||
|
if (session) {
|
||||||
|
supabase.realtime.setAuth(session.access_token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user