Skip to content

Commit 52be76c

Browse files
committed
remove active visitor counter in redis
1 parent 8dd3966 commit 52be76c

7 files changed

Lines changed: 213 additions & 231 deletions

File tree

apps/api/src/controllers/live.controller.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import type { WebSocket } from '@fastify/websocket';
22
import { eventBuffer } from '@openpanel/db';
33
import { setSuperJson } from '@openpanel/json';
4-
import {
5-
psubscribeToPublishedEvent,
6-
subscribeToPublishedEvent,
7-
} from '@openpanel/redis';
4+
import { subscribeToPublishedEvent } from '@openpanel/redis';
85
import { getProjectAccess } from '@openpanel/trpc';
96
import { getOrganizationAccess } from '@openpanel/trpc/src/access';
107
import type { FastifyRequest } from 'fastify';
@@ -39,19 +36,8 @@ export function wsVisitors(
3936
}
4037
);
4138

42-
const punsubscribe = psubscribeToPublishedEvent(
43-
'__keyevent@0__:expired',
44-
(key) => {
45-
const [, , projectId] = key.split(':');
46-
if (projectId === params.projectId) {
47-
sendCount();
48-
}
49-
}
50-
);
51-
5239
socket.on('close', () => {
5340
unsubscribe();
54-
punsubscribe();
5541
});
5642
}
5743

apps/start/src/components/overview/live-counter.tsx

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,25 @@
1-
import { TooltipComplete } from '@/components/tooltip-complete';
2-
import { useDebounceState } from '@/hooks/use-debounce-state';
3-
import useWS from '@/hooks/use-ws';
4-
import { useTRPC } from '@/integrations/trpc/react';
5-
import { cn } from '@/utils/cn';
6-
import { useQuery, useQueryClient } from '@tanstack/react-query';
7-
import { useEffect, useRef } from 'react';
1+
import { useQueryClient } from '@tanstack/react-query';
2+
import { useCallback } from 'react';
83
import { toast } from 'sonner';
94
import { AnimatedNumber } from '../animated-number';
5+
import { TooltipComplete } from '@/components/tooltip-complete';
6+
import { useLiveCounter } from '@/hooks/use-live-counter';
7+
import { cn } from '@/utils/cn';
108

119
export interface LiveCounterProps {
1210
projectId: string;
1311
shareId?: string;
1412
}
1513

16-
const FIFTEEN_SECONDS = 1000 * 30;
17-
1814
export function LiveCounter({ projectId, shareId }: LiveCounterProps) {
19-
const trpc = useTRPC();
2015
const client = useQueryClient();
21-
const counter = useDebounceState(0, 1000);
22-
const lastRefresh = useRef(Date.now());
23-
const query = useQuery(
24-
trpc.overview.liveVisitors.queryOptions({
25-
projectId,
26-
shareId,
27-
}),
28-
);
29-
30-
useEffect(() => {
31-
if (query.data) {
32-
counter.set(query.data);
33-
}
34-
}, [query.data]);
35-
36-
useWS<number>(
37-
`/live/visitors/${projectId}`,
38-
(value) => {
39-
if (!Number.isNaN(value)) {
40-
counter.set(value);
41-
if (Date.now() - lastRefresh.current > FIFTEEN_SECONDS) {
42-
lastRefresh.current = Date.now();
43-
if (!document.hidden) {
44-
toast('Refreshed data');
45-
client.refetchQueries({
46-
type: 'active',
47-
});
48-
}
49-
}
50-
}
51-
},
52-
{
53-
debounce: {
54-
delay: 1000,
55-
maxWait: 5000,
56-
},
57-
},
58-
);
16+
const onRefresh = useCallback(() => {
17+
toast('Refreshed data');
18+
client.refetchQueries({
19+
type: 'active',
20+
});
21+
}, [client]);
22+
const counter = useLiveCounter({ projectId, shareId, onRefresh });
5923

6024
return (
6125
<TooltipComplete
@@ -66,13 +30,13 @@ export function LiveCounter({ projectId, shareId }: LiveCounterProps) {
6630
<div
6731
className={cn(
6832
'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all',
69-
counter.debounced === 0 && 'bg-destructive opacity-0',
33+
counter.debounced === 0 && 'bg-destructive opacity-0'
7034
)}
7135
/>
7236
<div
7337
className={cn(
74-
'absolute left-0 top-0 h-3 w-3 rounded-full bg-emerald-500 transition-all',
75-
counter.debounced === 0 && 'bg-destructive',
38+
'absolute top-0 left-0 h-3 w-3 rounded-full bg-emerald-500 transition-all',
39+
counter.debounced === 0 && 'bg-destructive'
7640
)}
7741
/>
7842
</div>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { useQuery, useQueryClient } from '@tanstack/react-query';
2+
import { useEffect, useRef } from 'react';
3+
import { useDebounceState } from './use-debounce-state';
4+
import useWS from './use-ws';
5+
import { useTRPC } from '@/integrations/trpc/react';
6+
7+
const FIFTEEN_SECONDS = 1000 * 15;
8+
/** Refetch from API when WS-only updates may be stale (e.g. visitors left). */
9+
const FALLBACK_STALE_MS = 1000 * 60;
10+
11+
export function useLiveCounter({
12+
projectId,
13+
shareId,
14+
onRefresh,
15+
}: {
16+
projectId: string;
17+
shareId?: string;
18+
onRefresh?: () => void;
19+
}) {
20+
const trpc = useTRPC();
21+
const queryClient = useQueryClient();
22+
const counter = useDebounceState(0, 1000);
23+
const lastRefresh = useRef(Date.now());
24+
const query = useQuery(
25+
trpc.overview.liveVisitors.queryOptions({
26+
projectId,
27+
shareId: shareId ?? undefined,
28+
})
29+
);
30+
31+
useEffect(() => {
32+
if (query.data) {
33+
counter.set(query.data);
34+
}
35+
}, [query.data]);
36+
37+
useWS<number>(
38+
`/live/visitors/${projectId}`,
39+
(value) => {
40+
if (!Number.isNaN(value)) {
41+
counter.set(value);
42+
if (Date.now() - lastRefresh.current > FIFTEEN_SECONDS) {
43+
lastRefresh.current = Date.now();
44+
if (!document.hidden) {
45+
onRefresh?.();
46+
}
47+
}
48+
}
49+
},
50+
{
51+
debounce: {
52+
delay: 1000,
53+
maxWait: 5000,
54+
},
55+
}
56+
);
57+
58+
useEffect(() => {
59+
const id = setInterval(async () => {
60+
if (Date.now() - lastRefresh.current < FALLBACK_STALE_MS) {
61+
return;
62+
}
63+
const data = await queryClient.fetchQuery(
64+
trpc.overview.liveVisitors.queryOptions(
65+
{
66+
projectId,
67+
shareId: shareId ?? undefined,
68+
},
69+
// Default query staleTime is 5m; bypass cache so this reconciliation always hits the API.
70+
{ staleTime: 0 }
71+
)
72+
);
73+
counter.set(data);
74+
lastRefresh.current = Date.now();
75+
}, FALLBACK_STALE_MS);
76+
77+
return () => clearInterval(id);
78+
}, [projectId, shareId, trpc, queryClient, counter.set]);
79+
80+
return counter;
81+
}
Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1+
import { useQuery, useQueryClient } from '@tanstack/react-query';
2+
import { createFileRoute } from '@tanstack/react-router';
3+
import { z } from 'zod';
14
import { AnimatedNumber } from '@/components/animated-number';
25
import { Ping } from '@/components/ping';
3-
import { useNumber } from '@/hooks/use-numer-formatter';
46
import useWS from '@/hooks/use-ws';
57
import { useTRPC } from '@/integrations/trpc/react';
68
import type { RouterOutputs } from '@/trpc/client';
7-
import { useQuery, useQueryClient } from '@tanstack/react-query';
8-
import { createFileRoute } from '@tanstack/react-router';
9-
import { z } from 'zod';
109

1110
const widgetSearchSchema = z.object({
1211
shareId: z.string(),
@@ -20,33 +19,33 @@ export const Route = createFileRoute('/widget/counter')({
2019
});
2120

2221
function RouteComponent() {
23-
const { shareId, limit, color } = Route.useSearch();
22+
const { shareId } = Route.useSearch();
2423
const trpc = useTRPC();
2524

2625
// Fetch widget data
2726
const { data, isLoading } = useQuery(
28-
trpc.widget.counter.queryOptions({ shareId }),
27+
trpc.widget.counter.queryOptions({ shareId })
2928
);
3029

3130
if (isLoading) {
3231
return (
33-
<div className="flex items-center gap-2 px-2 h-8">
32+
<div className="flex h-8 items-center gap-2 px-2">
3433
<Ping />
35-
<AnimatedNumber value={0} suffix=" unique visitors" />
34+
<AnimatedNumber suffix=" unique visitors" value={0} />
3635
</div>
3736
);
3837
}
3938

4039
if (!data) {
4140
return (
42-
<div className="flex items-center gap-2 px-2 h-8">
41+
<div className="flex h-8 items-center gap-2 px-2">
4342
<Ping className="bg-orange-500" />
44-
<AnimatedNumber value={0} suffix=" unique visitors" />
43+
<AnimatedNumber suffix=" unique visitors" value={0} />
4544
</div>
4645
);
4746
}
4847

49-
return <CounterWidget shareId={shareId} data={data} />;
48+
return <CounterWidget data={data} shareId={shareId} />;
5049
}
5150

5251
interface RealtimeWidgetProps {
@@ -57,30 +56,29 @@ interface RealtimeWidgetProps {
5756
function CounterWidget({ shareId, data }: RealtimeWidgetProps) {
5857
const trpc = useTRPC();
5958
const queryClient = useQueryClient();
60-
const number = useNumber();
6159

6260
// WebSocket subscription for real-time updates
6361
useWS<number>(
6462
`/live/visitors/${data.projectId}`,
65-
(res) => {
63+
() => {
6664
if (!document.hidden) {
6765
queryClient.refetchQueries(
68-
trpc.widget.counter.queryFilter({ shareId }),
66+
trpc.widget.counter.queryFilter({ shareId })
6967
);
7068
}
7169
},
7270
{
7371
debounce: {
7472
delay: 1000,
75-
maxWait: 60000,
73+
maxWait: 60_000,
7674
},
77-
},
75+
}
7876
);
7977

8078
return (
81-
<div className="flex items-center gap-2 px-2 h-8">
79+
<div className="flex h-8 items-center gap-2 px-2">
8280
<Ping />
83-
<AnimatedNumber value={data.counter} suffix=" unique visitors" />
81+
<AnimatedNumber suffix=" unique visitors" value={data.counter} />
8482
</div>
8583
);
8684
}

0 commit comments

Comments
 (0)