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 =
| { type: "ping" }
| { type: "subscribe_party"; partyId: string }
| { type: "member_payload"; payload: unknown };
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(
ws: { publish: (topic: string, message: string) => void },
partyId: string,
@ -73,8 +107,8 @@ export const partySocketApp = new Elysia()
ws.subscribe(userTopic(user.id));
const membership = await getMemberRecord(db, user.id);
if (!membership) {
const subscribedPartyId = await subscribeWsToParty(ws, user.id);
if (!subscribedPartyId) {
ws.send(
JSON.stringify({
type: "party_status",
@ -85,21 +119,7 @@ export const partySocketApp = new Elysia()
return;
}
socketPartyId.set(ws, membership.partyId);
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);
}
await broadcastQuizState(ws, subscribedPartyId);
},
message: async (ws, message) => {
const data = ws.data;
@ -121,6 +141,16 @@ export const partySocketApp = new Elysia()
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;
const MAX_MEMBER_PAYLOAD_SIZE = 8_000;

View file

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

View file

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

View file

@ -1,5 +1,8 @@
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;
@ -24,6 +27,12 @@ export function usePartySocket({
const reconnectAttemptRef = useRef(0);
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(() => {
handlerRef.current = onMessage;
}, [onMessage]);
@ -42,7 +51,6 @@ export function usePartySocket({
ws.onmessage = (event) => {
const parsed = JSON.parse(event.data) as PartySocketEvent;
console.log(parsed);
handlerRef.current?.(parsed);
};
@ -120,8 +128,9 @@ export function usePartySocket({
isConnected: connectionState === "connected",
isConnecting: connectionState === "connecting",
isReconnecting: connectionState === "reconnecting",
send,
}),
[connectionState],
[connectionState, send],
);
return state;

View file

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