@smoregg/sdk 0.4.1 → 0.6.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.
- package/README.md +199 -0
- package/dist/cjs/SmoreHost.cjs +306 -0
- package/dist/cjs/SmoreHost.cjs.map +1 -0
- package/dist/cjs/SmorePlayer.cjs +229 -0
- package/dist/cjs/SmorePlayer.cjs.map +1 -0
- package/dist/cjs/components/IframeGameBridge.cjs +115 -0
- package/dist/cjs/components/IframeGameBridge.cjs.map +1 -0
- package/dist/cjs/context/RoomProvider.cjs +3 -3
- package/dist/cjs/context/RoomProvider.cjs.map +1 -1
- package/dist/cjs/controller.cjs +379 -0
- package/dist/cjs/controller.cjs.map +1 -0
- package/dist/cjs/hooks/useGameHost.cjs +86 -13
- package/dist/cjs/hooks/useGameHost.cjs.map +1 -1
- package/dist/cjs/hooks/useGamePlayer.cjs +60 -4
- package/dist/cjs/hooks/useGamePlayer.cjs.map +1 -1
- package/dist/cjs/iframe/index.cjs +50 -316
- package/dist/cjs/iframe/index.cjs.map +1 -1
- package/dist/cjs/index.cjs +8 -22
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/screen.cjs +526 -0
- package/dist/cjs/screen.cjs.map +1 -0
- package/dist/cjs/testing.cjs +257 -0
- package/dist/cjs/testing.cjs.map +1 -0
- package/dist/cjs/transport/protocol.cjs.map +1 -1
- package/dist/cjs/utils/connectionMonitor.cjs +77 -0
- package/dist/cjs/utils/connectionMonitor.cjs.map +1 -0
- package/dist/cjs/utils/preloadAssets.cjs +66 -0
- package/dist/cjs/utils/preloadAssets.cjs.map +1 -0
- package/dist/cjs/utils/serverTime.cjs +43 -0
- package/dist/cjs/utils/serverTime.cjs.map +1 -0
- package/dist/esm/SmoreHost.js +304 -0
- package/dist/esm/SmoreHost.js.map +1 -0
- package/dist/esm/SmorePlayer.js +227 -0
- package/dist/esm/SmorePlayer.js.map +1 -0
- package/dist/esm/components/IframeGameBridge.js +113 -0
- package/dist/esm/components/IframeGameBridge.js.map +1 -0
- package/dist/esm/context/RoomProvider.js +3 -3
- package/dist/esm/context/RoomProvider.js.map +1 -1
- package/dist/esm/controller.js +376 -0
- package/dist/esm/controller.js.map +1 -0
- package/dist/esm/hooks/useGameHost.js +87 -14
- package/dist/esm/hooks/useGameHost.js.map +1 -1
- package/dist/esm/hooks/useGamePlayer.js +61 -5
- package/dist/esm/hooks/useGamePlayer.js.map +1 -1
- package/dist/esm/iframe/index.js +51 -314
- package/dist/esm/iframe/index.js.map +1 -1
- package/dist/esm/index.js +3 -8
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/screen.js +523 -0
- package/dist/esm/screen.js.map +1 -0
- package/dist/esm/testing.js +254 -0
- package/dist/esm/testing.js.map +1 -0
- package/dist/esm/transport/protocol.js.map +1 -1
- package/dist/esm/utils/connectionMonitor.js +75 -0
- package/dist/esm/utils/connectionMonitor.js.map +1 -0
- package/dist/esm/utils/preloadAssets.js +63 -0
- package/dist/esm/utils/preloadAssets.js.map +1 -0
- package/dist/esm/utils/serverTime.js +41 -0
- package/dist/esm/utils/serverTime.js.map +1 -0
- package/dist/types/SmoreHost.d.ts +187 -0
- package/dist/types/SmoreHost.d.ts.map +1 -0
- package/dist/types/SmorePlayer.d.ts +146 -0
- package/dist/types/SmorePlayer.d.ts.map +1 -0
- package/dist/types/components/IframeGameBridge.d.ts +2 -2
- package/dist/types/components/IframeGameBridge.d.ts.map +1 -1
- package/dist/types/components/index.d.ts +2 -4
- package/dist/types/components/index.d.ts.map +1 -1
- package/dist/types/context/RoomProvider.d.ts +3 -3
- package/dist/types/context/RoomProvider.d.ts.map +1 -1
- package/dist/types/controller.d.ts +78 -0
- package/dist/types/controller.d.ts.map +1 -0
- package/dist/types/hooks/useGameHost.d.ts +33 -7
- package/dist/types/hooks/useGameHost.d.ts.map +1 -1
- package/dist/types/hooks/useGamePlayer.d.ts +29 -3
- package/dist/types/hooks/useGamePlayer.d.ts.map +1 -1
- package/dist/types/iframe/index.d.ts +10 -10
- package/dist/types/iframe/index.d.ts.map +1 -1
- package/dist/types/iframe/vanilla.d.ts +12 -4
- package/dist/types/iframe/vanilla.d.ts.map +1 -1
- package/dist/types/index.d.ts +36 -21
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/screen.d.ts +79 -0
- package/dist/types/screen.d.ts.map +1 -0
- package/dist/types/testing.d.ts +61 -0
- package/dist/types/testing.d.ts.map +1 -0
- package/dist/types/transport/protocol.d.ts +2 -5
- package/dist/types/transport/protocol.d.ts.map +1 -1
- package/dist/types/types.d.ts +869 -4
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils/connectionMonitor.d.ts +57 -0
- package/dist/types/utils/connectionMonitor.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +7 -0
- package/dist/types/utils/index.d.ts.map +1 -0
- package/dist/types/utils/preloadAssets.d.ts +29 -0
- package/dist/types/utils/preloadAssets.d.ts.map +1 -0
- package/dist/types/utils/serverTime.d.ts +28 -0
- package/dist/types/utils/serverTime.d.ts.map +1 -0
- package/dist/umd/smore-sdk-iframe.umd.js +54 -317
- package/dist/umd/smore-sdk-iframe.umd.js.map +1 -1
- package/dist/umd/smore-sdk-iframe.umd.min.js +1 -1
- package/dist/umd/smore-sdk-iframe.umd.min.js.map +1 -1
- package/dist/umd/smore-sdk-vanilla.umd.js +1166 -117
- package/dist/umd/smore-sdk-vanilla.umd.js.map +1 -1
- package/dist/umd/smore-sdk-vanilla.umd.min.js +1 -1
- package/dist/umd/smore-sdk-vanilla.umd.min.js.map +1 -1
- package/dist/umd/smore-sdk.umd.js +1139 -602
- package/dist/umd/smore-sdk.umd.js.map +1 -1
- package/dist/umd/smore-sdk.umd.min.js +1 -1
- package/dist/umd/smore-sdk.umd.min.js.map +1 -1
- package/package.json +1 -26
|
@@ -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
|
|
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,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createConnectionMonitor(socket, options) {
|
|
4
|
+
const {
|
|
5
|
+
onPause,
|
|
6
|
+
onResume,
|
|
7
|
+
latencyThreshold = 2e3,
|
|
8
|
+
resumeCountdown = 3e3,
|
|
9
|
+
onCountdown
|
|
10
|
+
} = options;
|
|
11
|
+
let paused = false;
|
|
12
|
+
let latency = 0;
|
|
13
|
+
let pingInterval = null;
|
|
14
|
+
let resumeTimeout = null;
|
|
15
|
+
let countdownInterval = null;
|
|
16
|
+
const checkConnection = () => {
|
|
17
|
+
const start = Date.now();
|
|
18
|
+
socket.emit("ping", {}, () => {
|
|
19
|
+
latency = Date.now() - start;
|
|
20
|
+
if (latency > latencyThreshold && !paused) {
|
|
21
|
+
paused = true;
|
|
22
|
+
console.warn(`[ConnectionMonitor] High latency (${latency}ms), pausing`);
|
|
23
|
+
onPause();
|
|
24
|
+
} else if (latency <= latencyThreshold && paused) {
|
|
25
|
+
console.log(`[ConnectionMonitor] Connection restored, starting countdown`);
|
|
26
|
+
startResumeCountdown();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
const startResumeCountdown = () => {
|
|
31
|
+
if (resumeTimeout) clearTimeout(resumeTimeout);
|
|
32
|
+
if (countdownInterval) clearInterval(countdownInterval);
|
|
33
|
+
let secondsLeft = Math.ceil(resumeCountdown / 1e3);
|
|
34
|
+
onCountdown?.(secondsLeft);
|
|
35
|
+
countdownInterval = setInterval(() => {
|
|
36
|
+
secondsLeft--;
|
|
37
|
+
if (secondsLeft > 0) {
|
|
38
|
+
onCountdown?.(secondsLeft);
|
|
39
|
+
}
|
|
40
|
+
}, 1e3);
|
|
41
|
+
resumeTimeout = setTimeout(() => {
|
|
42
|
+
if (countdownInterval) clearInterval(countdownInterval);
|
|
43
|
+
paused = false;
|
|
44
|
+
console.log(`[ConnectionMonitor] Resuming game`);
|
|
45
|
+
onResume();
|
|
46
|
+
}, resumeCountdown);
|
|
47
|
+
};
|
|
48
|
+
const handleDisconnect = () => {
|
|
49
|
+
if (!paused) {
|
|
50
|
+
paused = true;
|
|
51
|
+
console.warn("[ConnectionMonitor] Socket disconnected, pausing");
|
|
52
|
+
onPause();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const handleReconnect = () => {
|
|
56
|
+
console.log("[ConnectionMonitor] Socket reconnected");
|
|
57
|
+
checkConnection();
|
|
58
|
+
};
|
|
59
|
+
socket.on("disconnect", handleDisconnect);
|
|
60
|
+
socket.io.on("reconnect", handleReconnect);
|
|
61
|
+
pingInterval = setInterval(checkConnection, 5e3);
|
|
62
|
+
checkConnection();
|
|
63
|
+
return {
|
|
64
|
+
destroy: () => {
|
|
65
|
+
if (pingInterval) clearInterval(pingInterval);
|
|
66
|
+
if (resumeTimeout) clearTimeout(resumeTimeout);
|
|
67
|
+
if (countdownInterval) clearInterval(countdownInterval);
|
|
68
|
+
socket.off("disconnect", handleDisconnect);
|
|
69
|
+
socket.io.off("reconnect", handleReconnect);
|
|
70
|
+
},
|
|
71
|
+
isPaused: () => paused,
|
|
72
|
+
getLatency: () => latency
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
exports.createConnectionMonitor = createConnectionMonitor;
|
|
77
|
+
//# sourceMappingURL=connectionMonitor.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connectionMonitor.cjs","sources":["../../../src/utils/connectionMonitor.ts"],"sourcesContent":["/**\n * Connection monitor for detecting unstable connections.\n * AirConsole pattern: onPause/onResume for connection issues.\n *\n * @example\n * ```typescript\n * const monitor = createConnectionMonitor(socket, {\n * onPause: () => {\n * pauseGame();\n * muteAudio();\n * },\n * onResume: () => {\n * resumeGame();\n * unmuteAudio();\n * },\n * onCountdown: (secondsLeft) => {\n * showCountdown(secondsLeft);\n * },\n * });\n *\n * // Later\n * monitor.destroy();\n * ```\n */\n\nimport type { Socket } from 'socket.io-client';\n\nexport interface ConnectionMonitorOptions {\n /** Called when connection becomes unstable or disconnects */\n onPause: () => void;\n /** Called after connection restores and countdown completes */\n onResume: () => void;\n /** Latency threshold to trigger pause (ms), default 2000 */\n latencyThreshold?: number;\n /** Resume countdown duration (ms), default 3000 */\n resumeCountdown?: number;\n /** Called during resume countdown with seconds remaining */\n onCountdown?: (secondsLeft: number) => void;\n}\n\nexport interface ConnectionMonitor {\n /** Clean up listeners and intervals */\n destroy: () => void;\n /** Check if currently paused */\n isPaused: () => boolean;\n /** Get current latency (ms) */\n getLatency: () => number;\n}\n\n/**\n * Creates a connection monitor that detects network issues and triggers pause/resume.\n *\n * Monitors both:\n * - Socket.IO disconnect/reconnect events\n * - High latency via periodic ping\n *\n * When connection is lost or latency exceeds threshold, calls onPause().\n * When connection restores, starts countdown and calls onResume() after delay.\n */\nexport function createConnectionMonitor(\n socket: Socket,\n options: ConnectionMonitorOptions\n): ConnectionMonitor {\n const {\n onPause,\n onResume,\n latencyThreshold = 2000,\n resumeCountdown = 3000,\n onCountdown,\n } = options;\n\n let paused = false;\n let latency = 0;\n let pingInterval: NodeJS.Timeout | null = null;\n let resumeTimeout: NodeJS.Timeout | null = null;\n let countdownInterval: NodeJS.Timeout | null = null;\n\n /**\n * Check latency via ping/pong.\n * If latency is too high and not already paused, pause.\n * If latency is good and paused, start resume countdown.\n */\n const checkConnection = () => {\n const start = Date.now();\n\n socket.emit('ping', {}, () => {\n latency = Date.now() - start;\n\n if (latency > latencyThreshold && !paused) {\n // Connection unstable\n paused = true;\n console.warn(`[ConnectionMonitor] High latency (${latency}ms), pausing`);\n onPause();\n } else if (latency <= latencyThreshold && paused) {\n // Connection restored - start countdown\n console.log(`[ConnectionMonitor] Connection restored, starting countdown`);\n startResumeCountdown();\n }\n });\n };\n\n /**\n * Start resume countdown after connection restores.\n * Calls onCountdown every second, then onResume at the end.\n */\n const startResumeCountdown = () => {\n // Clear any existing countdown\n if (resumeTimeout) clearTimeout(resumeTimeout);\n if (countdownInterval) clearInterval(countdownInterval);\n\n let secondsLeft = Math.ceil(resumeCountdown / 1000);\n onCountdown?.(secondsLeft);\n\n countdownInterval = setInterval(() => {\n secondsLeft--;\n if (secondsLeft > 0) {\n onCountdown?.(secondsLeft);\n }\n }, 1000);\n\n resumeTimeout = setTimeout(() => {\n if (countdownInterval) clearInterval(countdownInterval);\n paused = false;\n console.log(`[ConnectionMonitor] Resuming game`);\n onResume();\n }, resumeCountdown);\n };\n\n /**\n * Handle socket disconnect event.\n * Immediately pause if not already paused.\n */\n const handleDisconnect = () => {\n if (!paused) {\n paused = true;\n console.warn('[ConnectionMonitor] Socket disconnected, pausing');\n onPause();\n }\n };\n\n /**\n * Handle socket reconnect event.\n * Will trigger resume via latency check.\n */\n const handleReconnect = () => {\n console.log('[ConnectionMonitor] Socket reconnected');\n // Let latency check handle resume to avoid premature resume\n checkConnection();\n };\n\n // Register socket event listeners\n socket.on('disconnect', handleDisconnect);\n socket.io.on('reconnect', handleReconnect);\n\n // Start periodic latency check\n pingInterval = setInterval(checkConnection, 5000);\n checkConnection(); // Initial check\n\n return {\n destroy: () => {\n if (pingInterval) clearInterval(pingInterval);\n if (resumeTimeout) clearTimeout(resumeTimeout);\n if (countdownInterval) clearInterval(countdownInterval);\n socket.off('disconnect', handleDisconnect);\n socket.io.off('reconnect', handleReconnect);\n },\n isPaused: () => paused,\n getLatency: () => latency,\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
async function preloadAssets(urls, options = {}) {
|
|
4
|
+
const { onProgress, timeout = 3e4 } = options;
|
|
5
|
+
const total = urls.length;
|
|
6
|
+
let loaded = 0;
|
|
7
|
+
const loadPromises = urls.map((url) => {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const timeoutId = setTimeout(() => {
|
|
10
|
+
reject(new Error(`Timeout loading: ${url}`));
|
|
11
|
+
}, timeout);
|
|
12
|
+
if (url.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i)) {
|
|
13
|
+
const img = new Image();
|
|
14
|
+
img.onload = () => {
|
|
15
|
+
clearTimeout(timeoutId);
|
|
16
|
+
loaded++;
|
|
17
|
+
onProgress?.({ loaded, total, percent: loaded / total * 100, currentUrl: url });
|
|
18
|
+
resolve();
|
|
19
|
+
};
|
|
20
|
+
img.onerror = () => {
|
|
21
|
+
clearTimeout(timeoutId);
|
|
22
|
+
reject(new Error(`Failed to load image: ${url}`));
|
|
23
|
+
};
|
|
24
|
+
img.src = url;
|
|
25
|
+
} else if (url.match(/\.(mp3|wav|ogg|m4a)$/i)) {
|
|
26
|
+
const audio = new Audio();
|
|
27
|
+
audio.oncanplaythrough = () => {
|
|
28
|
+
clearTimeout(timeoutId);
|
|
29
|
+
loaded++;
|
|
30
|
+
onProgress?.({ loaded, total, percent: loaded / total * 100, currentUrl: url });
|
|
31
|
+
resolve();
|
|
32
|
+
};
|
|
33
|
+
audio.onerror = () => {
|
|
34
|
+
clearTimeout(timeoutId);
|
|
35
|
+
reject(new Error(`Failed to load audio: ${url}`));
|
|
36
|
+
};
|
|
37
|
+
audio.src = url;
|
|
38
|
+
audio.load();
|
|
39
|
+
} else {
|
|
40
|
+
fetch(url).then((res) => {
|
|
41
|
+
clearTimeout(timeoutId);
|
|
42
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
43
|
+
loaded++;
|
|
44
|
+
onProgress?.({ loaded, total, percent: loaded / total * 100, currentUrl: url });
|
|
45
|
+
resolve();
|
|
46
|
+
}).catch((err) => {
|
|
47
|
+
clearTimeout(timeoutId);
|
|
48
|
+
reject(new Error(`Failed to fetch: ${url} - ${err.message}`));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
await Promise.all(loadPromises);
|
|
54
|
+
}
|
|
55
|
+
function preloadImage(url) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const img = new Image();
|
|
58
|
+
img.onload = () => resolve(img);
|
|
59
|
+
img.onerror = () => reject(new Error(`Failed to load: ${url}`));
|
|
60
|
+
img.src = url;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
exports.preloadAssets = preloadAssets;
|
|
65
|
+
exports.preloadImage = preloadImage;
|
|
66
|
+
//# sourceMappingURL=preloadAssets.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preloadAssets.cjs","sources":["../../../src/utils/preloadAssets.ts"],"sourcesContent":["/**\n * Preload assets (images, audio, etc.) before game starts.\n * AirConsole best practice: load all resources before onReady.\n *\n * @example\n * ```typescript\n * await preloadAssets([\n * '/sprites/player.png',\n * '/audio/background.mp3',\n * ]);\n * bridge.setLoaded(); // Signal ready after preload\n * ```\n */\n\nexport interface PreloadProgress {\n loaded: number;\n total: number;\n percent: number;\n currentUrl: string;\n}\n\nexport interface PreloadOptions {\n onProgress?: (progress: PreloadProgress) => void;\n timeout?: number; // ms, default 30000\n}\n\nexport async function preloadAssets(\n urls: string[],\n options: PreloadOptions = {}\n): Promise<void> {\n const { onProgress, timeout = 30000 } = options;\n const total = urls.length;\n let loaded = 0;\n\n const loadPromises = urls.map((url) => {\n return new Promise<void>((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n reject(new Error(`Timeout loading: ${url}`));\n }, timeout);\n\n if (url.match(/\\.(png|jpg|jpeg|gif|svg|webp)$/i)) {\n // Image\n const img = new Image();\n img.onload = () => {\n clearTimeout(timeoutId);\n loaded++;\n onProgress?.({ loaded, total, percent: (loaded / total) * 100, currentUrl: url });\n resolve();\n };\n img.onerror = () => {\n clearTimeout(timeoutId);\n reject(new Error(`Failed to load image: ${url}`));\n };\n img.src = url;\n } else if (url.match(/\\.(mp3|wav|ogg|m4a)$/i)) {\n // Audio\n const audio = new Audio();\n audio.oncanplaythrough = () => {\n clearTimeout(timeoutId);\n loaded++;\n onProgress?.({ loaded, total, percent: (loaded / total) * 100, currentUrl: url });\n resolve();\n };\n audio.onerror = () => {\n clearTimeout(timeoutId);\n reject(new Error(`Failed to load audio: ${url}`));\n };\n audio.src = url;\n audio.load();\n } else {\n // Generic fetch for other resources\n fetch(url)\n .then((res) => {\n clearTimeout(timeoutId);\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n loaded++;\n onProgress?.({ loaded, total, percent: (loaded / total) * 100, currentUrl: url });\n resolve();\n })\n .catch((err) => {\n clearTimeout(timeoutId);\n reject(new Error(`Failed to fetch: ${url} - ${err.message}`));\n });\n }\n });\n });\n\n await Promise.all(loadPromises);\n}\n\n/**\n * Preload a single image and return the Image element.\n */\nexport function preloadImage(url: string): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const img = new Image();\n img.onload = () => resolve(img);\n img.onerror = () => reject(new Error(`Failed to load: ${url}`));\n img.src = url;\n });\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createTimeSync(socket) {
|
|
4
|
+
let offset = 0;
|
|
5
|
+
let latency = 0;
|
|
6
|
+
let synced = false;
|
|
7
|
+
return {
|
|
8
|
+
sync: () => {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const requestTime = Date.now();
|
|
11
|
+
socket.emit("smore:time-sync", { requestTime }, (response) => {
|
|
12
|
+
if (!response?.success) {
|
|
13
|
+
reject(new Error("Time sync failed"));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const responseTime = Date.now();
|
|
17
|
+
const roundTrip = responseTime - requestTime;
|
|
18
|
+
latency = roundTrip / 2;
|
|
19
|
+
const serverTimeAtResponse = response.serverTime + latency;
|
|
20
|
+
offset = serverTimeAtResponse - responseTime;
|
|
21
|
+
synced = true;
|
|
22
|
+
console.log(`[TimeSync] Synced. Offset: ${offset}ms, Latency: ${latency}ms`);
|
|
23
|
+
resolve();
|
|
24
|
+
});
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
if (!synced) reject(new Error("Time sync timeout"));
|
|
27
|
+
}, 5e3);
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
getServerTime: () => {
|
|
31
|
+
if (!synced) {
|
|
32
|
+
console.warn("[TimeSync] Not synced yet, returning local time");
|
|
33
|
+
return Date.now();
|
|
34
|
+
}
|
|
35
|
+
return Date.now() + offset;
|
|
36
|
+
},
|
|
37
|
+
getLatency: () => latency,
|
|
38
|
+
isSynced: () => synced
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
exports.createTimeSync = createTimeSync;
|
|
43
|
+
//# sourceMappingURL=serverTime.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serverTime.cjs","sources":["../../../src/utils/serverTime.ts"],"sourcesContent":["/**\n * Server time synchronization utility.\n * AirConsole pattern: getServerTime() for synchronized clocks.\n *\n * Used for quiz games (who pressed first), action games (latency compensation).\n *\n * @example\n * ```typescript\n * const timeSync = createTimeSync(socket);\n * await timeSync.sync(); // Initial sync\n *\n * const serverNow = timeSync.getServerTime();\n * socket.emit('answer', { timestamp: serverNow });\n * ```\n */\n\nimport type { Socket } from 'socket.io-client';\n\nexport interface TimeSync {\n /** Synchronize with server (call once at start) */\n sync: () => Promise<void>;\n /** Get current server time (after sync) */\n getServerTime: () => number;\n /** Get latency to server in ms */\n getLatency: () => number;\n /** Check if synchronized */\n isSynced: () => boolean;\n}\n\nexport function createTimeSync(socket: Socket): TimeSync {\n let offset = 0; // serverTime - clientTime\n let latency = 0;\n let synced = false;\n\n return {\n sync: () => {\n return new Promise((resolve, reject) => {\n const requestTime = Date.now();\n\n socket.emit('smore:time-sync', { requestTime }, (response: any) => {\n if (!response?.success) {\n reject(new Error('Time sync failed'));\n return;\n }\n\n const responseTime = Date.now();\n const roundTrip = responseTime - requestTime;\n latency = roundTrip / 2;\n\n // Server time when we received response (estimated)\n const serverTimeAtResponse = response.serverTime + latency;\n offset = serverTimeAtResponse - responseTime;\n\n synced = true;\n console.log(`[TimeSync] Synced. Offset: ${offset}ms, Latency: ${latency}ms`);\n resolve();\n });\n\n // Timeout\n setTimeout(() => {\n if (!synced) reject(new Error('Time sync timeout'));\n }, 5000);\n });\n },\n\n getServerTime: () => {\n if (!synced) {\n console.warn('[TimeSync] Not synced yet, returning local time');\n return Date.now();\n }\n return Date.now() + offset;\n },\n\n getLatency: () => latency,\n\n isSynced: () => synced,\n };\n}\n"],"names":[],"mappings":";;AA6BO,SAAS,eAAe,MAAA,EAA0B;AACvD,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,IAAI,OAAA,GAAU,CAAA;AACd,EAAA,IAAI,MAAA,GAAS,KAAA;AAEb,EAAA,OAAO;AAAA,IACL,MAAM,MAAM;AACV,MAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,QAAA,MAAM,WAAA,GAAc,KAAK,GAAA,EAAI;AAE7B,QAAA,MAAA,CAAO,KAAK,iBAAA,EAAmB,EAAE,WAAA,EAAY,EAAG,CAAC,QAAA,KAAkB;AACjE,UAAA,IAAI,CAAC,UAAU,OAAA,EAAS;AACtB,YAAA,MAAA,CAAO,IAAI,KAAA,CAAM,kBAAkB,CAAC,CAAA;AACpC,YAAA;AAAA,UACF;AAEA,UAAA,MAAM,YAAA,GAAe,KAAK,GAAA,EAAI;AAC9B,UAAA,MAAM,YAAY,YAAA,GAAe,WAAA;AACjC,UAAA,OAAA,GAAU,SAAA,GAAY,CAAA;AAGtB,UAAA,MAAM,oBAAA,GAAuB,SAAS,UAAA,GAAa,OAAA;AACnD,UAAA,MAAA,GAAS,oBAAA,GAAuB,YAAA;AAEhC,UAAA,MAAA,GAAS,IAAA;AACT,UAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,2BAAA,EAA8B,MAAM,CAAA,aAAA,EAAgB,OAAO,CAAA,EAAA,CAAI,CAAA;AAC3E,UAAA,OAAA,EAAQ;AAAA,QACV,CAAC,CAAA;AAGD,QAAA,UAAA,CAAW,MAAM;AACf,UAAA,IAAI,CAAC,MAAA,EAAQ,MAAA,CAAO,IAAI,KAAA,CAAM,mBAAmB,CAAC,CAAA;AAAA,QACpD,GAAG,GAAI,CAAA;AAAA,MACT,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IAEA,eAAe,MAAM;AACnB,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,OAAA,CAAQ,KAAK,iDAAiD,CAAA;AAC9D,QAAA,OAAO,KAAK,GAAA,EAAI;AAAA,MAClB;AACA,MAAA,OAAO,IAAA,CAAK,KAAI,GAAI,MAAA;AAAA,IACtB,CAAA;AAAA,IAEA,YAAY,MAAM,OAAA;AAAA,IAElB,UAAU,MAAM;AAAA,GAClB;AACF;;;;"}
|