feat(activity): fan out activity updates to all connected instances
Switch pushActivities and setShowActivity from wsSend (home-only) to wsSendAll (all instances). Add myActivities cache for re-push on late connections.
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock useWebSocket before importing activityStore
|
||||||
|
const mockWsSend = vi.fn();
|
||||||
|
const mockWsSendAll = vi.fn();
|
||||||
|
vi.mock('../hooks/useWebSocket', () => ({
|
||||||
|
wsSend: (...args: unknown[]) => mockWsSend(...args),
|
||||||
|
wsSendAll: (...args: unknown[]) => mockWsSendAll(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { useActivityStore } from './activityStore';
|
||||||
|
import type { Activity } from '@backspace/shared';
|
||||||
|
|
||||||
|
const GAME_ACTIVITY: Activity = {
|
||||||
|
type: 'playing',
|
||||||
|
name: 'Minecraft',
|
||||||
|
timestamps: { start: Date.now() },
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
useActivityStore.getState().reset();
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('activityStore fan-out', () => {
|
||||||
|
it('pushActivities caches myActivities immediately before debounce', () => {
|
||||||
|
useActivityStore.getState().pushActivities([GAME_ACTIVITY]);
|
||||||
|
|
||||||
|
// myActivities should be set immediately (before the 5s debounce)
|
||||||
|
expect(useActivityStore.getState().myActivities).toEqual([GAME_ACTIVITY]);
|
||||||
|
|
||||||
|
// wsSendAll should NOT have been called yet (debounce hasn't fired)
|
||||||
|
expect(mockWsSendAll).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pushActivities calls wsSendAll (not wsSend) after debounce', () => {
|
||||||
|
useActivityStore.getState().pushActivities([GAME_ACTIVITY]);
|
||||||
|
vi.advanceTimersByTime(5000);
|
||||||
|
|
||||||
|
expect(mockWsSendAll).toHaveBeenCalledWith({
|
||||||
|
type: 'activity_update',
|
||||||
|
activities: [GAME_ACTIVITY],
|
||||||
|
});
|
||||||
|
expect(mockWsSend).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setShowActivity(false) fans out empty activities via wsSendAll and clears myActivities', () => {
|
||||||
|
// First set some activities
|
||||||
|
useActivityStore.getState().pushActivities([GAME_ACTIVITY]);
|
||||||
|
expect(useActivityStore.getState().myActivities).toEqual([GAME_ACTIVITY]);
|
||||||
|
|
||||||
|
// Now disable
|
||||||
|
useActivityStore.getState().setShowActivity(false);
|
||||||
|
|
||||||
|
expect(mockWsSendAll).toHaveBeenCalledWith({
|
||||||
|
type: 'activity_update',
|
||||||
|
activities: [],
|
||||||
|
});
|
||||||
|
expect(useActivityStore.getState().myActivities).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reset clears myActivities', () => {
|
||||||
|
useActivityStore.getState().pushActivities([GAME_ACTIVITY]);
|
||||||
|
expect(useActivityStore.getState().myActivities).toEqual([GAME_ACTIVITY]);
|
||||||
|
|
||||||
|
useActivityStore.getState().reset();
|
||||||
|
expect(useActivityStore.getState().myActivities).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pushActivities does nothing when showActivity is false', () => {
|
||||||
|
useActivityStore.getState().setShowActivity(false);
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
useActivityStore.getState().pushActivities([GAME_ACTIVITY]);
|
||||||
|
expect(useActivityStore.getState().myActivities).toBeNull();
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(5000);
|
||||||
|
expect(mockWsSendAll).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import type { Activity } from '@backspace/shared';
|
import type { Activity } from '@backspace/shared';
|
||||||
import { wsSend } from '../hooks/useWebSocket';
|
import { wsSendAll } from '../hooks/useWebSocket';
|
||||||
|
|
||||||
let pushTimer: ReturnType<typeof setTimeout> | null = null;
|
let pushTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
interface ActivityState {
|
interface ActivityState {
|
||||||
userActivities: Map<string, Activity[]>;
|
userActivities: Map<string, Activity[]>;
|
||||||
showActivity: boolean;
|
showActivity: boolean;
|
||||||
|
myActivities: Activity[] | null;
|
||||||
|
|
||||||
setUserActivities: (userId: string, activities: Activity[]) => void;
|
setUserActivities: (userId: string, activities: Activity[]) => void;
|
||||||
clearUserActivities: (userId: string) => void;
|
clearUserActivities: (userId: string) => void;
|
||||||
@@ -19,6 +20,7 @@ interface ActivityState {
|
|||||||
export const useActivityStore = create<ActivityState>((set, get) => ({
|
export const useActivityStore = create<ActivityState>((set, get) => ({
|
||||||
userActivities: new Map(),
|
userActivities: new Map(),
|
||||||
showActivity: true,
|
showActivity: true,
|
||||||
|
myActivities: null,
|
||||||
|
|
||||||
setUserActivities: (userId, activities) => {
|
setUserActivities: (userId, activities) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -58,21 +60,23 @@ export const useActivityStore = create<ActivityState>((set, get) => ({
|
|||||||
set({ showActivity: show });
|
set({ showActivity: show });
|
||||||
if (!show) {
|
if (!show) {
|
||||||
if (pushTimer) { clearTimeout(pushTimer); pushTimer = null; }
|
if (pushTimer) { clearTimeout(pushTimer); pushTimer = null; }
|
||||||
wsSend({ type: 'activity_update', activities: [] });
|
wsSendAll({ type: 'activity_update', activities: [] });
|
||||||
|
set({ myActivities: null });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
pushActivities: (activities) => {
|
pushActivities: (activities) => {
|
||||||
if (!get().showActivity) return;
|
if (!get().showActivity) return;
|
||||||
|
set({ myActivities: activities });
|
||||||
if (pushTimer) clearTimeout(pushTimer);
|
if (pushTimer) clearTimeout(pushTimer);
|
||||||
pushTimer = setTimeout(() => {
|
pushTimer = setTimeout(() => {
|
||||||
wsSend({ type: 'activity_update', activities });
|
wsSendAll({ type: 'activity_update', activities });
|
||||||
pushTimer = null;
|
pushTimer = null;
|
||||||
}, 5000);
|
}, 5000);
|
||||||
},
|
},
|
||||||
|
|
||||||
reset: () => {
|
reset: () => {
|
||||||
if (pushTimer) { clearTimeout(pushTimer); pushTimer = null; }
|
if (pushTimer) { clearTimeout(pushTimer); pushTimer = null; }
|
||||||
set({ userActivities: new Map(), showActivity: true });
|
set({ userActivities: new Map(), showActivity: true, myActivities: null });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user