attempt fix reactivity
This commit is contained in:
parent
aea6926a97
commit
91726d85b8
6 changed files with 99 additions and 43 deletions
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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]> {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue