feat: Challenges Page and Challenge Solve Modal

This commit is contained in:
OpenVelo Agent
2026-07-23 00:13:57 +00:00
parent 77d31e0ee1
commit cb88b21462
32 changed files with 4134 additions and 119 deletions
@@ -14,7 +14,7 @@ if (typeof (globalThis as any).TextDecoder === 'undefined') {
interface EventSourceLike {
addEventListener(
type: 'open' | 'message' | 'error' | 'unauthorized',
type: 'open' | 'message' | 'error' | 'unauthorized' | string,
listener: (ev: MessageEvent | Event) => void,
): void;
close(): void;
@@ -51,7 +51,7 @@ function openAuthenticatedLike(
token: string | null,
fetchImpl: typeof globalThis.fetch,
): EventSourceLike {
let handler: ((ev: MessageEvent) => void) | null = null;
const listeners = new Map<string, (ev: MessageEvent | Event) => void>();
let unauthorizedHandler: ((ev: Event) => void) | null = null;
void (async () => {
const headers: Record<string, string> = { Accept: 'text/event-stream' };
@@ -77,12 +77,22 @@ function openAuthenticatedLike(
while (idx >= 0) {
const raw = buf.slice(0, idx);
buf = buf.slice(idx + 2);
const data = raw
.split('\n')
.filter((l) => l.startsWith('data:'))
.map((l) => l.slice(5).trim())
.join('');
if (data && handler) handler({ data } as unknown as MessageEvent);
let event = 'message';
const dataLines: string[] = [];
for (const line of raw.split('\n')) {
if (line.startsWith('event:')) event = line.slice(6).trim();
else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
}
const data = dataLines.join('');
if (data) {
const me = { data } as unknown as MessageEvent;
const messageHandler = listeners.get('message');
if (messageHandler) messageHandler(me);
if (event !== 'message') {
const named = listeners.get(event);
if (named) named(me);
}
}
idx = buf.indexOf('\n\n');
}
}
@@ -90,10 +100,10 @@ function openAuthenticatedLike(
return {
addEventListener(type, cb) {
if (type === 'unauthorized') unauthorizedHandler = cb as (ev: Event) => void;
else handler = cb as (ev: MessageEvent) => void;
else listeners.set(type, cb);
},
close() {
handler = null;
listeners.clear();
unauthorizedHandler = null;
},
};
@@ -204,4 +214,33 @@ describe('authenticated event source transport contract', () => {
expect(received).toEqual(['unauthorized']);
source.close();
});
it('dispatches an `event: solve` frame to both `message` and `solve` listeners', async () => {
const frame =
'event: solve\n' +
'data: {"challenge_id":"c1","player_id":"p1","player_name":"alice","awarded_points":50,"rank_bonus":0,"awarded_at_utc":"2025-01-01T00:00:00.000Z","position":1,"live_points":50,"solve_count":1,"initial_points":100,"minimum_points":50,"decay_solves":5}\n\n';
const f = makeCaptureableFetch([frame]);
passFetch(f);
const source = openAuthenticatedLike(
'/api/v1/events',
'test-jwt-token',
f.fetchMock as unknown as typeof globalThis.fetch,
);
const messages: any[] = [];
const solves: any[] = [];
source.addEventListener('message', (ev) => {
const me = ev as MessageEvent;
messages.push(typeof me.data === 'string' ? JSON.parse(me.data) : me.data);
});
source.addEventListener('solve', (ev) => {
const me = ev as MessageEvent;
solves.push(typeof me.data === 'string' ? JSON.parse(me.data) : me.data);
});
await new Promise((r) => setTimeout(r, 30));
expect(messages.length).toBe(1);
expect(solves.length).toBe(1);
expect(solves[0].challenge_id).toBe('c1');
expect(solves[0].live_points).toBe(50);
source.close();
});
});