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,
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user