Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f3dd9ec92 | ||
|
|
30abf33ee8 | ||
|
|
caa2bea7a8 | ||
|
|
12c3c37ff8 | ||
|
|
5c32209a2c | ||
|
|
ebfc88c5ea | ||
|
|
7eb55899c8 | ||
|
|
49f12f8cab |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,3 +1,31 @@
|
||||
## [2.4.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.4.3...v2.4.4) (2025-10-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* correctly access getSocketIO from dynamic import ([30abf33](https://github.com/antialias/soroban-abacus-flashcards/commit/30abf33ee86b36f2a98014e5b017fa8e466a2107))
|
||||
|
||||
## [2.4.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.4.2...v2.4.3) (2025-10-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* resolve socket-server import path for Next.js build ([12c3c37](https://github.com/antialias/soroban-abacus-flashcards/commit/12c3c37ff8e1d3df71d72e527c08fa975043c504))
|
||||
|
||||
## [2.4.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.4.1...v2.4.2) (2025-10-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* broadcast member join/leave events immediately via API ([ebfc88c](https://github.com/antialias/soroban-abacus-flashcards/commit/ebfc88c5ea0a8a0fdda039fa129e1054b9c42e65))
|
||||
|
||||
## [2.4.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.4.0...v2.4.1) (2025-10-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make leave room button actually remove user from room ([49f12f8](https://github.com/antialias/soroban-abacus-flashcards/commit/49f12f8cab631fedd33f1bc09febfdc95e444625))
|
||||
|
||||
## [2.4.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.3.1...v2.4.0) (2025-10-08)
|
||||
|
||||
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
"Bash(git commit -m \"$(cat <<''EOF''\nfeat: add Biome + ESLint linting setup\n\nAdd Biome for formatting and general linting, with minimal ESLint\nconfiguration for React Hooks rules only. This provides:\n\n- Fast formatting via Biome (10-100x faster than Prettier)\n- General JS/TS linting via Biome\n- React Hooks validation via ESLint (rules-of-hooks)\n- Import organization via Biome\n\nConfiguration files:\n- biome.jsonc: Biome config with custom rule overrides\n- eslint.config.js: Minimal flat config for React Hooks only\n- .gitignore: Added Biome cache exclusion\n- LINTING.md: Documentation for the setup\n\nScripts added to package.json:\n- npm run lint: Check all files\n- npm run lint:fix: Auto-fix issues\n- npm run format: Format all files\n- npm run check: Full Biome check\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(npm run pre-commit:*)",
|
||||
"Bash(npm run:*)"
|
||||
"Bash(npm run:*)",
|
||||
"Bash(git pull:*)",
|
||||
"Bash(git stash:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Server as HTTPServer } from 'http'
|
||||
import { Server as SocketIOServer } from 'socket.io'
|
||||
import type { Server as SocketIOServerType } from 'socket.io'
|
||||
import {
|
||||
applyGameMove,
|
||||
createArcadeSession,
|
||||
@@ -18,8 +19,19 @@ import { getRoomActivePlayers } from './src/lib/arcade/player-manager'
|
||||
import type { GameMove, GameName } from './src/lib/arcade/validation'
|
||||
import { matchingGameValidator } from './src/lib/arcade/validation/MatchingGameValidator'
|
||||
|
||||
// Global socket.io server instance
|
||||
let io: SocketIOServerType | null = null
|
||||
|
||||
/**
|
||||
* Get the socket.io server instance
|
||||
* Returns null if not initialized
|
||||
*/
|
||||
export function getSocketIO(): SocketIOServerType | null {
|
||||
return io
|
||||
}
|
||||
|
||||
export function initializeSocketServer(httpServer: HTTPServer) {
|
||||
const io = new SocketIOServer(httpServer, {
|
||||
io = new SocketIOServer(httpServer, {
|
||||
path: '/api/socket',
|
||||
cors: {
|
||||
origin: process.env.NEXT_PUBLIC_URL || 'http://localhost:3000',
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getRoomById, touchRoom } from '@/lib/arcade/room-manager'
|
||||
import { addRoomMember } from '@/lib/arcade/room-membership'
|
||||
import { getActivePlayers } from '@/lib/arcade/player-manager'
|
||||
import { addRoomMember, getOnlineRoomMembers } from '@/lib/arcade/room-membership'
|
||||
import { getActivePlayers, getRoomActivePlayers } from '@/lib/arcade/player-manager'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
import { getSocketIO } from '@/lib/socket-io'
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ roomId: string }>
|
||||
@@ -56,6 +57,34 @@ export async function POST(req: NextRequest, context: RouteContext) {
|
||||
// Update room activity to refresh TTL
|
||||
await touchRoom(roomId)
|
||||
|
||||
// Broadcast to all users in the room via socket
|
||||
const io = await getSocketIO()
|
||||
if (io) {
|
||||
try {
|
||||
const onlineMembers = await getOnlineRoomMembers(roomId)
|
||||
const memberPlayers = await getRoomActivePlayers(roomId)
|
||||
|
||||
// Convert memberPlayers Map to object for JSON serialization
|
||||
const memberPlayersObj: Record<string, any[]> = {}
|
||||
for (const [uid, players] of memberPlayers.entries()) {
|
||||
memberPlayersObj[uid] = players
|
||||
}
|
||||
|
||||
// Broadcast to all users in this room
|
||||
io.to(`room:${roomId}`).emit('member-joined', {
|
||||
roomId,
|
||||
userId: viewerId,
|
||||
onlineMembers,
|
||||
memberPlayers: memberPlayersObj,
|
||||
})
|
||||
|
||||
console.log(`[Join API] Broadcasted member-joined for user ${viewerId} in room ${roomId}`)
|
||||
} catch (socketError) {
|
||||
// Log but don't fail the request if socket broadcast fails
|
||||
console.error('[Join API] Failed to broadcast member-joined:', socketError)
|
||||
}
|
||||
}
|
||||
|
||||
// Build response with auto-leave info if applicable
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getRoomById } from '@/lib/arcade/room-manager'
|
||||
import { isMember, removeMember } from '@/lib/arcade/room-membership'
|
||||
import { getOnlineRoomMembers, isMember, removeMember } from '@/lib/arcade/room-membership'
|
||||
import { getRoomActivePlayers } from '@/lib/arcade/player-manager'
|
||||
import { getViewerId } from '@/lib/viewer'
|
||||
import { getSocketIO } from '@/lib/socket-io'
|
||||
|
||||
type RouteContext = {
|
||||
params: Promise<{ roomId: string }>
|
||||
@@ -31,6 +33,34 @@ export async function POST(_req: NextRequest, context: RouteContext) {
|
||||
// Remove member
|
||||
await removeMember(roomId, viewerId)
|
||||
|
||||
// Broadcast to all remaining users in the room via socket
|
||||
const io = await getSocketIO()
|
||||
if (io) {
|
||||
try {
|
||||
const onlineMembers = await getOnlineRoomMembers(roomId)
|
||||
const memberPlayers = await getRoomActivePlayers(roomId)
|
||||
|
||||
// Convert memberPlayers Map to object for JSON serialization
|
||||
const memberPlayersObj: Record<string, any[]> = {}
|
||||
for (const [uid, players] of memberPlayers.entries()) {
|
||||
memberPlayersObj[uid] = players
|
||||
}
|
||||
|
||||
// Broadcast to all users in this room
|
||||
io.to(`room:${roomId}`).emit('member-left', {
|
||||
roomId,
|
||||
userId: viewerId,
|
||||
onlineMembers,
|
||||
memberPlayers: memberPlayersObj,
|
||||
})
|
||||
|
||||
console.log(`[Leave API] Broadcasted member-left for user ${viewerId} in room ${roomId}`)
|
||||
} catch (socketError) {
|
||||
// Log but don't fail the request if socket broadcast fails
|
||||
console.error('[Leave API] Failed to broadcast member-left:', socketError)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Failed to leave room:', error)
|
||||
|
||||
@@ -196,8 +196,24 @@ export default function RoomDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const leaveRoom = () => {
|
||||
router.push('/arcade/rooms')
|
||||
const leaveRoom = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/arcade/rooms/${roomId}/leave`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
// Navigate to arcade home after successfully leaving
|
||||
router.push('/arcade')
|
||||
} catch (err) {
|
||||
console.error('Failed to leave room:', err)
|
||||
alert('Failed to leave room')
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
|
||||
40
apps/web/src/lib/socket-io.ts
Normal file
40
apps/web/src/lib/socket-io.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Socket.IO server instance accessor for API routes
|
||||
* This module provides a way for API routes to access the socket.io server
|
||||
* to broadcast real-time updates.
|
||||
*/
|
||||
|
||||
import type { Server as SocketIOServerType } from 'socket.io'
|
||||
|
||||
// Cache for the socket server module
|
||||
let socketServerModule: any = null
|
||||
|
||||
/**
|
||||
* Get the socket.io server instance
|
||||
* Returns null if not initialized or if called on client-side
|
||||
*/
|
||||
export async function getSocketIO(): Promise<SocketIOServerType | null> {
|
||||
// Client-side: return null
|
||||
if (typeof window !== 'undefined') {
|
||||
return null
|
||||
}
|
||||
|
||||
// Lazy-load the socket server module on first call
|
||||
if (!socketServerModule) {
|
||||
try {
|
||||
// Dynamic import to avoid bundling issues
|
||||
socketServerModule = await import('../../socket-server')
|
||||
} catch (error) {
|
||||
console.error('[Socket IO] Failed to load socket server:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Call the exported getSocketIO function from the module
|
||||
if (socketServerModule && typeof socketServerModule.getSocketIO === 'function') {
|
||||
return socketServerModule.getSocketIO()
|
||||
}
|
||||
|
||||
console.warn('[Socket IO] getSocketIO function not found in socket-server module')
|
||||
return null
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.4",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user