@smoregg/sdk 0.3.1 → 0.4.1

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 (51) hide show
  1. package/dist/cjs/events.cjs +31 -0
  2. package/dist/cjs/events.cjs.map +1 -0
  3. package/dist/cjs/hooks/useGameHost.cjs +88 -65
  4. package/dist/cjs/hooks/useGameHost.cjs.map +1 -1
  5. package/dist/cjs/hooks/useGamePlayer.cjs +55 -44
  6. package/dist/cjs/hooks/useGamePlayer.cjs.map +1 -1
  7. package/dist/cjs/iframe/index.cjs +163 -109
  8. package/dist/cjs/iframe/index.cjs.map +1 -1
  9. package/dist/cjs/index.cjs +4 -0
  10. package/dist/cjs/index.cjs.map +1 -1
  11. package/dist/cjs/transport/protocol.cjs.map +1 -1
  12. package/dist/esm/events.js +27 -0
  13. package/dist/esm/events.js.map +1 -0
  14. package/dist/esm/hooks/useGameHost.js +88 -65
  15. package/dist/esm/hooks/useGameHost.js.map +1 -1
  16. package/dist/esm/hooks/useGamePlayer.js +56 -45
  17. package/dist/esm/hooks/useGamePlayer.js.map +1 -1
  18. package/dist/esm/iframe/index.js +163 -109
  19. package/dist/esm/iframe/index.js.map +1 -1
  20. package/dist/esm/index.js +1 -0
  21. package/dist/esm/index.js.map +1 -1
  22. package/dist/esm/transport/protocol.js.map +1 -1
  23. package/dist/types/components/IframeGameBridge.d.ts +1 -0
  24. package/dist/types/components/IframeGameBridge.d.ts.map +1 -1
  25. package/dist/types/events.d.ts +26 -0
  26. package/dist/types/events.d.ts.map +1 -0
  27. package/dist/types/hooks/index.d.ts +3 -0
  28. package/dist/types/hooks/index.d.ts.map +1 -1
  29. package/dist/types/hooks/useGameHost.d.ts +34 -26
  30. package/dist/types/hooks/useGameHost.d.ts.map +1 -1
  31. package/dist/types/hooks/useGamePlayer.d.ts +25 -9
  32. package/dist/types/hooks/useGamePlayer.d.ts.map +1 -1
  33. package/dist/types/iframe/vanilla.d.ts +1 -0
  34. package/dist/types/iframe/vanilla.d.ts.map +1 -1
  35. package/dist/types/index.d.ts +4 -6
  36. package/dist/types/index.d.ts.map +1 -1
  37. package/dist/types/transport/protocol.d.ts +4 -1
  38. package/dist/types/transport/protocol.d.ts.map +1 -1
  39. package/dist/umd/smore-sdk-iframe.umd.js +163 -109
  40. package/dist/umd/smore-sdk-iframe.umd.js.map +1 -1
  41. package/dist/umd/smore-sdk-iframe.umd.min.js +1 -1
  42. package/dist/umd/smore-sdk-iframe.umd.min.js.map +1 -1
  43. package/dist/umd/smore-sdk-vanilla.umd.js +20 -0
  44. package/dist/umd/smore-sdk-vanilla.umd.js.map +1 -1
  45. package/dist/umd/smore-sdk-vanilla.umd.min.js +1 -1
  46. package/dist/umd/smore-sdk-vanilla.umd.min.js.map +1 -1
  47. package/dist/umd/smore-sdk.umd.js +171 -109
  48. package/dist/umd/smore-sdk.umd.js.map +1 -1
  49. package/dist/umd/smore-sdk.umd.min.js +1 -1
  50. package/dist/umd/smore-sdk.umd.min.js.map +1 -1
  51. package/package.json +1 -1
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const SMORE_EVENTS = {
4
+ // 게임 lifecycle
5
+ READY: "smore:ready",
6
+ GAME_OVER: "smore:game-over",
7
+ RETURN_TO_LOBBY: "smore:return-to-lobby",
8
+ // 플레이어 관리
9
+ PLAYER_JOIN: "smore:player-join",
10
+ PLAYER_LEAVE: "smore:player-leave",
11
+ // 특정 플레이어에게 전송 (내부용)
12
+ SEND_TO_PLAYER: "smore:send-to-player",
13
+ // 초기화
14
+ INIT: "smore:init",
15
+ UPDATE: "smore:update"
16
+ };
17
+ function validateUserEvent(event) {
18
+ if (event.includes(":")) {
19
+ throw new Error(
20
+ `Invalid event name "${event}": User events cannot contain ':'. Use '_' or '-' instead. System events use 'smore:' prefix.`
21
+ );
22
+ }
23
+ }
24
+ function isSystemEvent(event) {
25
+ return event.startsWith("smore:");
26
+ }
27
+
28
+ exports.SMORE_EVENTS = SMORE_EVENTS;
29
+ exports.isSystemEvent = isSystemEvent;
30
+ exports.validateUserEvent = validateUserEvent;
31
+ //# sourceMappingURL=events.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.cjs","sources":["../../src/events.ts"],"sourcesContent":["/**\n * SDK 시스템 이벤트 상수\n * 모든 시스템 이벤트는 'smore:' prefix 사용\n * 유저 이벤트는 ':' 사용 불가\n */\n\nexport const SMORE_EVENTS = {\n // 게임 lifecycle\n READY: 'smore:ready',\n GAME_OVER: 'smore:game-over',\n RETURN_TO_LOBBY: 'smore:return-to-lobby',\n\n // 플레이어 관리\n PLAYER_JOIN: 'smore:player-join',\n PLAYER_LEAVE: 'smore:player-leave',\n\n // 특정 플레이어에게 전송 (내부용)\n SEND_TO_PLAYER: 'smore:send-to-player',\n\n // 초기화\n INIT: 'smore:init',\n UPDATE: 'smore:update',\n} as const;\n\nexport type SmoreEvent = typeof SMORE_EVENTS[keyof typeof SMORE_EVENTS];\n\n/**\n * 유저 이벤트명 검증\n * ':' 포함 시 에러, '_'와 '-'는 허용\n */\nexport function validateUserEvent(event: string): void {\n if (event.includes(':')) {\n throw new Error(\n `Invalid event name \"${event}\": User events cannot contain ':'. ` +\n `Use '_' or '-' instead. System events use 'smore:' prefix.`\n );\n }\n}\n\n/**\n * 시스템 이벤트인지 확인\n */\nexport function isSystemEvent(event: string): boolean {\n return event.startsWith('smore:');\n}\n"],"names":[],"mappings":";;AAMO,MAAM,YAAA,GAAe;AAAA;AAAA,EAE1B,KAAA,EAAO,aAAA;AAAA,EACP,SAAA,EAAW,iBAAA;AAAA,EACX,eAAA,EAAiB,uBAAA;AAAA;AAAA,EAGjB,WAAA,EAAa,mBAAA;AAAA,EACb,YAAA,EAAc,oBAAA;AAAA;AAAA,EAGd,cAAA,EAAgB,sBAAA;AAAA;AAAA,EAGhB,IAAA,EAAM,YAAA;AAAA,EACN,MAAA,EAAQ;AACV;AAQO,SAAS,kBAAkB,KAAA,EAAqB;AACrD,EAAA,IAAI,KAAA,CAAM,QAAA,CAAS,GAAG,CAAA,EAAG;AACvB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,uBAAuB,KAAK,CAAA,6FAAA;AAAA,KAE9B;AAAA,EACF;AACF;AAKO,SAAS,cAAc,KAAA,EAAwB;AACpD,EAAA,OAAO,KAAA,CAAM,WAAW,QAAQ,CAAA;AAClC;;;;;;"}
@@ -3,106 +3,129 @@
3
3
  var react = require('react');
4
4
  var RoomProvider = require('../context/RoomProvider.cjs');
5
5
 
6
- function useGameHost(config) {
7
- const { gameId, onStateRequest, onPlayerLeave, onPlayerDisconnect, onPlayerReconnect, listeners } = config;
6
+ const SYSTEM_PREFIX = "smore:";
7
+ const SYSTEM_EVENTS = {
8
+ READY: `${SYSTEM_PREFIX}ready`,
9
+ PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,
10
+ PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`,
11
+ GAME_OVER: `${SYSTEM_PREFIX}game-over`,
12
+ RETURN_TO_LOBBY: `${SYSTEM_PREFIX}return-to-lobby`,
13
+ SEND_TO_PLAYER: `${SYSTEM_PREFIX}send-to-player`,
14
+ BROADCAST: `${SYSTEM_PREFIX}broadcast`
15
+ };
16
+ const EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;
17
+ function validateEventName(event) {
18
+ if (!EVENT_NAME_REGEX.test(event)) {
19
+ throw new Error(
20
+ `[SDK] Invalid event name "${event}". Event names must:
21
+ - Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)
22
+ - Start and end with a letter (no leading/trailing - or _)`
23
+ );
24
+ }
25
+ }
26
+ function useGameHost(config = {}) {
27
+ const { onReady, onPlayerJoin, onPlayerLeave, listeners } = config;
8
28
  const hostRoom = RoomProvider.useHostRoom();
9
29
  const transport = RoomProvider.useTransport();
10
- const onStateRequestRef = react.useRef(onStateRequest);
30
+ const onReadyRef = react.useRef(onReady);
31
+ const onPlayerJoinRef = react.useRef(onPlayerJoin);
11
32
  const onPlayerLeaveRef = react.useRef(onPlayerLeave);
12
- const onPlayerDisconnectRef = react.useRef(onPlayerDisconnect);
13
- const onPlayerReconnectRef = react.useRef(onPlayerReconnect);
33
+ const listenersRef = react.useRef(listeners);
34
+ react.useEffect(() => {
35
+ onReadyRef.current = onReady;
36
+ }, [onReady]);
14
37
  react.useEffect(() => {
15
- onStateRequestRef.current = onStateRequest;
16
- }, [onStateRequest]);
38
+ onPlayerJoinRef.current = onPlayerJoin;
39
+ }, [onPlayerJoin]);
17
40
  react.useEffect(() => {
18
41
  onPlayerLeaveRef.current = onPlayerLeave;
19
42
  }, [onPlayerLeave]);
20
43
  react.useEffect(() => {
21
- onPlayerDisconnectRef.current = onPlayerDisconnect;
22
- }, [onPlayerDisconnect]);
23
- react.useEffect(() => {
24
- onPlayerReconnectRef.current = onPlayerReconnect;
25
- }, [onPlayerReconnect]);
44
+ listenersRef.current = listeners;
45
+ }, [listeners]);
26
46
  react.useEffect(() => {
27
47
  if (!transport) return;
28
- const handler = (data) => {
29
- const stateFn = onStateRequestRef.current;
30
- if (!stateFn) return;
31
- const gameState = stateFn(data.requesterId);
32
- transport.emit("game:state-response", {
33
- targetSessionId: data.requesterId,
34
- gameState: {
35
- gameId,
36
- ...gameState
37
- }
38
- });
39
- };
40
- transport.on("game:state-request", handler);
41
- return () => {
42
- transport.off("game:state-request", handler);
43
- };
44
- }, [transport, gameId]);
45
- react.useEffect(() => {
46
- if (!transport) return;
47
- const onLeft = (data) => {
48
- onPlayerLeaveRef.current?.(data?.sessionId ?? data?.playerId);
48
+ const handleReady = () => {
49
+ onReadyRef.current?.();
49
50
  };
50
- const onDisconnected = (data) => {
51
- onPlayerDisconnectRef.current?.(data?.sessionId ?? data?.playerId);
51
+ const handlePlayerJoin = (data) => {
52
+ onPlayerJoinRef.current?.(data.player);
52
53
  };
53
- const onReconnected = (data) => {
54
- onPlayerReconnectRef.current?.(data?.sessionId ?? data?.playerId);
54
+ const handlePlayerLeave = (data) => {
55
+ onPlayerLeaveRef.current?.(data.sessionId);
55
56
  };
56
- transport.on("room:player-left", onLeft);
57
- transport.on("room:player-disconnected", onDisconnected);
58
- transport.on("room:player-reconnected", onReconnected);
57
+ transport.on(SYSTEM_EVENTS.READY, handleReady);
58
+ transport.on(SYSTEM_EVENTS.PLAYER_JOIN, handlePlayerJoin);
59
+ transport.on(SYSTEM_EVENTS.PLAYER_LEAVE, handlePlayerLeave);
60
+ transport.on("room:player-left", (data) => {
61
+ onPlayerLeaveRef.current?.(data?.sessionId ?? data?.playerId);
62
+ });
63
+ transport.on("room:player-joined", (data) => {
64
+ if (data?.player) {
65
+ onPlayerJoinRef.current?.(data.player);
66
+ }
67
+ });
59
68
  return () => {
60
- transport.off("room:player-left", onLeft);
61
- transport.off("room:player-disconnected", onDisconnected);
62
- transport.off("room:player-reconnected", onReconnected);
69
+ transport.off(SYSTEM_EVENTS.READY, handleReady);
70
+ transport.off(SYSTEM_EVENTS.PLAYER_JOIN, handlePlayerJoin);
71
+ transport.off(SYSTEM_EVENTS.PLAYER_LEAVE, handlePlayerLeave);
72
+ transport.off("room:player-left");
73
+ transport.off("room:player-joined");
63
74
  };
64
75
  }, [transport]);
65
- const listenersRef = react.useRef(listeners);
66
- listenersRef.current = listeners;
67
76
  react.useEffect(() => {
68
- if (!transport || !listenersRef.current) return;
69
- const entries = Object.entries(listenersRef.current);
70
- const handlers = entries.map(([event, handler]) => {
71
- transport.on(event, handler);
72
- return () => {
73
- transport.off(event, handler);
77
+ if (!transport || !listeners) return;
78
+ const entries = Object.entries(listeners);
79
+ const cleanups = [];
80
+ for (const [event, handler] of entries) {
81
+ if (!handler) continue;
82
+ validateEventName(event);
83
+ const wrappedHandler = (data) => {
84
+ const { sessionId, ...rest } = data;
85
+ handler(sessionId, rest);
74
86
  };
75
- });
76
- return () => handlers.forEach((fn) => fn());
87
+ transport.on(event, wrappedHandler);
88
+ cleanups.push(() => transport.off(event, wrappedHandler));
89
+ }
90
+ return () => {
91
+ cleanups.forEach((cleanup) => cleanup());
92
+ };
77
93
  }, [transport, listeners]);
78
- const broadcast = react.useCallback(
94
+ const emit = react.useCallback(
79
95
  (event, data) => {
80
- transport?.emit(event, data);
96
+ validateEventName(event);
97
+ transport?.emit(SYSTEM_EVENTS.BROADCAST, { event, data });
81
98
  },
82
99
  [transport]
83
100
  );
84
101
  const sendToPlayer = react.useCallback(
85
102
  (sessionId, event, data) => {
86
- transport?.emit("game:state-to-player", {
103
+ validateEventName(event);
104
+ transport?.emit(SYSTEM_EVENTS.SEND_TO_PLAYER, {
87
105
  targetSessionId: sessionId,
88
- gameId,
89
106
  event,
90
- state: data
107
+ data
91
108
  });
92
109
  },
93
- [transport, gameId]
110
+ [transport]
94
111
  );
95
- const emitGameOver = react.useCallback(
112
+ const gameOver = react.useCallback(
96
113
  (results) => {
97
- transport?.emit("room:game-over", { gameId, results });
114
+ transport?.emit(SYSTEM_EVENTS.GAME_OVER, { results });
98
115
  },
99
- [transport, gameId]
116
+ [transport]
100
117
  );
118
+ const returnToLobby = react.useCallback(() => {
119
+ transport?.emit(SYSTEM_EVENTS.RETURN_TO_LOBBY, {});
120
+ }, [transport]);
101
121
  return {
102
- room: hostRoom,
103
- broadcast,
122
+ players: hostRoom.players,
123
+ leaderId: hostRoom.leaderId,
124
+ roomCode: hostRoom.roomCode,
125
+ emit,
104
126
  sendToPlayer,
105
- emitGameOver
127
+ gameOver,
128
+ returnToLobby
106
129
  };
107
130
  }
108
131
 
@@ -1 +1 @@
1
- {"version":3,"file":"useGameHost.cjs","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 * - Automatic room state access (via useHostRoom context)\n * - Event listeners (via listeners config)\n * - Broadcasting to all/specific players\n * - Game over emission\n * - Player lifecycle callbacks\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 } from '../context/RoomProvider';\nimport { useTransport } from '../context/RoomProvider';\nimport type { HostRoomState } from '../context/RoomProvider';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype InputHandler = (playerId: string, data?: any) => void;\n\nexport interface UseGameHostConfig {\n /** Unique game identifier (e.g. 'fibbage', 'wasd') */\n gameId: string;\n\n /**\n * Called when a player requests game state (e.g. after returning from background).\n * Return the current game state object to send back to that player.\n */\n onStateRequest?: (playerId: string) => Record<string, any>;\n\n /** Called when a player leaves the room. */\n onPlayerLeave?: (playerId: string) => void;\n\n /** Called when a player disconnects (may reconnect). */\n onPlayerDisconnect?: (playerId: string) => void;\n\n /** Called when a previously disconnected player reconnects. */\n onPlayerReconnect?: (playerId: string) => void;\n\n /**\n * Generic socket event listeners (for server broadcasts the host needs to receive).\n * Keys are full event names, values are handler functions.\n */\n listeners?: Record<string, (data: any) => void>;\n}\n\nexport interface UseGameHostReturn {\n /** Current room state from HostRoomProvider context. */\n room: HostRoomState;\n\n /** Broadcast an event to all players via the host socket. */\n broadcast: (event: string, data: any) => void;\n\n /** Send state/event to a specific player by sessionId. */\n sendToPlayer: (sessionId: string, event: string, data: any) => void;\n\n /** Emit game-over with optional results payload. */\n emitGameOver: (results?: any) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useGameHost(config: UseGameHostConfig): UseGameHostReturn {\n const { gameId, onStateRequest, onPlayerLeave, onPlayerDisconnect, onPlayerReconnect, listeners } = config;\n\n const hostRoom = useHostRoom();\n const transport = useTransport();\n\n // Stable refs to avoid stale closures in listeners\n const onStateRequestRef = useRef(onStateRequest);\n const onPlayerLeaveRef = useRef(onPlayerLeave);\n const onPlayerDisconnectRef = useRef(onPlayerDisconnect);\n const onPlayerReconnectRef = useRef(onPlayerReconnect);\n\n useEffect(() => { onStateRequestRef.current = onStateRequest; }, [onStateRequest]);\n useEffect(() => { onPlayerLeaveRef.current = onPlayerLeave; }, [onPlayerLeave]);\n useEffect(() => { onPlayerDisconnectRef.current = onPlayerDisconnect; }, [onPlayerDisconnect]);\n useEffect(() => { onPlayerReconnectRef.current = onPlayerReconnect; }, [onPlayerReconnect]);\n\n // -------------------------------------------------------------------------\n // State request handler\n // -------------------------------------------------------------------------\n useEffect(() => {\n if (!transport) return;\n\n const handler = (data: { requesterId: string }) => {\n const stateFn = onStateRequestRef.current;\n if (!stateFn) return;\n\n const gameState = stateFn(data.requesterId);\n transport.emit('game:state-response', {\n targetSessionId: data.requesterId,\n gameState: {\n gameId,\n ...gameState,\n },\n });\n };\n\n transport.on('game:state-request', handler);\n return () => {\n transport.off('game:state-request', handler);\n };\n }, [transport, gameId]);\n\n // -------------------------------------------------------------------------\n // Player lifecycle listeners\n // -------------------------------------------------------------------------\n useEffect(() => {\n if (!transport) return;\n\n const onLeft = (data: any) => {\n onPlayerLeaveRef.current?.(data?.sessionId ?? data?.playerId);\n };\n const onDisconnected = (data: any) => {\n onPlayerDisconnectRef.current?.(data?.sessionId ?? data?.playerId);\n };\n const onReconnected = (data: any) => {\n onPlayerReconnectRef.current?.(data?.sessionId ?? data?.playerId);\n };\n\n transport.on('room:player-left', onLeft);\n transport.on('room:player-disconnected', onDisconnected);\n transport.on('room:player-reconnected', onReconnected);\n\n return () => {\n transport.off('room:player-left', onLeft);\n transport.off('room:player-disconnected', onDisconnected);\n transport.off('room:player-reconnected', onReconnected);\n };\n }, [transport]);\n\n // -------------------------------------------------------------------------\n // Generic listeners\n // -------------------------------------------------------------------------\n const listenersRef = useRef(listeners);\n listenersRef.current = listeners;\n\n useEffect(() => {\n if (!transport || !listenersRef.current) return;\n const entries = Object.entries(listenersRef.current);\n const handlers = entries.map(([event, handler]) => {\n transport.on(event, handler);\n return () => { transport.off(event, handler); };\n });\n return () => handlers.forEach(fn => fn());\n }, [transport, listeners]);\n\n // -------------------------------------------------------------------------\n // Actions\n // -------------------------------------------------------------------------\n\n const broadcast = useCallback(\n (event: string, data: any) => {\n transport?.emit(event, data);\n },\n [transport],\n );\n\n const sendToPlayer = useCallback(\n (sessionId: string, event: string, data: any) => {\n transport?.emit('game:state-to-player', {\n targetSessionId: sessionId,\n gameId,\n event,\n state: data,\n });\n },\n [transport, gameId],\n );\n\n const emitGameOver = useCallback(\n (results?: any) => {\n transport?.emit('room:game-over', { gameId, results });\n },\n [transport, gameId],\n );\n\n return {\n room: hostRoom,\n broadcast,\n sendToPlayer,\n emitGameOver,\n };\n}\n"],"names":["useHostRoom","useTransport","useRef","useEffect","useCallback"],"mappings":"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"}
1
+ {"version":3,"file":"useGameHost.cjs","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":["useHostRoom","useTransport","useRef","useEffect","useCallback"],"mappings":"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"}
@@ -3,64 +3,75 @@
3
3
  var react = require('react');
4
4
  var RoomProvider = require('../context/RoomProvider.cjs');
5
5
 
6
- function useGamePlayer(config) {
6
+ const EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;
7
+ function validateEventName(event) {
8
+ if (!EVENT_NAME_REGEX.test(event)) {
9
+ throw new Error(
10
+ `[SDK] Invalid event name "${event}". Event names must:
11
+ - Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)
12
+ - Start and end with a letter (no leading/trailing - or _)`
13
+ );
14
+ }
15
+ }
16
+ function useGamePlayer(config = {}) {
7
17
  const room = RoomProvider.usePlayerRoom();
8
18
  const transport = RoomProvider.useTransport();
9
- const { isConnected } = room;
10
- const { gameId, listeners } = config;
11
- const [gameState, setGameState] = react.useState(null);
19
+ const { onReady, onPlayerJoin, onPlayerLeave, listeners } = config;
20
+ const onReadyRef = react.useRef(onReady);
21
+ const onPlayerJoinRef = react.useRef(onPlayerJoin);
22
+ const onPlayerLeaveRef = react.useRef(onPlayerLeave);
12
23
  const listenersRef = react.useRef(listeners);
24
+ onReadyRef.current = onReady;
25
+ onPlayerJoinRef.current = onPlayerJoin;
26
+ onPlayerLeaveRef.current = onPlayerLeave;
13
27
  listenersRef.current = listeners;
14
- react.useEffect(() => {
15
- if (!transport || !listenersRef.current) return;
16
- const cleanups = [];
17
- Object.entries(listenersRef.current).forEach(([event, handler]) => {
18
- transport.on(event, handler);
19
- cleanups.push(() => transport.off(event, handler));
20
- });
21
- return () => cleanups.forEach((fn) => fn());
22
- }, [transport, listeners]);
23
28
  react.useEffect(() => {
24
29
  if (!transport) return;
25
- const handler = (state) => {
26
- if (state.gameId !== gameId) return;
27
- setGameState(state);
30
+ const handleReady = () => {
31
+ onReadyRef.current?.();
28
32
  };
29
- transport.on("game:state-response", handler);
30
- return () => {
31
- transport.off("game:state-response", handler);
33
+ const handlePlayerJoin = (player) => {
34
+ onPlayerJoinRef.current?.(player);
32
35
  };
33
- }, [transport, gameId]);
34
- react.useEffect(() => {
35
- if (!transport) return;
36
- const handler = (data) => {
37
- if (data.gameId !== gameId) return;
38
- setGameState(data.state);
36
+ const handlePlayerLeave = (data) => {
37
+ onPlayerLeaveRef.current?.(data.sessionId);
39
38
  };
40
- transport.on("game:state-to-player", handler);
39
+ transport.on("smore:ready", handleReady);
40
+ transport.on("smore:player-join", handlePlayerJoin);
41
+ transport.on("smore:player-leave", handlePlayerLeave);
41
42
  return () => {
42
- transport.off("game:state-to-player", handler);
43
+ transport.off("smore:ready", handleReady);
44
+ transport.off("smore:player-join", handlePlayerJoin);
45
+ transport.off("smore:player-leave", handlePlayerLeave);
43
46
  };
44
- }, [transport, gameId]);
47
+ }, [transport]);
45
48
  react.useEffect(() => {
46
- if (!transport) return;
47
- const handler = () => {
48
- if (!document.hidden) {
49
- transport.emit("game:request-state", { gameId });
49
+ if (!transport || !listenersRef.current) return;
50
+ const cleanups = [];
51
+ Object.entries(listenersRef.current).forEach(([event, handler]) => {
52
+ if (handler) {
53
+ transport.on(event, handler);
54
+ cleanups.push(() => transport.off(event, handler));
50
55
  }
51
- };
52
- document.addEventListener("visibilitychange", handler);
53
- return () => document.removeEventListener("visibilitychange", handler);
54
- }, [transport, gameId]);
55
- const emit = react.useCallback((event, data, callback) => {
56
- if (!transport) return;
57
- if (callback) {
58
- transport.emit(event, data, callback);
59
- } else {
56
+ });
57
+ return () => cleanups.forEach((fn) => fn());
58
+ }, [transport, listeners]);
59
+ const emit = react.useCallback(
60
+ (event, data) => {
61
+ if (!transport) return;
62
+ validateEventName(event);
60
63
  transport.emit(event, data);
61
- }
62
- }, [transport]);
63
- return { room, emit, isConnected, gameState };
64
+ },
65
+ [transport]
66
+ );
67
+ return {
68
+ players: room.players,
69
+ leaderId: room.leaderId,
70
+ roomCode: room.roomCode,
71
+ mySessionId: room.mySessionId,
72
+ isLeader: room.isLeader,
73
+ emit
74
+ };
64
75
  }
65
76
 
66
77
  exports.useGamePlayer = useGamePlayer;
@@ -1 +1 @@
1
- {"version":3,"file":"useGamePlayer.cjs","sources":["../../../src/hooks/useGamePlayer.ts"],"sourcesContent":["import { useEffect, useCallback, useState, useRef } from 'react';\nimport { usePlayerRoom } from '../context/RoomProvider';\nimport { useTransport } from '../context/RoomProvider';\nimport type { PlayerRoomState } from '../context/RoomProvider';\n\nexport interface UseGamePlayerConfig {\n gameId: string;\n listeners?: Record<string, (data: any) => void>;\n}\n\nexport interface UseGamePlayerReturn {\n room: PlayerRoomState;\n emit: (event: string, data?: any, callback?: (response: any) => void) => void;\n isConnected: boolean;\n gameState: Record<string, any> | null;\n}\n\nexport function useGamePlayer(config: UseGamePlayerConfig): UseGamePlayerReturn {\n const room = usePlayerRoom();\n const transport = useTransport();\n const { isConnected } = room;\n const { gameId, listeners } = config;\n const [gameState, setGameState] = useState<Record<string, any> | null>(null);\n const listenersRef = useRef(listeners);\n listenersRef.current = listeners;\n\n // Transport listeners\n useEffect(() => {\n if (!transport || !listenersRef.current) return;\n const cleanups: (() => void)[] = [];\n Object.entries(listenersRef.current).forEach(([event, handler]) => {\n transport.on(event, handler);\n cleanups.push(() => transport.off(event, handler));\n });\n return () => cleanups.forEach(fn => fn());\n }, [transport, listeners]);\n\n // Reconnection: game:state-response\n useEffect(() => {\n if (!transport) return;\n const handler = (state: any) => {\n if (state.gameId !== gameId) return;\n setGameState(state);\n };\n transport.on('game:state-response', handler);\n return () => { transport.off('game:state-response', handler); };\n }, [transport, gameId]);\n\n // game:state-to-player (Host push)\n useEffect(() => {\n if (!transport) return;\n const handler = (data: { gameId: string; state: any }) => {\n if (data.gameId !== gameId) return;\n setGameState(data.state);\n };\n transport.on('game:state-to-player', handler);\n return () => { transport.off('game:state-to-player', handler); };\n }, [transport, gameId]);\n\n // Visibility change\n useEffect(() => {\n if (!transport) return;\n const handler = () => {\n if (!document.hidden) {\n transport.emit('game:request-state', { gameId });\n }\n };\n document.addEventListener('visibilitychange', handler);\n return () => document.removeEventListener('visibilitychange', handler);\n }, [transport, gameId]);\n\n // API\n const emit = useCallback((event: string, data?: any, callback?: (response: any) => void) => {\n if (!transport) return;\n if (callback) {\n transport.emit(event, data, callback);\n } else {\n transport.emit(event, data);\n }\n }, [transport]);\n\n return { room, emit, isConnected, gameState };\n}\n"],"names":["usePlayerRoom","useTransport","useState","useRef","useEffect","useCallback"],"mappings":"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"}
1
+ {"version":3,"file":"useGamePlayer.cjs","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":["usePlayerRoom","useTransport","useRef","useEffect","useCallback"],"mappings":";;;;;AAYA,MAAM,gBAAA,GAAmB,kCAAA;AAEzB,SAAS,kBAAkB,KAAA,EAAqB;AAC9C,EAAA,IAAI,CAAC,gBAAA,CAAiB,IAAA,CAAK,KAAK,CAAA,EAAG;AACjC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,6BAA6B,KAAK,CAAA;AAAA;AAAA,4DAAA;AAAA,KAGpC;AAAA,EACF;AACF;AAgCO,SAAS,aAAA,CACd,MAAA,GAAiC,EAAC,EACb;AACrB,EAAA,MAAM,OAAOA,0BAAA,EAAc;AAC3B,EAAA,MAAM,YAAYC,yBAAA,EAAa;AAC/B,EAAA,MAAM,EAAE,OAAA,EAAS,YAAA,EAAc,aAAA,EAAe,WAAU,GAAI,MAAA;AAG5D,EAAA,MAAM,UAAA,GAAaC,aAAO,OAAO,CAAA;AACjC,EAAA,MAAM,eAAA,GAAkBA,aAAO,YAAY,CAAA;AAC3C,EAAA,MAAM,gBAAA,GAAmBA,aAAO,aAAa,CAAA;AAC7C,EAAA,MAAM,YAAA,GAAeA,aAAO,SAAS,CAAA;AAErC,EAAA,UAAA,CAAW,OAAA,GAAU,OAAA;AACrB,EAAA,eAAA,CAAgB,OAAA,GAAU,YAAA;AAC1B,EAAA,gBAAA,CAAiB,OAAA,GAAU,aAAA;AAC3B,EAAA,YAAA,CAAa,OAAA,GAAU,SAAA;AAGvB,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,SAAA,EAAW;AAEhB,IAAA,MAAM,cAAc,MAAM;AACxB,MAAA,UAAA,CAAW,OAAA,IAAU;AAAA,IACvB,CAAA;AAEA,IAAA,MAAM,gBAAA,GAAmB,CAAC,MAAA,KAAmB;AAC3C,MAAA,eAAA,CAAgB,UAAU,MAAM,CAAA;AAAA,IAClC,CAAA;AAEA,IAAA,MAAM,iBAAA,GAAoB,CAAC,IAAA,KAAgC;AACzD,MAAA,gBAAA,CAAiB,OAAA,GAAU,KAAK,SAAS,CAAA;AAAA,IAC3C,CAAA;AAEA,IAAA,SAAA,CAAU,EAAA,CAAG,eAAe,WAAW,CAAA;AACvC,IAAA,SAAA,CAAU,EAAA,CAAG,qBAAqB,gBAAgB,CAAA;AAClD,IAAA,SAAA,CAAU,EAAA,CAAG,sBAAsB,iBAAiB,CAAA;AAEpD,IAAA,OAAO,MAAM;AACX,MAAA,SAAA,CAAU,GAAA,CAAI,eAAe,WAAW,CAAA;AACxC,MAAA,SAAA,CAAU,GAAA,CAAI,qBAAqB,gBAAgB,CAAA;AACnD,MAAA,SAAA,CAAU,GAAA,CAAI,sBAAsB,iBAAiB,CAAA;AAAA,IACvD,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,SAAS,CAAC,CAAA;AAGd,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,SAAA,IAAa,CAAC,YAAA,CAAa,OAAA,EAAS;AAEzC,IAAA,MAAM,WAA2B,EAAC;AAElC,IAAA,MAAA,CAAO,OAAA,CAAQ,aAAa,OAAO,CAAA,CAAE,QAAQ,CAAC,CAAC,KAAA,EAAO,OAAO,CAAA,KAAM;AACjE,MAAA,IAAI,OAAA,EAAS;AACX,QAAA,SAAA,CAAU,EAAA,CAAG,OAAO,OAA8B,CAAA;AAClD,QAAA,QAAA,CAAS,KAAK,MAAM,SAAA,CAAU,GAAA,CAAI,KAAA,EAAO,OAA8B,CAAC,CAAA;AAAA,MAC1E;AAAA,IACF,CAAC,CAAA;AAED,IAAA,OAAO,MAAM,QAAA,CAAS,OAAA,CAAQ,CAAC,EAAA,KAAO,IAAI,CAAA;AAAA,EAC5C,CAAA,EAAG,CAAC,SAAA,EAAW,SAAS,CAAC,CAAA;AAGzB,EAAA,MAAM,IAAA,GAAOC,iBAAA;AAAA,IACX,CAAC,OAAe,IAAA,KAAe;AAC7B,MAAA,IAAI,CAAC,SAAA,EAAW;AAChB,MAAA,iBAAA,CAAkB,KAAK,CAAA;AACvB,MAAA,SAAA,CAAU,IAAA,CAAK,OAAO,IAAI,CAAA;AAAA,IAC5B,CAAA;AAAA,IACA,CAAC,SAAS;AAAA,GACZ;AAEA,EAAA,OAAO;AAAA,IACL,SAAS,IAAA,CAAK,OAAA;AAAA,IACd,UAAU,IAAA,CAAK,QAAA;AAAA,IACf,UAAU,IAAA,CAAK,QAAA;AAAA,IACf,aAAa,IAAA,CAAK,WAAA;AAAA,IAClB,UAAU,IAAA,CAAK,QAAA;AAAA,IACf;AAAA,GACF;AACF;;;;"}