diff --git a/.gitignore b/.gitignore index 3b462cb..51f349b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# Firehose logs +/data diff --git a/package.json b/package.json index e9b2e27..ba5dd05 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "firehose": "npx tsx scripts/firehose-logger.ts" }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.10", diff --git a/scripts/firehose-logger.ts b/scripts/firehose-logger.ts new file mode 100644 index 0000000..4a772aa --- /dev/null +++ b/scripts/firehose-logger.ts @@ -0,0 +1,70 @@ +import { createWriteStream } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { mkdir } from 'fs/promises'; + +const JETSTREAM_URL = 'wss://jetstream2.us-east.bsky.network/subscribe'; +const COLLECTIONS = ['blue.checkmate.game', 'blue.checkmate.challenge']; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const logDir = join(__dirname, '..', 'data'); +const logFile = join(logDir, 'firehose.jsonl'); + +await mkdir(logDir, { recursive: true }); +const stream = createWriteStream(logFile, { flags: 'a' }); + +let cursor: number | null = null; +let reconnectAttempts = 0; + +function connect() { + const url = new URL(JETSTREAM_URL); + for (const c of COLLECTIONS) { + url.searchParams.append('wantedCollections', c); + } + if (cursor) url.searchParams.set('cursor', String(cursor)); + + console.log(`[firehose] connecting...${cursor ? ` (cursor: ${cursor})` : ''}`); + const ws = new WebSocket(url.toString()); + + ws.onopen = () => { + console.log('[firehose] connected'); + reconnectAttempts = 0; + }; + + ws.onmessage = (event) => { + const data = JSON.parse(String(event.data)); + if (data.time_us) cursor = data.time_us; + if (data.kind !== 'commit') return; + + const line = JSON.stringify({ + time: new Date().toISOString(), + time_us: data.time_us, + did: data.did, + operation: data.commit.operation, + collection: data.commit.collection, + rkey: data.commit.rkey, + record: data.commit.record ?? null, + }); + + stream.write(line + '\n'); + console.log( + `[firehose] ${data.commit.operation} ${data.commit.collection} ${data.did.slice(0, 24)}...` + ); + }; + + ws.onclose = (event: CloseEvent) => { + console.log(`[firehose] disconnected: ${event.code} ${event.reason}`); + const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30_000); + reconnectAttempts++; + console.log(`[firehose] reconnecting in ${delay}ms...`); + setTimeout(connect, delay); + }; + + ws.onerror = () => { + ws.close(); + }; +} + +console.log(`[firehose] logging to ${logFile}`); +console.log(`[firehose] watching: ${COLLECTIONS.join(', ')}`); +connect(); diff --git a/src/lib/components/GameCard.svelte b/src/lib/components/GameCard.svelte new file mode 100644 index 0000000..f203ded --- /dev/null +++ b/src/lib/components/GameCard.svelte @@ -0,0 +1,90 @@ + + + +
+
+ {whiteHandle} + vs + {blackHandle} +
+ {#if record.status === 'active'} + Live + {:else if record.status === 'waiting'} + Waiting + {/if} +
+
+ {statusText()} + {timeAgo(timestamp ?? record.createdAt)} +
+
diff --git a/src/lib/microcosm.ts b/src/lib/microcosm.ts index a5cf303..5a7219e 100644 --- a/src/lib/microcosm.ts +++ b/src/lib/microcosm.ts @@ -33,3 +33,14 @@ export async function resolveIdentity(identifier: string): Promise(); + +export async function resolveHandle(did: string): Promise { + const cached = handleCache.get(did); + if (cached) return cached; + const profile = await resolveIdentity(did); + const handle = profile?.handle ?? did; + handleCache.set(did, handle); + return handle; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f44d831..5e0fb88 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,47 +1,144 @@ -
-
+
+

checkmate.blue

Chess on the Atmosphere

-
- - {#if auth.isLoggedIn} -
-

- Signed in as {auth.handle} -

-
+ {#if auth.isLoggedIn} + -
- - {#if loadingGames} -

Loading games...

- {:else if games.length > 0} -
+ + {#if auth.isLoggedIn} + {#if loadingUserGames} +

Loading your games...

+ {:else} + {#if activeUserGames.length > 0} +
+

Your Active Games

+
+ {#each activeUserGames as game} + + {/each} +
+
+ {/if} + + {#if completedUserGames.length > 0} +
+

Completed

+
+ {#each completedUserGames.slice(0, 5) as game} + + {/each} +
+
+ {/if} + + {#if userGames.length === 0} +

+ No games yet. Start one! +

+ {/if} + {/if} {/if} + +
+
+

Live on the Atmosphere

+ {#if liveConnected} + + {/if} +
+ {#if liveGames.length > 0} +
+ {#each liveGames.slice(0, 20) as game} + + {/each} +
+ {:else} +

+ {liveConnected ? 'No active games right now. Be the first!' : 'Connecting to live feed...'} +

+ {/if} +