@smoregg/sdk 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/cjs/SmoreHost.cjs +306 -0
  2. package/dist/cjs/SmoreHost.cjs.map +1 -0
  3. package/dist/cjs/SmorePlayer.cjs +229 -0
  4. package/dist/cjs/SmorePlayer.cjs.map +1 -0
  5. package/dist/cjs/components/IframeGameBridge.cjs +115 -0
  6. package/dist/cjs/components/IframeGameBridge.cjs.map +1 -0
  7. package/dist/cjs/context/RoomProvider.cjs +3 -3
  8. package/dist/cjs/context/RoomProvider.cjs.map +1 -1
  9. package/dist/cjs/hooks/useGameHost.cjs +86 -13
  10. package/dist/cjs/hooks/useGameHost.cjs.map +1 -1
  11. package/dist/cjs/hooks/useGamePlayer.cjs +60 -4
  12. package/dist/cjs/hooks/useGamePlayer.cjs.map +1 -1
  13. package/dist/cjs/iframe/index.cjs +50 -316
  14. package/dist/cjs/iframe/index.cjs.map +1 -1
  15. package/dist/cjs/index.cjs +4 -22
  16. package/dist/cjs/index.cjs.map +1 -1
  17. package/dist/cjs/transport/protocol.cjs.map +1 -1
  18. package/dist/cjs/utils/connectionMonitor.cjs +77 -0
  19. package/dist/cjs/utils/connectionMonitor.cjs.map +1 -0
  20. package/dist/cjs/utils/preloadAssets.cjs +66 -0
  21. package/dist/cjs/utils/preloadAssets.cjs.map +1 -0
  22. package/dist/cjs/utils/serverTime.cjs +43 -0
  23. package/dist/cjs/utils/serverTime.cjs.map +1 -0
  24. package/dist/esm/SmoreHost.js +304 -0
  25. package/dist/esm/SmoreHost.js.map +1 -0
  26. package/dist/esm/SmorePlayer.js +227 -0
  27. package/dist/esm/SmorePlayer.js.map +1 -0
  28. package/dist/esm/components/IframeGameBridge.js +113 -0
  29. package/dist/esm/components/IframeGameBridge.js.map +1 -0
  30. package/dist/esm/context/RoomProvider.js +3 -3
  31. package/dist/esm/context/RoomProvider.js.map +1 -1
  32. package/dist/esm/hooks/useGameHost.js +87 -14
  33. package/dist/esm/hooks/useGameHost.js.map +1 -1
  34. package/dist/esm/hooks/useGamePlayer.js +61 -5
  35. package/dist/esm/hooks/useGamePlayer.js.map +1 -1
  36. package/dist/esm/iframe/index.js +51 -314
  37. package/dist/esm/iframe/index.js.map +1 -1
  38. package/dist/esm/index.js +2 -8
  39. package/dist/esm/index.js.map +1 -1
  40. package/dist/esm/transport/protocol.js.map +1 -1
  41. package/dist/esm/utils/connectionMonitor.js +75 -0
  42. package/dist/esm/utils/connectionMonitor.js.map +1 -0
  43. package/dist/esm/utils/preloadAssets.js +63 -0
  44. package/dist/esm/utils/preloadAssets.js.map +1 -0
  45. package/dist/esm/utils/serverTime.js +41 -0
  46. package/dist/esm/utils/serverTime.js.map +1 -0
  47. package/dist/types/SmoreHost.d.ts +187 -0
  48. package/dist/types/SmoreHost.d.ts.map +1 -0
  49. package/dist/types/SmorePlayer.d.ts +146 -0
  50. package/dist/types/SmorePlayer.d.ts.map +1 -0
  51. package/dist/types/components/IframeGameBridge.d.ts +2 -2
  52. package/dist/types/components/IframeGameBridge.d.ts.map +1 -1
  53. package/dist/types/components/index.d.ts +2 -4
  54. package/dist/types/components/index.d.ts.map +1 -1
  55. package/dist/types/context/RoomProvider.d.ts +3 -3
  56. package/dist/types/context/RoomProvider.d.ts.map +1 -1
  57. package/dist/types/hooks/useGameHost.d.ts +33 -7
  58. package/dist/types/hooks/useGameHost.d.ts.map +1 -1
  59. package/dist/types/hooks/useGamePlayer.d.ts +29 -3
  60. package/dist/types/hooks/useGamePlayer.d.ts.map +1 -1
  61. package/dist/types/iframe/index.d.ts +10 -10
  62. package/dist/types/iframe/index.d.ts.map +1 -1
  63. package/dist/types/iframe/vanilla.d.ts +12 -4
  64. package/dist/types/iframe/vanilla.d.ts.map +1 -1
  65. package/dist/types/index.d.ts +36 -20
  66. package/dist/types/index.d.ts.map +1 -1
  67. package/dist/types/transport/protocol.d.ts +1 -1
  68. package/dist/types/transport/protocol.d.ts.map +1 -1
  69. package/dist/types/utils/connectionMonitor.d.ts +57 -0
  70. package/dist/types/utils/connectionMonitor.d.ts.map +1 -0
  71. package/dist/types/utils/index.d.ts +7 -0
  72. package/dist/types/utils/index.d.ts.map +1 -0
  73. package/dist/types/utils/preloadAssets.d.ts +29 -0
  74. package/dist/types/utils/preloadAssets.d.ts.map +1 -0
  75. package/dist/types/utils/serverTime.d.ts +28 -0
  76. package/dist/types/utils/serverTime.d.ts.map +1 -0
  77. package/dist/umd/smore-sdk-iframe.umd.js +54 -317
  78. package/dist/umd/smore-sdk-iframe.umd.js.map +1 -1
  79. package/dist/umd/smore-sdk-iframe.umd.min.js +1 -1
  80. package/dist/umd/smore-sdk-iframe.umd.min.js.map +1 -1
  81. package/dist/umd/smore-sdk-vanilla.umd.js +550 -126
  82. package/dist/umd/smore-sdk-vanilla.umd.js.map +1 -1
  83. package/dist/umd/smore-sdk-vanilla.umd.min.js +1 -1
  84. package/dist/umd/smore-sdk-vanilla.umd.min.js.map +1 -1
  85. package/dist/umd/smore-sdk.umd.js +488 -576
  86. package/dist/umd/smore-sdk.umd.js.map +1 -1
  87. package/dist/umd/smore-sdk.umd.min.js +1 -1
  88. package/dist/umd/smore-sdk.umd.min.js.map +1 -1
  89. package/package.json +1 -26
@@ -1 +1 @@
1
- {"version":3,"file":"useGameHost.js","sources":["../../../src/hooks/useGameHost.ts"],"sourcesContent":["/**\n * useGameHost - Host-side game hook for the S'MORE SDK\n *\n * Provides host game developers with:\n * - Room state access (players, leaderId, roomCode)\n * - Event listeners for player inputs\n * - Broadcasting to all/specific players\n * - Game lifecycle management\n *\n * Internally uses Transport abstraction so the same API works over\n * Socket.IO (bundled games) or postMessage (iframe games).\n */\nimport { useEffect, useCallback, useRef } from 'react';\nimport { useHostRoom, useTransport } from '../context/RoomProvider';\nimport type { Player } from '../types';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst SYSTEM_PREFIX = 'smore:';\n\n// System events (internal use only)\nconst SYSTEM_EVENTS = {\n READY: `${SYSTEM_PREFIX}ready`,\n PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,\n PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`,\n GAME_OVER: `${SYSTEM_PREFIX}game-over`,\n RETURN_TO_LOBBY: `${SYSTEM_PREFIX}return-to-lobby`,\n SEND_TO_PLAYER: `${SYSTEM_PREFIX}send-to-player`,\n BROADCAST: `${SYSTEM_PREFIX}broadcast`,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validates event name format.\n * Rules:\n * - Only English letters (a-z, A-Z), hyphens (-), and underscores (_) allowed\n * - Must start and end with a letter (no leading/trailing - or _)\n * - Colons are reserved for system prefix\n */\nconst EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;\n\nfunction validateEventName(event: string): void {\n if (!EVENT_NAME_REGEX.test(event)) {\n throw new Error(\n `[SDK] Invalid event name \"${event}\". Event names must:\\n` +\n ` - Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)\\n` +\n ` - Start and end with a letter (no leading/trailing - or _)`\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface UseGameHostConfig<T extends Record<string, any> = Record<string, any>> {\n /** Called when the host is ready and connected. */\n onReady?: () => void;\n\n /** Called when a player joins the room. */\n onPlayerJoin?: (player: Player) => void;\n\n /** Called when a player leaves the room. */\n onPlayerLeave?: (sessionId: string) => void;\n\n /**\n * Event listeners for player inputs.\n * Keys are event names (without prefix), values are handler functions.\n * Handler receives (sessionId, data) where sessionId identifies the player.\n */\n listeners?: { [K in keyof T]?: (sessionId: string, data: T[K]) => void };\n}\n\nexport interface UseGameHostReturn {\n /** List of all players in the room. */\n players: Player[];\n\n /** The session ID of the room leader (host). */\n leaderId: string | null;\n\n /** The room code. */\n roomCode: string;\n\n /**\n * Emit an event to all players.\n * Event name must not contain colons.\n */\n emit: (event: string, data?: any) => void;\n\n /**\n * Send an event to a specific player.\n * Event name must not contain colons.\n */\n sendToPlayer: (sessionId: string, event: string, data?: any) => void;\n\n /** Signal game over with optional results. */\n gameOver: (results?: any) => void;\n\n /** Return all players to lobby. */\n returnToLobby: () => void;\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useGameHost<T extends Record<string, any> = Record<string, any>>(\n config: UseGameHostConfig<T> = {}\n): UseGameHostReturn {\n const { onReady, onPlayerJoin, onPlayerLeave, listeners } = config;\n\n const hostRoom = useHostRoom();\n const transport = useTransport();\n\n // Stable refs to avoid stale closures in listeners\n const onReadyRef = useRef(onReady);\n const onPlayerJoinRef = useRef(onPlayerJoin);\n const onPlayerLeaveRef = useRef(onPlayerLeave);\n const listenersRef = useRef(listeners);\n\n useEffect(() => {\n onReadyRef.current = onReady;\n }, [onReady]);\n useEffect(() => {\n onPlayerJoinRef.current = onPlayerJoin;\n }, [onPlayerJoin]);\n useEffect(() => {\n onPlayerLeaveRef.current = onPlayerLeave;\n }, [onPlayerLeave]);\n useEffect(() => {\n listenersRef.current = listeners;\n }, [listeners]);\n\n // -------------------------------------------------------------------------\n // System event listeners (player lifecycle)\n // -------------------------------------------------------------------------\n useEffect(() => {\n if (!transport) return;\n\n const handleReady = () => {\n onReadyRef.current?.();\n };\n\n const handlePlayerJoin = (data: { player: Player }) => {\n onPlayerJoinRef.current?.(data.player);\n };\n\n const handlePlayerLeave = (data: { sessionId: string }) => {\n onPlayerLeaveRef.current?.(data.sessionId);\n };\n\n transport.on(SYSTEM_EVENTS.READY, handleReady);\n transport.on(SYSTEM_EVENTS.PLAYER_JOIN, handlePlayerJoin);\n transport.on(SYSTEM_EVENTS.PLAYER_LEAVE, handlePlayerLeave);\n\n // Also listen to legacy room events for backward compatibility\n transport.on('room:player-left', (data: any) => {\n onPlayerLeaveRef.current?.(data?.sessionId ?? data?.playerId);\n });\n transport.on('room:player-joined', (data: any) => {\n if (data?.player) {\n onPlayerJoinRef.current?.(data.player);\n }\n });\n\n return () => {\n transport.off(SYSTEM_EVENTS.READY, handleReady);\n transport.off(SYSTEM_EVENTS.PLAYER_JOIN, handlePlayerJoin);\n transport.off(SYSTEM_EVENTS.PLAYER_LEAVE, handlePlayerLeave);\n transport.off('room:player-left');\n transport.off('room:player-joined');\n };\n }, [transport]);\n\n // -------------------------------------------------------------------------\n // User event listeners\n // -------------------------------------------------------------------------\n useEffect(() => {\n if (!transport || !listeners) return;\n\n const entries = Object.entries(listeners);\n const cleanups: (() => void)[] = [];\n\n for (const [event, handler] of entries) {\n if (!handler) continue;\n\n // Validate event name (no colons allowed)\n validateEventName(event);\n\n // Listen for the event directly\n // Server sends: { sessionId, ...playerData }\n const wrappedHandler = (data: { sessionId: string; [key: string]: any }) => {\n const { sessionId, ...rest } = data;\n (handler as (sessionId: string, data: any) => void)(sessionId, rest);\n };\n\n transport.on(event, wrappedHandler);\n cleanups.push(() => transport.off(event, wrappedHandler));\n }\n\n return () => {\n cleanups.forEach((cleanup) => cleanup());\n };\n }, [transport, listeners]);\n\n // -------------------------------------------------------------------------\n // Actions\n // -------------------------------------------------------------------------\n\n const emit = useCallback(\n (event: string, data?: any) => {\n validateEventName(event);\n // Broadcast to all players via system event\n transport?.emit(SYSTEM_EVENTS.BROADCAST, { event, data });\n },\n [transport]\n );\n\n const sendToPlayer = useCallback(\n (sessionId: string, event: string, data?: any) => {\n validateEventName(event);\n // Send to specific player via system event\n // The server will unwrap and send the inner event directly to the player\n transport?.emit(SYSTEM_EVENTS.SEND_TO_PLAYER, {\n targetSessionId: sessionId,\n event,\n data,\n });\n },\n [transport]\n );\n\n const gameOver = useCallback(\n (results?: any) => {\n transport?.emit(SYSTEM_EVENTS.GAME_OVER, { results });\n },\n [transport]\n );\n\n const returnToLobby = useCallback(() => {\n transport?.emit(SYSTEM_EVENTS.RETURN_TO_LOBBY, {});\n }, [transport]);\n\n return {\n players: hostRoom.players,\n leaderId: hostRoom.leaderId,\n roomCode: hostRoom.roomCode,\n emit,\n sendToPlayer,\n gameOver,\n returnToLobby,\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useGameHost.js","sources":["../../../src/hooks/useGameHost.ts"],"sourcesContent":["/**\n * useGameHost - Host-side game hook for the S'MORE SDK\n *\n * Provides host game developers with:\n * - Room state access (players, leaderIndex, roomCode)\n * - Event listeners for player inputs (using playerIndex)\n * - Broadcasting to all/specific players (by playerIndex)\n * - Game lifecycle management\n * - Optional connection monitoring (pause/resume on network issues)\n *\n * All callbacks receive playerIndex (number) instead of sessionId (string).\n * This aligns with the SDK's external API which never exposes sessionId.\n *\n * Internally uses Transport abstraction so the same API works over\n * Socket.IO (bundled games) or postMessage (iframe games).\n */\nimport { useEffect, useCallback, useRef, useState, useMemo } from 'react';\nimport { useHostRoom, useTransport } from '../context/RoomProvider';\nimport type { Player } from '../types';\nimport type { Socket } from 'socket.io-client';\nimport { createConnectionMonitor } from '../utils/connectionMonitor';\nimport type { ConnectionMonitorOptions } from '../utils/connectionMonitor';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst SYSTEM_PREFIX = 'smore:';\n\n// System events (internal use only)\nconst SYSTEM_EVENTS = {\n READY: `${SYSTEM_PREFIX}ready`,\n PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,\n PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`,\n GAME_OVER: `${SYSTEM_PREFIX}game-over`,\n RETURN_TO_LOBBY: `${SYSTEM_PREFIX}return-to-lobby`,\n SEND_TO_PLAYER: `${SYSTEM_PREFIX}send-to-player`,\n BROADCAST: `${SYSTEM_PREFIX}broadcast`,\n CUSTOM_STATE_CHANGE: `${SYSTEM_PREFIX}custom-state-change`,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validates event name format.\n * Rules:\n * - Only English letters (a-z, A-Z), hyphens (-), and underscores (_) allowed\n * - Must start and end with a letter (no leading/trailing - or _)\n * - Colons are reserved for system prefix\n */\nconst EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;\n\nfunction validateEventName(event: string): void {\n if (!EVENT_NAME_REGEX.test(event)) {\n throw new Error(\n `[SDK] Invalid event name \"${event}\". Event names must:\\n` +\n ` - Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)\\n` +\n ` - Start and end with a letter (no leading/trailing - or _)`\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface UseGameHostConfig<T extends Record<string, any> = Record<string, any>> {\n /** Called when the host is ready and connected. */\n onReady?: () => void;\n\n /** Called when a player joins the room. */\n onPlayerJoin?: (playerIndex: number) => void;\n\n /** Called when a player leaves the room. */\n onPlayerLeave?: (playerIndex: number) => void;\n\n /**\n * Event listeners for player inputs.\n * Keys are event names (without prefix), values are handler functions.\n * Handler receives (playerIndex, data) where playerIndex identifies the player (0, 1, 2, ...).\n */\n listeners?: { [K in keyof T]?: (playerIndex: number, data: T[K]) => void };\n\n /**\n * Optional connection monitoring (AirConsole pattern).\n * When enabled, automatically pauses/resumes game on connection issues.\n */\n connectionMonitor?: {\n /** Enable connection monitoring */\n enabled: boolean;\n /** Called when connection becomes unstable */\n onPause: () => void;\n /** Called after connection restores and countdown completes */\n onResume: () => void;\n /** Called during resume countdown with seconds remaining */\n onCountdown?: (secondsLeft: number) => void;\n /** Latency threshold to trigger pause (ms), default 2000 */\n latencyThreshold?: number;\n /** Resume countdown duration (ms), default 3000 */\n resumeCountdown?: number;\n };\n\n /** Called when any device's custom state changes (AirConsole pattern) */\n onCustomStateChange?: (playerIndex: number, state: Record<string, any>) => void;\n}\n\nexport interface UseGameHostReturn {\n /** List of all players in the room. */\n players: Player[];\n\n /** The player index of the room leader (-1 if no leader). */\n leaderIndex: number;\n\n /** The room code. */\n roomCode: string;\n\n /**\n * Emit an event to all players.\n * Event name must not contain colons.\n */\n emit: (event: string, data?: any) => void;\n\n /**\n * Send an event to a specific player.\n * Event name must not contain colons.\n */\n sendToPlayer: (playerIndex: number, event: string, data?: any) => void;\n\n /** Signal game over with optional results. */\n gameOver: (results?: any) => void;\n\n /** Return all players to lobby. */\n returnToLobby: () => void;\n\n /** Set custom device state (AirConsole pattern) - merges with existing state */\n setCustomState: (state: Record<string, any>) => void;\n\n /** Whether game is currently paused due to connection issues (only if connectionMonitor enabled) */\n isPaused: boolean;\n\n /** Current network latency in ms (only if connectionMonitor enabled) */\n latency: number;\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useGameHost<T extends Record<string, any> = Record<string, any>>(\n config: UseGameHostConfig<T> = {}\n): UseGameHostReturn {\n const { onReady, onPlayerJoin, onPlayerLeave, listeners, connectionMonitor, onCustomStateChange } = config;\n\n const hostRoom = useHostRoom();\n const transport = useTransport();\n\n // Connection monitor state\n const [isPaused, setIsPaused] = useState(false);\n const [latency, setLatency] = useState(0);\n const monitorRef = useRef<ReturnType<typeof createConnectionMonitor> | null>(null);\n\n // Stable refs to avoid stale closures in listeners\n const onReadyRef = useRef(onReady);\n const onPlayerJoinRef = useRef(onPlayerJoin);\n const onPlayerLeaveRef = useRef(onPlayerLeave);\n const listenersRef = useRef(listeners);\n const onCustomStateChangeRef = useRef(onCustomStateChange);\n\n useEffect(() => {\n onReadyRef.current = onReady;\n }, [onReady]);\n useEffect(() => {\n onPlayerJoinRef.current = onPlayerJoin;\n }, [onPlayerJoin]);\n useEffect(() => {\n onPlayerLeaveRef.current = onPlayerLeave;\n }, [onPlayerLeave]);\n useEffect(() => {\n listenersRef.current = listeners;\n }, [listeners]);\n useEffect(() => {\n onCustomStateChangeRef.current = onCustomStateChange;\n }, [onCustomStateChange]);\n\n // -------------------------------------------------------------------------\n // Connection Monitor\n // -------------------------------------------------------------------------\n useEffect(() => {\n if (!connectionMonitor?.enabled || !transport) return;\n\n // Check if transport has socket property (Socket.IO)\n const socket = (transport as any).socket as Socket | undefined;\n if (!socket) {\n console.warn('[useGameHost] Connection monitor requires Socket.IO transport');\n return;\n }\n\n const monitor = createConnectionMonitor(socket, {\n onPause: () => {\n setIsPaused(true);\n connectionMonitor.onPause();\n },\n onResume: () => {\n setIsPaused(false);\n connectionMonitor.onResume();\n },\n onCountdown: connectionMonitor.onCountdown,\n latencyThreshold: connectionMonitor.latencyThreshold,\n resumeCountdown: connectionMonitor.resumeCountdown,\n });\n\n monitorRef.current = monitor;\n\n // Periodically update latency state\n const latencyInterval = setInterval(() => {\n setLatency(monitor.getLatency());\n }, 1000);\n\n return () => {\n monitor.destroy();\n clearInterval(latencyInterval);\n monitorRef.current = null;\n };\n }, [connectionMonitor, transport]);\n\n // -------------------------------------------------------------------------\n // System event listeners (player lifecycle)\n // -------------------------------------------------------------------------\n useEffect(() => {\n if (!transport) return;\n\n const handleReady = () => {\n onReadyRef.current?.();\n };\n\n const handlePlayerJoin = (data: { player: Player }) => {\n const playerIndex = data.player?.playerIndex;\n if (playerIndex !== undefined) {\n onPlayerJoinRef.current?.(playerIndex);\n }\n };\n\n const handlePlayerLeave = (data: { playerIndex: number }) => {\n const playerIndex = data.playerIndex;\n if (playerIndex !== undefined) {\n onPlayerLeaveRef.current?.(playerIndex);\n }\n };\n\n const handleCustomStateChange = (data: { playerIndex: number; state: Record<string, any> }) => {\n if (data.playerIndex !== undefined) {\n onCustomStateChangeRef.current?.(data.playerIndex, data.state);\n }\n };\n\n transport.on(SYSTEM_EVENTS.READY, handleReady);\n transport.on(SYSTEM_EVENTS.PLAYER_JOIN, handlePlayerJoin);\n transport.on(SYSTEM_EVENTS.PLAYER_LEAVE, handlePlayerLeave);\n transport.on(SYSTEM_EVENTS.CUSTOM_STATE_CHANGE, handleCustomStateChange);\n\n // Also listen to legacy room events for backward compatibility\n transport.on('room:player-left', (data: any) => {\n const playerIndex = data?.playerIndex ?? data?.player?.playerIndex;\n if (playerIndex !== undefined) {\n onPlayerLeaveRef.current?.(playerIndex);\n }\n });\n transport.on('room:player-joined', (data: any) => {\n const playerIndex = data?.player?.playerIndex;\n if (playerIndex !== undefined) {\n onPlayerJoinRef.current?.(playerIndex);\n }\n });\n\n return () => {\n transport.off(SYSTEM_EVENTS.READY, handleReady);\n transport.off(SYSTEM_EVENTS.PLAYER_JOIN, handlePlayerJoin);\n transport.off(SYSTEM_EVENTS.PLAYER_LEAVE, handlePlayerLeave);\n transport.off(SYSTEM_EVENTS.CUSTOM_STATE_CHANGE, handleCustomStateChange);\n transport.off('room:player-left');\n transport.off('room:player-joined');\n };\n }, [transport]);\n\n // -------------------------------------------------------------------------\n // User event listeners\n // -------------------------------------------------------------------------\n useEffect(() => {\n if (!transport || !listeners) return;\n\n const entries = Object.entries(listeners);\n const cleanups: (() => void)[] = [];\n\n for (const [event, handler] of entries) {\n if (!handler) continue;\n\n // Validate event name (no colons allowed)\n validateEventName(event);\n\n // Listen for the event directly\n // Server sends: { playerIndex, ...playerData }\n const wrappedHandler = (data: { playerIndex: number; [key: string]: any }) => {\n const { playerIndex, ...rest } = data;\n if (playerIndex !== undefined) {\n (handler as (playerIndex: number, data: any) => void)(playerIndex, rest);\n }\n };\n\n transport.on(event, wrappedHandler);\n cleanups.push(() => transport.off(event, wrappedHandler));\n }\n\n return () => {\n cleanups.forEach((cleanup) => cleanup());\n };\n }, [transport, listeners]);\n\n // -------------------------------------------------------------------------\n // Actions\n // -------------------------------------------------------------------------\n\n const emit = useCallback(\n (event: string, data?: any) => {\n validateEventName(event);\n // Broadcast to all players via system event\n transport?.emit(SYSTEM_EVENTS.BROADCAST, { event, data });\n },\n [transport]\n );\n\n const sendToPlayer = useCallback(\n (playerIndex: number, event: string, data?: any) => {\n validateEventName(event);\n // Send to specific player via system event\n // The server will unwrap and send the inner event directly to the player\n transport?.emit(SYSTEM_EVENTS.SEND_TO_PLAYER, {\n targetPlayerIndex: playerIndex,\n event,\n data,\n });\n },\n [transport]\n );\n\n const gameOver = useCallback(\n (results?: any) => {\n transport?.emit(SYSTEM_EVENTS.GAME_OVER, { results });\n },\n [transport]\n );\n\n const returnToLobby = useCallback(() => {\n transport?.emit(SYSTEM_EVENTS.RETURN_TO_LOBBY, {});\n }, [transport]);\n\n const setCustomState = useCallback(\n (state: Record<string, any>) => {\n transport?.emit(SYSTEM_EVENTS.CUSTOM_STATE_CHANGE, { state });\n },\n [transport]\n );\n\n // Compute leaderIndex from leaderId by finding the matching player\n // If leaderId exists, find the player with that sessionId (legacy) or just use the first player as leader\n // Since we're transitioning away from sessionId, we look for leaderIndex in the room state\n // For now, we compute it from leaderId by finding the player at index 0 (leader is typically first joiner)\n // or return -1 if no leader\n const leaderIndex = useMemo(() => {\n // If hostRoom has leaderIndex directly, use it\n if ('leaderIndex' in hostRoom && typeof (hostRoom as any).leaderIndex === 'number') {\n return (hostRoom as any).leaderIndex;\n }\n // Fallback: if leaderId exists, we can't map it without sessionId, so return -1\n // This is a transitional state - the server should send leaderIndex instead\n return -1;\n }, [hostRoom]);\n\n return {\n players: hostRoom.players,\n leaderIndex,\n roomCode: hostRoom.roomCode,\n emit,\n sendToPlayer,\n gameOver,\n returnToLobby,\n setCustomState,\n isPaused,\n latency,\n };\n}\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
- import { useRef, useEffect, useCallback } from 'react';
1
+ import { useState, useRef, useEffect, useCallback } from 'react';
2
2
  import { usePlayerRoom, useTransport } from '../context/RoomProvider.js';
3
+ import { createConnectionMonitor } from '../utils/connectionMonitor.js';
3
4
 
4
5
  const EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;
5
6
  function validateEventName(event) {
@@ -14,15 +15,50 @@ function validateEventName(event) {
14
15
  function useGamePlayer(config = {}) {
15
16
  const room = usePlayerRoom();
16
17
  const transport = useTransport();
17
- const { onReady, onPlayerJoin, onPlayerLeave, listeners } = config;
18
+ const { onReady, onPlayerJoin, onPlayerLeave, listeners, onCustomStateChange, connectionMonitor } = config;
19
+ const [isPaused, setIsPaused] = useState(false);
20
+ const [latency, setLatency] = useState(0);
21
+ const monitorRef = useRef(null);
18
22
  const onReadyRef = useRef(onReady);
19
23
  const onPlayerJoinRef = useRef(onPlayerJoin);
20
24
  const onPlayerLeaveRef = useRef(onPlayerLeave);
21
25
  const listenersRef = useRef(listeners);
26
+ const onCustomStateChangeRef = useRef(onCustomStateChange);
22
27
  onReadyRef.current = onReady;
23
28
  onPlayerJoinRef.current = onPlayerJoin;
24
29
  onPlayerLeaveRef.current = onPlayerLeave;
25
30
  listenersRef.current = listeners;
31
+ onCustomStateChangeRef.current = onCustomStateChange;
32
+ useEffect(() => {
33
+ if (!connectionMonitor?.enabled || !transport) return;
34
+ const socket = transport.socket;
35
+ if (!socket) {
36
+ console.warn("[useGamePlayer] Connection monitor requires Socket.IO transport");
37
+ return;
38
+ }
39
+ const monitor = createConnectionMonitor(socket, {
40
+ onPause: () => {
41
+ setIsPaused(true);
42
+ connectionMonitor.onPause();
43
+ },
44
+ onResume: () => {
45
+ setIsPaused(false);
46
+ connectionMonitor.onResume();
47
+ },
48
+ onCountdown: connectionMonitor.onCountdown,
49
+ latencyThreshold: connectionMonitor.latencyThreshold,
50
+ resumeCountdown: connectionMonitor.resumeCountdown
51
+ });
52
+ monitorRef.current = monitor;
53
+ const latencyInterval = setInterval(() => {
54
+ setLatency(monitor.getLatency());
55
+ }, 1e3);
56
+ return () => {
57
+ monitor.destroy();
58
+ clearInterval(latencyInterval);
59
+ monitorRef.current = null;
60
+ };
61
+ }, [connectionMonitor, transport]);
26
62
  useEffect(() => {
27
63
  if (!transport) return;
28
64
  const handleReady = () => {
@@ -32,15 +68,25 @@ function useGamePlayer(config = {}) {
32
68
  onPlayerJoinRef.current?.(player);
33
69
  };
34
70
  const handlePlayerLeave = (data) => {
35
- onPlayerLeaveRef.current?.(data.sessionId);
71
+ const playerIndex = data.player?.playerIndex ?? data.playerIndex;
72
+ if (playerIndex !== void 0) {
73
+ onPlayerLeaveRef.current?.(playerIndex);
74
+ }
75
+ };
76
+ const handleCustomStateChange = (data) => {
77
+ if (data.playerIndex !== void 0) {
78
+ onCustomStateChangeRef.current?.(data.playerIndex, data.state);
79
+ }
36
80
  };
37
81
  transport.on("smore:ready", handleReady);
38
82
  transport.on("smore:player-join", handlePlayerJoin);
39
83
  transport.on("smore:player-leave", handlePlayerLeave);
84
+ transport.on("smore:custom-state-change", handleCustomStateChange);
40
85
  return () => {
41
86
  transport.off("smore:ready", handleReady);
42
87
  transport.off("smore:player-join", handlePlayerJoin);
43
88
  transport.off("smore:player-leave", handlePlayerLeave);
89
+ transport.off("smore:custom-state-change", handleCustomStateChange);
44
90
  };
45
91
  }, [transport]);
46
92
  useEffect(() => {
@@ -62,13 +108,23 @@ function useGamePlayer(config = {}) {
62
108
  },
63
109
  [transport]
64
110
  );
111
+ const setCustomState = useCallback(
112
+ (state) => {
113
+ if (!transport) return;
114
+ transport.emit("smore:custom-state-change", { state });
115
+ },
116
+ [transport]
117
+ );
65
118
  return {
66
119
  players: room.players,
67
120
  leaderId: room.leaderId,
68
121
  roomCode: room.roomCode,
69
- mySessionId: room.mySessionId,
122
+ myIndex: room.myIndex,
70
123
  isLeader: room.isLeader,
71
- emit
124
+ emit,
125
+ setCustomState,
126
+ isPaused,
127
+ latency
72
128
  };
73
129
  }
74
130
 
@@ -1 +1 @@
1
- {"version":3,"file":"useGamePlayer.js","sources":["../../../src/hooks/useGamePlayer.ts"],"sourcesContent":["import { useEffect, useCallback, useRef } from 'react';\nimport { usePlayerRoom } from '../context/RoomProvider';\nimport { useTransport } from '../context/RoomProvider';\nimport type { Player } from '@smoregg/shared';\n\n/**\n * Validates event name format.\n * Rules:\n * - Only English letters (a-z, A-Z), hyphens (-), and underscores (_) allowed\n * - Must start and end with a letter (no leading/trailing - or _)\n * - Colons are reserved for system prefix\n */\nconst EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;\n\nfunction validateEventName(event: string): void {\n if (!EVENT_NAME_REGEX.test(event)) {\n throw new Error(\n `[SDK] Invalid event name \"${event}\". Event names must:\\n` +\n ` - Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)\\n` +\n ` - Start and end with a letter (no leading/trailing - or _)`\n );\n }\n}\n\n// ===== Types =====\n\nexport interface UseGamePlayerConfig<T = Record<string, any>> {\n /** Called when smore:ready is received */\n onReady?: () => void;\n /** Called when a player joins */\n onPlayerJoin?: (player: Player) => void;\n /** Called when a player leaves */\n onPlayerLeave?: (sessionId: string) => void;\n /** Custom event listeners (event name -> handler) */\n listeners?: { [K in keyof T]?: (data: T[K]) => void };\n}\n\nexport interface UseGamePlayerReturn {\n /** All players in the room */\n players: Player[];\n /** Leader player's session ID */\n leaderId: string | null;\n /** Room code */\n roomCode: string;\n /** My session ID */\n mySessionId: string;\n /** Am I the leader? */\n isLeader: boolean;\n /** Emit event to host (event name must not contain ':') */\n emit: (event: string, data?: any) => void;\n}\n\n// ===== Hook =====\n\nexport function useGamePlayer<T = Record<string, any>>(\n config: UseGamePlayerConfig<T> = {}\n): UseGamePlayerReturn {\n const room = usePlayerRoom();\n const transport = useTransport();\n const { onReady, onPlayerJoin, onPlayerLeave, listeners } = config;\n\n // Use refs to avoid re-subscribing on every render\n const onReadyRef = useRef(onReady);\n const onPlayerJoinRef = useRef(onPlayerJoin);\n const onPlayerLeaveRef = useRef(onPlayerLeave);\n const listenersRef = useRef(listeners);\n\n onReadyRef.current = onReady;\n onPlayerJoinRef.current = onPlayerJoin;\n onPlayerLeaveRef.current = onPlayerLeave;\n listenersRef.current = listeners;\n\n // System event listeners (smore: prefix)\n useEffect(() => {\n if (!transport) return;\n\n const handleReady = () => {\n onReadyRef.current?.();\n };\n\n const handlePlayerJoin = (player: Player) => {\n onPlayerJoinRef.current?.(player);\n };\n\n const handlePlayerLeave = (data: { sessionId: string }) => {\n onPlayerLeaveRef.current?.(data.sessionId);\n };\n\n transport.on('smore:ready', handleReady);\n transport.on('smore:player-join', handlePlayerJoin);\n transport.on('smore:player-leave', handlePlayerLeave);\n\n return () => {\n transport.off('smore:ready', handleReady);\n transport.off('smore:player-join', handlePlayerJoin);\n transport.off('smore:player-leave', handlePlayerLeave);\n };\n }, [transport]);\n\n // User event listeners (passed through directly, no wrapping)\n useEffect(() => {\n if (!transport || !listenersRef.current) return;\n\n const cleanups: (() => void)[] = [];\n\n Object.entries(listenersRef.current).forEach(([event, handler]) => {\n if (handler) {\n transport.on(event, handler as (data: any) => void);\n cleanups.push(() => transport.off(event, handler as (data: any) => void));\n }\n });\n\n return () => cleanups.forEach((fn) => fn());\n }, [transport, listeners]);\n\n // Emit function with validation\n const emit = useCallback(\n (event: string, data?: any) => {\n if (!transport) return;\n validateEventName(event);\n transport.emit(event, data);\n },\n [transport]\n );\n\n return {\n players: room.players,\n leaderId: room.leaderId,\n roomCode: room.roomCode,\n mySessionId: room.mySessionId,\n isLeader: room.isLeader,\n emit,\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useGamePlayer.js","sources":["../../../src/hooks/useGamePlayer.ts"],"sourcesContent":["import { useEffect, useCallback, useRef, useState } from 'react';\nimport { usePlayerRoom } from '../context/RoomProvider';\nimport { useTransport } from '../context/RoomProvider';\nimport type { Player } from '@smoregg/shared';\nimport type { Socket } from 'socket.io-client';\nimport { createConnectionMonitor } from '../utils/connectionMonitor';\nimport type { ConnectionMonitorOptions } from '../utils/connectionMonitor';\n\n/**\n * Validates event name format.\n * Rules:\n * - Only English letters (a-z, A-Z), hyphens (-), and underscores (_) allowed\n * - Must start and end with a letter (no leading/trailing - or _)\n * - Colons are reserved for system prefix\n */\nconst EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;\n\nfunction validateEventName(event: string): void {\n if (!EVENT_NAME_REGEX.test(event)) {\n throw new Error(\n `[SDK] Invalid event name \"${event}\". Event names must:\\n` +\n ` - Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)\\n` +\n ` - Start and end with a letter (no leading/trailing - or _)`\n );\n }\n}\n\n// ===== Types =====\n\nexport interface UseGamePlayerConfig<T = Record<string, any>> {\n /** Called when smore:ready is received */\n onReady?: () => void;\n /** Called when a player joins */\n onPlayerJoin?: (player: Player) => void;\n /** Called when a player leaves */\n onPlayerLeave?: (playerIndex: number) => void;\n /** Custom event listeners (event name -> handler) */\n listeners?: { [K in keyof T]?: (data: T[K]) => void };\n /** Called when any device's custom state changes (AirConsole pattern) */\n onCustomStateChange?: (playerIndex: number, state: Record<string, any>) => void;\n /**\n * Optional connection monitoring (AirConsole pattern).\n * When enabled, automatically pauses/resumes game on connection issues.\n */\n connectionMonitor?: {\n /** Enable connection monitoring */\n enabled: boolean;\n /** Called when connection becomes unstable */\n onPause: () => void;\n /** Called after connection restores and countdown completes */\n onResume: () => void;\n /** Called during resume countdown with seconds remaining */\n onCountdown?: (secondsLeft: number) => void;\n /** Latency threshold to trigger pause (ms), default 2000 */\n latencyThreshold?: number;\n /** Resume countdown duration (ms), default 3000 */\n resumeCountdown?: number;\n };\n}\n\nexport interface UseGamePlayerReturn {\n /** All players in the room */\n players: Player[];\n /** Leader player's session ID */\n leaderId: string | null;\n /** Room code */\n roomCode: string;\n /** My player index (0, 1, 2, ...) */\n myIndex: number;\n /** Am I the leader? */\n isLeader: boolean;\n /** Emit event to host (event name must not contain ':') */\n emit: (event: string, data?: any) => void;\n /** Set custom device state (AirConsole pattern) - merges with existing state */\n setCustomState: (state: Record<string, any>) => void;\n /** Whether game is currently paused due to connection issues (only if connectionMonitor enabled) */\n isPaused: boolean;\n /** Current network latency in ms (only if connectionMonitor enabled) */\n latency: number;\n}\n\n// ===== Hook =====\n\nexport function useGamePlayer<T = Record<string, any>>(\n config: UseGamePlayerConfig<T> = {}\n): UseGamePlayerReturn {\n const room = usePlayerRoom();\n const transport = useTransport();\n const { onReady, onPlayerJoin, onPlayerLeave, listeners, onCustomStateChange, connectionMonitor } = config;\n\n // Connection monitor state\n const [isPaused, setIsPaused] = useState(false);\n const [latency, setLatency] = useState(0);\n const monitorRef = useRef<ReturnType<typeof createConnectionMonitor> | null>(null);\n\n // Use refs to avoid re-subscribing on every render\n const onReadyRef = useRef(onReady);\n const onPlayerJoinRef = useRef(onPlayerJoin);\n const onPlayerLeaveRef = useRef(onPlayerLeave);\n const listenersRef = useRef(listeners);\n const onCustomStateChangeRef = useRef(onCustomStateChange);\n\n onReadyRef.current = onReady;\n onPlayerJoinRef.current = onPlayerJoin;\n onPlayerLeaveRef.current = onPlayerLeave;\n listenersRef.current = listeners;\n onCustomStateChangeRef.current = onCustomStateChange;\n\n // Connection Monitor\n useEffect(() => {\n if (!connectionMonitor?.enabled || !transport) return;\n\n // Check if transport has socket property (Socket.IO)\n const socket = (transport as any).socket as Socket | undefined;\n if (!socket) {\n console.warn('[useGamePlayer] Connection monitor requires Socket.IO transport');\n return;\n }\n\n const monitor = createConnectionMonitor(socket, {\n onPause: () => {\n setIsPaused(true);\n connectionMonitor.onPause();\n },\n onResume: () => {\n setIsPaused(false);\n connectionMonitor.onResume();\n },\n onCountdown: connectionMonitor.onCountdown,\n latencyThreshold: connectionMonitor.latencyThreshold,\n resumeCountdown: connectionMonitor.resumeCountdown,\n });\n\n monitorRef.current = monitor;\n\n // Periodically update latency state\n const latencyInterval = setInterval(() => {\n setLatency(monitor.getLatency());\n }, 1000);\n\n return () => {\n monitor.destroy();\n clearInterval(latencyInterval);\n monitorRef.current = null;\n };\n }, [connectionMonitor, transport]);\n\n // System event listeners (smore: prefix)\n useEffect(() => {\n if (!transport) return;\n\n const handleReady = () => {\n onReadyRef.current?.();\n };\n\n const handlePlayerJoin = (player: Player) => {\n onPlayerJoinRef.current?.(player);\n };\n\n const handlePlayerLeave = (data: { player?: { playerIndex?: number }; playerIndex?: number }) => {\n const playerIndex = data.player?.playerIndex ?? data.playerIndex;\n if (playerIndex !== undefined) {\n onPlayerLeaveRef.current?.(playerIndex);\n }\n };\n\n const handleCustomStateChange = (data: { playerIndex?: number; state: Record<string, any> }) => {\n if (data.playerIndex !== undefined) {\n onCustomStateChangeRef.current?.(data.playerIndex, data.state);\n }\n };\n\n transport.on('smore:ready', handleReady);\n transport.on('smore:player-join', handlePlayerJoin);\n transport.on('smore:player-leave', handlePlayerLeave);\n transport.on('smore:custom-state-change', handleCustomStateChange);\n\n return () => {\n transport.off('smore:ready', handleReady);\n transport.off('smore:player-join', handlePlayerJoin);\n transport.off('smore:player-leave', handlePlayerLeave);\n transport.off('smore:custom-state-change', handleCustomStateChange);\n };\n }, [transport]);\n\n // User event listeners (passed through directly, no wrapping)\n useEffect(() => {\n if (!transport || !listenersRef.current) return;\n\n const cleanups: (() => void)[] = [];\n\n Object.entries(listenersRef.current).forEach(([event, handler]) => {\n if (handler) {\n transport.on(event, handler as (data: any) => void);\n cleanups.push(() => transport.off(event, handler as (data: any) => void));\n }\n });\n\n return () => cleanups.forEach((fn) => fn());\n }, [transport, listeners]);\n\n // Emit function with validation\n const emit = useCallback(\n (event: string, data?: any) => {\n if (!transport) return;\n validateEventName(event);\n transport.emit(event, data);\n },\n [transport]\n );\n\n // Set custom device state (AirConsole pattern)\n const setCustomState = useCallback(\n (state: Record<string, any>) => {\n if (!transport) return;\n transport.emit('smore:custom-state-change', { state });\n },\n [transport]\n );\n\n return {\n players: room.players,\n leaderId: room.leaderId,\n roomCode: room.roomCode,\n myIndex: room.myIndex,\n isLeader: room.isLeader,\n emit,\n setCustomState,\n isPaused,\n latency,\n };\n}\n"],"names":[],"mappings":"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"}