fix(ui): rendre les vues tickets et notifications live
This commit is contained in:
parent
e0b09e4c29
commit
f9b565cfda
4 changed files with 352 additions and 69 deletions
|
|
@ -4,7 +4,7 @@ import {
|
||||||
requestPermission,
|
requestPermission,
|
||||||
sendNotification,
|
sendNotification,
|
||||||
} from "@tauri-apps/plugin-notification";
|
} from "@tauri-apps/plugin-notification";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
listNotifications,
|
listNotifications,
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
markNotificationRead,
|
markNotificationRead,
|
||||||
} from "../../lib/api";
|
} from "../../lib/api";
|
||||||
import type { OrchaiNotification } from "../../lib/types";
|
import type { OrchaiNotification } from "../../lib/types";
|
||||||
|
import { useLiveRefresh } from "../../lib/useLiveRefresh";
|
||||||
import { buttonClass, cardClass, pillClass } from "../ui/primitives";
|
import { buttonClass, cardClass, pillClass } from "../ui/primitives";
|
||||||
|
|
||||||
type NewNotificationEvent = {
|
type NewNotificationEvent = {
|
||||||
|
|
@ -61,7 +62,7 @@ export default function NotificationCenter() {
|
||||||
"all"
|
"all"
|
||||||
);
|
);
|
||||||
|
|
||||||
async function loadNotifications() {
|
const loadNotifications = useCallback(async () => {
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
setNotifications([]);
|
setNotifications([]);
|
||||||
return;
|
return;
|
||||||
|
|
@ -73,11 +74,18 @@ export default function NotificationCenter() {
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore load errors in layout chrome
|
// Ignore load errors in layout chrome
|
||||||
}
|
}
|
||||||
}
|
}, [projectId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadNotifications();
|
void loadNotifications();
|
||||||
}, [projectId]);
|
}, [loadNotifications]);
|
||||||
|
|
||||||
|
useLiveRefresh({
|
||||||
|
enabled: Boolean(projectId),
|
||||||
|
projectId,
|
||||||
|
refresh: loadNotifications,
|
||||||
|
fallbackIntervalMs: 15_000,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
type TicketResourceConfig,
|
type TicketResourceConfig,
|
||||||
} from "../../lib/ticketResource";
|
} from "../../lib/ticketResource";
|
||||||
import type { ProcessedTicket, Worktree } from "../../lib/types";
|
import type { ProcessedTicket, Worktree } from "../../lib/types";
|
||||||
|
import { useLiveRefresh } from "../../lib/useLiveRefresh";
|
||||||
import ConfirmModal from "../ui/ConfirmModal";
|
import ConfirmModal from "../ui/ConfirmModal";
|
||||||
import TicketStatusBadge from "../ui/TicketStatusBadge";
|
import TicketStatusBadge from "../ui/TicketStatusBadge";
|
||||||
import {
|
import {
|
||||||
|
|
@ -76,6 +77,24 @@ export default function TicketDetail() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [isDeleteWorktreeModalOpen, setIsDeleteWorktreeModalOpen] = useState(false);
|
const [isDeleteWorktreeModalOpen, setIsDeleteWorktreeModalOpen] = useState(false);
|
||||||
|
const latestTicketIdRef = useRef<string | undefined>(ticketId);
|
||||||
|
const worktreeSignatureRef = useRef<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
latestTicketIdRef.current = ticketId;
|
||||||
|
}, [ticketId]);
|
||||||
|
|
||||||
|
const resetWorktreeUi = useCallback(() => {
|
||||||
|
setDiff(null);
|
||||||
|
setDiffError("");
|
||||||
|
setDiffLoading(false);
|
||||||
|
setAvailableBranches([]);
|
||||||
|
setBranchInputMode("select");
|
||||||
|
setTargetBranch("");
|
||||||
|
setBranchesError("");
|
||||||
|
setBranchesLoading(false);
|
||||||
|
setBranchesLoadedForWorktreeId(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
async function loadBranchOptions(worktreeId: string) {
|
async function loadBranchOptions(worktreeId: string) {
|
||||||
setBranchesLoading(true);
|
setBranchesLoading(true);
|
||||||
|
|
@ -103,41 +122,63 @@ export default function TicketDetail() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadData() {
|
const loadData = useCallback(async () => {
|
||||||
if (!ticketId) return;
|
if (!ticketId) {
|
||||||
|
setTicket(null);
|
||||||
|
setWorktree(null);
|
||||||
|
setResourceConfig(DEFAULT_TICKET_RESOURCE_CONFIG);
|
||||||
|
resetWorktreeUi();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
const result = await getTicketResult(ticketId);
|
const result = await getTicketResult(ticketId);
|
||||||
setTicket(result.ticket);
|
|
||||||
try {
|
|
||||||
const config = await fetchTicketResourceConfig(result.ticket.project_id);
|
|
||||||
setResourceConfig(config);
|
|
||||||
} catch {
|
|
||||||
setResourceConfig(DEFAULT_TICKET_RESOURCE_CONFIG);
|
|
||||||
}
|
|
||||||
setWorktree(result.worktree);
|
|
||||||
setTab("info");
|
|
||||||
setDiff(null);
|
|
||||||
setDiffError("");
|
|
||||||
setDiffLoading(false);
|
|
||||||
setAvailableBranches([]);
|
|
||||||
setBranchInputMode("select");
|
|
||||||
setTargetBranch("");
|
|
||||||
setBranchesError("");
|
|
||||||
setBranchesLoading(false);
|
|
||||||
setBranchesLoadedForWorktreeId(null);
|
|
||||||
|
|
||||||
if (result.worktree && result.worktree.status === "Active") {
|
if (latestTicketIdRef.current !== ticketId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTicket(result.ticket);
|
||||||
|
|
||||||
|
const config = await fetchTicketResourceConfig(result.ticket.project_id).catch(
|
||||||
|
() => DEFAULT_TICKET_RESOURCE_CONFIG
|
||||||
|
);
|
||||||
|
if (latestTicketIdRef.current !== ticketId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setResourceConfig(config);
|
||||||
|
|
||||||
|
const worktreeSignature = result.worktree
|
||||||
|
? `${result.worktree.id}:${result.worktree.status}`
|
||||||
|
: "none";
|
||||||
|
if (worktreeSignatureRef.current !== worktreeSignature) {
|
||||||
|
worktreeSignatureRef.current = worktreeSignature;
|
||||||
|
resetWorktreeUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
setWorktree(result.worktree);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getErrorMessage(err));
|
if (latestTicketIdRef.current === ticketId) {
|
||||||
|
setError(getErrorMessage(err));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}, [resetWorktreeUi, ticketId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
worktreeSignatureRef.current = "";
|
||||||
|
setTab("info");
|
||||||
|
resetWorktreeUi();
|
||||||
void loadData();
|
void loadData();
|
||||||
}, [ticketId]);
|
}, [loadData, resetWorktreeUi]);
|
||||||
|
|
||||||
|
useLiveRefresh({
|
||||||
|
enabled: Boolean(ticketId),
|
||||||
|
projectId: ticket?.project_id,
|
||||||
|
ticketId,
|
||||||
|
refresh: loadData,
|
||||||
|
fallbackIntervalMs: 7_000,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tab !== "info") return;
|
if (tab !== "info") return;
|
||||||
|
|
@ -199,8 +240,9 @@ export default function TicketDetail() {
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getErrorMessage(err));
|
setError(getErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCancel() {
|
async function handleCancel() {
|
||||||
|
|
@ -211,8 +253,9 @@ export default function TicketDetail() {
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getErrorMessage(err));
|
setError(getErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleApplyFix() {
|
async function handleApplyFix() {
|
||||||
|
|
@ -240,8 +283,9 @@ export default function TicketDetail() {
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getErrorMessage(err));
|
setError(getErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteWorktree() {
|
async function handleDeleteWorktree() {
|
||||||
|
|
@ -250,17 +294,12 @@ export default function TicketDetail() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await deleteWorktreeCmd(worktree.id);
|
await deleteWorktreeCmd(worktree.id);
|
||||||
setWorktree(null);
|
await loadData();
|
||||||
setDiff(null);
|
|
||||||
setAvailableBranches([]);
|
|
||||||
setTargetBranch("");
|
|
||||||
setBranchInputMode("select");
|
|
||||||
setBranchesError("");
|
|
||||||
setBranchesLoading(false);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getErrorMessage(err));
|
setError(getErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ticket) {
|
if (!ticket) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
import { getProject, listProcessedTickets } from "../../lib/api";
|
import { getProject, listProcessedTickets } from "../../lib/api";
|
||||||
import {
|
import {
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
type TicketResourceConfig,
|
type TicketResourceConfig,
|
||||||
} from "../../lib/ticketResource";
|
} from "../../lib/ticketResource";
|
||||||
import type { ProcessedTicket, Project } from "../../lib/types";
|
import type { ProcessedTicket, Project } from "../../lib/types";
|
||||||
|
import { useLiveRefresh } from "../../lib/useLiveRefresh";
|
||||||
import TicketStatusBadge from "../ui/TicketStatusBadge";
|
import TicketStatusBadge from "../ui/TicketStatusBadge";
|
||||||
import {
|
import {
|
||||||
cardContentClass,
|
cardContentClass,
|
||||||
|
|
@ -24,37 +25,50 @@ export default function TicketList() {
|
||||||
DEFAULT_TICKET_RESOURCE_CONFIG
|
DEFAULT_TICKET_RESOURCE_CONFIG
|
||||||
);
|
);
|
||||||
const [filter, setFilter] = useState<string>("all");
|
const [filter, setFilter] = useState<string>("all");
|
||||||
|
const latestProjectIdRef = useRef<string | undefined>(projectId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!projectId) return;
|
latestProjectIdRef.current = projectId;
|
||||||
let cancelled = false;
|
|
||||||
Promise.all([getProject(projectId), listProcessedTickets(projectId)])
|
|
||||||
.then(([proj, tkts]) => {
|
|
||||||
if (cancelled) return;
|
|
||||||
setProject(proj);
|
|
||||||
setTickets(tkts);
|
|
||||||
})
|
|
||||||
.catch((error: unknown) => {
|
|
||||||
console.error("Failed to load ticket list", error);
|
|
||||||
});
|
|
||||||
|
|
||||||
void fetchTicketResourceConfig(projectId)
|
|
||||||
.then((config) => {
|
|
||||||
if (!cancelled) {
|
|
||||||
setResourceConfig(config);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) {
|
|
||||||
setResourceConfig(DEFAULT_TICKET_RESOURCE_CONFIG);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
if (!projectId) {
|
||||||
|
setProject(null);
|
||||||
|
setTickets([]);
|
||||||
|
setResourceConfig(DEFAULT_TICKET_RESOURCE_CONFIG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [proj, tkts, config] = await Promise.all([
|
||||||
|
getProject(projectId),
|
||||||
|
listProcessedTickets(projectId),
|
||||||
|
fetchTicketResourceConfig(projectId).catch(() => DEFAULT_TICKET_RESOURCE_CONFIG),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (latestProjectIdRef.current !== projectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setProject(proj);
|
||||||
|
setTickets(tkts);
|
||||||
|
setResourceConfig(config);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("Failed to load ticket list", error);
|
||||||
|
}
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
useLiveRefresh({
|
||||||
|
enabled: Boolean(projectId),
|
||||||
|
projectId,
|
||||||
|
refresh: loadData,
|
||||||
|
fallbackIntervalMs: 8_000,
|
||||||
|
});
|
||||||
|
|
||||||
const filtered = filter === "all" ? tickets : tickets.filter((t) => t.status === filter);
|
const filtered = filter === "all" ? tickets : tickets.filter((t) => t.status === filter);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
222
src/lib/useLiveRefresh.ts
Normal file
222
src/lib/useLiveRefresh.ts
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
interface Scope {
|
||||||
|
projectId?: string | null;
|
||||||
|
ticketId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseLiveRefreshOptions extends Scope {
|
||||||
|
enabled?: boolean;
|
||||||
|
refresh: () => Promise<void> | void;
|
||||||
|
fallbackIntervalMs?: number;
|
||||||
|
debounceMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventScope {
|
||||||
|
projectId?: string;
|
||||||
|
ticketId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventDescriptor {
|
||||||
|
name: string;
|
||||||
|
extractScope: (payload: unknown) => EventScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LIVE_EVENTS: EventDescriptor[] = [
|
||||||
|
{ name: "polling-started", extractScope: extractProjectScope },
|
||||||
|
{ name: "polling-finished", extractScope: extractProjectScope },
|
||||||
|
{ name: "polling-error", extractScope: extractProjectScope },
|
||||||
|
{ name: "new-tickets-detected", extractScope: extractProjectScope },
|
||||||
|
{ name: "ticket-processing-started", extractScope: extractTicketScope },
|
||||||
|
{ name: "ticket-processing-done", extractScope: extractTicketScope },
|
||||||
|
{ name: "ticket-processing-error", extractScope: extractTicketScope },
|
||||||
|
{ name: "graylog-polling-started", extractScope: extractProjectScope },
|
||||||
|
{ name: "graylog-polling-finished", extractScope: extractProjectScope },
|
||||||
|
{ name: "graylog-polling-error", extractScope: extractProjectScope },
|
||||||
|
{ name: "graylog-subject-triggered", extractScope: extractProjectScope },
|
||||||
|
{ name: "new-notification", extractScope: extractNotificationScope },
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_FALLBACK_INTERVAL_MS = 12_000;
|
||||||
|
const DEFAULT_DEBOUNCE_MS = 300;
|
||||||
|
|
||||||
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||||
|
if (typeof value === "object" && value !== null) {
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function asString(value: unknown): string | undefined {
|
||||||
|
return typeof value === "string" ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractProjectScope(payload: unknown): EventScope {
|
||||||
|
const raw = asRecord(payload);
|
||||||
|
return {
|
||||||
|
projectId: asString(raw?.project_id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTicketScope(payload: unknown): EventScope {
|
||||||
|
const raw = asRecord(payload);
|
||||||
|
return {
|
||||||
|
projectId: asString(raw?.project_id),
|
||||||
|
ticketId: asString(raw?.ticket_id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractNotificationScope(payload: unknown): EventScope {
|
||||||
|
const raw = asRecord(payload);
|
||||||
|
const notification = asRecord(raw?.notification);
|
||||||
|
return {
|
||||||
|
projectId: asString(notification?.project_id),
|
||||||
|
ticketId: asString(notification?.ticket_id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesScope(scope: Scope, eventScope: EventScope): boolean {
|
||||||
|
if (scope.ticketId) {
|
||||||
|
if (eventScope.ticketId === scope.ticketId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope.projectId && eventScope.projectId === scope.projectId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope.projectId) {
|
||||||
|
return eventScope.projectId === scope.projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLiveRefresh({
|
||||||
|
enabled = true,
|
||||||
|
refresh,
|
||||||
|
projectId,
|
||||||
|
ticketId,
|
||||||
|
fallbackIntervalMs = DEFAULT_FALLBACK_INTERVAL_MS,
|
||||||
|
debounceMs = DEFAULT_DEBOUNCE_MS,
|
||||||
|
}: UseLiveRefreshOptions): void {
|
||||||
|
const refreshRef = useRef(refresh);
|
||||||
|
const inFlightRef = useRef(false);
|
||||||
|
const pendingRefreshRef = useRef(false);
|
||||||
|
const timeoutRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshRef.current = refresh;
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
|
const runRefresh = useCallback(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inFlightRef.current) {
|
||||||
|
pendingRefreshRef.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inFlightRef.current = true;
|
||||||
|
void Promise.resolve(refreshRef.current())
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
console.error("Live refresh failed", error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
inFlightRef.current = false;
|
||||||
|
if (pendingRefreshRef.current) {
|
||||||
|
pendingRefreshRef.current = false;
|
||||||
|
runRefresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [enabled]);
|
||||||
|
|
||||||
|
const scheduleRefresh = useCallback(
|
||||||
|
(delayMs = debounceMs) => {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeoutRef.current !== null) {
|
||||||
|
window.clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutRef.current = window.setTimeout(() => {
|
||||||
|
timeoutRef.current = null;
|
||||||
|
runRefresh();
|
||||||
|
}, delayMs);
|
||||||
|
},
|
||||||
|
[debounceMs, enabled, runRefresh]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
let unlistenFns: Array<() => void> = [];
|
||||||
|
|
||||||
|
const setup = async () => {
|
||||||
|
try {
|
||||||
|
const listeners = await Promise.all(
|
||||||
|
LIVE_EVENTS.map((descriptor) =>
|
||||||
|
listen<unknown>(descriptor.name, (event) => {
|
||||||
|
if (
|
||||||
|
!matchesScope({ projectId, ticketId }, descriptor.extractScope(event.payload))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scheduleRefresh();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
listeners.forEach((unlisten) => unlisten());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unlistenFns = listeners;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("Failed to subscribe to live refresh events", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void setup();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
unlistenFns.forEach((unlisten) => unlisten());
|
||||||
|
};
|
||||||
|
}, [enabled, projectId, scheduleRefresh, ticketId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || fallbackIntervalMs <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const intervalId = window.setInterval(() => {
|
||||||
|
runRefresh();
|
||||||
|
}, fallbackIntervalMs);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [enabled, fallbackIntervalMs, runRefresh]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
if (timeoutRef.current !== null) {
|
||||||
|
window.clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue