@smoregg/sdk 2.0.0 → 2.1.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 +193 -115
- 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 +19 -2
- 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 +185 -130
- 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 +125 -74
- 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 +195 -117
- 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 +18 -3
- 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 +187 -132
- 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 +125 -74
- 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 +10 -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 +63 -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 +4 -0
- package/dist/types/transport/protocol.d.ts.map +1 -1
- package/dist/types/types.d.ts +215 -347
- package/dist/types/types.d.ts.map +1 -1
- package/dist/umd/smore-sdk.umd.js +442 -787
- 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;
|
|
@@ -23,17 +23,16 @@ class ControllerImpl {
|
|
|
23
23
|
_controllers = [];
|
|
24
24
|
// Pending handlers registered via on() before transport is ready
|
|
25
25
|
_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();
|
|
26
|
+
// Unified lifecycle listener map (supports both onXxx() and on('$xxx') patterns)
|
|
27
|
+
_lifecycleListeners = /* @__PURE__ */ new Map();
|
|
28
|
+
// Outbound message buffer (messages sent before ready)
|
|
29
|
+
_outboundBuffer = [];
|
|
35
30
|
// Whether all-ready has fired
|
|
36
31
|
_allReadyFired = false;
|
|
32
|
+
// Self-connection awareness
|
|
33
|
+
_isConnected = false;
|
|
34
|
+
// Protocol versioning
|
|
35
|
+
_protocolVersion = PROTOCOL_VERSION;
|
|
37
36
|
// Ready promise
|
|
38
37
|
_readyResolve;
|
|
39
38
|
_readyReject;
|
|
@@ -50,8 +49,8 @@ class ControllerImpl {
|
|
|
50
49
|
// ---------------------------------------------------------------------------
|
|
51
50
|
// Properties (readonly)
|
|
52
51
|
// ---------------------------------------------------------------------------
|
|
53
|
-
get
|
|
54
|
-
return this.
|
|
52
|
+
get myPlayerIndex() {
|
|
53
|
+
return this._myPlayerIndex;
|
|
55
54
|
}
|
|
56
55
|
get roomCode() {
|
|
57
56
|
return this._roomCode;
|
|
@@ -62,6 +61,12 @@ class ControllerImpl {
|
|
|
62
61
|
get isDestroyed() {
|
|
63
62
|
return this._isDestroyed;
|
|
64
63
|
}
|
|
64
|
+
get isConnected() {
|
|
65
|
+
return this._isConnected;
|
|
66
|
+
}
|
|
67
|
+
get protocolVersion() {
|
|
68
|
+
return this._protocolVersion;
|
|
69
|
+
}
|
|
65
70
|
/**
|
|
66
71
|
* Read-only list of all known controllers (players) in the room.
|
|
67
72
|
* Returns full ControllerInfo including playerIndex, nickname, connected status, and appearance.
|
|
@@ -78,6 +83,9 @@ class ControllerImpl {
|
|
|
78
83
|
getControllerCount() {
|
|
79
84
|
return this._controllers.filter((c) => c.connected).length;
|
|
80
85
|
}
|
|
86
|
+
getController(playerIndex) {
|
|
87
|
+
return this._controllers.find((c) => c.playerIndex === playerIndex);
|
|
88
|
+
}
|
|
81
89
|
// ---------------------------------------------------------------------------
|
|
82
90
|
// Initialization
|
|
83
91
|
// ---------------------------------------------------------------------------
|
|
@@ -108,7 +116,7 @@ class ControllerImpl {
|
|
|
108
116
|
};
|
|
109
117
|
window.addEventListener("message", this.boundMessageHandler);
|
|
110
118
|
this.logger.lifecycle("Sending _bridge:ready to parent");
|
|
111
|
-
window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
|
|
119
|
+
window.parent.postMessage({ type: "_bridge:ready", protocolVersion: PROTOCOL_VERSION }, parentOrigin);
|
|
112
120
|
}
|
|
113
121
|
handleInit(msg, parentOrigin) {
|
|
114
122
|
const initPayload = msg.payload;
|
|
@@ -147,28 +155,44 @@ class ControllerImpl {
|
|
|
147
155
|
this._readyReject(error);
|
|
148
156
|
return;
|
|
149
157
|
}
|
|
150
|
-
this.transport = new PostMessageTransport(parentOrigin);
|
|
158
|
+
this.transport = this.config.transport ?? new PostMessageTransport(parentOrigin);
|
|
151
159
|
this._roomCode = initData.roomCode;
|
|
152
|
-
|
|
160
|
+
const serverProtocolVersion = initData.protocolVersion;
|
|
161
|
+
if (serverProtocolVersion !== void 0) {
|
|
162
|
+
this._protocolVersion = serverProtocolVersion;
|
|
163
|
+
if (serverProtocolVersion !== PROTOCOL_VERSION) {
|
|
164
|
+
this.logger.warn(
|
|
165
|
+
`Protocol version mismatch: SDK v${PROTOCOL_VERSION}, server v${serverProtocolVersion}. Some features may not work correctly.`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
this._myPlayerIndex = initData.myIndex;
|
|
153
170
|
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
|
-
}));
|
|
171
|
+
this._controllers = initPlayers.filter((p) => typeof p.playerIndex === "number").map((p, index) => mapPlayerDTO(p, index));
|
|
160
172
|
this.setupEventHandlers();
|
|
161
173
|
for (const { event, handler } of this._pendingHandlers) {
|
|
162
174
|
this.setupUserEventHandler(event, handler);
|
|
163
175
|
}
|
|
164
176
|
this._pendingHandlers = [];
|
|
177
|
+
this._isConnected = true;
|
|
165
178
|
this._isReady = true;
|
|
179
|
+
for (const buffered of this._outboundBuffer) {
|
|
180
|
+
try {
|
|
181
|
+
switch (buffered.method) {
|
|
182
|
+
case "send":
|
|
183
|
+
this.send(buffered.args[0], buffered.args[1]);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
this.handleError(err instanceof SmoreSDKError ? err : new SmoreSDKError("UNKNOWN", "Failed to flush buffered message"));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
this._outboundBuffer = [];
|
|
166
191
|
this.logger.lifecycle("Controller ready", {
|
|
167
192
|
roomCode: this._roomCode,
|
|
168
|
-
myIndex: this.
|
|
193
|
+
myIndex: this._myPlayerIndex
|
|
169
194
|
});
|
|
170
|
-
|
|
171
|
-
if (autoReady) {
|
|
195
|
+
if (this.config.autoReady !== false) {
|
|
172
196
|
this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
|
|
173
197
|
this.signalReady();
|
|
174
198
|
}
|
|
@@ -183,21 +207,16 @@ class ControllerImpl {
|
|
|
183
207
|
this.logger.debug("Received _bridge:update", updateData);
|
|
184
208
|
if (updateData.players && Array.isArray(updateData.players)) {
|
|
185
209
|
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
|
-
}));
|
|
210
|
+
const newControllers = players.filter((p) => typeof p.playerIndex === "number").map((p, index) => mapPlayerDTO(p, index));
|
|
192
211
|
const oldControllers = this._controllers;
|
|
193
212
|
for (const nc of newControllers) {
|
|
194
213
|
if (!oldControllers.some((oc) => oc.playerIndex === nc.playerIndex)) {
|
|
195
|
-
this.
|
|
214
|
+
this._emitLifecycle("$controller-join", nc.playerIndex, nc);
|
|
196
215
|
}
|
|
197
216
|
}
|
|
198
217
|
for (const oc of oldControllers) {
|
|
199
218
|
if (!newControllers.some((nc) => nc.playerIndex === oc.playerIndex)) {
|
|
200
|
-
this.
|
|
219
|
+
this._emitLifecycle("$controller-leave", oc.playerIndex);
|
|
201
220
|
}
|
|
202
221
|
}
|
|
203
222
|
for (const nc of newControllers) {
|
|
@@ -205,11 +224,11 @@ class ControllerImpl {
|
|
|
205
224
|
if (oc) {
|
|
206
225
|
if (oc.connected && !nc.connected) {
|
|
207
226
|
this.logger.debug("Player disconnected (via update)", { playerIndex: nc.playerIndex });
|
|
208
|
-
this.
|
|
227
|
+
this._emitLifecycle("$controller-disconnect", nc.playerIndex);
|
|
209
228
|
}
|
|
210
229
|
if (!oc.connected && nc.connected) {
|
|
211
230
|
this.logger.debug("Player reconnected (via update)", { playerIndex: nc.playerIndex });
|
|
212
|
-
this.
|
|
231
|
+
this._emitLifecycle("$controller-reconnect", nc.playerIndex, nc);
|
|
213
232
|
}
|
|
214
233
|
}
|
|
215
234
|
}
|
|
@@ -224,19 +243,10 @@ class ControllerImpl {
|
|
|
224
243
|
const playerIndex = playerInfo?.playerIndex ?? data.playerIndex;
|
|
225
244
|
if (playerIndex !== void 0) {
|
|
226
245
|
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
|
-
};
|
|
246
|
+
const controllerInfo = playerInfo ? mapPlayerDTO(playerInfo, playerIndex) : mapPlayerDTO({ playerIndex, connected: true }, playerIndex);
|
|
237
247
|
this._controllers = [...this._controllers, controllerInfo];
|
|
238
248
|
this.logger.debug("Player joined", { playerIndex });
|
|
239
|
-
this.
|
|
249
|
+
this._emitLifecycle("$controller-join", playerIndex, controllerInfo);
|
|
240
250
|
}
|
|
241
251
|
});
|
|
242
252
|
this.registerHandler(SMORE_EVENTS.PLAYER_LEFT, (raw) => {
|
|
@@ -246,7 +256,7 @@ class ControllerImpl {
|
|
|
246
256
|
if (!this._controllers.some((c) => c.playerIndex === playerIndex)) return;
|
|
247
257
|
this._controllers = this._controllers.filter((c) => c.playerIndex !== playerIndex);
|
|
248
258
|
this.logger.debug("Player left", { playerIndex });
|
|
249
|
-
this.
|
|
259
|
+
this._emitLifecycle("$controller-leave", playerIndex);
|
|
250
260
|
}
|
|
251
261
|
});
|
|
252
262
|
this.registerHandler(SMORE_EVENTS.PLAYER_DISCONNECTED, (raw) => {
|
|
@@ -258,7 +268,7 @@ class ControllerImpl {
|
|
|
258
268
|
(c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
|
|
259
269
|
);
|
|
260
270
|
this.logger.debug("Player disconnected", { playerIndex });
|
|
261
|
-
this.
|
|
271
|
+
this._emitLifecycle("$controller-disconnect", playerIndex);
|
|
262
272
|
}
|
|
263
273
|
});
|
|
264
274
|
this.registerHandler(SMORE_EVENTS.PLAYER_RECONNECTED, (raw) => {
|
|
@@ -266,21 +276,12 @@ class ControllerImpl {
|
|
|
266
276
|
const playerData = data.player;
|
|
267
277
|
const playerIndex = playerData?.playerIndex ?? data.playerIndex;
|
|
268
278
|
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
|
-
};
|
|
279
|
+
const controllerInfo = playerData ? mapPlayerDTO(playerData, playerIndex) : mapPlayerDTO({ playerIndex, connected: true }, playerIndex);
|
|
279
280
|
this._controllers = this._controllers.map(
|
|
280
281
|
(c) => c.playerIndex === playerIndex ? controllerInfo : c
|
|
281
282
|
);
|
|
282
283
|
this.logger.debug("Player reconnected", { playerIndex });
|
|
283
|
-
this.
|
|
284
|
+
this._emitLifecycle("$controller-reconnect", playerIndex, controllerInfo);
|
|
284
285
|
}
|
|
285
286
|
});
|
|
286
287
|
this.registerHandler(SMORE_EVENTS.PLAYER_CHARACTER_UPDATED, (raw) => {
|
|
@@ -293,19 +294,37 @@ class ControllerImpl {
|
|
|
293
294
|
(c) => c.playerIndex === pi ? { ...c, appearance } : c
|
|
294
295
|
);
|
|
295
296
|
this.logger.debug("Player character updated", { playerIndex: pi });
|
|
296
|
-
this.
|
|
297
|
+
this._emitLifecycle("$character-updated", pi, appearance ?? null);
|
|
297
298
|
}
|
|
298
299
|
});
|
|
299
300
|
this.registerHandler(SMORE_EVENTS.RATE_LIMITED, (raw) => {
|
|
300
301
|
const data = raw;
|
|
301
|
-
const
|
|
302
|
-
this.
|
|
303
|
-
|
|
302
|
+
const eventName = data?.event ?? "unknown";
|
|
303
|
+
this.handleError(
|
|
304
|
+
new SmoreSDKError("RATE_LIMITED", `Server rate-limited event: ${eventName}`, {
|
|
305
|
+
details: { event: eventName }
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
this.registerHandler(SMORE_EVENTS.GAME_OVER, (raw) => {
|
|
310
|
+
const data = raw;
|
|
311
|
+
this.logger.lifecycle("Game over", data?.results);
|
|
312
|
+
this._emitLifecycle("$game-over", data?.results);
|
|
304
313
|
});
|
|
305
314
|
this.registerHandler(SMORE_EVENTS.ALL_READY, () => {
|
|
306
315
|
this.logger.lifecycle("All participants ready");
|
|
307
316
|
this._allReadyFired = true;
|
|
308
|
-
this.
|
|
317
|
+
this._emitLifecycle("$all-ready");
|
|
318
|
+
});
|
|
319
|
+
this.registerHandler(SMORE_EVENTS.SELF_DISCONNECTED, () => {
|
|
320
|
+
this._isConnected = false;
|
|
321
|
+
this.logger.lifecycle("Connection lost");
|
|
322
|
+
this._emitLifecycle("$connection-change", false);
|
|
323
|
+
});
|
|
324
|
+
this.registerHandler(SMORE_EVENTS.SELF_RECONNECTED, () => {
|
|
325
|
+
this._isConnected = true;
|
|
326
|
+
this.logger.lifecycle("Connection restored");
|
|
327
|
+
this._emitLifecycle("$connection-change", true);
|
|
309
328
|
});
|
|
310
329
|
}
|
|
311
330
|
/**
|
|
@@ -344,58 +363,70 @@ class ControllerImpl {
|
|
|
344
363
|
this.registeredHandlers.push({ event, handler });
|
|
345
364
|
}
|
|
346
365
|
// ---------------------------------------------------------------------------
|
|
366
|
+
// Lifecycle Listener Helpers
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
_addLifecycleListener(event, listener) {
|
|
369
|
+
let set = this._lifecycleListeners.get(event);
|
|
370
|
+
if (!set) {
|
|
371
|
+
set = /* @__PURE__ */ new Set();
|
|
372
|
+
this._lifecycleListeners.set(event, set);
|
|
373
|
+
}
|
|
374
|
+
set.add(listener);
|
|
375
|
+
return () => {
|
|
376
|
+
set.delete(listener);
|
|
377
|
+
if (set.size === 0) this._lifecycleListeners.delete(event);
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
_emitLifecycle(event, ...args) {
|
|
381
|
+
this._lifecycleListeners.get(event)?.forEach((cb) => {
|
|
382
|
+
try {
|
|
383
|
+
cb(...args);
|
|
384
|
+
} catch (err) {
|
|
385
|
+
this.handleError(
|
|
386
|
+
new SmoreSDKError("UNKNOWN", `Error in lifecycle handler for "${event}"`, {
|
|
387
|
+
cause: err instanceof Error ? err : void 0,
|
|
388
|
+
details: { event }
|
|
389
|
+
})
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
_hasLifecycleListeners(event) {
|
|
395
|
+
const set = this._lifecycleListeners.get(event);
|
|
396
|
+
return set !== void 0 && set.size > 0;
|
|
397
|
+
}
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
347
399
|
// Lifecycle Methods
|
|
348
400
|
// ---------------------------------------------------------------------------
|
|
349
401
|
onAllReady(callback) {
|
|
350
402
|
if (this._allReadyFired) {
|
|
351
403
|
callback();
|
|
352
404
|
}
|
|
353
|
-
this.
|
|
354
|
-
return () => {
|
|
355
|
-
this._onAllReadyCallbacks.delete(callback);
|
|
356
|
-
};
|
|
405
|
+
return this._addLifecycleListener("$all-ready", callback);
|
|
357
406
|
}
|
|
358
407
|
onControllerJoin(callback) {
|
|
359
|
-
this.
|
|
360
|
-
return () => {
|
|
361
|
-
this._onControllerJoinCallbacks.delete(callback);
|
|
362
|
-
};
|
|
408
|
+
return this._addLifecycleListener("$controller-join", callback);
|
|
363
409
|
}
|
|
364
410
|
onControllerLeave(callback) {
|
|
365
|
-
this.
|
|
366
|
-
return () => {
|
|
367
|
-
this._onControllerLeaveCallbacks.delete(callback);
|
|
368
|
-
};
|
|
411
|
+
return this._addLifecycleListener("$controller-leave", callback);
|
|
369
412
|
}
|
|
370
413
|
onControllerDisconnect(callback) {
|
|
371
|
-
this.
|
|
372
|
-
return () => {
|
|
373
|
-
this._onControllerDisconnectCallbacks.delete(callback);
|
|
374
|
-
};
|
|
414
|
+
return this._addLifecycleListener("$controller-disconnect", callback);
|
|
375
415
|
}
|
|
376
416
|
onControllerReconnect(callback) {
|
|
377
|
-
this.
|
|
378
|
-
return () => {
|
|
379
|
-
this._onControllerReconnectCallbacks.delete(callback);
|
|
380
|
-
};
|
|
417
|
+
return this._addLifecycleListener("$controller-reconnect", callback);
|
|
381
418
|
}
|
|
382
419
|
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
|
-
};
|
|
420
|
+
return this._addLifecycleListener("$character-updated", callback);
|
|
393
421
|
}
|
|
394
422
|
onError(callback) {
|
|
395
|
-
this.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
423
|
+
return this._addLifecycleListener("$error", callback);
|
|
424
|
+
}
|
|
425
|
+
onConnectionChange(callback) {
|
|
426
|
+
return this._addLifecycleListener("$connection-change", callback);
|
|
427
|
+
}
|
|
428
|
+
onGameOver(callback) {
|
|
429
|
+
return this._addLifecycleListener("$game-over", callback);
|
|
399
430
|
}
|
|
400
431
|
// ---------------------------------------------------------------------------
|
|
401
432
|
// Communication Methods
|
|
@@ -411,8 +442,16 @@ class ControllerImpl {
|
|
|
411
442
|
* Use the onError callback or smore:rate-limited event to detect rate limiting.
|
|
412
443
|
*/
|
|
413
444
|
send(event, data) {
|
|
414
|
-
this.
|
|
445
|
+
if (this._isDestroyed) {
|
|
446
|
+
throw new SmoreSDKError("DESTROYED", "Cannot call send() after destroy()");
|
|
447
|
+
}
|
|
448
|
+
if (!this._isReady || !this.transport) {
|
|
449
|
+
this._outboundBuffer.push({ method: "send", args: [event, data] });
|
|
450
|
+
this.logger.debug(`Buffered send "${event}" (controller not ready yet)`);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
415
453
|
validateEventName(event);
|
|
454
|
+
validatePayloadSize(data);
|
|
416
455
|
if (typeof data !== "object" || data === null) {
|
|
417
456
|
this.logger.warn(
|
|
418
457
|
'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 +460,6 @@ class ControllerImpl {
|
|
|
421
460
|
this.logSend(event, data);
|
|
422
461
|
this.transport.emit(event, data);
|
|
423
462
|
}
|
|
424
|
-
sendRaw(event, data) {
|
|
425
|
-
this.ensureReady("sendRaw");
|
|
426
|
-
validateEventName(event);
|
|
427
|
-
this.logSend(event, data);
|
|
428
|
-
this.transport.emit(event, data);
|
|
429
|
-
}
|
|
430
463
|
signalReady() {
|
|
431
464
|
this.ensureReady("signalReady");
|
|
432
465
|
this.logSend(SMORE_EVENTS.GAME_READY, {});
|
|
@@ -448,6 +481,16 @@ class ControllerImpl {
|
|
|
448
481
|
* handler receives `(data)` -- targeted to this specific controller.
|
|
449
482
|
*/
|
|
450
483
|
on(event, handler) {
|
|
484
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
485
|
+
const validEvents = CONTROLLER_LIFECYCLE_EVENTS;
|
|
486
|
+
if (!validEvents.has(event)) {
|
|
487
|
+
throw new SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}". Valid lifecycle events: ${Array.from(validEvents).join(", ")}`);
|
|
488
|
+
}
|
|
489
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
490
|
+
handler();
|
|
491
|
+
}
|
|
492
|
+
return this._addLifecycleListener(event, handler);
|
|
493
|
+
}
|
|
451
494
|
validateEventName(event);
|
|
452
495
|
let listeners = this.eventListeners.get(event);
|
|
453
496
|
if (!listeners) {
|
|
@@ -500,6 +543,23 @@ class ControllerImpl {
|
|
|
500
543
|
* `off(event, originalHandler)`. Use the returned unsubscribe function instead.
|
|
501
544
|
*/
|
|
502
545
|
once(event, handler) {
|
|
546
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
547
|
+
const validEvents = CONTROLLER_LIFECYCLE_EVENTS;
|
|
548
|
+
if (!validEvents.has(event)) {
|
|
549
|
+
throw new SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}"`);
|
|
550
|
+
}
|
|
551
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
552
|
+
handler();
|
|
553
|
+
return () => {
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
const wrapper = (...args) => {
|
|
557
|
+
unsub();
|
|
558
|
+
handler(...args);
|
|
559
|
+
};
|
|
560
|
+
const unsub = this._addLifecycleListener(event, wrapper);
|
|
561
|
+
return unsub;
|
|
562
|
+
}
|
|
503
563
|
const unsubscribe = this.on(event, ((data) => {
|
|
504
564
|
unsubscribe();
|
|
505
565
|
handler(data);
|
|
@@ -507,6 +567,14 @@ class ControllerImpl {
|
|
|
507
567
|
return unsubscribe;
|
|
508
568
|
}
|
|
509
569
|
off(event, handler) {
|
|
570
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
571
|
+
if (!handler) {
|
|
572
|
+
this._lifecycleListeners.delete(event);
|
|
573
|
+
} else {
|
|
574
|
+
this._lifecycleListeners.get(event)?.delete(handler);
|
|
575
|
+
}
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
510
578
|
if (!handler) {
|
|
511
579
|
this.eventListeners.delete(event);
|
|
512
580
|
this.transport?.off(event);
|
|
@@ -534,6 +602,21 @@ class ControllerImpl {
|
|
|
534
602
|
);
|
|
535
603
|
}
|
|
536
604
|
}
|
|
605
|
+
removeAllListeners(event) {
|
|
606
|
+
if (event) {
|
|
607
|
+
this.eventListeners.delete(event);
|
|
608
|
+
this.transport?.off(event);
|
|
609
|
+
this.registeredHandlers = this.registeredHandlers.filter((h) => h.event !== event);
|
|
610
|
+
for (const [key, val] of this.handlerToTransport) {
|
|
611
|
+
if (val.event === event) this.handlerToTransport.delete(key);
|
|
612
|
+
}
|
|
613
|
+
this._pendingHandlers = this._pendingHandlers.filter((p) => p.event !== event);
|
|
614
|
+
} else {
|
|
615
|
+
for (const evt of [...this.eventListeners.keys()]) {
|
|
616
|
+
this.removeAllListeners(evt);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
537
620
|
// ---------------------------------------------------------------------------
|
|
538
621
|
// Cleanup
|
|
539
622
|
// ---------------------------------------------------------------------------
|
|
@@ -557,14 +640,9 @@ class ControllerImpl {
|
|
|
557
640
|
this.eventListeners.clear();
|
|
558
641
|
this.handlerToTransport.clear();
|
|
559
642
|
this._pendingHandlers = [];
|
|
560
|
-
this.
|
|
561
|
-
this.
|
|
562
|
-
this.
|
|
563
|
-
this._onControllerDisconnectCallbacks.clear();
|
|
564
|
-
this._onControllerReconnectCallbacks.clear();
|
|
565
|
-
this._onCharacterUpdatedCallbacks.clear();
|
|
566
|
-
this._onRateLimitedCallbacks.clear();
|
|
567
|
-
this._onErrorCallbacks.clear();
|
|
643
|
+
this._lifecycleListeners.clear();
|
|
644
|
+
this._isConnected = false;
|
|
645
|
+
this._outboundBuffer = [];
|
|
568
646
|
if (this.transport) {
|
|
569
647
|
this.transport.destroy();
|
|
570
648
|
this.transport = null;
|
|
@@ -596,8 +674,8 @@ class ControllerImpl {
|
|
|
596
674
|
handleError(error) {
|
|
597
675
|
this.logger.warn(`Error in handler: ${error.message}`);
|
|
598
676
|
const smoreError = error.toSmoreError();
|
|
599
|
-
if (this.
|
|
600
|
-
this.
|
|
677
|
+
if (this._hasLifecycleListeners("$error")) {
|
|
678
|
+
this._emitLifecycle("$error", smoreError);
|
|
601
679
|
} else {
|
|
602
680
|
this.logger.error(error.message, error.details);
|
|
603
681
|
}
|