claude thinks it works

This commit is contained in:
nicksales72
2026-03-13 14:27:46 -04:00
parent a9b076a5ec
commit 851fd87bdb
8 changed files with 1914 additions and 53 deletions

View File

@@ -0,0 +1,3 @@
CREATE INDEX "idx_mappool_slot_mappool" ON "mappool_slot" USING btree ("mappool_id");--> statement-breakpoint
CREATE INDEX "idx_match_created_at" ON "match" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "idx_mpp_user" ON "match_participant_player" USING btree ("user_id");

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,13 @@
"when": 1773082177642, "when": 1773082177642,
"tag": "0000_tricky_jane_foster", "tag": "0000_tricky_jane_foster",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1773426173223,
"tag": "0001_swift_miek",
"breakpoints": true
} }
] ]
} }

View File

@@ -45,7 +45,9 @@ export const mappool = pgTable('mappool', {
}); });
// ── Mappool Slots ─────────────────────────────────────────────────────── // ── Mappool Slots ───────────────────────────────────────────────────────
export const mappoolSlot = pgTable('mappool_slot', { export const mappoolSlot = pgTable(
'mappool_slot',
{
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
mappoolId: uuid('mappool_id') mappoolId: uuid('mappool_id')
.notNull() .notNull()
@@ -63,7 +65,9 @@ export const mappoolSlot = pgTable('mappool_slot', {
version: text('version'), version: text('version'),
coverUrl: text('cover_url'), coverUrl: text('cover_url'),
listCoverUrl: text('list_cover_url') listCoverUrl: text('list_cover_url')
}); },
(t) => [index('idx_mappool_slot_mappool').on(t.mappoolId)]
);
// ── Matches ───────────────────────────────────────────────────────────── // ── Matches ─────────────────────────────────────────────────────────────
export const match = pgTable( export const match = pgTable(
@@ -82,7 +86,7 @@ export const match = pgTable(
createdBy: text('created_by'), createdBy: text('created_by'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull() createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
}, },
(t) => [index('idx_match_state').on(t.state)] (t) => [index('idx_match_state').on(t.state), index('idx_match_created_at').on(t.createdAt)]
); );
// ── Match Participants (a team in a match) ────────────────────────────── // ── Match Participants (a team in a match) ──────────────────────────────
@@ -120,7 +124,8 @@ export const matchParticipantPlayer = pgTable(
}, },
(t) => [ (t) => [
uniqueIndex('idx_mpp_participant_user').on(t.participantId, t.userId), uniqueIndex('idx_mpp_participant_user').on(t.participantId, t.userId),
index('idx_mpp_participant').on(t.participantId) index('idx_mpp_participant').on(t.participantId),
index('idx_mpp_user').on(t.userId)
] ]
); );

View File

@@ -1,5 +1,4 @@
import { beforeEach, describe, expect, test, vi } from 'vitest'; import { beforeEach, describe, expect, test, vi } from 'vitest';
import { MATCH_STATES } from './types';
const mocks = { const mocks = {
db: { db: {
@@ -8,6 +7,7 @@ const mocks = {
playerRating: { findFirst: vi.fn() }, playerRating: { findFirst: vi.fn() },
matchQueue: { findFirst: vi.fn() } matchQueue: { findFirst: vi.fn() }
}, },
select: vi.fn(),
delete: vi.fn() delete: vi.fn()
} }
}; };
@@ -18,6 +18,7 @@ vi.mock('drizzle-orm', () => ({
eq: vi.fn(() => ({})), eq: vi.fn(() => ({})),
and: vi.fn(() => ({})), and: vi.fn(() => ({})),
asc: vi.fn(() => ({})), asc: vi.fn(() => ({})),
desc: vi.fn(() => ({})),
lt: vi.fn(() => ({})), lt: vi.fn(() => ({})),
inArray: vi.fn(() => ({})), inArray: vi.fn(() => ({})),
sql: vi.fn(() => ({})), sql: vi.fn(() => ({})),
@@ -26,6 +27,7 @@ vi.mock('drizzle-orm', () => ({
vi.mock('$lib/server/db/schema', () => ({ vi.mock('$lib/server/db/schema', () => ({
match: {}, match: {},
matchParticipant: {},
matchQueue: {}, matchQueue: {},
matchParticipantPlayer: {}, matchParticipantPlayer: {},
playerRating: {} playerRating: {}
@@ -43,11 +45,23 @@ vi.mock('$lib/server/match/engine', () => ({
const { joinQueue } = await import('./queue'); const { joinQueue } = await import('./queue');
function mockFindRecentActiveMatchRows(rows: Array<{ id: string }>) {
const limit = vi.fn().mockResolvedValue(rows);
const orderBy = vi.fn(() => ({ limit }));
const where = vi.fn(() => ({ orderBy }));
const innerJoinSecond = vi.fn(() => ({ where }));
const innerJoinFirst = vi.fn(() => ({ innerJoin: innerJoinSecond }));
const from = vi.fn(() => ({ innerJoin: innerJoinFirst }));
mocks.db.select.mockReturnValue({ from });
}
describe('joinQueue', () => { describe('joinQueue', () => {
beforeEach(() => { beforeEach(() => {
mocks.db.query.matchParticipantPlayer.findMany.mockReset(); mocks.db.query.matchParticipantPlayer.findMany.mockReset();
mocks.db.query.playerRating.findFirst.mockReset(); mocks.db.query.playerRating.findFirst.mockReset();
mocks.db.query.matchQueue.findFirst.mockReset(); mocks.db.query.matchQueue.findFirst.mockReset();
mocks.db.select.mockReset();
mocks.db.delete.mockReset(); mocks.db.delete.mockReset();
}); });
@@ -56,13 +70,7 @@ describe('joinQueue', () => {
where: vi.fn(() => ({ returning: vi.fn(() => Promise.resolve([])) })) where: vi.fn(() => ({ returning: vi.fn(() => Promise.resolve([])) }))
}); });
mocks.db.query.matchParticipantPlayer.findMany.mockResolvedValue([ mockFindRecentActiveMatchRows([{ id: 'match-1' }]);
{
participant: {
match: { id: 'match-1', state: MATCH_STATES.PLAYING, createdAt: new Date().toISOString() }
}
}
]);
await expect(joinQueue('user-1', 'team-1')).rejects.toThrow( await expect(joinQueue('user-1', 'team-1')).rejects.toThrow(
'You are already in an active match' 'You are already in an active match'
@@ -75,7 +83,7 @@ describe('joinQueue', () => {
where: vi.fn(() => ({ returning: vi.fn(() => Promise.resolve([])) })) where: vi.fn(() => ({ returning: vi.fn(() => Promise.resolve([])) }))
}); });
mocks.db.query.matchParticipantPlayer.findMany.mockResolvedValue([]); mockFindRecentActiveMatchRows([]);
mocks.db.query.playerRating.findFirst.mockResolvedValue(null); mocks.db.query.playerRating.findFirst.mockResolvedValue(null);
await expect(joinQueue('user-1', 'team-1')).rejects.toThrow( await expect(joinQueue('user-1', 'team-1')).rejects.toThrow(
@@ -88,7 +96,7 @@ describe('joinQueue', () => {
where: vi.fn(() => ({ returning: vi.fn(() => Promise.resolve([])) })) where: vi.fn(() => ({ returning: vi.fn(() => Promise.resolve([])) }))
}); });
mocks.db.query.matchParticipantPlayer.findMany.mockResolvedValue([]); mockFindRecentActiveMatchRows([]);
mocks.db.query.playerRating.findFirst.mockResolvedValue({ elo: 1500 }); mocks.db.query.playerRating.findFirst.mockResolvedValue({ elo: 1500 });
mocks.db.query.matchQueue.findFirst.mockResolvedValue({ userId: 'user-1', teamId: 'team-1' }); mocks.db.query.matchQueue.findFirst.mockResolvedValue({ userId: 'user-1', teamId: 'team-1' });

View File

@@ -1,6 +1,12 @@
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { match, matchQueue, matchParticipantPlayer, playerRating } from '$lib/server/db/schema'; import {
import { eq, asc, sql, inArray, lt } from 'drizzle-orm'; match,
matchParticipant,
matchParticipantPlayer,
matchQueue,
playerRating
} from '$lib/server/db/schema';
import { and, asc, desc, eq, inArray, lt, sql } from 'drizzle-orm';
import { MATCH_STATES } from './types'; import { MATCH_STATES } from './types';
import { createMatch } from './engine'; import { createMatch } from './engine';
@@ -75,19 +81,6 @@ export async function getQueueStatus(userId: string) {
* Used to redirect players who were matched via queue while polling. * Used to redirect players who were matched via queue while polling.
*/ */
async function findRecentActiveMatch(userId: string): Promise<string | null> { async function findRecentActiveMatch(userId: string): Promise<string | null> {
// Find matchParticipantPlayer records for this user
const playerEntries = await db.query.matchParticipantPlayer.findMany({
where: eq(matchParticipantPlayer.userId, userId),
with: {
participant: {
with: {
match: true
}
}
}
});
// Find the most recent match that's still active (not finished/cancelled)
const activeStates = [ const activeStates = [
MATCH_STATES.CREATED, MATCH_STATES.CREATED,
MATCH_STATES.LOBBY, MATCH_STATES.LOBBY,
@@ -96,12 +89,16 @@ async function findRecentActiveMatch(userId: string): Promise<string | null> {
MATCH_STATES.PLAYING MATCH_STATES.PLAYING
]; ];
const activeMatches = playerEntries const [activeMatch] = await db
.filter((pe) => activeStates.includes(pe.participant.match.state as any)) .select({ id: match.id })
.map((pe) => pe.participant.match) .from(matchParticipantPlayer)
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); .innerJoin(matchParticipant, eq(matchParticipantPlayer.participantId, matchParticipant.id))
.innerJoin(match, eq(matchParticipant.matchId, match.id))
.where(and(eq(matchParticipantPlayer.userId, userId), inArray(match.state, activeStates)))
.orderBy(desc(match.createdAt))
.limit(1);
return activeMatches[0]?.id ?? null; return activeMatch?.id ?? null;
} }
const MAX_CONCURRENT_MATCHES = 4; const MAX_CONCURRENT_MATCHES = 4;

View File

@@ -12,6 +12,8 @@ const queue = $state({
loading: false loading: false
}); });
const QUEUE_POLL_INTERVAL_MS = 8000;
let pollInterval: ReturnType<typeof setInterval> | null = null; let pollInterval: ReturnType<typeof setInterval> | null = null;
async function fetchQueueStatus() { async function fetchQueueStatus() {
@@ -57,7 +59,7 @@ async function leaveQueue() {
function startPolling() { function startPolling() {
if (pollInterval) return; if (pollInterval) return;
fetchQueueStatus(); fetchQueueStatus();
pollInterval = setInterval(fetchQueueStatus, 4000); pollInterval = setInterval(fetchQueueStatus, QUEUE_POLL_INTERVAL_MS);
} }
function stopPolling() { function stopPolling() {

View File

@@ -12,7 +12,40 @@ export const load: PageServerLoad = async ({ locals }) => {
requireAuth(locals); requireAuth(locals);
const matches = await db.query.match.findMany({ const matches = await db.query.match.findMany({
with: { participants: { with: { team: true } }, mappool: { with: { slots: true } } }, columns: {
id: true,
name: true,
state: true,
config: true,
createdAt: true,
finishedAt: true,
winnerId: true
},
with: {
participants: {
columns: {
teamId: true,
score: true,
slot: true
},
orderBy: (p, { asc }) => [asc(p.slot)],
with: {
team: {
columns: {
id: true,
name: true,
avatarUrl: true
}
}
}
},
mappool: {
columns: {
id: true,
name: true
}
}
},
orderBy: desc(match.createdAt), orderBy: desc(match.createdAt),
limit: 50 limit: 50
}); });