claude thinks it works
This commit is contained in:
3
drizzle/0001_swift_miek.sql
Normal file
3
drizzle/0001_swift_miek.sql
Normal 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");
|
||||
1806
drizzle/meta/0001_snapshot.json
Normal file
1806
drizzle/meta/0001_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,13 @@
|
||||
"when": 1773082177642,
|
||||
"tag": "0000_tricky_jane_foster",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1773426173223,
|
||||
"tag": "0001_swift_miek",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -45,7 +45,9 @@ export const mappool = pgTable('mappool', {
|
||||
});
|
||||
|
||||
// ── Mappool Slots ───────────────────────────────────────────────────────
|
||||
export const mappoolSlot = pgTable('mappool_slot', {
|
||||
export const mappoolSlot = pgTable(
|
||||
'mappool_slot',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
mappoolId: uuid('mappool_id')
|
||||
.notNull()
|
||||
@@ -63,7 +65,9 @@ export const mappoolSlot = pgTable('mappool_slot', {
|
||||
version: text('version'),
|
||||
coverUrl: text('cover_url'),
|
||||
listCoverUrl: text('list_cover_url')
|
||||
});
|
||||
},
|
||||
(t) => [index('idx_mappool_slot_mappool').on(t.mappoolId)]
|
||||
);
|
||||
|
||||
// ── Matches ─────────────────────────────────────────────────────────────
|
||||
export const match = pgTable(
|
||||
@@ -82,7 +86,7 @@ export const match = pgTable(
|
||||
createdBy: text('created_by'),
|
||||
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) ──────────────────────────────
|
||||
@@ -120,7 +124,8 @@ export const matchParticipantPlayer = pgTable(
|
||||
},
|
||||
(t) => [
|
||||
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)
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { MATCH_STATES } from './types';
|
||||
|
||||
const mocks = {
|
||||
db: {
|
||||
@@ -8,6 +7,7 @@ const mocks = {
|
||||
playerRating: { findFirst: vi.fn() },
|
||||
matchQueue: { findFirst: vi.fn() }
|
||||
},
|
||||
select: vi.fn(),
|
||||
delete: vi.fn()
|
||||
}
|
||||
};
|
||||
@@ -18,6 +18,7 @@ vi.mock('drizzle-orm', () => ({
|
||||
eq: vi.fn(() => ({})),
|
||||
and: vi.fn(() => ({})),
|
||||
asc: vi.fn(() => ({})),
|
||||
desc: vi.fn(() => ({})),
|
||||
lt: vi.fn(() => ({})),
|
||||
inArray: vi.fn(() => ({})),
|
||||
sql: vi.fn(() => ({})),
|
||||
@@ -26,6 +27,7 @@ vi.mock('drizzle-orm', () => ({
|
||||
|
||||
vi.mock('$lib/server/db/schema', () => ({
|
||||
match: {},
|
||||
matchParticipant: {},
|
||||
matchQueue: {},
|
||||
matchParticipantPlayer: {},
|
||||
playerRating: {}
|
||||
@@ -43,11 +45,23 @@ vi.mock('$lib/server/match/engine', () => ({
|
||||
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
mocks.db.query.matchParticipantPlayer.findMany.mockReset();
|
||||
mocks.db.query.playerRating.findFirst.mockReset();
|
||||
mocks.db.query.matchQueue.findFirst.mockReset();
|
||||
mocks.db.select.mockReset();
|
||||
mocks.db.delete.mockReset();
|
||||
});
|
||||
|
||||
@@ -56,13 +70,7 @@ describe('joinQueue', () => {
|
||||
where: vi.fn(() => ({ returning: vi.fn(() => Promise.resolve([])) }))
|
||||
});
|
||||
|
||||
mocks.db.query.matchParticipantPlayer.findMany.mockResolvedValue([
|
||||
{
|
||||
participant: {
|
||||
match: { id: 'match-1', state: MATCH_STATES.PLAYING, createdAt: new Date().toISOString() }
|
||||
}
|
||||
}
|
||||
]);
|
||||
mockFindRecentActiveMatchRows([{ id: 'match-1' }]);
|
||||
|
||||
await expect(joinQueue('user-1', 'team-1')).rejects.toThrow(
|
||||
'You are already in an active match'
|
||||
@@ -75,7 +83,7 @@ describe('joinQueue', () => {
|
||||
where: vi.fn(() => ({ returning: vi.fn(() => Promise.resolve([])) }))
|
||||
});
|
||||
|
||||
mocks.db.query.matchParticipantPlayer.findMany.mockResolvedValue([]);
|
||||
mockFindRecentActiveMatchRows([]);
|
||||
mocks.db.query.playerRating.findFirst.mockResolvedValue(null);
|
||||
|
||||
await expect(joinQueue('user-1', 'team-1')).rejects.toThrow(
|
||||
@@ -88,7 +96,7 @@ describe('joinQueue', () => {
|
||||
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.matchQueue.findFirst.mockResolvedValue({ userId: 'user-1', teamId: 'team-1' });
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import { match, matchQueue, matchParticipantPlayer, playerRating } from '$lib/server/db/schema';
|
||||
import { eq, asc, sql, inArray, lt } from 'drizzle-orm';
|
||||
import {
|
||||
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 { createMatch } from './engine';
|
||||
|
||||
@@ -75,19 +81,6 @@ export async function getQueueStatus(userId: string) {
|
||||
* Used to redirect players who were matched via queue while polling.
|
||||
*/
|
||||
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 = [
|
||||
MATCH_STATES.CREATED,
|
||||
MATCH_STATES.LOBBY,
|
||||
@@ -96,12 +89,16 @@ async function findRecentActiveMatch(userId: string): Promise<string | null> {
|
||||
MATCH_STATES.PLAYING
|
||||
];
|
||||
|
||||
const activeMatches = playerEntries
|
||||
.filter((pe) => activeStates.includes(pe.participant.match.state as any))
|
||||
.map((pe) => pe.participant.match)
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
const [activeMatch] = await db
|
||||
.select({ id: match.id })
|
||||
.from(matchParticipantPlayer)
|
||||
.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;
|
||||
|
||||
@@ -12,6 +12,8 @@ const queue = $state({
|
||||
loading: false
|
||||
});
|
||||
|
||||
const QUEUE_POLL_INTERVAL_MS = 8000;
|
||||
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
async function fetchQueueStatus() {
|
||||
@@ -57,7 +59,7 @@ async function leaveQueue() {
|
||||
function startPolling() {
|
||||
if (pollInterval) return;
|
||||
fetchQueueStatus();
|
||||
pollInterval = setInterval(fetchQueueStatus, 4000);
|
||||
pollInterval = setInterval(fetchQueueStatus, QUEUE_POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
|
||||
@@ -12,7 +12,40 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||
requireAuth(locals);
|
||||
|
||||
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),
|
||||
limit: 50
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user