attempt fix reactivity

This commit is contained in:
Daniel Bulant 2026-05-13 11:48:18 +02:00
parent aea6926a97
commit 91726d85b8
No known key found for this signature in database
6 changed files with 99 additions and 43 deletions

View file

@ -24,6 +24,7 @@ export type PartyState = {
export type PartySocketOutgoing = export type PartySocketOutgoing =
| { type: "ping" } | { type: "ping" }
| { type: "subscribe_party"; partyId: string }
| { type: "member_payload"; payload: unknown }; | { type: "member_payload"; payload: unknown };
type BaseQuestion = { type BaseQuestion = {

View file

@ -29,6 +29,40 @@ export const pubsub = {
}, },
}; };
async function subscribeWsToParty(
ws: {
subscribe: (topic: string) => void;
unsubscribe: (topic: string) => void;
send: (message: string) => void;
},
userId: string,
) {
const membership = await getMemberRecord(db, userId);
if (!membership) return null;
const nextPartyId = membership.partyId;
const currentPartyId = socketPartyId.get(ws as object);
if (currentPartyId && currentPartyId !== nextPartyId) {
ws.unsubscribe(partyTopic(currentPartyId));
}
socketPartyId.set(ws as object, nextPartyId);
ws.subscribe(partyTopic(nextPartyId));
const snapshot = await getPartyStatus(nextPartyId);
if (snapshot) {
ws.send(
JSON.stringify({
type: "party_status",
party: snapshot.party,
members: snapshot.members,
} satisfies PartySocketEvent),
);
}
return nextPartyId;
}
export async function broadcastQuizState( export async function broadcastQuizState(
ws: { publish: (topic: string, message: string) => void }, ws: { publish: (topic: string, message: string) => void },
partyId: string, partyId: string,
@ -73,8 +107,8 @@ export const partySocketApp = new Elysia()
ws.subscribe(userTopic(user.id)); ws.subscribe(userTopic(user.id));
const membership = await getMemberRecord(db, user.id); const subscribedPartyId = await subscribeWsToParty(ws, user.id);
if (!membership) { if (!subscribedPartyId) {
ws.send( ws.send(
JSON.stringify({ JSON.stringify({
type: "party_status", type: "party_status",
@ -85,21 +119,7 @@ export const partySocketApp = new Elysia()
return; return;
} }
socketPartyId.set(ws, membership.partyId); await broadcastQuizState(ws, subscribedPartyId);
ws.subscribe(partyTopic(membership.partyId));
const snapshot = await getPartyStatus(membership.partyId);
if (snapshot) {
ws.send(
JSON.stringify({
type: "party_status",
party: snapshot.party,
members: snapshot.members,
} as PartySocketEvent),
);
await broadcastQuizState(ws, membership.partyId);
}
}, },
message: async (ws, message) => { message: async (ws, message) => {
const data = ws.data; const data = ws.data;
@ -121,6 +141,16 @@ export const partySocketApp = new Elysia()
return; return;
} }
if (parsed.type === "subscribe_party") {
const payload = parsed as {
type: "subscribe_party";
partyId: string;
};
if (typeof payload.partyId !== "string") return;
await subscribeWsToParty(ws, user.id);
return;
}
if (parsed.type !== "member_payload") return; if (parsed.type !== "member_payload") return;
const MAX_MEMBER_PAYLOAD_SIZE = 8_000; const MAX_MEMBER_PAYLOAD_SIZE = 8_000;

View file

@ -3,8 +3,8 @@ import { eq } from "drizzle-orm";
import Elysia, { t } from "elysia"; import Elysia, { t } from "elysia";
import { betterAuthElysia } from "../auth"; import { betterAuthElysia } from "../auth";
import { db } from "../db"; import { db } from "../db";
import { party, partyMember } from "../db/schema"; import { party } from "../db/schema";
import { getMemberRecord } from "../party-data"; import { getMemberRecord, getPartyStatus } from "../party-data";
import type { QuizState } from "../party-types"; import type { QuizState } from "../party-types";
import { QuizWorkflow, quizQueue } from "../workflows/quiz"; import { QuizWorkflow, quizQueue } from "../workflows/quiz";
import { pubsub } from "./party-socket"; import { pubsub } from "./party-socket";
@ -49,22 +49,17 @@ export const quizRoutes = new Elysia()
}) })
.where(eq(party.id, params.partyId)); .where(eq(party.id, params.partyId));
const members = await db const status = await getPartyStatus(params.partyId);
.select({ if (status) {
id: partyMember.id, pubsub.publish(
userId: partyMember.userId, `party:${params.partyId}`,
}) JSON.stringify({
.from(partyMember) type: "party_status",
.where(eq(partyMember.partyId, params.partyId)); party: status.party,
members: status.members,
pubsub.publish( }),
`party:${params.partyId}`, );
JSON.stringify({ }
type: "party_status",
party: { status: "started" },
members,
}),
);
return { return {
message: "Quiz started", message: "Quiz started",

View file

@ -155,13 +155,17 @@ export class QuizWorkflow extends ConfiguredInstance {
}, },
}); });
const analytics = (partyRecord?.analysisData ?? null) as PartyAnalytics; const analytics = (partyRecord?.analysisData ?? null) as PartyAnalytics;
return generatePartyQuestion({ const question = await generatePartyQuestion({
db, db,
partyId, partyId,
quizState, quizState,
analytics, analytics,
index, index,
}); });
if (!question) {
throw new Error("Failed to generate quiz question");
}
return question;
} }
private static scoreRound(round: QuizRound): Array<[string, number]> { private static scoreRound(round: QuizRound): Array<[string, number]> {

View file

@ -1,5 +1,8 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { PartySocketEvent } from "../../../api/src/party-types"; import type {
PartySocketEvent,
PartySocketOutgoing,
} from "../../../api/src/party-types";
type Handler = (event: PartySocketEvent) => void; type Handler = (event: PartySocketEvent) => void;
@ -24,6 +27,12 @@ export function usePartySocket({
const reconnectAttemptRef = useRef(0); const reconnectAttemptRef = useRef(0);
const handlerRef = useRef(onMessage); const handlerRef = useRef(onMessage);
const send = useCallback((message: PartySocketOutgoing) => {
const ws = wsRef.current;
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify(message));
}, []);
useEffect(() => { useEffect(() => {
handlerRef.current = onMessage; handlerRef.current = onMessage;
}, [onMessage]); }, [onMessage]);
@ -42,7 +51,6 @@ export function usePartySocket({
ws.onmessage = (event) => { ws.onmessage = (event) => {
const parsed = JSON.parse(event.data) as PartySocketEvent; const parsed = JSON.parse(event.data) as PartySocketEvent;
console.log(parsed);
handlerRef.current?.(parsed); handlerRef.current?.(parsed);
}; };
@ -120,8 +128,9 @@ export function usePartySocket({
isConnected: connectionState === "connected", isConnected: connectionState === "connected",
isConnecting: connectionState === "connecting", isConnecting: connectionState === "connecting",
isReconnecting: connectionState === "reconnecting", isReconnecting: connectionState === "reconnecting",
send,
}), }),
[connectionState], [connectionState, send],
); );
return state; return state;

View file

@ -6,6 +6,7 @@ import {
useContext, useContext,
useEffect, useEffect,
useMemo, useMemo,
useRef,
useState, useState,
} from "react"; } from "react";
import type { import type {
@ -75,6 +76,11 @@ export function PartyProvider({ children }: { children: ReactNode }) {
if (!url) return null; if (!url) return null;
return url; return url;
}, []); }, []);
const subscribedPartyIdRef = useRef<string | null>(null);
const wsState = usePartySocket({
apiUrl: userId ? apiUrl : null,
onMessage: userId ? handleMessage : null,
});
const resetParty = useCallback(() => { const resetParty = useCallback(() => {
setState(emptyPartyState); setState(emptyPartyState);
@ -84,10 +90,21 @@ export function PartyProvider({ children }: { children: ReactNode }) {
if (!userId) resetParty(); if (!userId) resetParty();
}, [resetParty, userId]); }, [resetParty, userId]);
const wsState = usePartySocket({ useEffect(() => {
apiUrl: userId ? apiUrl : null, if (wsState.connectionState !== "connected") {
onMessage: userId ? handleMessage : null, subscribedPartyIdRef.current = null;
}); return;
}
if (!userId || !state.party?.id) return;
if (subscribedPartyIdRef.current === state.party.id) return;
wsState.send({
type: "subscribe_party",
partyId: state.party.id,
});
subscribedPartyIdRef.current = state.party.id;
}, [state.party?.id, userId, wsState.connectionState, wsState.send]);
const value = useMemo( const value = useMemo(
() => ({ () => ({