@smoregg/sdk 0.5.0 → 0.6.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 (40) hide show
  1. package/README.md +199 -0
  2. package/dist/cjs/controller.cjs +379 -0
  3. package/dist/cjs/controller.cjs.map +1 -0
  4. package/dist/cjs/index.cjs +8 -4
  5. package/dist/cjs/index.cjs.map +1 -1
  6. package/dist/cjs/screen.cjs +526 -0
  7. package/dist/cjs/screen.cjs.map +1 -0
  8. package/dist/cjs/testing.cjs +257 -0
  9. package/dist/cjs/testing.cjs.map +1 -0
  10. package/dist/cjs/transport/protocol.cjs.map +1 -1
  11. package/dist/esm/controller.js +376 -0
  12. package/dist/esm/controller.js.map +1 -0
  13. package/dist/esm/index.js +3 -2
  14. package/dist/esm/index.js.map +1 -1
  15. package/dist/esm/screen.js +523 -0
  16. package/dist/esm/screen.js.map +1 -0
  17. package/dist/esm/testing.js +254 -0
  18. package/dist/esm/testing.js.map +1 -0
  19. package/dist/esm/transport/protocol.js.map +1 -1
  20. package/dist/types/controller.d.ts +78 -0
  21. package/dist/types/controller.d.ts.map +1 -0
  22. package/dist/types/index.d.ts +17 -18
  23. package/dist/types/index.d.ts.map +1 -1
  24. package/dist/types/screen.d.ts +79 -0
  25. package/dist/types/screen.d.ts.map +1 -0
  26. package/dist/types/testing.d.ts +61 -0
  27. package/dist/types/testing.d.ts.map +1 -0
  28. package/dist/types/transport/protocol.d.ts +1 -4
  29. package/dist/types/transport/protocol.d.ts.map +1 -1
  30. package/dist/types/types.d.ts +869 -4
  31. package/dist/types/types.d.ts.map +1 -1
  32. package/dist/umd/smore-sdk-vanilla.umd.js +956 -331
  33. package/dist/umd/smore-sdk-vanilla.umd.js.map +1 -1
  34. package/dist/umd/smore-sdk-vanilla.umd.min.js +1 -1
  35. package/dist/umd/smore-sdk-vanilla.umd.min.js.map +1 -1
  36. package/dist/umd/smore-sdk.umd.js +956 -331
  37. package/dist/umd/smore-sdk.umd.js.map +1 -1
  38. package/dist/umd/smore-sdk.umd.min.js +1 -1
  39. package/dist/umd/smore-sdk.umd.min.js.map +1 -1
  40. package/package.json +1 -1
@@ -0,0 +1,257 @@
1
+ 'use strict';
2
+
3
+ function createMockScreen(options = {}) {
4
+ const {
5
+ roomCode = "TEST",
6
+ controllers: initialControllers = [],
7
+ autoReady = true
8
+ } = options;
9
+ let _controllers = [...initialControllers];
10
+ let _isReady = false;
11
+ let _isDestroyed = false;
12
+ let _leaderIndex = initialControllers[0]?.playerIndex ?? -1;
13
+ const listeners = /* @__PURE__ */ new Map();
14
+ const broadcasts = [];
15
+ const sends = [];
16
+ const screen = {
17
+ // === Properties ===
18
+ get controllers() {
19
+ return [..._controllers];
20
+ },
21
+ get roomCode() {
22
+ return roomCode;
23
+ },
24
+ get leaderIndex() {
25
+ return _leaderIndex;
26
+ },
27
+ get isReady() {
28
+ return _isReady;
29
+ },
30
+ get isDestroyed() {
31
+ return _isDestroyed;
32
+ },
33
+ // === Communication Methods ===
34
+ broadcast(event, data) {
35
+ if (_isDestroyed) {
36
+ throw new Error("Cannot broadcast: screen is destroyed");
37
+ }
38
+ broadcasts.push({ event, data });
39
+ },
40
+ broadcastRaw(event, data) {
41
+ if (_isDestroyed) {
42
+ throw new Error("Cannot broadcast: screen is destroyed");
43
+ }
44
+ broadcasts.push({ event, data });
45
+ },
46
+ sendToController(playerIndex, event, data) {
47
+ if (_isDestroyed) {
48
+ throw new Error("Cannot send: screen is destroyed");
49
+ }
50
+ if (!_controllers.some((c) => c.playerIndex === playerIndex)) {
51
+ throw new Error(`Invalid player index: ${playerIndex}`);
52
+ }
53
+ sends.push({ playerIndex, event, data });
54
+ },
55
+ sendToControllerRaw(playerIndex, event, data) {
56
+ if (_isDestroyed) {
57
+ throw new Error("Cannot send: screen is destroyed");
58
+ }
59
+ if (!_controllers.some((c) => c.playerIndex === playerIndex)) {
60
+ throw new Error(`Invalid player index: ${playerIndex}`);
61
+ }
62
+ sends.push({ playerIndex, event, data });
63
+ },
64
+ // === Game Lifecycle ===
65
+ gameOver(results) {
66
+ screen.broadcastRaw("game-over", results);
67
+ },
68
+ // === Event Subscription ===
69
+ on(event, handler) {
70
+ const eventStr = event;
71
+ if (!listeners.has(eventStr)) {
72
+ listeners.set(eventStr, /* @__PURE__ */ new Set());
73
+ }
74
+ listeners.get(eventStr).add(handler);
75
+ return () => {
76
+ listeners.get(eventStr)?.delete(handler);
77
+ };
78
+ },
79
+ once(event, handler) {
80
+ const wrapper = (playerIndex, data) => {
81
+ handler(playerIndex, data);
82
+ screen.off(event, wrapper);
83
+ };
84
+ return screen.on(event, wrapper);
85
+ },
86
+ off(event, handler) {
87
+ const eventStr = event;
88
+ if (!handler) {
89
+ listeners.delete(eventStr);
90
+ } else {
91
+ listeners.get(eventStr)?.delete(handler);
92
+ }
93
+ },
94
+ // === Utilities ===
95
+ getController(playerIndex) {
96
+ return _controllers.find((c) => c.playerIndex === playerIndex);
97
+ },
98
+ isLeader(playerIndex) {
99
+ return playerIndex === _leaderIndex;
100
+ },
101
+ getControllerCount() {
102
+ return _controllers.length;
103
+ },
104
+ // === Cleanup ===
105
+ destroy() {
106
+ _isDestroyed = true;
107
+ listeners.clear();
108
+ broadcasts.length = 0;
109
+ sends.length = 0;
110
+ },
111
+ // === Mock-specific methods ===
112
+ simulateEvent(playerIndex, event, data) {
113
+ const eventStr = event;
114
+ const handlers = listeners.get(eventStr);
115
+ if (handlers) {
116
+ handlers.forEach((handler) => {
117
+ handler(playerIndex, data);
118
+ });
119
+ }
120
+ },
121
+ simulateControllerJoin(info) {
122
+ _controllers.push(info);
123
+ if (_leaderIndex === -1) {
124
+ _leaderIndex = info.playerIndex;
125
+ }
126
+ },
127
+ simulateControllerLeave(playerIndex) {
128
+ _controllers = _controllers.filter((c) => c.playerIndex !== playerIndex);
129
+ if (_leaderIndex === playerIndex) {
130
+ _leaderIndex = _controllers[0]?.playerIndex ?? -1;
131
+ }
132
+ },
133
+ getBroadcasts() {
134
+ return [...broadcasts];
135
+ },
136
+ getSentToController(playerIndex) {
137
+ return sends.filter((s) => s.playerIndex === playerIndex).map((s) => ({ event: s.event, data: s.data }));
138
+ },
139
+ clearRecordedEvents() {
140
+ broadcasts.length = 0;
141
+ sends.length = 0;
142
+ },
143
+ triggerReady() {
144
+ _isReady = true;
145
+ }
146
+ };
147
+ if (autoReady) {
148
+ setTimeout(() => screen.triggerReady(), 0);
149
+ }
150
+ return screen;
151
+ }
152
+ function createMockController(options = {}) {
153
+ const {
154
+ roomCode = "TEST",
155
+ myIndex = 0,
156
+ isLeader: initialIsLeader = false,
157
+ autoReady = true
158
+ } = options;
159
+ let _isReady = false;
160
+ let _isDestroyed = false;
161
+ let _isLeader = initialIsLeader;
162
+ const listeners = /* @__PURE__ */ new Map();
163
+ const sentEvents = [];
164
+ const controller = {
165
+ // === Properties ===
166
+ get myIndex() {
167
+ return myIndex;
168
+ },
169
+ get isLeader() {
170
+ return _isLeader;
171
+ },
172
+ get roomCode() {
173
+ return roomCode;
174
+ },
175
+ get isReady() {
176
+ return _isReady;
177
+ },
178
+ get isDestroyed() {
179
+ return _isDestroyed;
180
+ },
181
+ // === Communication Methods ===
182
+ send(event, data) {
183
+ if (_isDestroyed) {
184
+ throw new Error("Cannot send: controller is destroyed");
185
+ }
186
+ sentEvents.push({ event, data });
187
+ },
188
+ sendRaw(event, data) {
189
+ if (_isDestroyed) {
190
+ throw new Error("Cannot send: controller is destroyed");
191
+ }
192
+ sentEvents.push({ event, data });
193
+ },
194
+ // === Event Subscription ===
195
+ on(event, handler) {
196
+ const eventStr = event;
197
+ if (!listeners.has(eventStr)) {
198
+ listeners.set(eventStr, /* @__PURE__ */ new Set());
199
+ }
200
+ listeners.get(eventStr).add(handler);
201
+ return () => {
202
+ listeners.get(eventStr)?.delete(handler);
203
+ };
204
+ },
205
+ once(event, handler) {
206
+ const wrapper = (data) => {
207
+ handler(data);
208
+ controller.off(event, wrapper);
209
+ };
210
+ return controller.on(event, wrapper);
211
+ },
212
+ off(event, handler) {
213
+ const eventStr = event;
214
+ if (!handler) {
215
+ listeners.delete(eventStr);
216
+ } else {
217
+ listeners.get(eventStr)?.delete(handler);
218
+ }
219
+ },
220
+ // === Cleanup ===
221
+ destroy() {
222
+ _isDestroyed = true;
223
+ listeners.clear();
224
+ sentEvents.length = 0;
225
+ },
226
+ // === Mock-specific methods ===
227
+ simulateEvent(event, data) {
228
+ const eventStr = event;
229
+ const handlers = listeners.get(eventStr);
230
+ if (handlers) {
231
+ handlers.forEach((handler) => {
232
+ handler(data);
233
+ });
234
+ }
235
+ },
236
+ getSentEvents() {
237
+ return [...sentEvents];
238
+ },
239
+ clearRecordedEvents() {
240
+ sentEvents.length = 0;
241
+ },
242
+ triggerReady() {
243
+ _isReady = true;
244
+ },
245
+ setLeader(isLeader) {
246
+ _isLeader = isLeader;
247
+ }
248
+ };
249
+ if (autoReady) {
250
+ setTimeout(() => controller.triggerReady(), 0);
251
+ }
252
+ return controller;
253
+ }
254
+
255
+ exports.createMockController = createMockController;
256
+ exports.createMockScreen = createMockScreen;
257
+ //# sourceMappingURL=testing.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.cjs","sources":["../../src/testing.ts"],"sourcesContent":["/**\n * @smoregg/sdk - Testing Utilities\n *\n * Mock implementations of Screen and Controller for unit testing.\n * All methods work synchronously for predictable test execution.\n *\n * @packageDocumentation\n */\n\nimport type {\n EventMap,\n EventNames,\n EventData,\n ControllerInfo,\n PlayerIndex,\n GameResults,\n ScreenEventHandler,\n ControllerEventHandler,\n MockScreen,\n MockController,\n MockOptions,\n} from './types';\n\n// =============================================================================\n// MOCK SCREEN IMPLEMENTATION\n// =============================================================================\n\ninterface RecordedBroadcast {\n event: string;\n data: unknown;\n}\n\ninterface RecordedSend {\n playerIndex: PlayerIndex;\n event: string;\n data: unknown;\n}\n\n/**\n * Create a mock Screen for testing game logic.\n *\n * All methods work synchronously. Events can be simulated and recorded\n * for assertions in unit tests.\n *\n * @example\n * ```ts\n * const screen = createMockScreen<MyEvents>({\n * controllers: [\n * { playerIndex: 0, nickname: 'Player 1', connected: true },\n * { playerIndex: 1, nickname: 'Player 2', connected: true },\n * ],\n * });\n *\n * // Simulate player input\n * screen.simulateEvent(0, 'tap', { x: 100, y: 200 });\n *\n * // Check what was broadcast\n * expect(screen.getBroadcasts()).toContainEqual({\n * event: 'score-update',\n * data: { scores: { 0: 10 } },\n * });\n * ```\n */\nexport function createMockScreen<TEvents extends EventMap = EventMap>(\n options: MockOptions = {},\n): MockScreen<TEvents> {\n const {\n roomCode = 'TEST',\n controllers: initialControllers = [],\n autoReady = true,\n } = options;\n\n // Internal state\n let _controllers: ControllerInfo[] = [...initialControllers];\n let _isReady = false;\n let _isDestroyed = false;\n let _leaderIndex: PlayerIndex = initialControllers[0]?.playerIndex ?? -1;\n\n // Event listeners\n const listeners = new Map<string, Set<ScreenEventHandler>>();\n\n // Lifecycle callbacks\n let onReadyCallback: (() => void) | undefined;\n let onControllerJoinCallback:\n | ((index: PlayerIndex, info: ControllerInfo) => void)\n | undefined;\n let onControllerLeaveCallback: ((index: PlayerIndex) => void) | undefined;\n\n // Recorded events for testing\n const broadcasts: RecordedBroadcast[] = [];\n const sends: RecordedSend[] = [];\n\n // Screen implementation\n const screen: MockScreen<TEvents> = {\n // === Properties ===\n get controllers() {\n return [..._controllers];\n },\n get roomCode() {\n return roomCode;\n },\n get leaderIndex() {\n return _leaderIndex;\n },\n get isReady() {\n return _isReady;\n },\n get isDestroyed() {\n return _isDestroyed;\n },\n\n // === Communication Methods ===\n broadcast<K extends EventNames<TEvents>>(\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n if (_isDestroyed) {\n throw new Error('Cannot broadcast: screen is destroyed');\n }\n broadcasts.push({ event: event as string, data });\n },\n\n broadcastRaw(event: string, data?: unknown): void {\n if (_isDestroyed) {\n throw new Error('Cannot broadcast: screen is destroyed');\n }\n broadcasts.push({ event, data });\n },\n\n sendToController<K extends EventNames<TEvents>>(\n playerIndex: PlayerIndex,\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n if (_isDestroyed) {\n throw new Error('Cannot send: screen is destroyed');\n }\n if (!_controllers.some((c) => c.playerIndex === playerIndex)) {\n throw new Error(`Invalid player index: ${playerIndex}`);\n }\n sends.push({ playerIndex, event: event as string, data });\n },\n\n sendToControllerRaw(\n playerIndex: PlayerIndex,\n event: string,\n data?: unknown,\n ): void {\n if (_isDestroyed) {\n throw new Error('Cannot send: screen is destroyed');\n }\n if (!_controllers.some((c) => c.playerIndex === playerIndex)) {\n throw new Error(`Invalid player index: ${playerIndex}`);\n }\n sends.push({ playerIndex, event, data });\n },\n\n // === Game Lifecycle ===\n gameOver(results?: GameResults): void {\n screen.broadcastRaw('game-over', results);\n },\n\n // === Event Subscription ===\n on<K extends EventNames<TEvents>>(\n event: K,\n handler: ScreenEventHandler<EventData<TEvents, K>>,\n ): () => void {\n const eventStr = event as string;\n if (!listeners.has(eventStr)) {\n listeners.set(eventStr, new Set());\n }\n listeners.get(eventStr)!.add(handler as ScreenEventHandler);\n\n return () => {\n listeners.get(eventStr)?.delete(handler as ScreenEventHandler);\n };\n },\n\n once<K extends EventNames<TEvents>>(\n event: K,\n handler: ScreenEventHandler<EventData<TEvents, K>>,\n ): () => void {\n const wrapper: ScreenEventHandler<EventData<TEvents, K>> = (playerIndex, data) => {\n handler(playerIndex, data);\n screen.off(event, wrapper);\n };\n return screen.on(event, wrapper);\n },\n\n off<K extends EventNames<TEvents>>(\n event: K,\n handler?: ScreenEventHandler<EventData<TEvents, K>>,\n ): void {\n const eventStr = event as string;\n if (!handler) {\n listeners.delete(eventStr);\n } else {\n listeners.get(eventStr)?.delete(handler as ScreenEventHandler);\n }\n },\n\n // === Utilities ===\n getController(playerIndex: PlayerIndex): ControllerInfo | undefined {\n return _controllers.find((c) => c.playerIndex === playerIndex);\n },\n\n isLeader(playerIndex: PlayerIndex): boolean {\n return playerIndex === _leaderIndex;\n },\n\n getControllerCount(): number {\n return _controllers.length;\n },\n\n // === Cleanup ===\n destroy(): void {\n _isDestroyed = true;\n listeners.clear();\n broadcasts.length = 0;\n sends.length = 0;\n },\n\n // === Mock-specific methods ===\n simulateEvent<K extends EventNames<TEvents>>(\n playerIndex: PlayerIndex,\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n const eventStr = event as string;\n const handlers = listeners.get(eventStr);\n if (handlers) {\n handlers.forEach((handler) => {\n handler(playerIndex, data);\n });\n }\n },\n\n simulateControllerJoin(info: ControllerInfo): void {\n _controllers.push(info);\n if (_leaderIndex === -1) {\n _leaderIndex = info.playerIndex;\n }\n if (onControllerJoinCallback) {\n onControllerJoinCallback(info.playerIndex, info);\n }\n },\n\n simulateControllerLeave(playerIndex: PlayerIndex): void {\n _controllers = _controllers.filter((c) => c.playerIndex !== playerIndex);\n if (_leaderIndex === playerIndex) {\n _leaderIndex = _controllers[0]?.playerIndex ?? -1;\n }\n if (onControllerLeaveCallback) {\n onControllerLeaveCallback(playerIndex);\n }\n },\n\n getBroadcasts(): Array<{ event: string; data: unknown }> {\n return [...broadcasts];\n },\n\n getSentToController(\n playerIndex: PlayerIndex,\n ): Array<{ event: string; data: unknown }> {\n return sends\n .filter((s) => s.playerIndex === playerIndex)\n .map((s) => ({ event: s.event, data: s.data }));\n },\n\n clearRecordedEvents(): void {\n broadcasts.length = 0;\n sends.length = 0;\n },\n\n triggerReady(): void {\n _isReady = true;\n if (onReadyCallback) {\n onReadyCallback();\n }\n },\n };\n\n // Auto-trigger ready if enabled\n if (autoReady) {\n setTimeout(() => screen.triggerReady(), 0);\n }\n\n return screen;\n}\n\n// =============================================================================\n// MOCK CONTROLLER IMPLEMENTATION\n// =============================================================================\n\ninterface RecordedEvent {\n event: string;\n data: unknown;\n}\n\n/**\n * Create a mock Controller for testing player input logic.\n *\n * All methods work synchronously. Events can be simulated and recorded\n * for assertions in unit tests.\n *\n * @example\n * ```ts\n * const controller = createMockController<MyEvents>({\n * myIndex: 0,\n * isLeader: true,\n * });\n *\n * // Simulate receiving from screen\n * controller.simulateEvent('your-turn', { timeLimit: 30 });\n *\n * // Check what was sent\n * expect(controller.getSentEvents()).toContainEqual({\n * event: 'answer',\n * data: { choice: 2 },\n * });\n * ```\n */\nexport function createMockController<TEvents extends EventMap = EventMap>(\n options: MockOptions = {},\n): MockController<TEvents> {\n const {\n roomCode = 'TEST',\n myIndex = 0,\n isLeader: initialIsLeader = false,\n autoReady = true,\n } = options;\n\n // Internal state\n let _isReady = false;\n let _isDestroyed = false;\n let _isLeader = initialIsLeader;\n\n // Event listeners\n const listeners = new Map<string, Set<ControllerEventHandler>>();\n\n // Lifecycle callbacks\n let onReadyCallback: (() => void) | undefined;\n\n // Recorded events for testing\n const sentEvents: RecordedEvent[] = [];\n\n // Controller implementation\n const controller: MockController<TEvents> = {\n // === Properties ===\n get myIndex() {\n return myIndex;\n },\n get isLeader() {\n return _isLeader;\n },\n get roomCode() {\n return roomCode;\n },\n get isReady() {\n return _isReady;\n },\n get isDestroyed() {\n return _isDestroyed;\n },\n\n // === Communication Methods ===\n send<K extends EventNames<TEvents>>(\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n if (_isDestroyed) {\n throw new Error('Cannot send: controller is destroyed');\n }\n sentEvents.push({ event: event as string, data });\n },\n\n sendRaw(event: string, data?: unknown): void {\n if (_isDestroyed) {\n throw new Error('Cannot send: controller is destroyed');\n }\n sentEvents.push({ event, data });\n },\n\n // === Event Subscription ===\n on<K extends EventNames<TEvents>>(\n event: K,\n handler: ControllerEventHandler<EventData<TEvents, K>>,\n ): () => void {\n const eventStr = event as string;\n if (!listeners.has(eventStr)) {\n listeners.set(eventStr, new Set());\n }\n listeners.get(eventStr)!.add(handler as ControllerEventHandler);\n\n return () => {\n listeners.get(eventStr)?.delete(handler as ControllerEventHandler);\n };\n },\n\n once<K extends EventNames<TEvents>>(\n event: K,\n handler: ControllerEventHandler<EventData<TEvents, K>>,\n ): () => void {\n const wrapper: ControllerEventHandler<EventData<TEvents, K>> = (data) => {\n handler(data);\n controller.off(event, wrapper);\n };\n return controller.on(event, wrapper);\n },\n\n off<K extends EventNames<TEvents>>(\n event: K,\n handler?: ControllerEventHandler<EventData<TEvents, K>>,\n ): void {\n const eventStr = event as string;\n if (!handler) {\n listeners.delete(eventStr);\n } else {\n listeners.get(eventStr)?.delete(handler as ControllerEventHandler);\n }\n },\n\n // === Cleanup ===\n destroy(): void {\n _isDestroyed = true;\n listeners.clear();\n sentEvents.length = 0;\n },\n\n // === Mock-specific methods ===\n simulateEvent<K extends EventNames<TEvents>>(\n event: K,\n data: EventData<TEvents, K>,\n ): void {\n const eventStr = event as string;\n const handlers = listeners.get(eventStr);\n if (handlers) {\n handlers.forEach((handler) => {\n handler(data);\n });\n }\n },\n\n getSentEvents(): Array<{ event: string; data: unknown }> {\n return [...sentEvents];\n },\n\n clearRecordedEvents(): void {\n sentEvents.length = 0;\n },\n\n triggerReady(): void {\n _isReady = true;\n if (onReadyCallback) {\n onReadyCallback();\n }\n },\n\n setLeader(isLeader: boolean): void {\n _isLeader = isLeader;\n },\n };\n\n // Auto-trigger ready if enabled\n if (autoReady) {\n setTimeout(() => controller.triggerReady(), 0);\n }\n\n return controller;\n}\n\n// =============================================================================\n// EXPORTS\n// =============================================================================\n\nexport type { MockScreen, MockController, MockOptions };\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.cjs","sources":["../../../src/transport/protocol.ts"],"sourcesContent":["/**\n * postMessage protocol types for iframe ↔ parent communication.\n */\n\nexport const SMORE_MSG_PREFIX = 'smore:' as const;\n\nexport interface SmoreReadyMessage {\n type: 'smore:ready';\n}\n\nexport interface SmoreInitMessage {\n type: 'smore:init';\n payload: {\n side: 'host' | 'player';\n roomCode: string;\n players: any[];\n leaderId: string | null;\n myIndex?: number;\n isLeader?: boolean;\n };\n}\n\nexport interface SmoreEmitMessage {\n type: 'smore:emit';\n payload: {\n event: string;\n data?: any;\n ackId?: string;\n };\n}\n\nexport interface SmoreEventMessage {\n type: 'smore:event';\n payload: {\n event: string;\n data?: any;\n };\n}\n\nexport interface SmoreAckMessage {\n type: 'smore:ack';\n payload: {\n ackId: string;\n data?: any;\n };\n}\n\nexport interface SmoreUpdateMessage {\n type: 'smore:update';\n payload: {\n players?: any[];\n leaderId?: string | null;\n };\n}\n\nexport interface SmoreLoadedMessage {\n type: 'smore:loaded';\n}\n\nexport type SmoreMessage =\n | SmoreReadyMessage\n | SmoreInitMessage\n | SmoreEmitMessage\n | SmoreEventMessage\n | SmoreAckMessage\n | SmoreUpdateMessage\n | SmoreLoadedMessage;\n\nexport function isSmoreMessage(data: any): data is SmoreMessage {\n return data && typeof data === 'object' && typeof data.type === 'string' && data.type.startsWith(SMORE_MSG_PREFIX);\n}\n"],"names":[],"mappings":";;AAIO,MAAM,gBAAA,GAAmB;AAgEzB,SAAS,eAAe,IAAA,EAAiC;AAC9D,EAAA,OAAO,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,IAAY,OAAO,IAAA,CAAK,IAAA,KAAS,QAAA,IAAY,IAAA,CAAK,IAAA,CAAK,UAAA,CAAW,gBAAgB,CAAA;AACnH;;;;;"}
1
+ {"version":3,"file":"protocol.cjs","sources":["../../../src/transport/protocol.ts"],"sourcesContent":["/**\n * postMessage protocol types for iframe ↔ parent communication.\n */\n\nexport const SMORE_MSG_PREFIX = 'smore:' as const;\n\nexport interface SmoreReadyMessage {\n type: 'smore:ready';\n}\n\nexport interface SmoreInitMessage {\n type: 'smore:init';\n payload: {\n side: 'host' | 'player';\n roomCode: string;\n players: any[];\n leaderId: string | null;\n myIndex?: number;\n isLeader?: boolean;\n };\n}\n\nexport interface SmoreEmitMessage {\n type: 'smore:emit';\n payload: {\n event: string;\n data?: any;\n ackId?: string;\n };\n}\n\nexport interface SmoreEventMessage {\n type: 'smore:event';\n payload: {\n event: string;\n data?: any;\n };\n}\n\nexport interface SmoreAckMessage {\n type: 'smore:ack';\n payload: {\n ackId: string;\n data?: any;\n };\n}\n\nexport interface SmoreUpdateMessage {\n type: 'smore:update';\n payload: {\n players?: any[];\n leaderId?: string | null;\n };\n}\n\n// DEPRECATED: SmoreLoadedMessage removed - no longer used in protocol\n// Previously: interface SmoreLoadedMessage { type: 'smore:loaded' }\n\nexport type SmoreMessage =\n | SmoreReadyMessage\n | SmoreInitMessage\n | SmoreEmitMessage\n | SmoreEventMessage\n | SmoreAckMessage\n | SmoreUpdateMessage;\n\nexport function isSmoreMessage(data: any): data is SmoreMessage {\n return data && typeof data === 'object' && typeof data.type === 'string' && data.type.startsWith(SMORE_MSG_PREFIX);\n}\n"],"names":[],"mappings":";;AAIO,MAAM,gBAAA,GAAmB;AA8DzB,SAAS,eAAe,IAAA,EAAiC;AAC9D,EAAA,OAAO,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,IAAY,OAAO,IAAA,CAAK,IAAA,KAAS,QAAA,IAAY,IAAA,CAAK,IAAA,CAAK,UAAA,CAAW,gBAAgB,CAAA;AACnH;;;;;"}
@@ -0,0 +1,376 @@
1
+ import { PostMessageTransport } from './transport/PostMessageTransport.js';
2
+ import { isSmoreMessage } from './transport/protocol.js';
3
+
4
+ const SYSTEM_PREFIX = "smore:";
5
+ const SYSTEM_EVENTS = {
6
+ PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,
7
+ PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`
8
+ };
9
+ const DEFAULT_TIMEOUT = 1e4;
10
+ class SmoreSDKError extends Error {
11
+ code;
12
+ cause;
13
+ details;
14
+ constructor(code, message, options) {
15
+ super(message);
16
+ this.name = "SmoreSDKError";
17
+ this.code = code;
18
+ this.cause = options?.cause;
19
+ this.details = options?.details;
20
+ const ErrorWithCapture = Error;
21
+ if (typeof ErrorWithCapture.captureStackTrace === "function") {
22
+ ErrorWithCapture.captureStackTrace(this, SmoreSDKError);
23
+ }
24
+ }
25
+ toSmoreError() {
26
+ return {
27
+ code: this.code,
28
+ message: this.message,
29
+ cause: this.cause,
30
+ details: this.details
31
+ };
32
+ }
33
+ }
34
+ const EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
35
+ function validateEventName(event) {
36
+ if (!event || typeof event !== "string") {
37
+ throw new SmoreSDKError("INVALID_EVENT", "Event name must be a non-empty string");
38
+ }
39
+ if (!EVENT_NAME_REGEX.test(event)) {
40
+ throw new SmoreSDKError(
41
+ "INVALID_EVENT",
42
+ `Invalid event name "${event}". Event names must:
43
+ - Start with a letter (a-z, A-Z)
44
+ - Only contain letters, numbers, hyphens (-), and underscores (_)
45
+ - End with a letter or number`,
46
+ { details: { event } }
47
+ );
48
+ }
49
+ }
50
+ function createLogger(options) {
51
+ const enabled = typeof options === "boolean" ? options : options?.enabled ?? false;
52
+ const level = (typeof options === "object" ? options.level : void 0) ?? "debug";
53
+ const prefix = (typeof options === "object" ? options.prefix : void 0) ?? "[SmoreController]";
54
+ const customLogger = typeof options === "object" ? options.logger : void 0;
55
+ const levelPriority = {
56
+ debug: 0,
57
+ info: 1,
58
+ warn: 2,
59
+ error: 3
60
+ };
61
+ const shouldLog = (msgLevel) => {
62
+ if (!enabled) return false;
63
+ return levelPriority[msgLevel] >= levelPriority[level];
64
+ };
65
+ const log = (msgLevel, message, data) => {
66
+ if (!shouldLog(msgLevel)) return;
67
+ if (customLogger) {
68
+ customLogger(msgLevel, message, data);
69
+ return;
70
+ }
71
+ const fullMessage = `${prefix} ${message}`;
72
+ const consoleFn = console[msgLevel] ?? console.log;
73
+ if (data !== void 0) {
74
+ consoleFn(fullMessage, data);
75
+ } else {
76
+ consoleFn(fullMessage);
77
+ }
78
+ };
79
+ return {
80
+ debug: (msg, data) => log("debug", msg, data),
81
+ info: (msg, data) => log("info", msg, data),
82
+ warn: (msg, data) => log("warn", msg, data),
83
+ error: (msg, data) => log("error", msg, data)
84
+ };
85
+ }
86
+ class ControllerImpl {
87
+ transport = null;
88
+ config;
89
+ logger;
90
+ _roomCode = "";
91
+ _myIndex = -1;
92
+ _isLeader = false;
93
+ _isReady = false;
94
+ _isDestroyed = false;
95
+ boundMessageHandler = null;
96
+ registeredHandlers = [];
97
+ eventListeners = /* @__PURE__ */ new Map();
98
+ constructor(config = {}) {
99
+ this.config = config;
100
+ this.logger = createLogger(config.debug);
101
+ if (config.listeners) {
102
+ for (const event of Object.keys(config.listeners)) {
103
+ validateEventName(event);
104
+ }
105
+ }
106
+ }
107
+ // ---------------------------------------------------------------------------
108
+ // Properties (readonly)
109
+ // ---------------------------------------------------------------------------
110
+ get myIndex() {
111
+ return this._myIndex;
112
+ }
113
+ get isLeader() {
114
+ return this._isLeader;
115
+ }
116
+ get roomCode() {
117
+ return this._roomCode;
118
+ }
119
+ get isReady() {
120
+ return this._isReady;
121
+ }
122
+ get isDestroyed() {
123
+ return this._isDestroyed;
124
+ }
125
+ // ---------------------------------------------------------------------------
126
+ // Initialization
127
+ // ---------------------------------------------------------------------------
128
+ async initialize() {
129
+ const parentOrigin = this.config.parentOrigin ?? "*";
130
+ const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
131
+ this.logger.debug("Initializing controller...", { parentOrigin, timeout });
132
+ return new Promise((resolve, reject) => {
133
+ const timeoutId = setTimeout(() => {
134
+ this.cleanup();
135
+ const error = new SmoreSDKError(
136
+ "TIMEOUT",
137
+ `Controller initialization timed out after ${timeout}ms. Make sure the parent window sends smore:init message.`,
138
+ { details: { timeout } }
139
+ );
140
+ this.handleError(error);
141
+ reject(error);
142
+ }, timeout);
143
+ this.boundMessageHandler = (e) => {
144
+ if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
145
+ const msg = e.data;
146
+ if (!isSmoreMessage(msg)) return;
147
+ if (msg.type === "smore:init") {
148
+ clearTimeout(timeoutId);
149
+ this.handleInit(msg, parentOrigin, resolve, reject);
150
+ } else if (msg.type === "smore:update") {
151
+ this.handleUpdate(msg);
152
+ }
153
+ };
154
+ window.addEventListener("message", this.boundMessageHandler);
155
+ this.logger.debug("Sending smore:ready to parent");
156
+ window.parent.postMessage({ type: "smore:ready" }, parentOrigin);
157
+ });
158
+ }
159
+ handleInit(msg, parentOrigin, resolve, reject) {
160
+ const initData = msg.payload;
161
+ this.logger.debug("Received smore:init", initData);
162
+ if (initData.side !== "player") {
163
+ const error = new SmoreSDKError(
164
+ "INIT_FAILED",
165
+ `Controller received init for wrong side: ${initData.side}`,
166
+ { details: { side: initData.side } }
167
+ );
168
+ this.handleError(error);
169
+ reject(error);
170
+ return;
171
+ }
172
+ if (initData.myIndex === void 0) {
173
+ const error = new SmoreSDKError(
174
+ "INIT_FAILED",
175
+ "Missing myIndex in init payload",
176
+ { details: initData }
177
+ );
178
+ this.handleError(error);
179
+ reject(error);
180
+ return;
181
+ }
182
+ this.transport = new PostMessageTransport(parentOrigin);
183
+ this._roomCode = initData.roomCode;
184
+ this._myIndex = initData.myIndex;
185
+ this._isLeader = initData.isLeader ?? false;
186
+ this.setupEventHandlers();
187
+ this._isReady = true;
188
+ this.logger.info("Controller ready", {
189
+ roomCode: this._roomCode,
190
+ myIndex: this._myIndex,
191
+ isLeader: this._isLeader
192
+ });
193
+ this.config.onReady?.();
194
+ resolve();
195
+ }
196
+ handleUpdate(msg) {
197
+ const updateData = msg.payload;
198
+ this.logger.debug("Received smore:update", updateData);
199
+ }
200
+ setupEventHandlers() {
201
+ if (!this.transport) return;
202
+ this.registerHandler(
203
+ SYSTEM_EVENTS.PLAYER_JOIN,
204
+ (data) => {
205
+ const playerIndex = data.player?.playerIndex ?? data.playerIndex;
206
+ if (playerIndex !== void 0) {
207
+ this.logger.debug("Player joined", { playerIndex });
208
+ this.config.onControllerJoin?.(playerIndex, data.player);
209
+ }
210
+ }
211
+ );
212
+ this.registerHandler(
213
+ SYSTEM_EVENTS.PLAYER_LEAVE,
214
+ (data) => {
215
+ const playerIndex = data.player?.playerIndex ?? data.playerIndex;
216
+ if (playerIndex !== void 0) {
217
+ this.logger.debug("Player left", { playerIndex });
218
+ this.config.onControllerLeave?.(playerIndex);
219
+ }
220
+ }
221
+ );
222
+ if (this.config.listeners) {
223
+ for (const [event, handler] of Object.entries(this.config.listeners)) {
224
+ if (!handler) continue;
225
+ this.registerHandler(event, (data) => {
226
+ this.logReceive(event, data);
227
+ handler(data);
228
+ });
229
+ }
230
+ }
231
+ }
232
+ registerHandler(event, handler) {
233
+ if (!this.transport) return;
234
+ this.transport.on(event, handler);
235
+ this.registeredHandlers.push({ event, handler });
236
+ }
237
+ // ---------------------------------------------------------------------------
238
+ // Communication Methods
239
+ // ---------------------------------------------------------------------------
240
+ send(event, data) {
241
+ this.ensureReady("send");
242
+ validateEventName(event);
243
+ this.logSend(event, data);
244
+ this.transport.emit(event, data);
245
+ }
246
+ sendRaw(event, data) {
247
+ this.ensureReady("sendRaw");
248
+ validateEventName(event);
249
+ this.logSend(event, data);
250
+ this.transport.emit(event, data);
251
+ }
252
+ // ---------------------------------------------------------------------------
253
+ // Event Subscription
254
+ // ---------------------------------------------------------------------------
255
+ on(event, handler) {
256
+ validateEventName(event);
257
+ let listeners = this.eventListeners.get(event);
258
+ if (!listeners) {
259
+ listeners = /* @__PURE__ */ new Set();
260
+ this.eventListeners.set(event, listeners);
261
+ }
262
+ listeners.add(handler);
263
+ const transportHandler = (data) => {
264
+ this.logReceive(event, data);
265
+ handler(data);
266
+ };
267
+ if (this.transport) {
268
+ this.transport.on(event, transportHandler);
269
+ this.registeredHandlers.push({ event, handler: transportHandler });
270
+ }
271
+ return () => {
272
+ listeners?.delete(handler);
273
+ if (listeners?.size === 0) {
274
+ this.eventListeners.delete(event);
275
+ }
276
+ this.transport?.off(event, transportHandler);
277
+ this.registeredHandlers = this.registeredHandlers.filter(
278
+ (h) => h.handler !== transportHandler
279
+ );
280
+ };
281
+ }
282
+ once(event, handler) {
283
+ const unsubscribe = this.on(event, ((data) => {
284
+ unsubscribe();
285
+ handler(data);
286
+ }));
287
+ return unsubscribe;
288
+ }
289
+ off(event, handler) {
290
+ if (!handler) {
291
+ this.eventListeners.delete(event);
292
+ this.transport?.off(event);
293
+ this.registeredHandlers = this.registeredHandlers.filter((h) => h.event !== event);
294
+ } else {
295
+ const listeners = this.eventListeners.get(event);
296
+ listeners?.delete(handler);
297
+ if (listeners?.size === 0) {
298
+ this.eventListeners.delete(event);
299
+ }
300
+ }
301
+ }
302
+ // ---------------------------------------------------------------------------
303
+ // Cleanup
304
+ // ---------------------------------------------------------------------------
305
+ destroy() {
306
+ if (this._isDestroyed) return;
307
+ this.logger.info("Destroying controller");
308
+ this.cleanup();
309
+ this._isDestroyed = true;
310
+ }
311
+ cleanup() {
312
+ this._isReady = false;
313
+ for (const { event, handler } of this.registeredHandlers) {
314
+ this.transport?.off(event, handler);
315
+ }
316
+ this.registeredHandlers = [];
317
+ this.eventListeners.clear();
318
+ if (this.transport) {
319
+ this.transport.destroy();
320
+ this.transport = null;
321
+ }
322
+ if (this.boundMessageHandler) {
323
+ window.removeEventListener("message", this.boundMessageHandler);
324
+ this.boundMessageHandler = null;
325
+ }
326
+ }
327
+ // ---------------------------------------------------------------------------
328
+ // Private Helpers
329
+ // ---------------------------------------------------------------------------
330
+ ensureReady(method) {
331
+ if (this._isDestroyed) {
332
+ throw new SmoreSDKError(
333
+ "DESTROYED",
334
+ `Cannot call ${method}() after destroy()`,
335
+ { details: { method } }
336
+ );
337
+ }
338
+ if (!this._isReady || !this.transport) {
339
+ throw new SmoreSDKError(
340
+ "NOT_READY",
341
+ `Cannot call ${method}() before controller is ready. Use await createController() or wait for onReady callback.`,
342
+ { details: { method, isReady: this._isReady } }
343
+ );
344
+ }
345
+ }
346
+ handleError(error) {
347
+ if (this.config.onError) {
348
+ this.config.onError(error.toSmoreError());
349
+ } else {
350
+ this.logger.error(error.message, error.details);
351
+ }
352
+ }
353
+ logSend(event, data) {
354
+ const options = this.config.debug;
355
+ const shouldLog = typeof options === "object" ? options.logSend ?? true : Boolean(options);
356
+ if (shouldLog) {
357
+ this.logger.debug(`\u2192 SEND [${event}]`, data);
358
+ }
359
+ }
360
+ logReceive(event, data) {
361
+ const options = this.config.debug;
362
+ const shouldLog = typeof options === "object" ? options.logReceive ?? true : Boolean(options);
363
+ if (shouldLog) {
364
+ this.logger.debug(`\u2190 RECV [${event}]`, data);
365
+ }
366
+ }
367
+ }
368
+ function createController(config) {
369
+ const controller = new ControllerImpl(config ?? {});
370
+ const promise = controller.initialize().then(() => controller);
371
+ promise.instance = controller;
372
+ return promise;
373
+ }
374
+
375
+ export { SmoreSDKError, createController };
376
+ //# sourceMappingURL=controller.js.map