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