Compare commits

...

4 Commits

Author SHA1 Message Date
semantic-release-bot
caa2bea7a8 chore(release): 2.4.3 [skip ci]
## [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](12c3c37ff8))
2025-10-08 13:56:18 +00:00
Thomas Hallock
12c3c37ff8 fix: resolve socket-server import path for Next.js build
Create a wrapper module in src/lib/socket-io.ts that provides access
to the socket.io server instance for API routes, avoiding the build
error caused by importing from outside the src directory.

The wrapper uses dynamic imports to lazy-load the socket server module
only on the server-side, making it safe for Next.js to bundle.

Changes:
- Add src/lib/socket-io.ts with async getSocketIO() function
- Update join route to use @/lib/socket-io import
- Update leave route to use @/lib/socket-io import
- Both routes now await getSocketIO() since it's async

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 08:55:28 -05:00
semantic-release-bot
5c32209a2c chore(release): 2.4.2 [skip ci]
## [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](ebfc88c5ea))
2025-10-08 13:52:13 +00:00
Thomas Hallock
ebfc88c5ea fix: broadcast member join/leave events immediately via API
Add socket.io broadcasts to join and leave API routes to notify
all users in a room immediately when someone joins or leaves,
without waiting for socket connection.

Previously:
- API added member to database
- No socket event until user's browser connected
- Other users didn't see new member until page reload
- Players showed up but member didn't (timing race condition)

Now:
- API broadcasts `member-joined` immediately after adding member
- API broadcasts `member-left` immediately after removing member
- All connected users get real-time updates instantly
- Both members list and players list update simultaneously

Changes:
- Export `getSocketIO()` from socket-server.ts for API access
- Join route broadcasts member-joined with updated members/players
- Leave route broadcasts member-left with updated members/players
- Socket broadcasts are non-blocking (won't fail the request)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 08:51:17 -05:00
7 changed files with 132 additions and 6 deletions

View File

@@ -1,3 +1,17 @@
## [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)

View File

@@ -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": []

View File

@@ -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',

View File

@@ -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(
{

View File

@@ -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)

View File

@@ -0,0 +1,39 @@
/**
* 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'
// Import the socket server module (this is safe because it's only used in Node.js context)
let socketServer: { getSocketIO: () => SocketIOServerType | null } | null = null
// Lazy-load the socket server module (only works on server-side)
async function loadSocketServer() {
if (typeof window !== 'undefined') {
// Client-side: return null
return null
}
if (!socketServer) {
try {
// Dynamic import to avoid bundling issues
socketServer = await import('../../socket-server')
} catch (error) {
console.error('[Socket IO] Failed to load socket server:', error)
return null
}
}
return socketServer
}
/**
* Get the socket.io server instance
* Returns null if not initialized or if called on client-side
*/
export async function getSocketIO(): Promise<SocketIOServerType | null> {
const server = await loadSocketServer()
return server ? server.getSocketIO() : null
}

View File

@@ -1,6 +1,6 @@
{
"name": "soroban-monorepo",
"version": "2.4.1",
"version": "2.4.3",
"private": true,
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
"workspaces": [