@smoregg/sdk 2.0.0 → 2.2.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/dist/cjs/controller.cjs +260 -113
- package/dist/cjs/controller.cjs.map +1 -1
- package/dist/cjs/errors.cjs +1 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/events.cjs +26 -3
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/index.cjs +2 -7
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/screen.cjs +244 -128
- package/dist/cjs/screen.cjs.map +1 -1
- package/dist/cjs/shared.cjs +34 -0
- package/dist/cjs/shared.cjs.map +1 -0
- package/dist/cjs/testing.cjs +181 -73
- package/dist/cjs/testing.cjs.map +1 -1
- package/dist/cjs/transport/PostMessageTransport.cjs +12 -0
- package/dist/cjs/transport/PostMessageTransport.cjs.map +1 -1
- package/dist/cjs/transport/protocol.cjs +2 -0
- package/dist/cjs/transport/protocol.cjs.map +1 -1
- package/dist/cjs/types.cjs +16 -0
- package/dist/cjs/types.cjs.map +1 -0
- package/dist/esm/controller.js +262 -115
- package/dist/esm/controller.js.map +1 -1
- package/dist/esm/errors.js +1 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/events.js +25 -4
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/index.js +1 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/screen.js +246 -130
- package/dist/esm/screen.js.map +1 -1
- package/dist/esm/shared.js +30 -0
- package/dist/esm/shared.js.map +1 -0
- package/dist/esm/testing.js +181 -73
- package/dist/esm/testing.js.map +1 -1
- package/dist/esm/transport/PostMessageTransport.js +12 -0
- package/dist/esm/transport/PostMessageTransport.js.map +1 -1
- package/dist/esm/transport/protocol.js +2 -1
- package/dist/esm/transport/protocol.js.map +1 -1
- package/dist/esm/types.js +14 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/types/controller.d.ts +1 -1
- package/dist/types/controller.d.ts.map +1 -1
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/events.d.ts +14 -1
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/screen.d.ts +3 -3
- package/dist/types/screen.d.ts.map +1 -1
- package/dist/types/shared.d.ts +21 -0
- package/dist/types/shared.d.ts.map +1 -0
- package/dist/types/testing.d.ts +65 -4
- package/dist/types/testing.d.ts.map +1 -1
- package/dist/types/transport/PostMessageTransport.d.ts +1 -0
- package/dist/types/transport/PostMessageTransport.d.ts.map +1 -1
- package/dist/types/transport/protocol.d.ts +5 -0
- package/dist/types/transport/protocol.d.ts.map +1 -1
- package/dist/types/types.d.ts +254 -345
- package/dist/types/types.d.ts.map +1 -1
- package/dist/umd/smore-sdk.umd.js +575 -784
- 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 +7 -1
- package/dist/cjs/config.cjs +0 -13
- package/dist/cjs/config.cjs.map +0 -1
- package/dist/esm/config.js +0 -10
- package/dist/esm/config.js.map +0 -1
- package/dist/types/config.d.ts +0 -35
- package/dist/types/config.d.ts.map +0 -1
package/dist/esm/controller.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { PostMessageTransport } from './transport/PostMessageTransport.js';
|
|
2
|
-
import { isBridgeMessage, validateInitPayload } from './transport/protocol.js';
|
|
2
|
+
import { PROTOCOL_VERSION, isBridgeMessage, validateInitPayload } from './transport/protocol.js';
|
|
3
3
|
import { SmoreSDKError } from './errors.js';
|
|
4
|
-
import { SMORE_EVENTS, validateEventName } from './events.js';
|
|
4
|
+
import { SMORE_EVENTS, validateEventName, CONTROLLER_LIFECYCLE_EVENTS } from './events.js';
|
|
5
5
|
import { DebugLogger } from './logger.js';
|
|
6
|
-
import {
|
|
6
|
+
import { mapPlayerDTO, validatePayloadSize } from './shared.js';
|
|
7
7
|
|
|
8
8
|
const DEFAULT_TIMEOUT = 1e4;
|
|
9
9
|
class ControllerImpl {
|
|
@@ -11,7 +11,7 @@ class ControllerImpl {
|
|
|
11
11
|
config;
|
|
12
12
|
logger;
|
|
13
13
|
_roomCode = "";
|
|
14
|
-
|
|
14
|
+
_myPlayerIndex = -1;
|
|
15
15
|
_isReady = false;
|
|
16
16
|
_isDestroyed = false;
|
|
17
17
|
_initTimeoutId = null;
|
|
@@ -21,19 +21,20 @@ class ControllerImpl {
|
|
|
21
21
|
// Maps user-facing handler -> transport wrappedHandler for proper cleanup in on()/off()
|
|
22
22
|
handlerToTransport = /* @__PURE__ */ new Map();
|
|
23
23
|
_controllers = [];
|
|
24
|
+
_customStates = /* @__PURE__ */ new Map();
|
|
25
|
+
_stateChangeListeners = /* @__PURE__ */ new Set();
|
|
24
26
|
// Pending handlers registered via on() before transport is ready
|
|
25
27
|
_pendingHandlers = [];
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
_onControllerDisconnectCallbacks = /* @__PURE__ */ new Set();
|
|
31
|
-
_onControllerReconnectCallbacks = /* @__PURE__ */ new Set();
|
|
32
|
-
_onCharacterUpdatedCallbacks = /* @__PURE__ */ new Set();
|
|
33
|
-
_onRateLimitedCallbacks = /* @__PURE__ */ new Set();
|
|
34
|
-
_onErrorCallbacks = /* @__PURE__ */ new Set();
|
|
28
|
+
// Unified lifecycle listener map (supports both onXxx() and on('$xxx') patterns)
|
|
29
|
+
_lifecycleListeners = /* @__PURE__ */ new Map();
|
|
30
|
+
// Outbound message buffer (messages sent before ready)
|
|
31
|
+
_outboundBuffer = [];
|
|
35
32
|
// Whether all-ready has fired
|
|
36
33
|
_allReadyFired = false;
|
|
34
|
+
// Self-connection awareness
|
|
35
|
+
_isConnected = false;
|
|
36
|
+
// Protocol versioning
|
|
37
|
+
_protocolVersion = PROTOCOL_VERSION;
|
|
37
38
|
// Ready promise
|
|
38
39
|
_readyResolve;
|
|
39
40
|
_readyReject;
|
|
@@ -50,8 +51,8 @@ class ControllerImpl {
|
|
|
50
51
|
// ---------------------------------------------------------------------------
|
|
51
52
|
// Properties (readonly)
|
|
52
53
|
// ---------------------------------------------------------------------------
|
|
53
|
-
get
|
|
54
|
-
return this.
|
|
54
|
+
get myPlayerIndex() {
|
|
55
|
+
return this._myPlayerIndex;
|
|
55
56
|
}
|
|
56
57
|
get roomCode() {
|
|
57
58
|
return this._roomCode;
|
|
@@ -62,6 +63,12 @@ class ControllerImpl {
|
|
|
62
63
|
get isDestroyed() {
|
|
63
64
|
return this._isDestroyed;
|
|
64
65
|
}
|
|
66
|
+
get isConnected() {
|
|
67
|
+
return this._isConnected;
|
|
68
|
+
}
|
|
69
|
+
get protocolVersion() {
|
|
70
|
+
return this._protocolVersion;
|
|
71
|
+
}
|
|
65
72
|
/**
|
|
66
73
|
* Read-only list of all known controllers (players) in the room.
|
|
67
74
|
* Returns full ControllerInfo including playerIndex, nickname, connected status, and appearance.
|
|
@@ -72,12 +79,18 @@ class ControllerImpl {
|
|
|
72
79
|
get controllers() {
|
|
73
80
|
return [...this._controllers];
|
|
74
81
|
}
|
|
82
|
+
get me() {
|
|
83
|
+
return this._controllers.find((c) => c.playerIndex === this._myPlayerIndex);
|
|
84
|
+
}
|
|
75
85
|
/**
|
|
76
86
|
* Returns the number of currently connected players.
|
|
77
87
|
*/
|
|
78
88
|
getControllerCount() {
|
|
79
89
|
return this._controllers.filter((c) => c.connected).length;
|
|
80
90
|
}
|
|
91
|
+
getController(playerIndex) {
|
|
92
|
+
return this._controllers.find((c) => c.playerIndex === playerIndex);
|
|
93
|
+
}
|
|
81
94
|
// ---------------------------------------------------------------------------
|
|
82
95
|
// Initialization
|
|
83
96
|
// ---------------------------------------------------------------------------
|
|
@@ -108,7 +121,7 @@ class ControllerImpl {
|
|
|
108
121
|
};
|
|
109
122
|
window.addEventListener("message", this.boundMessageHandler);
|
|
110
123
|
this.logger.lifecycle("Sending _bridge:ready to parent");
|
|
111
|
-
window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
|
|
124
|
+
window.parent.postMessage({ type: "_bridge:ready", protocolVersion: PROTOCOL_VERSION }, parentOrigin);
|
|
112
125
|
}
|
|
113
126
|
handleInit(msg, parentOrigin) {
|
|
114
127
|
const initPayload = msg.payload;
|
|
@@ -147,28 +160,48 @@ class ControllerImpl {
|
|
|
147
160
|
this._readyReject(error);
|
|
148
161
|
return;
|
|
149
162
|
}
|
|
150
|
-
this.transport = new PostMessageTransport(parentOrigin);
|
|
163
|
+
this.transport = this.config.transport ?? new PostMessageTransport(parentOrigin);
|
|
151
164
|
this._roomCode = initData.roomCode;
|
|
152
|
-
|
|
165
|
+
const serverProtocolVersion = initData.protocolVersion;
|
|
166
|
+
if (serverProtocolVersion !== void 0) {
|
|
167
|
+
this._protocolVersion = serverProtocolVersion;
|
|
168
|
+
if (serverProtocolVersion !== PROTOCOL_VERSION) {
|
|
169
|
+
this.logger.warn(
|
|
170
|
+
`Protocol version mismatch: SDK v${PROTOCOL_VERSION}, server v${serverProtocolVersion}. Some features may not work correctly.`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
this._myPlayerIndex = initData.myIndex;
|
|
153
175
|
const initPlayers = initData.players;
|
|
154
|
-
this._controllers = initPlayers.filter((p) => typeof p.playerIndex === "number").map((p) => (
|
|
155
|
-
playerIndex: p.playerIndex,
|
|
156
|
-
nickname: p.nickname || p.name || `Player ${p.playerIndex + 1}`,
|
|
157
|
-
connected: p.connected !== false,
|
|
158
|
-
appearance: p.appearance ?? p.character
|
|
159
|
-
}));
|
|
176
|
+
this._controllers = initPlayers.filter((p) => typeof p.playerIndex === "number").map((p, index) => mapPlayerDTO(p, index));
|
|
160
177
|
this.setupEventHandlers();
|
|
161
178
|
for (const { event, handler } of this._pendingHandlers) {
|
|
162
179
|
this.setupUserEventHandler(event, handler);
|
|
163
180
|
}
|
|
164
181
|
this._pendingHandlers = [];
|
|
182
|
+
this._isConnected = true;
|
|
165
183
|
this._isReady = true;
|
|
184
|
+
if (initData.gameInProgress && this.transport) {
|
|
185
|
+
this.logger.lifecycle("Game in progress detected, requesting state recovery");
|
|
186
|
+
this.transport.emit(SMORE_EVENTS.STATE_GET_ALL, {});
|
|
187
|
+
}
|
|
188
|
+
for (const buffered of this._outboundBuffer) {
|
|
189
|
+
try {
|
|
190
|
+
switch (buffered.method) {
|
|
191
|
+
case "send":
|
|
192
|
+
this.send(buffered.args[0], buffered.args[1]);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
this.handleError(err instanceof SmoreSDKError ? err : new SmoreSDKError("UNKNOWN", "Failed to flush buffered message"));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
this._outboundBuffer = [];
|
|
166
200
|
this.logger.lifecycle("Controller ready", {
|
|
167
201
|
roomCode: this._roomCode,
|
|
168
|
-
myIndex: this.
|
|
202
|
+
myIndex: this._myPlayerIndex
|
|
169
203
|
});
|
|
170
|
-
|
|
171
|
-
if (autoReady) {
|
|
204
|
+
if (this.config.autoReady !== false) {
|
|
172
205
|
this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
|
|
173
206
|
this.signalReady();
|
|
174
207
|
}
|
|
@@ -183,21 +216,16 @@ class ControllerImpl {
|
|
|
183
216
|
this.logger.debug("Received _bridge:update", updateData);
|
|
184
217
|
if (updateData.players && Array.isArray(updateData.players)) {
|
|
185
218
|
const players = updateData.players;
|
|
186
|
-
const newControllers = players.filter((p) => typeof p.playerIndex === "number").map((p) => (
|
|
187
|
-
playerIndex: p.playerIndex,
|
|
188
|
-
nickname: p.nickname || p.name || `Player ${p.playerIndex + 1}`,
|
|
189
|
-
connected: p.connected !== false,
|
|
190
|
-
appearance: p.appearance ?? p.character
|
|
191
|
-
}));
|
|
219
|
+
const newControllers = players.filter((p) => typeof p.playerIndex === "number").map((p, index) => mapPlayerDTO(p, index));
|
|
192
220
|
const oldControllers = this._controllers;
|
|
193
221
|
for (const nc of newControllers) {
|
|
194
222
|
if (!oldControllers.some((oc) => oc.playerIndex === nc.playerIndex)) {
|
|
195
|
-
this.
|
|
223
|
+
this._emitLifecycle("$controller-join", nc.playerIndex, nc);
|
|
196
224
|
}
|
|
197
225
|
}
|
|
198
226
|
for (const oc of oldControllers) {
|
|
199
227
|
if (!newControllers.some((nc) => nc.playerIndex === oc.playerIndex)) {
|
|
200
|
-
this.
|
|
228
|
+
this._emitLifecycle("$controller-leave", oc.playerIndex);
|
|
201
229
|
}
|
|
202
230
|
}
|
|
203
231
|
for (const nc of newControllers) {
|
|
@@ -205,11 +233,11 @@ class ControllerImpl {
|
|
|
205
233
|
if (oc) {
|
|
206
234
|
if (oc.connected && !nc.connected) {
|
|
207
235
|
this.logger.debug("Player disconnected (via update)", { playerIndex: nc.playerIndex });
|
|
208
|
-
this.
|
|
236
|
+
this._emitLifecycle("$controller-disconnect", nc.playerIndex);
|
|
209
237
|
}
|
|
210
238
|
if (!oc.connected && nc.connected) {
|
|
211
239
|
this.logger.debug("Player reconnected (via update)", { playerIndex: nc.playerIndex });
|
|
212
|
-
this.
|
|
240
|
+
this._emitLifecycle("$controller-reconnect", nc.playerIndex, nc);
|
|
213
241
|
}
|
|
214
242
|
}
|
|
215
243
|
}
|
|
@@ -224,19 +252,10 @@ class ControllerImpl {
|
|
|
224
252
|
const playerIndex = playerInfo?.playerIndex ?? data.playerIndex;
|
|
225
253
|
if (playerIndex !== void 0) {
|
|
226
254
|
if (this._controllers.some((c) => c.playerIndex === playerIndex)) return;
|
|
227
|
-
const controllerInfo = playerInfo ? {
|
|
228
|
-
playerIndex,
|
|
229
|
-
nickname: playerInfo.nickname || playerInfo.name || `Player ${playerIndex + 1}`,
|
|
230
|
-
connected: playerInfo.connected !== false,
|
|
231
|
-
appearance: playerInfo.appearance ?? playerInfo.character
|
|
232
|
-
} : {
|
|
233
|
-
playerIndex,
|
|
234
|
-
nickname: `Player ${playerIndex + 1}`,
|
|
235
|
-
connected: true
|
|
236
|
-
};
|
|
255
|
+
const controllerInfo = playerInfo ? mapPlayerDTO(playerInfo, playerIndex) : mapPlayerDTO({ playerIndex, connected: true }, playerIndex);
|
|
237
256
|
this._controllers = [...this._controllers, controllerInfo];
|
|
238
257
|
this.logger.debug("Player joined", { playerIndex });
|
|
239
|
-
this.
|
|
258
|
+
this._emitLifecycle("$controller-join", playerIndex, controllerInfo);
|
|
240
259
|
}
|
|
241
260
|
});
|
|
242
261
|
this.registerHandler(SMORE_EVENTS.PLAYER_LEFT, (raw) => {
|
|
@@ -246,7 +265,7 @@ class ControllerImpl {
|
|
|
246
265
|
if (!this._controllers.some((c) => c.playerIndex === playerIndex)) return;
|
|
247
266
|
this._controllers = this._controllers.filter((c) => c.playerIndex !== playerIndex);
|
|
248
267
|
this.logger.debug("Player left", { playerIndex });
|
|
249
|
-
this.
|
|
268
|
+
this._emitLifecycle("$controller-leave", playerIndex);
|
|
250
269
|
}
|
|
251
270
|
});
|
|
252
271
|
this.registerHandler(SMORE_EVENTS.PLAYER_DISCONNECTED, (raw) => {
|
|
@@ -258,7 +277,7 @@ class ControllerImpl {
|
|
|
258
277
|
(c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
|
|
259
278
|
);
|
|
260
279
|
this.logger.debug("Player disconnected", { playerIndex });
|
|
261
|
-
this.
|
|
280
|
+
this._emitLifecycle("$controller-disconnect", playerIndex);
|
|
262
281
|
}
|
|
263
282
|
});
|
|
264
283
|
this.registerHandler(SMORE_EVENTS.PLAYER_RECONNECTED, (raw) => {
|
|
@@ -266,21 +285,12 @@ class ControllerImpl {
|
|
|
266
285
|
const playerData = data.player;
|
|
267
286
|
const playerIndex = playerData?.playerIndex ?? data.playerIndex;
|
|
268
287
|
if (playerIndex !== void 0) {
|
|
269
|
-
const controllerInfo = playerData ? {
|
|
270
|
-
playerIndex,
|
|
271
|
-
nickname: playerData.nickname || playerData.name || `Player ${playerIndex + 1}`,
|
|
272
|
-
connected: true,
|
|
273
|
-
appearance: playerData.appearance ?? playerData.character
|
|
274
|
-
} : {
|
|
275
|
-
playerIndex,
|
|
276
|
-
nickname: `Player ${playerIndex + 1}`,
|
|
277
|
-
connected: true
|
|
278
|
-
};
|
|
288
|
+
const controllerInfo = playerData ? mapPlayerDTO(playerData, playerIndex) : mapPlayerDTO({ playerIndex, connected: true }, playerIndex);
|
|
279
289
|
this._controllers = this._controllers.map(
|
|
280
290
|
(c) => c.playerIndex === playerIndex ? controllerInfo : c
|
|
281
291
|
);
|
|
282
292
|
this.logger.debug("Player reconnected", { playerIndex });
|
|
283
|
-
this.
|
|
293
|
+
this._emitLifecycle("$controller-reconnect", playerIndex, controllerInfo);
|
|
284
294
|
}
|
|
285
295
|
});
|
|
286
296
|
this.registerHandler(SMORE_EVENTS.PLAYER_CHARACTER_UPDATED, (raw) => {
|
|
@@ -293,19 +303,75 @@ class ControllerImpl {
|
|
|
293
303
|
(c) => c.playerIndex === pi ? { ...c, appearance } : c
|
|
294
304
|
);
|
|
295
305
|
this.logger.debug("Player character updated", { playerIndex: pi });
|
|
296
|
-
this.
|
|
306
|
+
this._emitLifecycle("$character-updated", pi, appearance ?? null);
|
|
297
307
|
}
|
|
298
308
|
});
|
|
299
309
|
this.registerHandler(SMORE_EVENTS.RATE_LIMITED, (raw) => {
|
|
300
310
|
const data = raw;
|
|
301
|
-
const
|
|
302
|
-
this.
|
|
303
|
-
|
|
311
|
+
const eventName = data?.event ?? "unknown";
|
|
312
|
+
this.handleError(
|
|
313
|
+
new SmoreSDKError("RATE_LIMITED", `Server rate-limited event: ${eventName}`, {
|
|
314
|
+
details: { event: eventName }
|
|
315
|
+
})
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
this.registerHandler(SMORE_EVENTS.GAME_OVER, (raw) => {
|
|
319
|
+
const data = raw;
|
|
320
|
+
this.logger.lifecycle("Game over", data?.results);
|
|
321
|
+
this._emitLifecycle("$game-over", data?.results);
|
|
304
322
|
});
|
|
305
323
|
this.registerHandler(SMORE_EVENTS.ALL_READY, () => {
|
|
306
324
|
this.logger.lifecycle("All participants ready");
|
|
307
325
|
this._allReadyFired = true;
|
|
308
|
-
this.
|
|
326
|
+
this._emitLifecycle("$all-ready");
|
|
327
|
+
});
|
|
328
|
+
this.registerHandler(SMORE_EVENTS.SELF_DISCONNECTED, () => {
|
|
329
|
+
this._isConnected = false;
|
|
330
|
+
this.logger.lifecycle("Connection lost");
|
|
331
|
+
this._emitLifecycle("$connection-change", false);
|
|
332
|
+
});
|
|
333
|
+
this.registerHandler(SMORE_EVENTS.SELF_RECONNECTED, () => {
|
|
334
|
+
this._isConnected = true;
|
|
335
|
+
this.logger.lifecycle("Connection restored");
|
|
336
|
+
this._emitLifecycle("$connection-change", true);
|
|
337
|
+
});
|
|
338
|
+
this.registerHandler(SMORE_EVENTS.STATE_CHANGED, (raw) => {
|
|
339
|
+
const data = raw;
|
|
340
|
+
if (typeof data?.playerIndex === "number" && data.state) {
|
|
341
|
+
this._customStates.set(data.playerIndex, data.state);
|
|
342
|
+
this._stateChangeListeners.forEach((cb) => {
|
|
343
|
+
try {
|
|
344
|
+
cb(data.playerIndex, data.state);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
this.handleError(
|
|
347
|
+
new SmoreSDKError("UNKNOWN", "Error in custom state change listener", {
|
|
348
|
+
cause: err instanceof Error ? err : void 0
|
|
349
|
+
})
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
this.registerHandler(SMORE_EVENTS.STATE_ALL, (raw) => {
|
|
356
|
+
const data = raw;
|
|
357
|
+
if (data?.states) {
|
|
358
|
+
for (const [key, value] of Object.entries(data.states)) {
|
|
359
|
+
const pi = Number(key);
|
|
360
|
+
this._customStates.set(pi, value);
|
|
361
|
+
this._stateChangeListeners.forEach((cb) => {
|
|
362
|
+
try {
|
|
363
|
+
cb(pi, value);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
this.handleError(
|
|
366
|
+
new SmoreSDKError("UNKNOWN", "Error in custom state change listener", {
|
|
367
|
+
cause: err instanceof Error ? err : void 0
|
|
368
|
+
})
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
this._emitLifecycle("$state-recovery", data.states);
|
|
374
|
+
}
|
|
309
375
|
});
|
|
310
376
|
}
|
|
311
377
|
/**
|
|
@@ -344,57 +410,89 @@ class ControllerImpl {
|
|
|
344
410
|
this.registeredHandlers.push({ event, handler });
|
|
345
411
|
}
|
|
346
412
|
// ---------------------------------------------------------------------------
|
|
413
|
+
// Lifecycle Listener Helpers
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
_addLifecycleListener(event, listener) {
|
|
416
|
+
let set = this._lifecycleListeners.get(event);
|
|
417
|
+
if (!set) {
|
|
418
|
+
set = /* @__PURE__ */ new Set();
|
|
419
|
+
this._lifecycleListeners.set(event, set);
|
|
420
|
+
}
|
|
421
|
+
set.add(listener);
|
|
422
|
+
return () => {
|
|
423
|
+
set.delete(listener);
|
|
424
|
+
if (set.size === 0) this._lifecycleListeners.delete(event);
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
_emitLifecycle(event, ...args) {
|
|
428
|
+
this._lifecycleListeners.get(event)?.forEach((cb) => {
|
|
429
|
+
try {
|
|
430
|
+
cb(...args);
|
|
431
|
+
} catch (err) {
|
|
432
|
+
this.handleError(
|
|
433
|
+
new SmoreSDKError("UNKNOWN", `Error in lifecycle handler for "${event}"`, {
|
|
434
|
+
cause: err instanceof Error ? err : void 0,
|
|
435
|
+
details: { event }
|
|
436
|
+
})
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
_hasLifecycleListeners(event) {
|
|
442
|
+
const set = this._lifecycleListeners.get(event);
|
|
443
|
+
return set !== void 0 && set.size > 0;
|
|
444
|
+
}
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
347
446
|
// Lifecycle Methods
|
|
348
447
|
// ---------------------------------------------------------------------------
|
|
349
448
|
onAllReady(callback) {
|
|
350
449
|
if (this._allReadyFired) {
|
|
351
450
|
callback();
|
|
352
451
|
}
|
|
353
|
-
this.
|
|
354
|
-
return () => {
|
|
355
|
-
this._onAllReadyCallbacks.delete(callback);
|
|
356
|
-
};
|
|
452
|
+
return this._addLifecycleListener("$all-ready", callback);
|
|
357
453
|
}
|
|
358
454
|
onControllerJoin(callback) {
|
|
359
|
-
this.
|
|
360
|
-
return () => {
|
|
361
|
-
this._onControllerJoinCallbacks.delete(callback);
|
|
362
|
-
};
|
|
455
|
+
return this._addLifecycleListener("$controller-join", callback);
|
|
363
456
|
}
|
|
364
457
|
onControllerLeave(callback) {
|
|
365
|
-
this.
|
|
366
|
-
return () => {
|
|
367
|
-
this._onControllerLeaveCallbacks.delete(callback);
|
|
368
|
-
};
|
|
458
|
+
return this._addLifecycleListener("$controller-leave", callback);
|
|
369
459
|
}
|
|
370
460
|
onControllerDisconnect(callback) {
|
|
371
|
-
this.
|
|
372
|
-
return () => {
|
|
373
|
-
this._onControllerDisconnectCallbacks.delete(callback);
|
|
374
|
-
};
|
|
461
|
+
return this._addLifecycleListener("$controller-disconnect", callback);
|
|
375
462
|
}
|
|
376
463
|
onControllerReconnect(callback) {
|
|
377
|
-
this.
|
|
378
|
-
return () => {
|
|
379
|
-
this._onControllerReconnectCallbacks.delete(callback);
|
|
380
|
-
};
|
|
464
|
+
return this._addLifecycleListener("$controller-reconnect", callback);
|
|
381
465
|
}
|
|
382
466
|
onCharacterUpdated(callback) {
|
|
383
|
-
this.
|
|
384
|
-
return () => {
|
|
385
|
-
this._onCharacterUpdatedCallbacks.delete(callback);
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
onRateLimited(callback) {
|
|
389
|
-
this._onRateLimitedCallbacks.add(callback);
|
|
390
|
-
return () => {
|
|
391
|
-
this._onRateLimitedCallbacks.delete(callback);
|
|
392
|
-
};
|
|
467
|
+
return this._addLifecycleListener("$character-updated", callback);
|
|
393
468
|
}
|
|
394
469
|
onError(callback) {
|
|
395
|
-
this.
|
|
470
|
+
return this._addLifecycleListener("$error", callback);
|
|
471
|
+
}
|
|
472
|
+
onConnectionChange(callback) {
|
|
473
|
+
return this._addLifecycleListener("$connection-change", callback);
|
|
474
|
+
}
|
|
475
|
+
onGameOver(callback) {
|
|
476
|
+
return this._addLifecycleListener("$game-over", callback);
|
|
477
|
+
}
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
// Custom State Methods
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
setState(state) {
|
|
482
|
+
const current = this._customStates.get(this._myPlayerIndex) ?? {};
|
|
483
|
+
const merged = { ...current, ...state };
|
|
484
|
+
this._customStates.set(this._myPlayerIndex, merged);
|
|
485
|
+
if (this.transport) {
|
|
486
|
+
this.transport.emit(SMORE_EVENTS.STATE_SET, { state });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
getMyState() {
|
|
490
|
+
return this._customStates.get(this._myPlayerIndex);
|
|
491
|
+
}
|
|
492
|
+
onCustomStateChange(listener) {
|
|
493
|
+
this._stateChangeListeners.add(listener);
|
|
396
494
|
return () => {
|
|
397
|
-
this.
|
|
495
|
+
this._stateChangeListeners.delete(listener);
|
|
398
496
|
};
|
|
399
497
|
}
|
|
400
498
|
// ---------------------------------------------------------------------------
|
|
@@ -411,8 +509,16 @@ class ControllerImpl {
|
|
|
411
509
|
* Use the onError callback or smore:rate-limited event to detect rate limiting.
|
|
412
510
|
*/
|
|
413
511
|
send(event, data) {
|
|
414
|
-
this.
|
|
512
|
+
if (this._isDestroyed) {
|
|
513
|
+
throw new SmoreSDKError("DESTROYED", "Cannot call send() after destroy()");
|
|
514
|
+
}
|
|
515
|
+
if (!this._isReady || !this.transport) {
|
|
516
|
+
this._outboundBuffer.push({ method: "send", args: [event, data] });
|
|
517
|
+
this.logger.debug(`Buffered send "${event}" (controller not ready yet)`);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
415
520
|
validateEventName(event);
|
|
521
|
+
validatePayloadSize(data);
|
|
416
522
|
if (typeof data !== "object" || data === null) {
|
|
417
523
|
this.logger.warn(
|
|
418
524
|
'Event data should be an object. Primitive values will be wrapped as { data: value } by the relay server. To avoid confusion, wrap explicitly: send("event", { value: 42 }) instead of send("event", 42).'
|
|
@@ -421,12 +527,6 @@ class ControllerImpl {
|
|
|
421
527
|
this.logSend(event, data);
|
|
422
528
|
this.transport.emit(event, data);
|
|
423
529
|
}
|
|
424
|
-
sendRaw(event, data) {
|
|
425
|
-
this.ensureReady("sendRaw");
|
|
426
|
-
validateEventName(event);
|
|
427
|
-
this.logSend(event, data);
|
|
428
|
-
this.transport.emit(event, data);
|
|
429
|
-
}
|
|
430
530
|
signalReady() {
|
|
431
531
|
this.ensureReady("signalReady");
|
|
432
532
|
this.logSend(SMORE_EVENTS.GAME_READY, {});
|
|
@@ -448,6 +548,16 @@ class ControllerImpl {
|
|
|
448
548
|
* handler receives `(data)` -- targeted to this specific controller.
|
|
449
549
|
*/
|
|
450
550
|
on(event, handler) {
|
|
551
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
552
|
+
const validEvents = CONTROLLER_LIFECYCLE_EVENTS;
|
|
553
|
+
if (!validEvents.has(event)) {
|
|
554
|
+
throw new SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}". Valid lifecycle events: ${Array.from(validEvents).join(", ")}`);
|
|
555
|
+
}
|
|
556
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
557
|
+
handler();
|
|
558
|
+
}
|
|
559
|
+
return this._addLifecycleListener(event, handler);
|
|
560
|
+
}
|
|
451
561
|
validateEventName(event);
|
|
452
562
|
let listeners = this.eventListeners.get(event);
|
|
453
563
|
if (!listeners) {
|
|
@@ -500,6 +610,23 @@ class ControllerImpl {
|
|
|
500
610
|
* `off(event, originalHandler)`. Use the returned unsubscribe function instead.
|
|
501
611
|
*/
|
|
502
612
|
once(event, handler) {
|
|
613
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
614
|
+
const validEvents = CONTROLLER_LIFECYCLE_EVENTS;
|
|
615
|
+
if (!validEvents.has(event)) {
|
|
616
|
+
throw new SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}"`);
|
|
617
|
+
}
|
|
618
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
619
|
+
handler();
|
|
620
|
+
return () => {
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
const wrapper = (...args) => {
|
|
624
|
+
unsub();
|
|
625
|
+
handler(...args);
|
|
626
|
+
};
|
|
627
|
+
const unsub = this._addLifecycleListener(event, wrapper);
|
|
628
|
+
return unsub;
|
|
629
|
+
}
|
|
503
630
|
const unsubscribe = this.on(event, ((data) => {
|
|
504
631
|
unsubscribe();
|
|
505
632
|
handler(data);
|
|
@@ -507,6 +634,14 @@ class ControllerImpl {
|
|
|
507
634
|
return unsubscribe;
|
|
508
635
|
}
|
|
509
636
|
off(event, handler) {
|
|
637
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
638
|
+
if (!handler) {
|
|
639
|
+
this._lifecycleListeners.delete(event);
|
|
640
|
+
} else {
|
|
641
|
+
this._lifecycleListeners.get(event)?.delete(handler);
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
510
645
|
if (!handler) {
|
|
511
646
|
this.eventListeners.delete(event);
|
|
512
647
|
this.transport?.off(event);
|
|
@@ -534,6 +669,21 @@ class ControllerImpl {
|
|
|
534
669
|
);
|
|
535
670
|
}
|
|
536
671
|
}
|
|
672
|
+
removeAllListeners(event) {
|
|
673
|
+
if (event) {
|
|
674
|
+
this.eventListeners.delete(event);
|
|
675
|
+
this.transport?.off(event);
|
|
676
|
+
this.registeredHandlers = this.registeredHandlers.filter((h) => h.event !== event);
|
|
677
|
+
for (const [key, val] of this.handlerToTransport) {
|
|
678
|
+
if (val.event === event) this.handlerToTransport.delete(key);
|
|
679
|
+
}
|
|
680
|
+
this._pendingHandlers = this._pendingHandlers.filter((p) => p.event !== event);
|
|
681
|
+
} else {
|
|
682
|
+
for (const evt of [...this.eventListeners.keys()]) {
|
|
683
|
+
this.removeAllListeners(evt);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
537
687
|
// ---------------------------------------------------------------------------
|
|
538
688
|
// Cleanup
|
|
539
689
|
// ---------------------------------------------------------------------------
|
|
@@ -557,14 +707,11 @@ class ControllerImpl {
|
|
|
557
707
|
this.eventListeners.clear();
|
|
558
708
|
this.handlerToTransport.clear();
|
|
559
709
|
this._pendingHandlers = [];
|
|
560
|
-
this.
|
|
561
|
-
this.
|
|
562
|
-
this.
|
|
563
|
-
this.
|
|
564
|
-
this.
|
|
565
|
-
this._onCharacterUpdatedCallbacks.clear();
|
|
566
|
-
this._onRateLimitedCallbacks.clear();
|
|
567
|
-
this._onErrorCallbacks.clear();
|
|
710
|
+
this._lifecycleListeners.clear();
|
|
711
|
+
this._customStates.clear();
|
|
712
|
+
this._stateChangeListeners.clear();
|
|
713
|
+
this._isConnected = false;
|
|
714
|
+
this._outboundBuffer = [];
|
|
568
715
|
if (this.transport) {
|
|
569
716
|
this.transport.destroy();
|
|
570
717
|
this.transport = null;
|
|
@@ -596,8 +743,8 @@ class ControllerImpl {
|
|
|
596
743
|
handleError(error) {
|
|
597
744
|
this.logger.warn(`Error in handler: ${error.message}`);
|
|
598
745
|
const smoreError = error.toSmoreError();
|
|
599
|
-
if (this.
|
|
600
|
-
this.
|
|
746
|
+
if (this._hasLifecycleListeners("$error")) {
|
|
747
|
+
this._emitLifecycle("$error", smoreError);
|
|
601
748
|
} else {
|
|
602
749
|
this.logger.error(error.message, error.details);
|
|
603
750
|
}
|