feat: Challenges Page and Challenge Solve Modal
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user