@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
@@ -0,0 +1,227 @@
1
+ import { DirectTransport } from './transport/DirectTransport.js';
2
+ import { PostMessageTransport } from './transport/PostMessageTransport.js';
3
+ import { isSmoreMessage } from './transport/protocol.js';
4
+
5
+ const SYSTEM_PREFIX = "smore:";
6
+ const SYSTEM_EVENTS = {
7
+ PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,
8
+ PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`
9
+ };
10
+ const EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;
11
+ function validateEventName(event) {
12
+ if (!EVENT_NAME_REGEX.test(event)) {
13
+ throw new Error(
14
+ `[SmorePlayer] Invalid event name "${event}". Event names must:
15
+ - Only contain letters (a-z, A-Z), hyphens (-), and underscores (_)
16
+ - Start and end with a letter (no leading/trailing - or _)`
17
+ );
18
+ }
19
+ }
20
+ class SmorePlayer {
21
+ transport = null;
22
+ config;
23
+ _roomCode = "";
24
+ _myIndex = -1;
25
+ _isLeader = false;
26
+ _isReady = false;
27
+ _isDestroyed = false;
28
+ boundMessageHandler = null;
29
+ registeredHandlers = [];
30
+ constructor(config = {}) {
31
+ this.config = config;
32
+ if (config.listeners) {
33
+ for (const event of Object.keys(config.listeners)) {
34
+ validateEventName(event);
35
+ }
36
+ }
37
+ if (config.socket) {
38
+ this.initBundled(config);
39
+ } else {
40
+ this.initIframe(config);
41
+ }
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // Initialization
45
+ // ---------------------------------------------------------------------------
46
+ initBundled(config) {
47
+ if (!config.socket) {
48
+ throw new Error("[SmorePlayer] socket is required for bundled games");
49
+ }
50
+ this.transport = new DirectTransport(config.socket);
51
+ this._roomCode = config.roomCode || "";
52
+ this._myIndex = config.myIndex ?? -1;
53
+ this._isLeader = config.isLeader ?? false;
54
+ this.setupEventHandlers();
55
+ this._isReady = true;
56
+ this.config.onReady?.();
57
+ }
58
+ initIframe(config) {
59
+ const parentOrigin = config.parentOrigin || "*";
60
+ window.parent.postMessage({ type: "smore:ready" }, parentOrigin);
61
+ this.boundMessageHandler = (e) => {
62
+ if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
63
+ const msg = e.data;
64
+ if (!isSmoreMessage(msg)) return;
65
+ if (msg.type === "smore:init") {
66
+ const initData = msg.payload;
67
+ if (initData.side !== "player") {
68
+ console.error("[SmorePlayer] Received init for wrong side:", initData.side);
69
+ return;
70
+ }
71
+ if (initData.myIndex === void 0) {
72
+ console.error("[SmorePlayer] Missing myIndex in init payload");
73
+ return;
74
+ }
75
+ this.transport = new PostMessageTransport(parentOrigin);
76
+ this._roomCode = initData.roomCode;
77
+ this._myIndex = initData.myIndex;
78
+ this._isLeader = initData.isLeader ?? false;
79
+ this.setupEventHandlers();
80
+ this._isReady = true;
81
+ this.config.onReady?.();
82
+ } else if (msg.type === "smore:update") {
83
+ const updateData = msg.payload;
84
+ if (updateData.leaderId !== void 0) ;
85
+ }
86
+ };
87
+ window.addEventListener("message", this.boundMessageHandler);
88
+ }
89
+ setupEventHandlers() {
90
+ if (!this.transport) return;
91
+ this.registerHandler(SYSTEM_EVENTS.PLAYER_JOIN, (data) => {
92
+ const playerIndex = data.player?.playerIndex ?? data.playerIndex;
93
+ if (playerIndex !== void 0) {
94
+ this.config.onPlayerJoin?.(playerIndex);
95
+ }
96
+ });
97
+ this.registerHandler(SYSTEM_EVENTS.PLAYER_LEAVE, (data) => {
98
+ const playerIndex = data.player?.playerIndex ?? data.playerIndex;
99
+ if (playerIndex !== void 0) {
100
+ this.config.onPlayerLeave?.(playerIndex);
101
+ }
102
+ });
103
+ if (this.config.listeners) {
104
+ for (const [event, handler] of Object.entries(this.config.listeners)) {
105
+ if (!handler) continue;
106
+ this.registerHandler(event, handler);
107
+ }
108
+ }
109
+ }
110
+ registerHandler(event, handler) {
111
+ if (!this.transport) return;
112
+ this.transport.on(event, handler);
113
+ this.registeredHandlers.push({ event, handler });
114
+ }
115
+ // ---------------------------------------------------------------------------
116
+ // Public Properties
117
+ // ---------------------------------------------------------------------------
118
+ /**
119
+ * Get my player index (0, 1, 2, ...).
120
+ */
121
+ get myIndex() {
122
+ return this._myIndex;
123
+ }
124
+ /**
125
+ * Check if I am the room leader.
126
+ */
127
+ get isLeader() {
128
+ return this._isLeader;
129
+ }
130
+ /**
131
+ * Get the room code.
132
+ */
133
+ get roomCode() {
134
+ return this._roomCode;
135
+ }
136
+ /**
137
+ * Check if the player is initialized and ready.
138
+ */
139
+ get isReady() {
140
+ return this._isReady;
141
+ }
142
+ // ---------------------------------------------------------------------------
143
+ // Public Methods
144
+ // ---------------------------------------------------------------------------
145
+ /**
146
+ * Send an event to the host.
147
+ *
148
+ * @param event - Event name (no colons allowed)
149
+ * @param data - Optional data payload
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * player.send('tap', { timestamp: Date.now() });
154
+ * player.send('answer', { choice: 2 });
155
+ * ```
156
+ */
157
+ send(event, data) {
158
+ this.ensureReady("send");
159
+ validateEventName(event);
160
+ this.transport.emit(event, data);
161
+ }
162
+ /**
163
+ * Add a listener for a specific event after construction.
164
+ *
165
+ * @param event - Event name (no colons allowed)
166
+ * @param handler - Handler function (data) => void
167
+ * @returns Cleanup function to remove the listener
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * const cleanup = player.on('phase-update', (data) => {
172
+ * console.log('New phase:', data.phase);
173
+ * });
174
+ *
175
+ * // Later
176
+ * cleanup();
177
+ * ```
178
+ */
179
+ on(event, handler) {
180
+ validateEventName(event);
181
+ if (this.transport) {
182
+ this.transport.on(event, handler);
183
+ this.registeredHandlers.push({ event, handler });
184
+ }
185
+ return () => {
186
+ this.transport?.off(event, handler);
187
+ this.registeredHandlers = this.registeredHandlers.filter(
188
+ (h) => h.event !== event || h.handler !== handler
189
+ );
190
+ };
191
+ }
192
+ /**
193
+ * Clean up all resources.
194
+ * Call this when unmounting/destroying the game.
195
+ */
196
+ destroy() {
197
+ if (this._isDestroyed) return;
198
+ this._isDestroyed = true;
199
+ this._isReady = false;
200
+ for (const { event, handler } of this.registeredHandlers) {
201
+ this.transport?.off(event, handler);
202
+ }
203
+ this.registeredHandlers = [];
204
+ if (this.transport instanceof PostMessageTransport) {
205
+ this.transport.destroy();
206
+ }
207
+ this.transport = null;
208
+ if (this.boundMessageHandler) {
209
+ window.removeEventListener("message", this.boundMessageHandler);
210
+ this.boundMessageHandler = null;
211
+ }
212
+ }
213
+ // ---------------------------------------------------------------------------
214
+ // Private Helpers
215
+ // ---------------------------------------------------------------------------
216
+ ensureReady(method) {
217
+ if (!this._isReady || !this.transport) {
218
+ throw new Error(`[SmorePlayer] Cannot call ${method}() before player is ready. Wait for onReady callback.`);
219
+ }
220
+ if (this._isDestroyed) {
221
+ throw new Error(`[SmorePlayer] Cannot call ${method}() after destroy()`);
222
+ }
223
+ }
224
+ }
225
+
226
+ export { SmorePlayer };
227
+ //# sourceMappingURL=SmorePlayer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SmorePlayer.js","sources":["../../src/SmorePlayer.ts"],"sourcesContent":["/**\n * SmorePlayer - Unified Player-side class for the S'MORE SDK (AirConsole style)\n *\n * Works in any environment: React, Phaser, Vanilla JS.\n * Automatically detects iframe vs bundled environment.\n *\n * @example Iframe game (auto-detection)\n * ```ts\n * const player = new SmorePlayer({\n * onReady: () => console.log('Ready! My index:', player.myIndex),\n * listeners: {\n * 'phase-update': (data) => handlePhaseUpdate(data),\n * },\n * });\n *\n * // Later\n * player.send('tap', { timestamp: Date.now() });\n * ```\n *\n * @example Bundled game (direct socket)\n * ```ts\n * const player = new SmorePlayer({\n * socket,\n * roomCode: 'ABCD',\n * myIndex: 0,\n * isLeader: true,\n * listeners: { ... },\n * });\n * ```\n */\n\nimport type { Socket } from 'socket.io-client';\nimport type { Transport, TransportEventHandler } from './transport/types';\nimport { DirectTransport } from './transport/DirectTransport';\nimport { PostMessageTransport } from './transport/PostMessageTransport';\nimport { isSmoreMessage, type SmoreInitMessage, type SmoreUpdateMessage } from './transport/protocol';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst SYSTEM_PREFIX = 'smore:';\n\nconst SYSTEM_EVENTS = {\n READY: `${SYSTEM_PREFIX}ready`,\n PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,\n PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\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 `[SmorePlayer] 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\n/**\n * Player information.\n */\nexport interface SmorePlayerInfo {\n /** Player index (0, 1, 2, ...) */\n playerIndex: number;\n /** Player's chosen nickname */\n nickname: string;\n /** Whether player is currently connected */\n connected: boolean;\n}\n\n/**\n * Configuration for SmorePlayer constructor.\n */\nexport interface SmorePlayerConfig {\n // === Callbacks ===\n\n /** Called when the player is ready and initialized (iframe games only) */\n onReady?: () => void;\n\n /** Called when another player joins the room */\n onPlayerJoin?: (playerIndex: number) => void;\n\n /** Called when another player leaves the room */\n onPlayerLeave?: (playerIndex: number) => void;\n\n /**\n * Event listeners for specific events.\n * Keys are event names (no colons), values are handler functions.\n * Handler receives (data) only - player side doesn't need playerIndex.\n */\n listeners?: Record<string, (data: any) => void>;\n\n // === Bundled game options (skip iframe detection) ===\n\n /** Socket.IO socket instance (bundled games only) */\n socket?: Socket;\n\n /** Room code (bundled games only) */\n roomCode?: string;\n\n /** My player index (bundled games only) */\n myIndex?: number;\n\n /** Am I the leader? (bundled games only) */\n isLeader?: boolean;\n\n // === Iframe game options ===\n\n /** Parent window origin for postMessage validation (iframe games) */\n parentOrigin?: string;\n}\n\n// ---------------------------------------------------------------------------\n// SmorePlayer Class\n// ---------------------------------------------------------------------------\n\n/**\n * SmorePlayer - Main player-side class for game development.\n *\n * Automatically detects iframe vs bundled environment:\n * - Iframe: Uses PostMessageTransport, waits for smore:init from parent\n * - Bundled: Uses DirectTransport with provided socket\n */\nexport class SmorePlayer {\n private transport: Transport | null = null;\n private config: SmorePlayerConfig;\n private _roomCode: string = '';\n private _myIndex: number = -1;\n private _isLeader: boolean = false;\n private _isReady: boolean = false;\n private _isDestroyed: boolean = false;\n private boundMessageHandler: ((e: MessageEvent) => void) | null = null;\n private registeredHandlers: Array<{ event: string; handler: TransportEventHandler }> = [];\n\n constructor(config: SmorePlayerConfig = {}) {\n this.config = config;\n\n // Validate event names in listeners\n if (config.listeners) {\n for (const event of Object.keys(config.listeners)) {\n validateEventName(event);\n }\n }\n\n // Detect environment and initialize\n if (config.socket) {\n // Bundled game mode: use DirectTransport\n this.initBundled(config);\n } else {\n // Iframe game mode: use PostMessageTransport\n this.initIframe(config);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Initialization\n // ---------------------------------------------------------------------------\n\n private initBundled(config: SmorePlayerConfig): void {\n if (!config.socket) {\n throw new Error('[SmorePlayer] socket is required for bundled games');\n }\n\n this.transport = new DirectTransport(config.socket);\n this._roomCode = config.roomCode || '';\n this._myIndex = config.myIndex ?? -1;\n this._isLeader = config.isLeader ?? false;\n\n this.setupEventHandlers();\n\n // Mark as ready immediately for bundled games\n this._isReady = true;\n this.config.onReady?.();\n }\n\n private initIframe(config: SmorePlayerConfig): void {\n const parentOrigin = config.parentOrigin || '*';\n\n // Signal ready to parent\n window.parent.postMessage({ type: 'smore:ready' }, parentOrigin);\n\n // Listen for init message from parent\n this.boundMessageHandler = (e: MessageEvent) => {\n if (parentOrigin !== '*' && e.origin !== parentOrigin) return;\n\n const msg = e.data;\n if (!isSmoreMessage(msg)) return;\n\n if (msg.type === 'smore:init') {\n const initData = (msg as SmoreInitMessage).payload;\n\n if (initData.side !== 'player') {\n console.error('[SmorePlayer] Received init for wrong side:', initData.side);\n return;\n }\n\n if (initData.myIndex === undefined) {\n console.error('[SmorePlayer] Missing myIndex in init payload');\n return;\n }\n\n // Initialize transport\n this.transport = new PostMessageTransport(parentOrigin);\n this._roomCode = initData.roomCode;\n this._myIndex = initData.myIndex;\n this._isLeader = initData.isLeader ?? false;\n\n this.setupEventHandlers();\n\n this._isReady = true;\n this.config.onReady?.();\n } else if (msg.type === 'smore:update') {\n const updateData = (msg as SmoreUpdateMessage).payload;\n\n // Update leader status if changed\n if (updateData.leaderId !== undefined) {\n // Note: Without players array, we can't determine isLeader change\n // This would require the parent to send myIndex in update\n }\n }\n };\n\n window.addEventListener('message', this.boundMessageHandler);\n }\n\n private setupEventHandlers(): void {\n if (!this.transport) return;\n\n // System events: player join/leave\n this.registerHandler(SYSTEM_EVENTS.PLAYER_JOIN, (data: { player?: SmorePlayerInfo; playerIndex?: number }) => {\n const playerIndex = data.player?.playerIndex ?? data.playerIndex;\n if (playerIndex !== undefined) {\n this.config.onPlayerJoin?.(playerIndex);\n }\n });\n\n this.registerHandler(SYSTEM_EVENTS.PLAYER_LEAVE, (data: { player?: { playerIndex?: number }; playerIndex?: number }) => {\n const playerIndex = data.player?.playerIndex ?? data.playerIndex;\n if (playerIndex !== undefined) {\n this.config.onPlayerLeave?.(playerIndex);\n }\n });\n\n // User event listeners\n if (this.config.listeners) {\n for (const [event, handler] of Object.entries(this.config.listeners)) {\n if (!handler) continue;\n\n // Player side receives data directly (no playerIndex unwrapping)\n this.registerHandler(event, handler);\n }\n }\n }\n\n private registerHandler(event: string, handler: TransportEventHandler): void {\n if (!this.transport) return;\n this.transport.on(event, handler);\n this.registeredHandlers.push({ event, handler });\n }\n\n // ---------------------------------------------------------------------------\n // Public Properties\n // ---------------------------------------------------------------------------\n\n /**\n * Get my player index (0, 1, 2, ...).\n */\n get myIndex(): number {\n return this._myIndex;\n }\n\n /**\n * Check if I am the room leader.\n */\n get isLeader(): boolean {\n return this._isLeader;\n }\n\n /**\n * Get the room code.\n */\n get roomCode(): string {\n return this._roomCode;\n }\n\n /**\n * Check if the player is initialized and ready.\n */\n get isReady(): boolean {\n return this._isReady;\n }\n\n // ---------------------------------------------------------------------------\n // Public Methods\n // ---------------------------------------------------------------------------\n\n /**\n * Send an event to the host.\n *\n * @param event - Event name (no colons allowed)\n * @param data - Optional data payload\n *\n * @example\n * ```ts\n * player.send('tap', { timestamp: Date.now() });\n * player.send('answer', { choice: 2 });\n * ```\n */\n send(event: string, data?: any): void {\n this.ensureReady('send');\n validateEventName(event);\n this.transport!.emit(event, data);\n }\n\n /**\n * Add a listener for a specific event after construction.\n *\n * @param event - Event name (no colons allowed)\n * @param handler - Handler function (data) => void\n * @returns Cleanup function to remove the listener\n *\n * @example\n * ```ts\n * const cleanup = player.on('phase-update', (data) => {\n * console.log('New phase:', data.phase);\n * });\n *\n * // Later\n * cleanup();\n * ```\n */\n on(event: string, handler: (data: any) => void): () => void {\n validateEventName(event);\n\n if (this.transport) {\n this.transport.on(event, handler);\n this.registeredHandlers.push({ event, handler });\n }\n\n return () => {\n this.transport?.off(event, handler);\n this.registeredHandlers = this.registeredHandlers.filter(\n (h) => h.event !== event || h.handler !== handler\n );\n };\n }\n\n /**\n * Clean up all resources.\n * Call this when unmounting/destroying the game.\n */\n destroy(): void {\n if (this._isDestroyed) return;\n\n this._isDestroyed = true;\n this._isReady = false;\n\n // Remove all registered handlers\n for (const { event, handler } of this.registeredHandlers) {\n this.transport?.off(event, handler);\n }\n this.registeredHandlers = [];\n\n // Destroy transport\n if (this.transport instanceof PostMessageTransport) {\n (this.transport as PostMessageTransport).destroy();\n }\n this.transport = null;\n\n // Remove message listener\n if (this.boundMessageHandler) {\n window.removeEventListener('message', this.boundMessageHandler);\n this.boundMessageHandler = null;\n }\n }\n\n // ---------------------------------------------------------------------------\n // Private Helpers\n // ---------------------------------------------------------------------------\n\n private ensureReady(method: string): void {\n if (!this._isReady || !this.transport) {\n throw new Error(`[SmorePlayer] Cannot call ${method}() before player is ready. Wait for onReady callback.`);\n }\n if (this._isDestroyed) {\n throw new Error(`[SmorePlayer] Cannot call ${method}() after destroy()`);\n }\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { useRef, useCallback, useEffect } from 'react';
3
+ import { isSmoreMessage } from '../transport/protocol.js';
4
+
5
+ const IframeGameBridge = ({
6
+ gameId,
7
+ url,
8
+ socket,
9
+ side,
10
+ roomCode,
11
+ players,
12
+ leaderId,
13
+ myIndex,
14
+ isLeader,
15
+ onReady,
16
+ onLoaded,
17
+ onGameOver,
18
+ style,
19
+ className
20
+ }) => {
21
+ const iframeRef = useRef(null);
22
+ const readyRef = useRef(false);
23
+ const loadedRef = useRef(false);
24
+ const postToIframe = useCallback((msg) => {
25
+ iframeRef.current?.contentWindow?.postMessage(msg, "*");
26
+ }, []);
27
+ useEffect(() => {
28
+ const handler = (e) => {
29
+ if (e.source !== iframeRef.current?.contentWindow) return;
30
+ const msg = e.data;
31
+ if (!isSmoreMessage(msg)) return;
32
+ if (msg.type === "smore:ready" && !readyRef.current) {
33
+ readyRef.current = true;
34
+ postToIframe({
35
+ type: "smore:init",
36
+ payload: {
37
+ side,
38
+ roomCode,
39
+ players,
40
+ leaderId,
41
+ myIndex,
42
+ isLeader
43
+ }
44
+ });
45
+ onReady?.();
46
+ setTimeout(() => {
47
+ if (!loadedRef.current) {
48
+ console.warn("[IframeGameBridge] Game did not send smore:loaded, auto-triggering");
49
+ loadedRef.current = true;
50
+ onLoaded?.();
51
+ }
52
+ }, 2e3);
53
+ }
54
+ if (msg.type === "smore:loaded" && !loadedRef.current) {
55
+ loadedRef.current = true;
56
+ onLoaded?.();
57
+ }
58
+ if (msg.type === "smore:emit") {
59
+ const { event, data, ackId } = msg.payload;
60
+ if (event === "room:game-over") {
61
+ onGameOver?.(data?.results);
62
+ }
63
+ if (ackId) {
64
+ socket.emit(event, data, (response) => {
65
+ postToIframe({ type: "smore:ack", payload: { ackId, data: response } });
66
+ });
67
+ } else {
68
+ socket.emit(event, data);
69
+ }
70
+ }
71
+ };
72
+ window.addEventListener("message", handler);
73
+ return () => window.removeEventListener("message", handler);
74
+ }, [socket, side, roomCode, players, leaderId, myIndex, isLeader, onReady, onLoaded, onGameOver, postToIframe]);
75
+ useEffect(() => {
76
+ if (!readyRef.current) return;
77
+ postToIframe({
78
+ type: "smore:update",
79
+ payload: { players, leaderId }
80
+ });
81
+ }, [players, leaderId, postToIframe]);
82
+ useEffect(() => {
83
+ if (!socket) return;
84
+ const handler = (event, ...args) => {
85
+ postToIframe({
86
+ type: "smore:event",
87
+ payload: { event, data: args[0] }
88
+ });
89
+ };
90
+ socket.onAny(handler);
91
+ return () => {
92
+ socket.offAny(handler);
93
+ };
94
+ }, [socket, postToIframe]);
95
+ return /* @__PURE__ */ jsx(
96
+ "iframe",
97
+ {
98
+ ref: iframeRef,
99
+ src: url,
100
+ sandbox: "allow-scripts allow-same-origin",
101
+ style: {
102
+ border: "none",
103
+ width: "100%",
104
+ height: "100%",
105
+ ...style
106
+ },
107
+ className
108
+ }
109
+ );
110
+ };
111
+
112
+ export { IframeGameBridge };
113
+ //# sourceMappingURL=IframeGameBridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IframeGameBridge.js","sources":["../../../src/components/IframeGameBridge.tsx"],"sourcesContent":["/**\n * IframeGameBridge - Parent-side bridge between iframe and Socket.IO.\n *\n * Renders an iframe loading the external game URL.\n * Bridges postMessage ↔ Socket.IO:\n * - iframe sends `smore:emit` → bridge relays to `socket.emit()` as-is\n * - socket receives events → bridge relays to iframe via `smore:event` as-is\n *\n * No event translation is performed; events are passed through unchanged.\n *\n * Used by GameOverlay (host) and GameView (player) when the game type is 'external'.\n */\n\nimport React, { useEffect, useRef, useCallback } from 'react';\nimport type { Socket } from 'socket.io-client';\nimport { isSmoreMessage } from '../transport/protocol';\nimport type { SmoreEmitMessage } from '../transport/protocol';\n\ninterface Player {\n playerIndex: number;\n name: string;\n connected?: boolean;\n}\n\nexport interface IframeGameBridgeProps {\n gameId: string;\n url: string;\n socket: Socket;\n side: 'host' | 'player';\n roomCode: string;\n players: Player[];\n leaderId: string | null;\n myIndex?: number;\n isLeader?: boolean;\n onReady?: () => void;\n onLoaded?: () => void;\n onGameOver?: (results: any) => void;\n style?: React.CSSProperties;\n className?: string;\n}\n\nexport const IframeGameBridge: React.FC<IframeGameBridgeProps> = ({\n gameId,\n url,\n socket,\n side,\n roomCode,\n players,\n leaderId,\n myIndex,\n isLeader,\n onReady,\n onLoaded,\n onGameOver,\n style,\n className,\n}) => {\n const iframeRef = useRef<HTMLIFrameElement>(null);\n const readyRef = useRef(false);\n const loadedRef = useRef(false);\n\n // Send message to iframe\n const postToIframe = useCallback((msg: object) => {\n iframeRef.current?.contentWindow?.postMessage(msg, '*');\n }, []);\n\n // Handle messages from iframe (smore:ready, smore:emit)\n useEffect(() => {\n const handler = (e: MessageEvent) => {\n if (e.source !== iframeRef.current?.contentWindow) return;\n\n const msg = e.data;\n if (!isSmoreMessage(msg)) return;\n\n if (msg.type === 'smore:ready' && !readyRef.current) {\n readyRef.current = true;\n\n postToIframe({\n type: 'smore:init',\n payload: {\n side,\n roomCode,\n players,\n leaderId,\n myIndex,\n isLeader,\n },\n });\n\n onReady?.();\n\n // Fallback: if game doesn't send smore:loaded within 2s, auto-trigger\n setTimeout(() => {\n if (!loadedRef.current) {\n console.warn('[IframeGameBridge] Game did not send smore:loaded, auto-triggering');\n loadedRef.current = true;\n onLoaded?.();\n }\n }, 2000);\n }\n\n if (msg.type === 'smore:loaded' && !loadedRef.current) {\n loadedRef.current = true;\n onLoaded?.();\n }\n\n if (msg.type === 'smore:emit') {\n const { event, data, ackId } = (msg as SmoreEmitMessage).payload;\n\n // Intercept game-over\n if (event === 'room:game-over') {\n onGameOver?.(data?.results);\n }\n\n if (ackId) {\n socket.emit(event, data, (response: any) => {\n postToIframe({ type: 'smore:ack', payload: { ackId, data: response } });\n });\n } else {\n socket.emit(event, data);\n }\n }\n };\n\n window.addEventListener('message', handler);\n return () => window.removeEventListener('message', handler);\n }, [socket, side, roomCode, players, leaderId, myIndex, isLeader, onReady, onLoaded, onGameOver, postToIframe]);\n\n // Push player/leader updates to iframe after init\n useEffect(() => {\n if (!readyRef.current) return;\n postToIframe({\n type: 'smore:update',\n payload: { players, leaderId },\n });\n }, [players, leaderId, postToIframe]);\n\n // Bridge socket events to iframe\n useEffect(() => {\n if (!socket) return;\n\n const handler = (event: string, ...args: any[]) => {\n postToIframe({\n type: 'smore:event',\n payload: { event, data: args[0] },\n });\n };\n\n socket.onAny(handler);\n return () => {\n socket.offAny(handler);\n };\n }, [socket, postToIframe]);\n\n return (\n <iframe\n ref={iframeRef}\n src={url}\n sandbox=\"allow-scripts allow-same-origin\"\n style={{\n border: 'none',\n width: '100%',\n height: '100%',\n ...style,\n }}\n className={className}\n />\n );\n};\n"],"names":[],"mappings":"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"}
@@ -45,7 +45,7 @@ const PlayerRoomProvider = ({
45
45
  roomCode,
46
46
  players,
47
47
  leaderId,
48
- mySessionId,
48
+ myIndex,
49
49
  isLeader,
50
50
  socket,
51
51
  isConnected,
@@ -61,12 +61,12 @@ const PlayerRoomProvider = ({
61
61
  players,
62
62
  connectedPlayers,
63
63
  leaderId,
64
- mySessionId,
64
+ myIndex,
65
65
  isLeader,
66
66
  socket,
67
67
  isConnected
68
68
  }),
69
- [roomCode, players, connectedPlayers, leaderId, mySessionId, isLeader, socket, isConnected]
69
+ [roomCode, players, connectedPlayers, leaderId, myIndex, isLeader, socket, isConnected]
70
70
  );
71
71
  const value = useMemo(
72
72
  () => ({
@@ -1 +1 @@
1
- {"version":3,"file":"RoomProvider.js","sources":["../../../src/context/RoomProvider.tsx"],"sourcesContent":["/**\n * RoomProvider - SDK Room Context\n *\n * Foundation context that all other SDK hooks depend on.\n * Provides room state (players, roomCode, leaderId) for both host and player sides.\n *\n * Also provides a Transport abstraction via TransportContext.\n * - HostRoomProvider / PlayerRoomProvider create a DirectTransport from the socket.\n * - IframeRoomProvider (external) provides a PostMessageTransport.\n *\n * Usage:\n * - Host: <HostRoomProvider roomCode={...} players={...} leaderId={...} socket={...}>\n * - Player: <PlayerRoomProvider roomCode={...} players={...} leaderId={...} mySessionId={...} isLeader={...} socket={...} isConnected={...}>\n */\n\nimport React, { createContext, useContext, useMemo } from 'react';\nimport type { Player } from '@smoregg/shared';\nimport type { Socket } from 'socket.io-client';\nimport type { Transport } from '../transport/types';\nimport { DirectTransport } from '../transport/DirectTransport';\n\n// ===== Transport Context =====\n\nconst TransportContext = createContext<Transport | null>(null);\n\nexport function useTransport(): Transport {\n const transport = useContext(TransportContext);\n if (!transport) {\n throw new Error('useTransport must be used within a RoomProvider that supplies a Transport');\n }\n return transport;\n}\n\nexport { TransportContext };\n\n// ===== State Types =====\n\nexport interface RoomState {\n roomCode: string;\n players: Player[];\n connectedPlayers: Player[];\n leaderId: string | null;\n}\n\nexport interface HostRoomState extends RoomState {\n socket: Socket;\n}\n\nexport interface PlayerRoomState extends RoomState {\n mySessionId: string;\n isLeader: boolean;\n socket: Socket;\n isConnected: boolean;\n}\n\nexport interface RoomContextValue {\n roomCode: string;\n players: Player[];\n connectedPlayers: Player[];\n leaderId: string | null;\n side: 'host' | 'player';\n host: HostRoomState | null;\n player: PlayerRoomState | null;\n}\n\n// ===== Context =====\n\nexport const RoomContext = createContext<RoomContextValue | null>(null);\n\n// ===== Host Provider =====\n\ninterface HostRoomProviderProps {\n roomCode: string;\n players: Player[];\n leaderId: string | null;\n socket: Socket;\n children: React.ReactNode;\n}\n\nexport const HostRoomProvider: React.FC<HostRoomProviderProps> = ({\n roomCode,\n players,\n leaderId,\n socket,\n children,\n}) => {\n const connectedPlayers = useMemo(\n () => players.filter((p) => p.connected !== false),\n [players]\n );\n\n const hostState: HostRoomState = useMemo(\n () => ({ roomCode, players, connectedPlayers, leaderId, socket }),\n [roomCode, players, connectedPlayers, leaderId, socket]\n );\n\n const value: RoomContextValue = useMemo(\n () => ({\n roomCode,\n players,\n connectedPlayers,\n leaderId,\n side: 'host' as const,\n host: hostState,\n player: null,\n }),\n [roomCode, players, connectedPlayers, leaderId, hostState]\n );\n\n const transport = useMemo(() => new DirectTransport(socket), [socket]);\n\n return (\n <TransportContext.Provider value={transport}>\n <RoomContext.Provider value={value}>{children}</RoomContext.Provider>\n </TransportContext.Provider>\n );\n};\n\n// ===== Player Provider =====\n\ninterface PlayerRoomProviderProps {\n roomCode: string;\n players: Player[];\n leaderId: string | null;\n mySessionId: string;\n isLeader: boolean;\n socket: Socket;\n isConnected: boolean;\n children: React.ReactNode;\n}\n\nexport const PlayerRoomProvider: React.FC<PlayerRoomProviderProps> = ({\n roomCode,\n players,\n leaderId,\n mySessionId,\n isLeader,\n socket,\n isConnected,\n children,\n}) => {\n const connectedPlayers = useMemo(\n () => players.filter((p) => p.connected !== false),\n [players]\n );\n\n const playerState: PlayerRoomState = useMemo(\n () => ({\n roomCode,\n players,\n connectedPlayers,\n leaderId,\n mySessionId,\n isLeader,\n socket,\n isConnected,\n }),\n [roomCode, players, connectedPlayers, leaderId, mySessionId, isLeader, socket, isConnected]\n );\n\n const value: RoomContextValue = useMemo(\n () => ({\n roomCode,\n players,\n connectedPlayers,\n leaderId,\n side: 'player' as const,\n host: null,\n player: playerState,\n }),\n [roomCode, players, connectedPlayers, leaderId, playerState]\n );\n\n const transport = useMemo(() => new DirectTransport(socket), [socket]);\n\n return (\n <TransportContext.Provider value={transport}>\n <RoomContext.Provider value={value}>{children}</RoomContext.Provider>\n </TransportContext.Provider>\n );\n};\n\n// ===== Hooks =====\n\nexport function useRoom(): RoomContextValue {\n const context = useContext(RoomContext);\n if (!context) {\n throw new Error('useRoom must be used within HostRoomProvider or PlayerRoomProvider');\n }\n return context;\n}\n\nexport function useHostRoom(): HostRoomState {\n const context = useRoom();\n if (context.side !== 'host' || !context.host) {\n throw new Error('useHostRoom must be used within HostRoomProvider');\n }\n return context.host;\n}\n\nexport function usePlayerRoom(): PlayerRoomState {\n const context = useRoom();\n if (context.side !== 'player' || !context.player) {\n throw new Error('usePlayerRoom must be used within PlayerRoomProvider');\n }\n return context.player;\n}\n"],"names":[],"mappings":";;;;AAuBA,MAAM,gBAAA,GAAmB,cAAgC,IAAI;AAEtD,SAAS,YAAA,GAA0B;AACxC,EAAA,MAAM,SAAA,GAAY,WAAW,gBAAgB,CAAA;AAC7C,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,MAAM,IAAI,MAAM,2EAA2E,CAAA;AAAA,EAC7F;AACA,EAAA,OAAO,SAAA;AACT;AAoCO,MAAM,WAAA,GAAc,cAAuC,IAAI;AAY/D,MAAM,mBAAoD,CAAC;AAAA,EAChE,QAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAA,KAAM;AACJ,EAAA,MAAM,gBAAA,GAAmB,OAAA;AAAA,IACvB,MAAM,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,cAAc,KAAK,CAAA;AAAA,IACjD,CAAC,OAAO;AAAA,GACV;AAEA,EAAA,MAAM,SAAA,GAA2B,OAAA;AAAA,IAC/B,OAAO,EAAE,QAAA,EAAU,OAAA,EAAS,gBAAA,EAAkB,UAAU,MAAA,EAAO,CAAA;AAAA,IAC/D,CAAC,QAAA,EAAU,OAAA,EAAS,gBAAA,EAAkB,UAAU,MAAM;AAAA,GACxD;AAEA,EAAA,MAAM,KAAA,GAA0B,OAAA;AAAA,IAC9B,OAAO;AAAA,MACL,QAAA;AAAA,MACA,OAAA;AAAA,MACA,gBAAA;AAAA,MACA,QAAA;AAAA,MACA,IAAA,EAAM,MAAA;AAAA,MACN,IAAA,EAAM,SAAA;AAAA,MACN,MAAA,EAAQ;AAAA,KACV,CAAA;AAAA,IACA,CAAC,QAAA,EAAU,OAAA,EAAS,gBAAA,EAAkB,UAAU,SAAS;AAAA,GAC3D;AAEA,EAAA,MAAM,SAAA,GAAY,QAAQ,MAAM,IAAI,gBAAgB,MAAM,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAErE,EAAA,uBACE,GAAA,CAAC,gBAAA,CAAiB,QAAA,EAAjB,EAA0B,KAAA,EAAO,SAAA,EAChC,QAAA,kBAAA,GAAA,CAAC,WAAA,CAAY,QAAA,EAAZ,EAAqB,KAAA,EAAe,QAAA,EAAS,CAAA,EAChD,CAAA;AAEJ;AAeO,MAAM,qBAAwD,CAAC;AAAA,EACpE,QAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,WAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,WAAA;AAAA,EACA;AACF,CAAA,KAAM;AACJ,EAAA,MAAM,gBAAA,GAAmB,OAAA;AAAA,IACvB,MAAM,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,cAAc,KAAK,CAAA;AAAA,IACjD,CAAC,OAAO;AAAA,GACV;AAEA,EAAA,MAAM,WAAA,GAA+B,OAAA;AAAA,IACnC,OAAO;AAAA,MACL,QAAA;AAAA,MACA,OAAA;AAAA,MACA,gBAAA;AAAA,MACA,QAAA;AAAA,MACA,WAAA;AAAA,MACA,QAAA;AAAA,MACA,MAAA;AAAA,MACA;AAAA,KACF,CAAA;AAAA,IACA,CAAC,UAAU,OAAA,EAAS,gBAAA,EAAkB,UAAU,WAAA,EAAa,QAAA,EAAU,QAAQ,WAAW;AAAA,GAC5F;AAEA,EAAA,MAAM,KAAA,GAA0B,OAAA;AAAA,IAC9B,OAAO;AAAA,MACL,QAAA;AAAA,MACA,OAAA;AAAA,MACA,gBAAA;AAAA,MACA,QAAA;AAAA,MACA,IAAA,EAAM,QAAA;AAAA,MACN,IAAA,EAAM,IAAA;AAAA,MACN,MAAA,EAAQ;AAAA,KACV,CAAA;AAAA,IACA,CAAC,QAAA,EAAU,OAAA,EAAS,gBAAA,EAAkB,UAAU,WAAW;AAAA,GAC7D;AAEA,EAAA,MAAM,SAAA,GAAY,QAAQ,MAAM,IAAI,gBAAgB,MAAM,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAErE,EAAA,uBACE,GAAA,CAAC,gBAAA,CAAiB,QAAA,EAAjB,EAA0B,KAAA,EAAO,SAAA,EAChC,QAAA,kBAAA,GAAA,CAAC,WAAA,CAAY,QAAA,EAAZ,EAAqB,KAAA,EAAe,QAAA,EAAS,CAAA,EAChD,CAAA;AAEJ;AAIO,SAAS,OAAA,GAA4B;AAC1C,EAAA,MAAM,OAAA,GAAU,WAAW,WAAW,CAAA;AACtC,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,MAAM,IAAI,MAAM,oEAAoE,CAAA;AAAA,EACtF;AACA,EAAA,OAAO,OAAA;AACT;AAEO,SAAS,WAAA,GAA6B;AAC3C,EAAA,MAAM,UAAU,OAAA,EAAQ;AACxB,EAAA,IAAI,OAAA,CAAQ,IAAA,KAAS,MAAA,IAAU,CAAC,QAAQ,IAAA,EAAM;AAC5C,IAAA,MAAM,IAAI,MAAM,kDAAkD,CAAA;AAAA,EACpE;AACA,EAAA,OAAO,OAAA,CAAQ,IAAA;AACjB;AAEO,SAAS,aAAA,GAAiC;AAC/C,EAAA,MAAM,UAAU,OAAA,EAAQ;AACxB,EAAA,IAAI,OAAA,CAAQ,IAAA,KAAS,QAAA,IAAY,CAAC,QAAQ,MAAA,EAAQ;AAChD,IAAA,MAAM,IAAI,MAAM,sDAAsD,CAAA;AAAA,EACxE;AACA,EAAA,OAAO,OAAA,CAAQ,MAAA;AACjB;;;;"}
1
+ {"version":3,"file":"RoomProvider.js","sources":["../../../src/context/RoomProvider.tsx"],"sourcesContent":["/**\n * RoomProvider - SDK Room Context\n *\n * Foundation context that all other SDK hooks depend on.\n * Provides room state (players, roomCode, leaderId) for both host and player sides.\n *\n * Also provides a Transport abstraction via TransportContext.\n * - HostRoomProvider / PlayerRoomProvider create a DirectTransport from the socket.\n * - IframeRoomProvider (external) provides a PostMessageTransport.\n *\n * Usage:\n * - Host: <HostRoomProvider roomCode={...} players={...} leaderId={...} socket={...}>\n * - Player: <PlayerRoomProvider roomCode={...} players={...} leaderId={...} myIndex={...} isLeader={...} socket={...} isConnected={...}>\n */\n\nimport React, { createContext, useContext, useMemo } from 'react';\nimport type { Player } from '@smoregg/shared';\nimport type { Socket } from 'socket.io-client';\nimport type { Transport } from '../transport/types';\nimport { DirectTransport } from '../transport/DirectTransport';\n\n// ===== Transport Context =====\n\nconst TransportContext = createContext<Transport | null>(null);\n\nexport function useTransport(): Transport {\n const transport = useContext(TransportContext);\n if (!transport) {\n throw new Error('useTransport must be used within a RoomProvider that supplies a Transport');\n }\n return transport;\n}\n\nexport { TransportContext };\n\n// ===== State Types =====\n\nexport interface RoomState {\n roomCode: string;\n players: Player[];\n connectedPlayers: Player[];\n leaderId: string | null;\n}\n\nexport interface HostRoomState extends RoomState {\n socket: Socket;\n}\n\nexport interface PlayerRoomState extends RoomState {\n myIndex: number;\n isLeader: boolean;\n socket: Socket;\n isConnected: boolean;\n}\n\nexport interface RoomContextValue {\n roomCode: string;\n players: Player[];\n connectedPlayers: Player[];\n leaderId: string | null;\n side: 'host' | 'player';\n host: HostRoomState | null;\n player: PlayerRoomState | null;\n}\n\n// ===== Context =====\n\nexport const RoomContext = createContext<RoomContextValue | null>(null);\n\n// ===== Host Provider =====\n\ninterface HostRoomProviderProps {\n roomCode: string;\n players: Player[];\n leaderId: string | null;\n socket: Socket;\n children: React.ReactNode;\n}\n\nexport const HostRoomProvider: React.FC<HostRoomProviderProps> = ({\n roomCode,\n players,\n leaderId,\n socket,\n children,\n}) => {\n const connectedPlayers = useMemo(\n () => players.filter((p) => p.connected !== false),\n [players]\n );\n\n const hostState: HostRoomState = useMemo(\n () => ({ roomCode, players, connectedPlayers, leaderId, socket }),\n [roomCode, players, connectedPlayers, leaderId, socket]\n );\n\n const value: RoomContextValue = useMemo(\n () => ({\n roomCode,\n players,\n connectedPlayers,\n leaderId,\n side: 'host' as const,\n host: hostState,\n player: null,\n }),\n [roomCode, players, connectedPlayers, leaderId, hostState]\n );\n\n const transport = useMemo(() => new DirectTransport(socket), [socket]);\n\n return (\n <TransportContext.Provider value={transport}>\n <RoomContext.Provider value={value}>{children}</RoomContext.Provider>\n </TransportContext.Provider>\n );\n};\n\n// ===== Player Provider =====\n\ninterface PlayerRoomProviderProps {\n roomCode: string;\n players: Player[];\n leaderId: string | null;\n myIndex: number;\n isLeader: boolean;\n socket: Socket;\n isConnected: boolean;\n children: React.ReactNode;\n}\n\nexport const PlayerRoomProvider: React.FC<PlayerRoomProviderProps> = ({\n roomCode,\n players,\n leaderId,\n myIndex,\n isLeader,\n socket,\n isConnected,\n children,\n}) => {\n const connectedPlayers = useMemo(\n () => players.filter((p) => p.connected !== false),\n [players]\n );\n\n const playerState: PlayerRoomState = useMemo(\n () => ({\n roomCode,\n players,\n connectedPlayers,\n leaderId,\n myIndex,\n isLeader,\n socket,\n isConnected,\n }),\n [roomCode, players, connectedPlayers, leaderId, myIndex, isLeader, socket, isConnected]\n );\n\n const value: RoomContextValue = useMemo(\n () => ({\n roomCode,\n players,\n connectedPlayers,\n leaderId,\n side: 'player' as const,\n host: null,\n player: playerState,\n }),\n [roomCode, players, connectedPlayers, leaderId, playerState]\n );\n\n const transport = useMemo(() => new DirectTransport(socket), [socket]);\n\n return (\n <TransportContext.Provider value={transport}>\n <RoomContext.Provider value={value}>{children}</RoomContext.Provider>\n </TransportContext.Provider>\n );\n};\n\n// ===== Hooks =====\n\nexport function useRoom(): RoomContextValue {\n const context = useContext(RoomContext);\n if (!context) {\n throw new Error('useRoom must be used within HostRoomProvider or PlayerRoomProvider');\n }\n return context;\n}\n\nexport function useHostRoom(): HostRoomState {\n const context = useRoom();\n if (context.side !== 'host' || !context.host) {\n throw new Error('useHostRoom must be used within HostRoomProvider');\n }\n return context.host;\n}\n\nexport function usePlayerRoom(): PlayerRoomState {\n const context = useRoom();\n if (context.side !== 'player' || !context.player) {\n throw new Error('usePlayerRoom must be used within PlayerRoomProvider');\n }\n return context.player;\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, useMemo } from 'react';
2
2
  import { useHostRoom, useTransport } from '../context/RoomProvider.js';
3
+ import { createConnectionMonitor } from '../utils/connectionMonitor.js';
3
4
 
4
5
  const SYSTEM_PREFIX = "smore:";
5
6
  const SYSTEM_EVENTS = {
@@ -9,7 +10,8 @@ const SYSTEM_EVENTS = {
9
10
  GAME_OVER: `${SYSTEM_PREFIX}game-over`,
10
11
  RETURN_TO_LOBBY: `${SYSTEM_PREFIX}return-to-lobby`,
11
12
  SEND_TO_PLAYER: `${SYSTEM_PREFIX}send-to-player`,
12
- BROADCAST: `${SYSTEM_PREFIX}broadcast`
13
+ BROADCAST: `${SYSTEM_PREFIX}broadcast`,
14
+ CUSTOM_STATE_CHANGE: `${SYSTEM_PREFIX}custom-state-change`
13
15
  };
14
16
  const EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z_-]*[a-zA-Z])?$/;
15
17
  function validateEventName(event) {
@@ -22,13 +24,17 @@ function validateEventName(event) {
22
24
  }
23
25
  }
24
26
  function useGameHost(config = {}) {
25
- const { onReady, onPlayerJoin, onPlayerLeave, listeners } = config;
27
+ const { onReady, onPlayerJoin, onPlayerLeave, listeners, connectionMonitor, onCustomStateChange } = config;
26
28
  const hostRoom = useHostRoom();
27
29
  const transport = useTransport();
30
+ const [isPaused, setIsPaused] = useState(false);
31
+ const [latency, setLatency] = useState(0);
32
+ const monitorRef = useRef(null);
28
33
  const onReadyRef = useRef(onReady);
29
34
  const onPlayerJoinRef = useRef(onPlayerJoin);
30
35
  const onPlayerLeaveRef = useRef(onPlayerLeave);
31
36
  const listenersRef = useRef(listeners);
37
+ const onCustomStateChangeRef = useRef(onCustomStateChange);
32
38
  useEffect(() => {
33
39
  onReadyRef.current = onReady;
34
40
  }, [onReady]);
@@ -41,32 +47,82 @@ function useGameHost(config = {}) {
41
47
  useEffect(() => {
42
48
  listenersRef.current = listeners;
43
49
  }, [listeners]);
50
+ useEffect(() => {
51
+ onCustomStateChangeRef.current = onCustomStateChange;
52
+ }, [onCustomStateChange]);
53
+ useEffect(() => {
54
+ if (!connectionMonitor?.enabled || !transport) return;
55
+ const socket = transport.socket;
56
+ if (!socket) {
57
+ console.warn("[useGameHost] Connection monitor requires Socket.IO transport");
58
+ return;
59
+ }
60
+ const monitor = createConnectionMonitor(socket, {
61
+ onPause: () => {
62
+ setIsPaused(true);
63
+ connectionMonitor.onPause();
64
+ },
65
+ onResume: () => {
66
+ setIsPaused(false);
67
+ connectionMonitor.onResume();
68
+ },
69
+ onCountdown: connectionMonitor.onCountdown,
70
+ latencyThreshold: connectionMonitor.latencyThreshold,
71
+ resumeCountdown: connectionMonitor.resumeCountdown
72
+ });
73
+ monitorRef.current = monitor;
74
+ const latencyInterval = setInterval(() => {
75
+ setLatency(monitor.getLatency());
76
+ }, 1e3);
77
+ return () => {
78
+ monitor.destroy();
79
+ clearInterval(latencyInterval);
80
+ monitorRef.current = null;
81
+ };
82
+ }, [connectionMonitor, transport]);
44
83
  useEffect(() => {
45
84
  if (!transport) return;
46
85
  const handleReady = () => {
47
86
  onReadyRef.current?.();
48
87
  };
49
88
  const handlePlayerJoin = (data) => {
50
- onPlayerJoinRef.current?.(data.player);
89
+ const playerIndex = data.player?.playerIndex;
90
+ if (playerIndex !== void 0) {
91
+ onPlayerJoinRef.current?.(playerIndex);
92
+ }
51
93
  };
52
94
  const handlePlayerLeave = (data) => {
53
- onPlayerLeaveRef.current?.(data.sessionId);
95
+ const playerIndex = data.playerIndex;
96
+ if (playerIndex !== void 0) {
97
+ onPlayerLeaveRef.current?.(playerIndex);
98
+ }
99
+ };
100
+ const handleCustomStateChange = (data) => {
101
+ if (data.playerIndex !== void 0) {
102
+ onCustomStateChangeRef.current?.(data.playerIndex, data.state);
103
+ }
54
104
  };
55
105
  transport.on(SYSTEM_EVENTS.READY, handleReady);
56
106
  transport.on(SYSTEM_EVENTS.PLAYER_JOIN, handlePlayerJoin);
57
107
  transport.on(SYSTEM_EVENTS.PLAYER_LEAVE, handlePlayerLeave);
108
+ transport.on(SYSTEM_EVENTS.CUSTOM_STATE_CHANGE, handleCustomStateChange);
58
109
  transport.on("room:player-left", (data) => {
59
- onPlayerLeaveRef.current?.(data?.sessionId ?? data?.playerId);
110
+ const playerIndex = data?.playerIndex ?? data?.player?.playerIndex;
111
+ if (playerIndex !== void 0) {
112
+ onPlayerLeaveRef.current?.(playerIndex);
113
+ }
60
114
  });
61
115
  transport.on("room:player-joined", (data) => {
62
- if (data?.player) {
63
- onPlayerJoinRef.current?.(data.player);
116
+ const playerIndex = data?.player?.playerIndex;
117
+ if (playerIndex !== void 0) {
118
+ onPlayerJoinRef.current?.(playerIndex);
64
119
  }
65
120
  });
66
121
  return () => {
67
122
  transport.off(SYSTEM_EVENTS.READY, handleReady);
68
123
  transport.off(SYSTEM_EVENTS.PLAYER_JOIN, handlePlayerJoin);
69
124
  transport.off(SYSTEM_EVENTS.PLAYER_LEAVE, handlePlayerLeave);
125
+ transport.off(SYSTEM_EVENTS.CUSTOM_STATE_CHANGE, handleCustomStateChange);
70
126
  transport.off("room:player-left");
71
127
  transport.off("room:player-joined");
72
128
  };
@@ -79,8 +135,10 @@ function useGameHost(config = {}) {
79
135
  if (!handler) continue;
80
136
  validateEventName(event);
81
137
  const wrappedHandler = (data) => {
82
- const { sessionId, ...rest } = data;
83
- handler(sessionId, rest);
138
+ const { playerIndex, ...rest } = data;
139
+ if (playerIndex !== void 0) {
140
+ handler(playerIndex, rest);
141
+ }
84
142
  };
85
143
  transport.on(event, wrappedHandler);
86
144
  cleanups.push(() => transport.off(event, wrappedHandler));
@@ -97,10 +155,10 @@ function useGameHost(config = {}) {
97
155
  [transport]
98
156
  );
99
157
  const sendToPlayer = useCallback(
100
- (sessionId, event, data) => {
158
+ (playerIndex, event, data) => {
101
159
  validateEventName(event);
102
160
  transport?.emit(SYSTEM_EVENTS.SEND_TO_PLAYER, {
103
- targetSessionId: sessionId,
161
+ targetPlayerIndex: playerIndex,
104
162
  event,
105
163
  data
106
164
  });
@@ -116,14 +174,29 @@ function useGameHost(config = {}) {
116
174
  const returnToLobby = useCallback(() => {
117
175
  transport?.emit(SYSTEM_EVENTS.RETURN_TO_LOBBY, {});
118
176
  }, [transport]);
177
+ const setCustomState = useCallback(
178
+ (state) => {
179
+ transport?.emit(SYSTEM_EVENTS.CUSTOM_STATE_CHANGE, { state });
180
+ },
181
+ [transport]
182
+ );
183
+ const leaderIndex = useMemo(() => {
184
+ if ("leaderIndex" in hostRoom && typeof hostRoom.leaderIndex === "number") {
185
+ return hostRoom.leaderIndex;
186
+ }
187
+ return -1;
188
+ }, [hostRoom]);
119
189
  return {
120
190
  players: hostRoom.players,
121
- leaderId: hostRoom.leaderId,
191
+ leaderIndex,
122
192
  roomCode: hostRoom.roomCode,
123
193
  emit,
124
194
  sendToPlayer,
125
195
  gameOver,
126
- returnToLobby
196
+ returnToLobby,
197
+ setCustomState,
198
+ isPaused,
199
+ latency
127
200
  };
128
201
  }
129
202