Compare commits

..

No commits in common. "c14b7a7308a00d65c3de12fca002ee318d6bb3c6" and "93861bb7ce90990a4e0e40b15760a9e763b989bb" have entirely different histories.

12 changed files with 69 additions and 253 deletions

View file

@ -1,12 +1,11 @@
import type { InferSelectModel } from "drizzle-orm"; import type { InferSelectModel } from "drizzle-orm";
import type { party, partyMember, track, user } from "./db/schema"; import type { party, partyMember, user } from "./db/schema";
export type Party = Omit<InferSelectModel<typeof party>, "data"> & { export type Party = Omit<InferSelectModel<typeof party>, "data"> & {
data: QuizState; data: QuizState;
}; };
export type PartyMember = InferSelectModel<typeof partyMember>; export type PartyMember = InferSelectModel<typeof partyMember>;
export type User = InferSelectModel<typeof user>; export type User = InferSelectModel<typeof user>;
export type Song = InferSelectModel<typeof track>;
export type PartyMemberWithUser = PartyMember & { user: User | null }; export type PartyMemberWithUser = PartyMember & { user: User | null };
@ -25,7 +24,6 @@ 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 = {
@ -34,7 +32,6 @@ type BaseQuestion = {
startTimestamp: number; startTimestamp: number;
endTimestamp: number; endTimestamp: number;
points: number; points: number;
song?: Song;
}; };
export type Question = export type Question =

View file

@ -1,4 +1,3 @@
import type { db as Db } from "../db";
import type { Question } from "../party-types"; import type { Question } from "../party-types";
import { import {
buildOptionsWithCorrect, buildOptionsWithCorrect,
@ -9,19 +8,16 @@ import {
getTopClusterTracks, getTopClusterTracks,
type PartyAnalytics, type PartyAnalytics,
pickRandom, pickRandom,
resolveQuestionSong,
} from "./question-utils"; } from "./question-utils";
export async function buildAudioMetadataQuestion( export function buildAudioMetadataQuestion(
dbClient: typeof Db,
analytics: PartyAnalytics, analytics: PartyAnalytics,
index: number, index: number,
): Promise<Question | null> { ): Question {
type ChoiceQuestion = Extract<Question, { type: "choice" }>; type ChoiceQuestion = Extract<Question, { type: "choice" }>;
const questions: Array< const questions: Array<
Omit<ChoiceQuestion, "startTimestamp" | "endTimestamp"> Omit<ChoiceQuestion, "startTimestamp" | "endTimestamp">
> = []; > = [];
const topSong = await resolveQuestionSong(dbClient, analytics);
const genreOptions = buildOrderedOptions( const genreOptions = buildOrderedOptions(
getMostSharedGenreNames(analytics), getMostSharedGenreNames(analytics),
@ -34,7 +30,6 @@ export async function buildAudioMetadataQuestion(
options: genreOptions, options: genreOptions,
correct: 0, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined,
}); });
} }
@ -49,7 +44,6 @@ export async function buildAudioMetadataQuestion(
options: artistOptions, options: artistOptions,
correct: 0, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined,
}); });
} }
} }
@ -68,18 +62,12 @@ export async function buildAudioMetadataQuestion(
options: trackOptions, options: trackOptions,
correct: 0, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined,
}); });
} }
} }
const randomTopTrack = pickRandom(topTracks); const randomTopTrack = pickRandom(topTracks);
if (randomTopTrack) { if (randomTopTrack) {
const randomTrackSong = await resolveQuestionSong(dbClient, analytics, {
trackName: randomTopTrack.name,
artistNames: randomTopTrack.artists?.map((artist) => artist.name),
albumName: randomTopTrack.albumName,
});
const trackArtists = const trackArtists =
randomTopTrack.artists?.map((artist) => artist.name) ?? []; randomTopTrack.artists?.map((artist) => artist.name) ?? [];
const allArtists = topArtists.length > 0 ? topArtists : trackArtists; const allArtists = topArtists.length > 0 ? topArtists : trackArtists;
@ -97,7 +85,6 @@ export async function buildAudioMetadataQuestion(
options: artistOptions, options: artistOptions,
correct: 0, correct: 0,
points: 10, points: 10,
song: randomTrackSong ?? topSong ?? undefined,
}); });
} }
@ -114,7 +101,6 @@ export async function buildAudioMetadataQuestion(
options: trackNameOptions, options: trackNameOptions,
correct: 0, correct: 0,
points: 10, points: 10,
song: randomTrackSong ?? topSong ?? undefined,
}); });
} }
} }
@ -135,14 +121,13 @@ export async function buildAudioMetadataQuestion(
options: albumOptions, options: albumOptions,
correct: 0, correct: 0,
points: 10, points: 10,
song: randomTrackSong ?? topSong ?? undefined,
}); });
} }
} }
} }
if (questions.length === 0) { if (questions.length === 0) {
return null; throw new Error("Question not found");
} }
const question = questions[index % questions.length]; const question = questions[index % questions.length];

View file

@ -10,7 +10,6 @@ import {
getQuestionRange, getQuestionRange,
type PartyAnalytics, type PartyAnalytics,
type PartyQuestionMember, type PartyQuestionMember,
resolveQuestionSong,
} from "./question-utils"; } from "./question-utils";
type NumericQuestion = Omit< type NumericQuestion = Omit<
@ -37,9 +36,6 @@ async function getAlbumReleaseYear({
with: { album: true }, with: { album: true },
}) })
: null; : null;
const song = await resolveQuestionSong(db, analytics, {
trackName: track?.name ?? trackName ?? undefined,
});
const correct = const correct =
track?.album?.release_date?.getFullYear() ?? track?.album?.release_date?.getFullYear() ??
new Date().getFullYear() - 1 - index; new Date().getFullYear() - 1 - index;
@ -50,7 +46,6 @@ async function getAlbumReleaseYear({
correct, correct,
range: getQuestionRange(correct, 10, 3), range: getQuestionRange(correct, 10, 3),
points: 10, points: 10,
song: song ?? undefined,
}; };
} }
@ -65,7 +60,6 @@ async function countTopTrackListeners({
where: { name: trackName }, where: { name: trackName },
}); });
if (!dbTrack) return null; if (!dbTrack) return null;
const song = await resolveQuestionSong(db, analytics, { trackName });
const memberIds = members.map((m) => m.userId); const memberIds = members.map((m) => m.userId);
const entries = await db const entries = await db
.select({ userId: topTrackTable.userId }) .select({ userId: topTrackTable.userId })
@ -83,7 +77,6 @@ async function countTopTrackListeners({
correct, correct,
range: { min: 0, max: members.length }, range: { min: 0, max: members.length },
points: 10, points: 10,
song: song ?? undefined,
}; };
} }
@ -98,9 +91,6 @@ async function countFavouriteArtistListeners({
where: { name: artistName }, where: { name: artistName },
}); });
if (!dbArtist) return null; if (!dbArtist) return null;
const song = await resolveQuestionSong(db, analytics, {
artistNames: [artistName],
});
const memberIds = members.map((m) => m.userId); const memberIds = members.map((m) => m.userId);
const entries = await db const entries = await db
.select({ userId: topArtistTable.userId }) .select({ userId: topArtistTable.userId })
@ -118,13 +108,12 @@ async function countFavouriteArtistListeners({
correct, correct,
range: { min: 0, max: members.length }, range: { min: 0, max: members.length },
points: 10, points: 10,
song: song ?? undefined,
}; };
} }
export async function buildNumericQuestion( export async function buildNumericQuestion(
input: BuildNumericQuestionInput, input: BuildNumericQuestionInput,
): Promise<Question | null> { ): Promise<Question> {
const questions: NumericQuestion[] = []; const questions: NumericQuestion[] = [];
questions.push(await getAlbumReleaseYear(input)); questions.push(await getAlbumReleaseYear(input));
@ -136,6 +125,6 @@ export async function buildNumericQuestion(
if (artistQ) questions.push(artistQ); if (artistQ) questions.push(artistQ);
const question = questions[input.index % questions.length] ?? questions[0]; const question = questions[input.index % questions.length] ?? questions[0];
if (!question) return null; if (!question) throw new Error("Question not found");
return buildQuestionWindow(question); return buildQuestionWindow(question);
} }

View file

@ -22,23 +22,13 @@ export async function generatePartyQuestion({
quizState, quizState,
analytics, analytics,
index, index,
}: GenerateQuestionInput): Promise<Question | null> { }: GenerateQuestionInput): Promise<Question> {
const members = await fetchPartyMembers(dbClient, partyId); const members = await fetchPartyMembers(dbClient, partyId);
const type: PartyQuestionType = const type: PartyQuestionType =
index === 2 ? "numeric" : index % 2 === 0 ? "audio-metadata" : "social"; index === 2 ? "numeric" : index % 2 === 0 ? "audio-metadata" : "social";
return type === "audio-metadata"
if (type === "audio-metadata") { ? buildAudioMetadataQuestion(analytics, index)
const q = await buildAudioMetadataQuestion(dbClient, analytics, index); : type === "numeric"
if (q) return q; ? buildNumericQuestion({ db: dbClient, analytics, index, members })
} : buildSocialQuestion(quizState, analytics, members, index);
if (type === "numeric") {
const q = await buildNumericQuestion({
db: dbClient,
analytics,
index,
members,
});
if (q) return q;
}
return buildSocialQuestion(dbClient, quizState, analytics, members, index);
} }

View file

@ -1,6 +1,4 @@
import type { InferSelectModel } from "drizzle-orm";
import type { db as Db } from "../db"; import type { db as Db } from "../db";
import type { track as trackTable } from "../db/schema";
export type PartyQuestionMember = { export type PartyQuestionMember = {
userId: string; userId: string;
@ -25,14 +23,6 @@ export type PartyAnalytics = {
pairwise?: { userIdA: string; userIdB: string }[]; pairwise?: { userIdA: string; userIdB: string }[];
} | null; } | null;
export type AnalyticsTrack = {
name: string;
artists?: { name: string }[];
albumName?: string;
memberScores?: { userId: string; score: number }[];
};
export type QuestionSong = InferSelectModel<typeof trackTable>;
export const QUESTION_DURATION_MS = 60_000; export const QUESTION_DURATION_MS = 60_000;
export const MIN_PARTY_SIZE = 2; export const MIN_PARTY_SIZE = 2;
export const MAX_PARTY_SIZE = 4; export const MAX_PARTY_SIZE = 4;
@ -110,80 +100,15 @@ export function getTopClusterArtists(analytics: PartyAnalytics): string[] {
); );
} }
export function getTopClusterTracks( export function getTopClusterTracks(analytics: PartyAnalytics): Array<{
analytics: PartyAnalytics, name: string;
): AnalyticsTrack[] { artists?: { name: string }[];
albumName?: string;
memberScores?: { userId: string; score: number }[];
}> {
return analytics?.storyClusters?.[0]?.tracks ?? []; return analytics?.storyClusters?.[0]?.tracks ?? [];
} }
export function pickRelevantTrack(
analytics: PartyAnalytics,
hints: {
trackName?: string;
artistNames?: string[];
albumName?: string;
} = {},
): AnalyticsTrack | null {
const tracks = getTopClusterTracks(analytics);
if (tracks.length === 0) return null;
const scored = tracks.map((track) => {
const trackArtistNames = track.artists?.map((artist) => artist.name) ?? [];
const matchesTrackName = hints.trackName && track.name === hints.trackName;
const matchesAlbum = hints.albumName && track.albumName === hints.albumName;
const matchesArtist =
hints.artistNames?.some((name) => trackArtistNames.includes(name)) ??
false;
return {
track,
score:
(matchesTrackName ? 4 : 0) +
(matchesAlbum ? 2 : 0) +
(matchesArtist ? 2 : 0),
};
});
const best = scored.sort((a, b) => b.score - a.score).at(0);
return best?.track ?? tracks[0] ?? null;
}
export async function resolveQuestionSong(
db: typeof Db,
analytics: PartyAnalytics,
hints: {
trackName?: string;
artistNames?: string[];
albumName?: string;
} = {},
): Promise<QuestionSong | null> {
const trackHint = pickRelevantTrack(analytics, hints);
if (!trackHint?.name) return null;
const tracks = await db.query.track.findMany({
where: { name: trackHint.name },
with: { album: true, artists: true },
});
if (tracks.length === 0) return null;
const scoreTrack = (track: (typeof tracks)[number]) => {
const artistNames = track.artists?.map((artist) => artist.name) ?? [];
const matchesAlbum = trackHint.albumName
? track.album?.name === trackHint.albumName
: false;
const matchesArtist =
hints.artistNames?.some((name) => artistNames.includes(name)) ?? false;
return (matchesAlbum ? 2 : 0) + (matchesArtist ? 2 : 0);
};
const chosen = tracks
.slice()
.sort((a, b) => scoreTrack(b) - scoreTrack(a))[0];
if (!chosen) return null;
const { album: _album, artists: _artists, ...song } = chosen;
return song;
}
export function getTopTrackListener( export function getTopTrackListener(
track: { memberScores?: { userId: string; score: number }[] }, track: { memberScores?: { userId: string; score: number }[] },
members: PartyQuestionMember[], members: PartyQuestionMember[],

View file

@ -1,4 +1,3 @@
import type { db as Db } from "../db";
import type { Question, QuizState } from "../party-types"; import type { Question, QuizState } from "../party-types";
import { import {
buildMemberOptions, buildMemberOptions,
@ -13,21 +12,18 @@ import {
type PartyAnalytics, type PartyAnalytics,
type PartyQuestionMember, type PartyQuestionMember,
pickRandom, pickRandom,
resolveQuestionSong,
} from "./question-utils"; } from "./question-utils";
export async function buildSocialQuestion( export function buildSocialQuestion(
dbClient: typeof Db,
quizState: QuizState, quizState: QuizState,
analytics: PartyAnalytics, analytics: PartyAnalytics,
members: PartyQuestionMember[], members: PartyQuestionMember[],
index: number, index: number,
): Promise<Question | null> { ): Question {
type ChoiceQuestion = Extract<Question, { type: "choice" }>; type ChoiceQuestion = Extract<Question, { type: "choice" }>;
const questions: Array< const questions: Array<
Omit<ChoiceQuestion, "startTimestamp" | "endTimestamp"> Omit<ChoiceQuestion, "startTimestamp" | "endTimestamp">
> = []; > = [];
const topSong = await resolveQuestionSong(dbClient, analytics);
const hasMultipleMembers = members.length >= 2; const hasMultipleMembers = members.length >= 2;
if (hasMultipleMembers && hasClearLeader(quizState)) { if (hasMultipleMembers && hasClearLeader(quizState)) {
@ -38,7 +34,6 @@ export async function buildSocialQuestion(
options: buildMemberOptions(leader, members), options: buildMemberOptions(leader, members),
correct: 0, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined,
}); });
} }
@ -50,7 +45,6 @@ export async function buildSocialQuestion(
options: buildMemberOptions(diverse, members), options: buildMemberOptions(diverse, members),
correct: 0, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined,
}); });
const aligned = getMostAlignedMember(analytics, members); const aligned = getMostAlignedMember(analytics, members);
@ -60,7 +54,6 @@ export async function buildSocialQuestion(
options: buildMemberOptions(aligned, members), options: buildMemberOptions(aligned, members),
correct: 0, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined,
}); });
} }
@ -69,18 +62,12 @@ export async function buildSocialQuestion(
if (randomTrack && hasMultipleMembers) { if (randomTrack && hasMultipleMembers) {
const topListener = getTopTrackListener(randomTrack, members); const topListener = getTopTrackListener(randomTrack, members);
if (topListener) { if (topListener) {
const randomTrackSong = await resolveQuestionSong(dbClient, analytics, {
trackName: randomTrack.name,
artistNames: randomTrack.artists?.map((artist) => artist.name),
albumName: randomTrack.albumName,
});
questions.push({ questions.push({
type: "choice", type: "choice",
text: `Who listens the most to "${randomTrack.name}"?`, text: `Who listens the most to "${randomTrack.name}"?`,
options: buildMemberOptions(topListener, members), options: buildMemberOptions(topListener, members),
correct: 0, correct: 0,
points: 10, points: 10,
song: randomTrackSong ?? topSong ?? undefined,
}); });
questions.push({ questions.push({
type: "choice", type: "choice",
@ -88,7 +75,6 @@ export async function buildSocialQuestion(
options: buildMemberOptions(topListener, members), options: buildMemberOptions(topListener, members),
correct: 0, correct: 0,
points: 10, points: 10,
song: randomTrackSong ?? topSong ?? undefined,
}); });
} }
} }
@ -107,14 +93,13 @@ export async function buildSocialQuestion(
options: pairOptions, options: pairOptions,
correct: 0, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined,
}); });
} }
} }
} }
if (questions.length === 0) { if (questions.length === 0) {
return null; throw new Error("Question not found");
} }
const question = questions[index % questions.length]; const question = questions[index % questions.length];

View file

@ -29,40 +29,6 @@ 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,
@ -107,8 +73,8 @@ export const partySocketApp = new Elysia()
ws.subscribe(userTopic(user.id)); ws.subscribe(userTopic(user.id));
const subscribedPartyId = await subscribeWsToParty(ws, user.id); const membership = await getMemberRecord(db, user.id);
if (!subscribedPartyId) { if (!membership) {
ws.send( ws.send(
JSON.stringify({ JSON.stringify({
type: "party_status", type: "party_status",
@ -119,7 +85,21 @@ export const partySocketApp = new Elysia()
return; return;
} }
await broadcastQuizState(ws, subscribedPartyId); 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);
}
}, },
message: async (ws, message) => { message: async (ws, message) => {
const data = ws.data; const data = ws.data;
@ -141,16 +121,6 @@ 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 } from "../db/schema"; import { party, partyMember } from "../db/schema";
import { getMemberRecord, getPartyStatus } from "../party-data"; import { getMemberRecord } 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,17 +49,22 @@ export const quizRoutes = new Elysia()
}) })
.where(eq(party.id, params.partyId)); .where(eq(party.id, params.partyId));
const status = await getPartyStatus(params.partyId); const members = await db
if (status) { .select({
id: partyMember.id,
userId: partyMember.userId,
})
.from(partyMember)
.where(eq(partyMember.partyId, params.partyId));
pubsub.publish( pubsub.publish(
`party:${params.partyId}`, `party:${params.partyId}`,
JSON.stringify({ JSON.stringify({
type: "party_status", type: "party_status",
party: status.party, party: { status: "started" },
members: status.members, members,
}), }),
); );
}
return { return {
message: "Quiz started", message: "Quiz started",

View file

@ -155,17 +155,13 @@ export class QuizWorkflow extends ConfiguredInstance {
}, },
}); });
const analytics = (partyRecord?.analysisData ?? null) as PartyAnalytics; const analytics = (partyRecord?.analysisData ?? null) as PartyAnalytics;
const question = await generatePartyQuestion({ return 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

@ -137,7 +137,7 @@ pub async fn main_loop() {
println!("Main loop started"); println!("Main loop started");
let mut last = (String::new(), String::new()); let mut last = (String::new(), String::new());
loop { loop {
embassy_time::Timer::after_millis(300).await; embassy_time::Timer::after_millis(100).await;
let mut state = STATE.lock().await; let mut state = STATE.lock().await;
state.tick(); state.tick();
let lines = state.render_lines(); let lines = state.render_lines();
@ -198,7 +198,7 @@ async fn main(spawner: Spawner) -> ! {
p.GPIO26.degrade(), p.GPIO26.degrade(),
GpioInputConfig::default().with_pull(Pull::Up), GpioInputConfig::default().with_pull(Pull::Up),
), ),
0, 1,
) )
.expect("spawn btn1"), .expect("spawn btn1"),
); );
@ -208,7 +208,7 @@ async fn main(spawner: Spawner) -> ! {
p.GPIO25.degrade(), p.GPIO25.degrade(),
GpioInputConfig::default().with_pull(Pull::Up), GpioInputConfig::default().with_pull(Pull::Up),
), ),
1, 2,
) )
.expect("spawn btn2"), .expect("spawn btn2"),
); );
@ -218,7 +218,7 @@ async fn main(spawner: Spawner) -> ! {
p.GPIO33.degrade(), p.GPIO33.degrade(),
GpioInputConfig::default().with_pull(Pull::Up), GpioInputConfig::default().with_pull(Pull::Up),
), ),
2, 3,
) )
.expect("spawn btn3"), .expect("spawn btn3"),
); );
@ -228,7 +228,7 @@ async fn main(spawner: Spawner) -> ! {
p.GPIO32.degrade(), p.GPIO32.degrade(),
GpioInputConfig::default().with_pull(Pull::Up), GpioInputConfig::default().with_pull(Pull::Up),
), ),
3, 4,
) )
.expect("spawn btn4"), .expect("spawn btn4"),
); );

View file

@ -1,8 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { import type { PartySocketEvent } from "../../../api/src/party-types";
PartySocketEvent,
PartySocketOutgoing,
} from "../../../api/src/party-types";
type Handler = (event: PartySocketEvent) => void; type Handler = (event: PartySocketEvent) => void;
@ -27,12 +24,6 @@ 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]);
@ -51,6 +42,7 @@ 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);
}; };
@ -128,9 +120,8 @@ export function usePartySocket({
isConnected: connectionState === "connected", isConnected: connectionState === "connected",
isConnecting: connectionState === "connecting", isConnecting: connectionState === "connecting",
isReconnecting: connectionState === "reconnecting", isReconnecting: connectionState === "reconnecting",
send,
}), }),
[connectionState, send], [connectionState],
); );
return state; return state;

View file

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